Statistics
| Branch: | Tag: | Revision:

root / lib / qlang.py @ 68a856ef

History | View | Annotate | Download (9 kB)

1
#
2
#
3

    
4
# Copyright (C) 2010, 2011 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_REGEXP = "=~"
62
OP_CONTAINS = "=[]"
63

    
64

    
65
#: Characters used for detecting user-written filters (see L{_CheckFilter})
66
FILTER_DETECTION_CHARS = frozenset("()=/!~'\"\\" + string.whitespace)
67

    
68
#: Characters used to detect globbing filters (see L{_CheckGlobbing})
69
GLOB_DETECTION_CHARS = frozenset("*?")
70

    
71

    
72
def MakeSimpleFilter(namefield, values):
73
  """Builds simple a filter.
74

75
  @param namefield: Name of field containing item name
76
  @param values: List of names
77

78
  """
79
  if values:
80
    return [OP_OR] + [[OP_EQUAL, namefield, i] for i in values]
81

    
82
  return None
83

    
84

    
85
def _ConvertLogicOp(op):
86
  """Creates parsing action function for logic operator.
87

88
  @type op: string
89
  @param op: Operator for data structure, e.g. L{OP_AND}
90

91
  """
92
  def fn(toks):
93
    """Converts parser tokens to query operator structure.
94

95
    @rtype: list
96
    @return: Query operator structure, e.g. C{[OP_AND, ["=", "foo", "bar"]]}
97

98
    """
99
    operands = toks[0]
100

    
101
    if len(operands) == 1:
102
      return operands[0]
103

    
104
    # Build query operator structure
105
    return [[op] + operands.asList()]
106

    
107
  return fn
108

    
109

    
110
_KNOWN_REGEXP_DELIM = "/#^|"
111
_KNOWN_REGEXP_FLAGS = frozenset("si")
112

    
113

    
114
def _ConvertRegexpValue(_, loc, toks):
115
  """Regular expression value for condition.
116

117
  """
118
  (regexp, flags) = toks[0]
119

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

    
126
  if flags:
127
    re_flags = "(?%s)" % "".join(sorted(flags))
128
  else:
129
    re_flags = ""
130

    
131
  re_cond = re_flags + regexp
132

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

    
139
  return [re_cond]
140

    
141

    
142
def BuildFilterParser():
143
  """Builds a parser for query filter strings.
144

145
  @rtype: pyparsing.ParserElement
146

147
  """
148
  field_name = pyp.Word(pyp.alphas, pyp.alphanums + "_/.")
149

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

    
155
  quoted_string = pyp.quotedString.copy().setParseAction(pyp.removeQuotes)
156

    
157
  # Right-hand-side value
158
  rval = (number | quoted_string)
159

    
160
  # Boolean condition
161
  bool_cond = field_name.copy()
162
  bool_cond.setParseAction(lambda (fname, ): [[OP_TRUE, fname]])
163

    
164
  # Simple binary conditions
165
  binopstbl = {
166
    "==": OP_EQUAL,
167
    "!=": OP_NOT_EQUAL,
168
    }
169

    
170
  binary_cond = (field_name + pyp.oneOf(binopstbl.keys()) + rval)
171
  binary_cond.setParseAction(lambda (lhs, op, rhs): [[binopstbl[op], lhs, rhs]])
172

    
173
  # "in" condition
174
  in_cond = (rval + pyp.Suppress("in") + field_name)
175
  in_cond.setParseAction(lambda (value, field): [[OP_CONTAINS, field, value]])
176

    
177
  # "not in" condition
178
  not_in_cond = (rval + pyp.Suppress("not") + pyp.Suppress("in") + field_name)
179
  not_in_cond.setParseAction(lambda (value, field): [[OP_NOT, [OP_CONTAINS,
180
                                                               field, value]]])
181

    
182
  # Regular expression, e.g. m/foobar/i
183
  regexp_val = pyp.Group(pyp.Optional("m").suppress() +
184
                         pyp.MatchFirst([pyp.QuotedString(i, escChar="\\")
185
                                         for i in _KNOWN_REGEXP_DELIM]) +
186
                         pyp.Optional(pyp.Word(pyp.alphas), default=""))
187
  regexp_val.setParseAction(_ConvertRegexpValue)
188
  regexp_cond = (field_name + pyp.Suppress("=~") + regexp_val)
189
  regexp_cond.setParseAction(lambda (field, value): [[OP_REGEXP, field, value]])
190

    
191
  not_regexp_cond = (field_name + pyp.Suppress("!~") + regexp_val)
192
  not_regexp_cond.setParseAction(lambda (field, value):
193
                                 [[OP_NOT, [OP_REGEXP, field, value]]])
194

    
195
  # Globbing, e.g. name =* "*.site"
196
  glob_cond = (field_name + pyp.Suppress("=*") + quoted_string)
197
  glob_cond.setParseAction(lambda (field, value):
198
                           [[OP_REGEXP, field,
199
                             utils.DnsNameGlobPattern(value)]])
200

    
201
  not_glob_cond = (field_name + pyp.Suppress("!*") + quoted_string)
202
  not_glob_cond.setParseAction(lambda (field, value):
203
                               [[OP_NOT, [OP_REGEXP, field,
204
                                          utils.DnsNameGlobPattern(value)]]])
205

    
206
  # All possible conditions
207
  condition = (binary_cond ^ bool_cond ^
208
               in_cond ^ not_in_cond ^
209
               regexp_cond ^ not_regexp_cond ^
210
               glob_cond ^ not_glob_cond)
211

    
212
  # Associativity operators
213
  filter_expr = pyp.operatorPrecedence(condition, [
214
    (pyp.Keyword("not").suppress(), 1, pyp.opAssoc.RIGHT,
215
     lambda toks: [[OP_NOT, toks[0][0]]]),
216
    (pyp.Keyword("and").suppress(), 2, pyp.opAssoc.LEFT,
217
     _ConvertLogicOp(OP_AND)),
218
    (pyp.Keyword("or").suppress(), 2, pyp.opAssoc.LEFT,
219
     _ConvertLogicOp(OP_OR)),
220
    ])
221

    
222
  parser = pyp.StringStart() + filter_expr + pyp.StringEnd()
223
  parser.parseWithTabs()
224

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

    
228
  return parser
229

    
230

    
231
def ParseFilter(text, parser=None):
232
  """Parses a query filter.
233

234
  @type text: string
235
  @param text: Query filter
236
  @type parser: pyparsing.ParserElement
237
  @param parser: Pyparsing object
238
  @rtype: list
239

240
  """
241
  logging.debug("Parsing as query filter: %s", text)
242

    
243
  if parser is None:
244
    parser = BuildFilterParser()
245

    
246
  try:
247
    return parser.parseString(text)[0]
248
  except pyp.ParseBaseException, err:
249
    raise errors.QueryFilterParseError("Failed to parse query filter"
250
                                       " '%s': %s" % (text, err), err)
251

    
252

    
253
def _IsHostname(text):
254
  """Checks if a string could be a hostname.
255

256
  @rtype: bool
257

258
  """
259
  try:
260
    netutils.Hostname.GetNormalizedName(text)
261
  except errors.OpPrereqError:
262
    return False
263
  else:
264
    return True
265

    
266

    
267
def _CheckFilter(text):
268
  """CHecks if a string could be a filter.
269

270
  @rtype: bool
271

272
  """
273
  return bool(frozenset(text) & FILTER_DETECTION_CHARS)
274

    
275

    
276
def _CheckGlobbing(text):
277
  """Checks if a string could be a globbing pattern.
278

279
  @rtype: bool
280

281
  """
282
  return bool(frozenset(text) & GLOB_DETECTION_CHARS)
283

    
284

    
285
def _MakeFilterPart(namefield, text):
286
  """Generates filter for one argument.
287

288
  """
289
  if _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):
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
  @rtype: list
307
  @return: Query filter
308

309
  """
310
  if (force_filter or
311
      (args and len(args) == 1 and _CheckFilter(args[0]))):
312
    try:
313
      (filter_text, ) = args
314
    except (TypeError, ValueError):
315
      raise errors.OpPrereqError("Exactly one argument must be given as a"
316
                                 " filter")
317

    
318
    result = ParseFilter(filter_text)
319
  elif args:
320
    result = [OP_OR] + map(compat.partial(_MakeFilterPart, "name"), args)
321
  else:
322
    result = None
323

    
324
  return result