Statistics
| Branch: | Tag: | Revision:

root / lib / qlang.py @ 76b62028

History | View | Annotate | Download (9.8 kB)

1
#
2
#
3

    
4
# Copyright (C) 2010, 2011, 2012 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

    
21

    
22
"""Module for a simple query language
23

24
A query filter is always a list. The first item in the list is the operator
25
(e.g. C{[OP_AND, ...]}), while the other items depend on the operator. For
26
logic operators (e.g. L{OP_AND}, L{OP_OR}), they are subfilters whose results
27
are combined. Unary operators take exactly one other item (e.g. a subfilter for
28
L{OP_NOT} and a field name for L{OP_TRUE}). Binary operators take exactly two
29
operands, usually a field name and a value to compare against. Filters are
30
converted to callable functions by L{query._CompileFilter}.
31

32
"""
33

    
34
import re
35
import string # pylint: disable=W0402
36
import logging
37

    
38
import pyparsing as pyp
39

    
40
from ganeti import errors
41
from ganeti import netutils
42
from ganeti import utils
43
from ganeti import compat
44

    
45

    
46
# Logic operators with one or more operands, each of which is a filter on its
47
# own
48
OP_OR = "|"
49
OP_AND = "&"
50

    
51

    
52
# Unary operators with exactly one operand
53
OP_NOT = "!"
54
OP_TRUE = "?"
55

    
56

    
57
# Binary operators with exactly two operands, the field name and an
58
# operator-specific value
59
OP_EQUAL = "="
60
OP_NOT_EQUAL = "!="
61
OP_LT = "<"
62
OP_LE = "<="
63
OP_GT = ">"
64
OP_GE = ">="
65
OP_REGEXP = "=~"
66
OP_CONTAINS = "=[]"
67

    
68

    
69
#: Characters used for detecting user-written filters (see L{_CheckFilter})
70
FILTER_DETECTION_CHARS = frozenset("()=/!~'\"\\<>" + string.whitespace)
71

    
72
#: Characters used to detect globbing filters (see L{_CheckGlobbing})
73
GLOB_DETECTION_CHARS = frozenset("*?")
74

    
75

    
76
def MakeSimpleFilter(namefield, values):
77
  """Builds simple a filter.
78

79
  @param namefield: Name of field containing item name
80
  @param values: List of names
81

82
  """
83
  if values:
84
    return [OP_OR] + [[OP_EQUAL, namefield, i] for i in values]
85

    
86
  return None
87

    
88

    
89
def _ConvertLogicOp(op):
90
  """Creates parsing action function for logic operator.
91

92
  @type op: string
93
  @param op: Operator for data structure, e.g. L{OP_AND}
94

95
  """
96
  def fn(toks):
97
    """Converts parser tokens to query operator structure.
98

99
    @rtype: list
100
    @return: Query operator structure, e.g. C{[OP_AND, ["=", "foo", "bar"]]}
101

102
    """
103
    operands = toks[0]
104

    
105
    if len(operands) == 1:
106
      return operands[0]
107

    
108
    # Build query operator structure
109
    return [[op] + operands.asList()]
110

    
111
  return fn
112

    
113

    
114
_KNOWN_REGEXP_DELIM = "/#^|"
115
_KNOWN_REGEXP_FLAGS = frozenset("si")
116

    
117

    
118
def _ConvertRegexpValue(_, loc, toks):
119
  """Regular expression value for condition.
120

121
  """
122
  (regexp, flags) = toks[0]
123

    
124
  # Ensure only whitelisted flags are used
125
  unknown_flags = (frozenset(flags) - _KNOWN_REGEXP_FLAGS)
126
  if unknown_flags:
127
    raise pyp.ParseFatalException("Unknown regular expression flags: '%s'" %
128
                                  "".join(unknown_flags), loc)
129

    
130
  if flags:
131
    re_flags = "(?%s)" % "".join(sorted(flags))
132
  else:
133
    re_flags = ""
134

    
135
  re_cond = re_flags + regexp
136

    
137
  # Test if valid
138
  try:
139
    re.compile(re_cond)
140
  except re.error, err:
141
    raise pyp.ParseFatalException("Invalid regular expression (%s)" % err, loc)
142

    
143
  return [re_cond]
144

    
145

    
146
def BuildFilterParser():
147
  """Builds a parser for query filter strings.
148

149
  @rtype: pyparsing.ParserElement
150

151
  """
152
  field_name = pyp.Word(pyp.alphas, pyp.alphanums + "_/.")
153

    
154
  # Integer
155
  num_sign = pyp.Word("-+", exact=1)
156
  number = pyp.Combine(pyp.Optional(num_sign) + pyp.Word(pyp.nums))
157
  number.setParseAction(lambda toks: int(toks[0]))
158

    
159
  quoted_string = pyp.quotedString.copy().setParseAction(pyp.removeQuotes)
160

    
161
  # Right-hand-side value
162
  rval = (number | quoted_string)
163

    
164
  # Boolean condition
165
  bool_cond = field_name.copy()
166
  bool_cond.setParseAction(lambda (fname, ): [[OP_TRUE, fname]])
167

    
168
  # Simple binary conditions
169
  binopstbl = {
170
    "==": OP_EQUAL,
171
    "!=": OP_NOT_EQUAL,
172
    "<": OP_LT,
173
    "<=": OP_LE,
174
    ">": OP_GT,
175
    ">=": OP_GE,
176
    }
177

    
178
  binary_cond = (field_name + pyp.oneOf(binopstbl.keys()) + rval)
179
  binary_cond.setParseAction(lambda (lhs, op, rhs): [[binopstbl[op], lhs, rhs]])
180

    
181
  # "in" condition
182
  in_cond = (rval + pyp.Suppress("in") + field_name)
183
  in_cond.setParseAction(lambda (value, field): [[OP_CONTAINS, field, value]])
184

    
185
  # "not in" condition
186
  not_in_cond = (rval + pyp.Suppress("not") + pyp.Suppress("in") + field_name)
187
  not_in_cond.setParseAction(lambda (value, field): [[OP_NOT, [OP_CONTAINS,
188
                                                               field, value]]])
189

    
190
  # Regular expression, e.g. m/foobar/i
191
  regexp_val = pyp.Group(pyp.Optional("m").suppress() +
192
                         pyp.MatchFirst([pyp.QuotedString(i, escChar="\\")
193
                                         for i in _KNOWN_REGEXP_DELIM]) +
194
                         pyp.Optional(pyp.Word(pyp.alphas), default=""))
195
  regexp_val.setParseAction(_ConvertRegexpValue)
196
  regexp_cond = (field_name + pyp.Suppress("=~") + regexp_val)
197
  regexp_cond.setParseAction(lambda (field, value): [[OP_REGEXP, field, value]])
198

    
199
  not_regexp_cond = (field_name + pyp.Suppress("!~") + regexp_val)
200
  not_regexp_cond.setParseAction(lambda (field, value):
201
                                 [[OP_NOT, [OP_REGEXP, field, value]]])
202

    
203
  # Globbing, e.g. name =* "*.site"
204
  glob_cond = (field_name + pyp.Suppress("=*") + quoted_string)
205
  glob_cond.setParseAction(lambda (field, value):
206
                           [[OP_REGEXP, field,
207
                             utils.DnsNameGlobPattern(value)]])
208

    
209
  not_glob_cond = (field_name + pyp.Suppress("!*") + quoted_string)
210
  not_glob_cond.setParseAction(lambda (field, value):
211
                               [[OP_NOT, [OP_REGEXP, field,
212
                                          utils.DnsNameGlobPattern(value)]]])
213

    
214
  # All possible conditions
215
  condition = (binary_cond ^ bool_cond ^
216
               in_cond ^ not_in_cond ^
217
               regexp_cond ^ not_regexp_cond ^
218
               glob_cond ^ not_glob_cond)
219

    
220
  # Associativity operators
221
  filter_expr = pyp.operatorPrecedence(condition, [
222
    (pyp.Keyword("not").suppress(), 1, pyp.opAssoc.RIGHT,
223
     lambda toks: [[OP_NOT, toks[0][0]]]),
224
    (pyp.Keyword("and").suppress(), 2, pyp.opAssoc.LEFT,
225
     _ConvertLogicOp(OP_AND)),
226
    (pyp.Keyword("or").suppress(), 2, pyp.opAssoc.LEFT,
227
     _ConvertLogicOp(OP_OR)),
228
    ])
229

    
230
  parser = pyp.StringStart() + filter_expr + pyp.StringEnd()
231
  parser.parseWithTabs()
232

    
233
  # Originally C{parser.validate} was called here, but there seems to be some
234
  # issue causing it to fail whenever the "not" operator is included above.
235

    
236
  return parser
237

    
238

    
239
def ParseFilter(text, parser=None):
240
  """Parses a query filter.
241

242
  @type text: string
243
  @param text: Query filter
244
  @type parser: pyparsing.ParserElement
245
  @param parser: Pyparsing object
246
  @rtype: list
247

248
  """
249
  logging.debug("Parsing as query filter: %s", text)
250

    
251
  if parser is None:
252
    parser = BuildFilterParser()
253

    
254
  try:
255
    return parser.parseString(text)[0]
256
  except pyp.ParseBaseException, err:
257
    raise errors.QueryFilterParseError("Failed to parse query filter"
258
                                       " '%s': %s" % (text, err), err)
259

    
260

    
261
def _IsHostname(text):
262
  """Checks if a string could be a hostname.
263

264
  @rtype: bool
265

266
  """
267
  try:
268
    netutils.Hostname.GetNormalizedName(text)
269
  except errors.OpPrereqError:
270
    return False
271
  else:
272
    return True
273

    
274

    
275
def _CheckFilter(text):
276
  """CHecks if a string could be a filter.
277

278
  @rtype: bool
279

280
  """
281
  return bool(frozenset(text) & FILTER_DETECTION_CHARS)
282

    
283

    
284
def _CheckGlobbing(text):
285
  """Checks if a string could be a globbing pattern.
286

287
  @rtype: bool
288

289
  """
290
  return bool(frozenset(text) & GLOB_DETECTION_CHARS)
291

    
292

    
293
def _MakeFilterPart(namefield, text, isnumeric=False):
294
  """Generates filter for one argument.
295

296
  """
297
  if isnumeric:
298
    try:
299
      number = int(text)
300
    except (TypeError, ValueError), err:
301
      raise errors.OpPrereqError("Invalid job ID passed: %s" % str(err),
302
                                 errors.ECODE_INVAL)
303
    return [OP_EQUAL, namefield, number]
304
  elif _CheckGlobbing(text):
305
    return [OP_REGEXP, namefield, utils.DnsNameGlobPattern(text)]
306
  else:
307
    return [OP_EQUAL, namefield, text]
308

    
309

    
310
def MakeFilter(args, force_filter, namefield=None, isnumeric=False):
311
  """Try to make a filter from arguments to a command.
312

313
  If the name could be a filter it is parsed as such. If it's just a globbing
314
  pattern, e.g. "*.site", such a filter is constructed. As a last resort the
315
  names are treated just as a plain name filter.
316

317
  @type args: list of string
318
  @param args: Arguments to command
319
  @type force_filter: bool
320
  @param force_filter: Whether to force treatment as a full-fledged filter
321
  @type namefield: string
322
  @param namefield: Name of field to use for simple filters (use L{None} for
323
    a default of "name")
324
  @type isnumeric: bool
325
  @param isnumeric: Whether the namefield type is numeric, as opposed to
326
    the default string type; this influences how the filter is built
327
  @rtype: list
328
  @return: Query filter
329

330
  """
331
  if namefield is None:
332
    namefield = "name"
333

    
334
  if (force_filter or
335
      (args and len(args) == 1 and _CheckFilter(args[0]))):
336
    try:
337
      (filter_text, ) = args
338
    except (TypeError, ValueError):
339
      raise errors.OpPrereqError("Exactly one argument must be given as a"
340
                                 " filter", errors.ECODE_INVAL)
341

    
342
    result = ParseFilter(filter_text)
343
  elif args:
344
    result = [OP_OR] + map(compat.partial(_MakeFilterPart, namefield,
345
                                          isnumeric=isnumeric), args)
346
  else:
347
    result = None
348

    
349
  return result