Revision 7578ab0a lib/qlang.py

b/lib/qlang.py
31 31

  
32 32
"""
33 33

  
34
import re
35

  
36
import pyparsing as pyp
37

  
38
from ganeti import errors
39

  
40

  
34 41
# Logic operators with one or more operands, each of which is a filter on its
35 42
# own
36 43
OP_OR = "|"
......
61 68
    return [OP_OR] + [[OP_EQUAL, namefield, i] for i in values]
62 69

  
63 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)

Also available in: Unified diff