Statistics
| Branch: | Tag: | Revision:

root / lib / qlang.py @ 7578ab0a

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

    
36
import pyparsing as pyp
37

    
38
from ganeti import errors
39

    
40

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

    
46

    
47
# Unary operators with exactly one operand
48
OP_NOT = "!"
49
OP_TRUE = "?"
50

    
51

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

    
59

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

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

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

    
70
  return None
71

    
72

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

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

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

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

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

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

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

    
95
  return fn
96

    
97

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

    
101

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

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

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

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

    
119
  re_cond = re_flags + regexp
120

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

    
127
  return [re_cond]
128

    
129

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

133
  @rtype: pyparsing.ParserElement
134

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

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

    
143
  # Right-hand-side value
144
  rval = (number | pyp.quotedString.setParseAction(pyp.removeQuotes))
145

    
146
  # Boolean condition
147
  bool_cond = field_name.copy()
148
  bool_cond.setParseAction(lambda (fname, ): [[OP_TRUE, fname]])
149

    
150
  # Simple binary conditions
151
  binopstbl = {
152
    "==": OP_EQUAL,
153
    "!=": OP_NOT_EQUAL,
154
    }
155

    
156
  binary_cond = (field_name + pyp.oneOf(binopstbl.keys()) + rval)
157
  binary_cond.setParseAction(lambda (lhs, op, rhs): [[binopstbl[op], lhs, rhs]])
158

    
159
  # "in" condition
160
  in_cond = (rval + pyp.Suppress("in") + field_name)
161
  in_cond.setParseAction(lambda (value, field): [[OP_CONTAINS, field, value]])
162

    
163
  # "not in" condition
164
  not_in_cond = (rval + pyp.Suppress("not") + pyp.Suppress("in") + field_name)
165
  not_in_cond.setParseAction(lambda (value, field): [[OP_NOT, [OP_CONTAINS,
166
                                                               field, value]]])
167

    
168
  # Regular expression, e.g. m/foobar/i
169
  regexp_val = pyp.Group(pyp.Optional("m").suppress() +
170
                         pyp.MatchFirst([pyp.QuotedString(i, escChar="\\")
171
                                         for i in _KNOWN_REGEXP_DELIM]) +
172
                         pyp.Optional(pyp.Word(pyp.alphas), default=""))
173
  regexp_val.setParseAction(_ConvertRegexpValue)
174
  regexp_cond = (field_name + pyp.Suppress("=~") + regexp_val)
175
  regexp_cond.setParseAction(lambda (field, value): [[OP_REGEXP, field, value]])
176

    
177
  not_regexp_cond = (field_name + pyp.Suppress("!~") + regexp_val)
178
  not_regexp_cond.setParseAction(lambda (field, value):
179
                                 [[OP_NOT, [OP_REGEXP, field, value]]])
180

    
181
  # All possible conditions
182
  condition = (binary_cond ^ bool_cond ^
183
               in_cond ^ not_in_cond ^
184
               regexp_cond ^ not_regexp_cond)
185

    
186
  # Associativity operators
187
  filter_expr = pyp.operatorPrecedence(condition, [
188
    (pyp.Keyword("not").suppress(), 1, pyp.opAssoc.RIGHT,
189
     lambda toks: [[OP_NOT, toks[0][0]]]),
190
    (pyp.Keyword("and").suppress(), 2, pyp.opAssoc.LEFT,
191
     _ConvertLogicOp(OP_AND)),
192
    (pyp.Keyword("or").suppress(), 2, pyp.opAssoc.LEFT,
193
     _ConvertLogicOp(OP_OR)),
194
    ])
195

    
196
  parser = pyp.StringStart() + filter_expr + pyp.StringEnd()
197
  parser.parseWithTabs()
198

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

    
202
  return parser
203

    
204

    
205
def ParseFilter(text, parser=None):
206
  """Parses a query filter.
207

208
  @type text: string
209
  @param text: Query filter
210
  @type parser: pyparsing.ParserElement
211
  @param parser: Pyparsing object
212
  @rtype: list
213

214
  """
215
  if parser is None:
216
    parser = BuildFilterParser()
217

    
218
  try:
219
    return parser.parseString(text)[0]
220
  except pyp.ParseBaseException, err:
221
    raise errors.QueryFilterParseError("Failed to parse query filter"
222
                                       " '%s': %s" % (text, err), err)