Statistics
| Branch: | Tag: | Revision:

root / lib / qlang.py @ 6b3f0d7e

History | View | Annotate | Download (9.6 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 utils
42
from ganeti import compat
43

    
44

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

    
50

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

    
55

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

    
67

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

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

    
74

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

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

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

    
85
  return None
86

    
87

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

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

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

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

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

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

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

    
110
  return fn
111

    
112

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

    
116

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

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

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

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

    
134
  re_cond = re_flags + regexp
135

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

    
142
  return [re_cond]
143

    
144

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

148
  @rtype: pyparsing.ParserElement
149

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
235
  return parser
236

    
237

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

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

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

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

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

    
259

    
260
def _CheckFilter(text):
261
  """CHecks if a string could be a filter.
262

263
  @rtype: bool
264

265
  """
266
  return bool(frozenset(text) & FILTER_DETECTION_CHARS)
267

    
268

    
269
def _CheckGlobbing(text):
270
  """Checks if a string could be a globbing pattern.
271

272
  @rtype: bool
273

274
  """
275
  return bool(frozenset(text) & GLOB_DETECTION_CHARS)
276

    
277

    
278
def _MakeFilterPart(namefield, text, isnumeric=False):
279
  """Generates filter for one argument.
280

281
  """
282
  if isnumeric:
283
    try:
284
      number = int(text)
285
    except (TypeError, ValueError), err:
286
      raise errors.OpPrereqError("Invalid job ID passed: %s" % str(err),
287
                                 errors.ECODE_INVAL)
288
    return [OP_EQUAL, namefield, number]
289
  elif _CheckGlobbing(text):
290
    return [OP_REGEXP, namefield, utils.DnsNameGlobPattern(text)]
291
  else:
292
    return [OP_EQUAL, namefield, text]
293

    
294

    
295
def MakeFilter(args, force_filter, namefield=None, isnumeric=False):
296
  """Try to make a filter from arguments to a command.
297

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

302
  @type args: list of string
303
  @param args: Arguments to command
304
  @type force_filter: bool
305
  @param force_filter: Whether to force treatment as a full-fledged filter
306
  @type namefield: string
307
  @param namefield: Name of field to use for simple filters (use L{None} for
308
    a default of "name")
309
  @type isnumeric: bool
310
  @param isnumeric: Whether the namefield type is numeric, as opposed to
311
    the default string type; this influences how the filter is built
312
  @rtype: list
313
  @return: Query filter
314

315
  """
316
  if namefield is None:
317
    namefield = "name"
318

    
319
  if (force_filter or
320
      (args and len(args) == 1 and _CheckFilter(args[0]))):
321
    try:
322
      (filter_text, ) = args
323
    except (TypeError, ValueError):
324
      raise errors.OpPrereqError("Exactly one argument must be given as a"
325
                                 " filter", errors.ECODE_INVAL)
326

    
327
    result = ParseFilter(filter_text)
328
  elif args:
329
    result = [OP_OR] + map(compat.partial(_MakeFilterPart, namefield,
330
                                          isnumeric=isnumeric), args)
331
  else:
332
    result = None
333

    
334
  return result