Statistics
| Branch: | Tag: | Revision:

root / lib / qlang.py @ bc57fa8d

History | View | Annotate | Download (9.5 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 logging
36

    
37
import pyparsing as pyp
38

    
39
from ganeti import constants
40
from ganeti import errors
41
from ganeti import utils
42
from ganeti import compat
43

    
44

    
45
OP_OR = constants.QLANG_OP_OR
46
OP_AND = constants.QLANG_OP_AND
47
OP_NOT = constants.QLANG_OP_NOT
48
OP_TRUE = constants.QLANG_OP_TRUE
49
OP_EQUAL = constants.QLANG_OP_EQUAL
50
OP_NOT_EQUAL = constants.QLANG_OP_NOT_EQUAL
51
OP_LT = constants.QLANG_OP_LT
52
OP_LE = constants.QLANG_OP_LE
53
OP_GT = constants.QLANG_OP_GT
54
OP_GE = constants.QLANG_OP_GE
55
OP_REGEXP = constants.QLANG_OP_REGEXP
56
OP_CONTAINS = constants.QLANG_OP_CONTAINS
57
FILTER_DETECTION_CHARS = constants.QLANG_FILTER_DETECTION_CHARS
58
GLOB_DETECTION_CHARS = constants.QLANG_GLOB_DETECTION_CHARS
59

    
60

    
61
def MakeSimpleFilter(namefield, values):
62
  """Builds simple a filter.
63

64
  @param namefield: Name of field containing item name
65
  @param values: List of names
66

67
  """
68
  if values:
69
    return [OP_OR] + [[OP_EQUAL, namefield, i] for i in values]
70

    
71
  return None
72

    
73

    
74
def _ConvertLogicOp(op):
75
  """Creates parsing action function for logic operator.
76

77
  @type op: string
78
  @param op: Operator for data structure, e.g. L{OP_AND}
79

80
  """
81
  def fn(toks):
82
    """Converts parser tokens to query operator structure.
83

84
    @rtype: list
85
    @return: Query operator structure, e.g. C{[OP_AND, ["=", "foo", "bar"]]}
86

87
    """
88
    operands = toks[0]
89

    
90
    if len(operands) == 1:
91
      return operands[0]
92

    
93
    # Build query operator structure
94
    return [[op] + operands.asList()]
95

    
96
  return fn
97

    
98

    
99
_KNOWN_REGEXP_DELIM = "/#^|"
100
_KNOWN_REGEXP_FLAGS = frozenset("si")
101

    
102

    
103
def _ConvertRegexpValue(_, loc, toks):
104
  """Regular expression value for condition.
105

106
  """
107
  (regexp, flags) = toks[0]
108

    
109
  # Ensure only whitelisted flags are used
110
  unknown_flags = (frozenset(flags) - _KNOWN_REGEXP_FLAGS)
111
  if unknown_flags:
112
    raise pyp.ParseFatalException("Unknown regular expression flags: '%s'" %
113
                                  "".join(unknown_flags), loc)
114

    
115
  if flags:
116
    re_flags = "(?%s)" % "".join(sorted(flags))
117
  else:
118
    re_flags = ""
119

    
120
  re_cond = re_flags + regexp
121

    
122
  # Test if valid
123
  try:
124
    re.compile(re_cond)
125
  except re.error, err:
126
    raise pyp.ParseFatalException("Invalid regular expression (%s)" % err, loc)
127

    
128
  return [re_cond]
129

    
130

    
131
def BuildFilterParser():
132
  """Builds a parser for query filter strings.
133

134
  @rtype: pyparsing.ParserElement
135

136
  """
137
  field_name = pyp.Word(pyp.alphas, pyp.alphanums + "_/.")
138

    
139
  # Integer
140
  num_sign = pyp.Word("-+", exact=1)
141
  number = pyp.Combine(pyp.Optional(num_sign) + pyp.Word(pyp.nums))
142
  number.setParseAction(lambda toks: int(toks[0]))
143

    
144
  quoted_string = pyp.quotedString.copy().setParseAction(pyp.removeQuotes)
145

    
146
  # Right-hand-side value
147
  rval = (number | quoted_string)
148

    
149
  # Boolean condition
150
  bool_cond = field_name.copy()
151
  bool_cond.setParseAction(lambda (fname, ): [[OP_TRUE, fname]])
152

    
153
  # Simple binary conditions
154
  binopstbl = {
155
    "==": OP_EQUAL,
156
    "!=": OP_NOT_EQUAL,
157
    "<": OP_LT,
158
    "<=": OP_LE,
159
    ">": OP_GT,
160
    ">=": OP_GE,
161
    }
162

    
163
  binary_cond = (field_name + pyp.oneOf(binopstbl.keys()) + rval)
164
  binary_cond.setParseAction(lambda (lhs, op, rhs): [[binopstbl[op], lhs, rhs]])
165

    
166
  # "in" condition
167
  in_cond = (rval + pyp.Suppress("in") + field_name)
168
  in_cond.setParseAction(lambda (value, field): [[OP_CONTAINS, field, value]])
169

    
170
  # "not in" condition
171
  not_in_cond = (rval + pyp.Suppress("not") + pyp.Suppress("in") + field_name)
172
  not_in_cond.setParseAction(lambda (value, field): [[OP_NOT, [OP_CONTAINS,
173
                                                               field, value]]])
174

    
175
  # Regular expression, e.g. m/foobar/i
176
  regexp_val = pyp.Group(pyp.Optional("m").suppress() +
177
                         pyp.MatchFirst([pyp.QuotedString(i, escChar="\\")
178
                                         for i in _KNOWN_REGEXP_DELIM]) +
179
                         pyp.Optional(pyp.Word(pyp.alphas), default=""))
180
  regexp_val.setParseAction(_ConvertRegexpValue)
181
  regexp_cond = (field_name + pyp.Suppress("=~") + regexp_val)
182
  regexp_cond.setParseAction(lambda (field, value): [[OP_REGEXP, field, value]])
183

    
184
  not_regexp_cond = (field_name + pyp.Suppress("!~") + regexp_val)
185
  not_regexp_cond.setParseAction(lambda (field, value):
186
                                 [[OP_NOT, [OP_REGEXP, field, value]]])
187

    
188
  # Globbing, e.g. name =* "*.site"
189
  glob_cond = (field_name + pyp.Suppress("=*") + quoted_string)
190
  glob_cond.setParseAction(lambda (field, value):
191
                           [[OP_REGEXP, field,
192
                             utils.DnsNameGlobPattern(value)]])
193

    
194
  not_glob_cond = (field_name + pyp.Suppress("!*") + quoted_string)
195
  not_glob_cond.setParseAction(lambda (field, value):
196
                               [[OP_NOT, [OP_REGEXP, field,
197
                                          utils.DnsNameGlobPattern(value)]]])
198

    
199
  # All possible conditions
200
  condition = (binary_cond ^ bool_cond ^
201
               in_cond ^ not_in_cond ^
202
               regexp_cond ^ not_regexp_cond ^
203
               glob_cond ^ not_glob_cond)
204

    
205
  # Associativity operators
206
  filter_expr = pyp.operatorPrecedence(condition, [
207
    (pyp.Keyword("not").suppress(), 1, pyp.opAssoc.RIGHT,
208
     lambda toks: [[OP_NOT, toks[0][0]]]),
209
    (pyp.Keyword("and").suppress(), 2, pyp.opAssoc.LEFT,
210
     _ConvertLogicOp(OP_AND)),
211
    (pyp.Keyword("or").suppress(), 2, pyp.opAssoc.LEFT,
212
     _ConvertLogicOp(OP_OR)),
213
    ])
214

    
215
  parser = pyp.StringStart() + filter_expr + pyp.StringEnd()
216
  parser.parseWithTabs()
217

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

    
221
  return parser
222

    
223

    
224
def ParseFilter(text, parser=None):
225
  """Parses a query filter.
226

227
  @type text: string
228
  @param text: Query filter
229
  @type parser: pyparsing.ParserElement
230
  @param parser: Pyparsing object
231
  @rtype: list
232

233
  """
234
  logging.debug("Parsing as query filter: %s", text)
235

    
236
  if parser is None:
237
    parser = BuildFilterParser()
238

    
239
  try:
240
    return parser.parseString(text)[0]
241
  except pyp.ParseBaseException, err:
242
    raise errors.QueryFilterParseError("Failed to parse query filter"
243
                                       " '%s': %s" % (text, err), err)
244

    
245

    
246
def _CheckFilter(text):
247
  """CHecks if a string could be a filter.
248

249
  @rtype: bool
250

251
  """
252
  return bool(frozenset(text) & FILTER_DETECTION_CHARS)
253

    
254

    
255
def _CheckGlobbing(text):
256
  """Checks if a string could be a globbing pattern.
257

258
  @rtype: bool
259

260
  """
261
  return bool(frozenset(text) & GLOB_DETECTION_CHARS)
262

    
263

    
264
def _MakeFilterPart(namefield, text, isnumeric=False):
265
  """Generates filter for one argument.
266

267
  """
268
  if isnumeric:
269
    try:
270
      number = int(text)
271
    except (TypeError, ValueError), err:
272
      raise errors.OpPrereqError("Invalid job ID passed: %s" % str(err),
273
                                 errors.ECODE_INVAL)
274
    return [OP_EQUAL, namefield, number]
275
  elif _CheckGlobbing(text):
276
    return [OP_REGEXP, namefield, utils.DnsNameGlobPattern(text)]
277
  else:
278
    return [OP_EQUAL, namefield, text]
279

    
280

    
281
def MakeFilter(args, force_filter, namefield=None, isnumeric=False):
282
  """Try to make a filter from arguments to a command.
283

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

288
  @type args: list of string
289
  @param args: Arguments to command
290
  @type force_filter: bool
291
  @param force_filter: Whether to force treatment as a full-fledged filter
292
  @type namefield: string
293
  @param namefield: Name of field to use for simple filters (use L{None} for
294
    a default of "name")
295
  @type isnumeric: bool
296
  @param isnumeric: Whether the namefield type is numeric, as opposed to
297
    the default string type; this influences how the filter is built
298
  @rtype: list
299
  @return: Query filter
300

301
  """
302
  if namefield is None:
303
    namefield = "name"
304

    
305
  if (force_filter or
306
      (args and len(args) == 1 and _CheckFilter(args[0]))):
307
    try:
308
      (filter_text, ) = args
309
    except (TypeError, ValueError):
310
      raise errors.OpPrereqError("Exactly one argument must be given as a"
311
                                 " filter", errors.ECODE_INVAL)
312

    
313
    result = ParseFilter(filter_text)
314
  elif args:
315
    result = [OP_OR] + map(compat.partial(_MakeFilterPart, namefield,
316
                                          isnumeric=isnumeric), args)
317
  else:
318
    result = None
319

    
320
  return result