Statistics
| Branch: | Tag: | Revision:

root / lib / qlang.py @ 3f2f55bb

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

    
42

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

    
48

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

    
53

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

    
61

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

    
65

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

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

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

    
76
  return None
77

    
78

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

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

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

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

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

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

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

    
101
  return fn
102

    
103

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

    
107

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

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

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

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

    
125
  re_cond = re_flags + regexp
126

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

    
133
  return [re_cond]
134

    
135

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

139
  @rtype: pyparsing.ParserElement
140

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

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

    
149
  # Right-hand-side value
150
  rval = (number | pyp.quotedString.setParseAction(pyp.removeQuotes))
151

    
152
  # Boolean condition
153
  bool_cond = field_name.copy()
154
  bool_cond.setParseAction(lambda (fname, ): [[OP_TRUE, fname]])
155

    
156
  # Simple binary conditions
157
  binopstbl = {
158
    "==": OP_EQUAL,
159
    "!=": OP_NOT_EQUAL,
160
    }
161

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

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

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

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

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

    
187
  # All possible conditions
188
  condition = (binary_cond ^ bool_cond ^
189
               in_cond ^ not_in_cond ^
190
               regexp_cond ^ not_regexp_cond)
191

    
192
  # Associativity operators
193
  filter_expr = pyp.operatorPrecedence(condition, [
194
    (pyp.Keyword("not").suppress(), 1, pyp.opAssoc.RIGHT,
195
     lambda toks: [[OP_NOT, toks[0][0]]]),
196
    (pyp.Keyword("and").suppress(), 2, pyp.opAssoc.LEFT,
197
     _ConvertLogicOp(OP_AND)),
198
    (pyp.Keyword("or").suppress(), 2, pyp.opAssoc.LEFT,
199
     _ConvertLogicOp(OP_OR)),
200
    ])
201

    
202
  parser = pyp.StringStart() + filter_expr + pyp.StringEnd()
203
  parser.parseWithTabs()
204

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

    
208
  return parser
209

    
210

    
211
def ParseFilter(text, parser=None):
212
  """Parses a query filter.
213

214
  @type text: string
215
  @param text: Query filter
216
  @type parser: pyparsing.ParserElement
217
  @param parser: Pyparsing object
218
  @rtype: list
219

220
  """
221
  if parser is None:
222
    parser = BuildFilterParser()
223

    
224
  try:
225
    return parser.parseString(text)[0]
226
  except pyp.ParseBaseException, err:
227
    raise errors.QueryFilterParseError("Failed to parse query filter"
228
                                       " '%s': %s" % (text, err), err)
229

    
230

    
231
def MaybeFilter(text):
232
  """Try to determine if a string is a filter or a name.
233

234
  If in doubt, this function treats a text as a name.
235

236
  @type text: string
237
  @param text: String to be examined
238
  @rtype: bool
239

240
  """
241
  # Quick check for punctuation and whitespace
242
  if frozenset(text) & FILTER_DETECTION_CHARS:
243
    return True
244

    
245
  try:
246
    netutils.Hostname.GetNormalizedName(text)
247
  except errors.OpPrereqError:
248
    # Not a valid hostname, treat as filter
249
    return True
250

    
251
  # Most probably a name
252
  return False