Statistics
| Branch: | Tag: | Revision:

root / lib / qlang.py @ 16629d10

History | View | Annotate | Download (7.6 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-msg=W0402
36

    
37
import pyparsing as pyp
38

    
39
from ganeti import errors
40
from ganeti import netutils
41
from ganeti import utils
42

    
43

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

    
49

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

    
54

    
55
# Binary operators with exactly two operands, the field name and an
56
# operator-specific value
57
OP_EQUAL = "="
58
OP_NOT_EQUAL = "!="
59
OP_REGEXP = "=~"
60
OP_CONTAINS = "=[]"
61

    
62

    
63
#: Characters used for detecting user-written filters (see L{MaybeFilter})
64
FILTER_DETECTION_CHARS = frozenset("()=/!~" + string.whitespace)
65

    
66

    
67
def MakeSimpleFilter(namefield, values):
68
  """Builds simple a filter.
69

70
  @param namefield: Name of field containing item name
71
  @param values: List of names
72

73
  """
74
  if values:
75
    return [OP_OR] + [[OP_EQUAL, namefield, i] for i in values]
76

    
77
  return None
78

    
79

    
80
def _ConvertLogicOp(op):
81
  """Creates parsing action function for logic operator.
82

83
  @type op: string
84
  @param op: Operator for data structure, e.g. L{OP_AND}
85

86
  """
87
  def fn(toks):
88
    """Converts parser tokens to query operator structure.
89

90
    @rtype: list
91
    @return: Query operator structure, e.g. C{[OP_AND, ["=", "foo", "bar"]]}
92

93
    """
94
    operands = toks[0]
95

    
96
    if len(operands) == 1:
97
      return operands[0]
98

    
99
    # Build query operator structure
100
    return [[op] + operands.asList()]
101

    
102
  return fn
103

    
104

    
105
_KNOWN_REGEXP_DELIM = "/#^|"
106
_KNOWN_REGEXP_FLAGS = frozenset("si")
107

    
108

    
109
def _ConvertRegexpValue(_, loc, toks):
110
  """Regular expression value for condition.
111

112
  """
113
  (regexp, flags) = toks[0]
114

    
115
  # Ensure only whitelisted flags are used
116
  unknown_flags = (frozenset(flags) - _KNOWN_REGEXP_FLAGS)
117
  if unknown_flags:
118
    raise pyp.ParseFatalException("Unknown regular expression flags: '%s'" %
119
                                  "".join(unknown_flags), loc)
120

    
121
  if flags:
122
    re_flags = "(?%s)" % "".join(sorted(flags))
123
  else:
124
    re_flags = ""
125

    
126
  re_cond = re_flags + regexp
127

    
128
  # Test if valid
129
  try:
130
    re.compile(re_cond)
131
  except re.error, err:
132
    raise pyp.ParseFatalException("Invalid regular expression (%s)" % err, loc)
133

    
134
  return [re_cond]
135

    
136

    
137
def BuildFilterParser():
138
  """Builds a parser for query filter strings.
139

140
  @rtype: pyparsing.ParserElement
141

142
  """
143
  field_name = pyp.Word(pyp.alphas, pyp.alphanums + "_/.")
144

    
145
  # Integer
146
  num_sign = pyp.Word("-+", exact=1)
147
  number = pyp.Combine(pyp.Optional(num_sign) + pyp.Word(pyp.nums))
148
  number.setParseAction(lambda toks: int(toks[0]))
149

    
150
  quoted_string = pyp.quotedString.copy().setParseAction(pyp.removeQuotes)
151

    
152
  # Right-hand-side value
153
  rval = (number | quoted_string)
154

    
155
  # Boolean condition
156
  bool_cond = field_name.copy()
157
  bool_cond.setParseAction(lambda (fname, ): [[OP_TRUE, fname]])
158

    
159
  # Simple binary conditions
160
  binopstbl = {
161
    "==": OP_EQUAL,
162
    "!=": OP_NOT_EQUAL,
163
    }
164

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

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

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

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

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

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

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

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

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

    
217
  parser = pyp.StringStart() + filter_expr + pyp.StringEnd()
218
  parser.parseWithTabs()
219

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

    
223
  return parser
224

    
225

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

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

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 MaybeFilter(text):
247
  """Try to determine if a string is a filter or a name.
248

249
  If in doubt, this function treats a text as a name.
250

251
  @type text: string
252
  @param text: String to be examined
253
  @rtype: bool
254

255
  """
256
  # Quick check for punctuation and whitespace
257
  if frozenset(text) & FILTER_DETECTION_CHARS:
258
    return True
259

    
260
  try:
261
    netutils.Hostname.GetNormalizedName(text)
262
  except errors.OpPrereqError:
263
    # Not a valid hostname, treat as filter
264
    return True
265

    
266
  # Most probably a name
267
  return False