Revision f8638e28

b/lib/cli.py
2661 2661
  if not names:
2662 2662
    names = None
2663 2663

  
2664
  if (force_filter or
2665
      (names and len(names) == 1 and qlang.MaybeFilter(names[0]))):
2666
    try:
2667
      (filter_text, ) = names
2668
    except ValueError:
2669
      raise errors.OpPrereqError("Exactly one argument must be given as a"
2670
                                 " filter")
2671

  
2672
    logging.debug("Parsing '%s' as filter", filter_text)
2673
    filter_ = qlang.ParseFilter(filter_text)
2674
  else:
2675
    filter_ = qlang.MakeSimpleFilter("name", names)
2664
  filter_ = qlang.MakeFilter(names, force_filter)
2676 2665

  
2677 2666
  response = cl.Query(resource, fields, filter_)
2678 2667

  
b/lib/qlang.py
33 33

  
34 34
import re
35 35
import string # pylint: disable-msg=W0402
36
import logging
36 37

  
37 38
import pyparsing as pyp
38 39

  
39 40
from ganeti import errors
40 41
from ganeti import netutils
41 42
from ganeti import utils
43
from ganeti import compat
42 44

  
43 45

  
44 46
# Logic operators with one or more operands, each of which is a filter on its
......
61 63

  
62 64

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

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

  
66 71

  
67 72
def MakeSimpleFilter(namefield, values):
......
233 238
  @rtype: list
234 239

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

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

  
......
243 250
                                       " '%s': %s" % (text, err), err)
244 251

  
245 252

  
246
def MaybeFilter(text):
247
  """Try to determine if a string is a filter or a name.
253
def _IsHostname(text):
254
  """Checks if a string could be a hostname.
248 255

  
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 256
  @rtype: bool
254 257

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

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

  
266
  # Most probably a name
267
  return False
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
b/test/ganeti.qlang_unittest.py
54 54
    self.parser = qlang.BuildFilterParser()
55 55

  
56 56
  def _Test(self, filter_, expected, expect_filter=True):
57
    if expect_filter:
58
      self.assertTrue(qlang.MaybeFilter(filter_),
59
                      msg="'%s' was not recognized as a filter" % filter_)
60
    else:
61
      self.assertFalse(qlang.MaybeFilter(filter_),
62
                       msg=("'%s' should not be recognized as a filter" %
63
                            filter_))
57
    self.assertEqual(qlang.MakeFilter([filter_], not expect_filter), expected)
64 58
    self.assertEqual(qlang.ParseFilter(filter_, parser=self.parser), expected)
65 59

  
66 60
  def test(self):
......
182 176
        self.fail("Invalid filter '%s' did not raise exception" % filter_)
183 177

  
184 178

  
185
class TestMaybeFilter(unittest.TestCase):
186
  def test(self):
187
    self.assertTrue(qlang.MaybeFilter(""))
188
    self.assertTrue(qlang.MaybeFilter("foo/bar"))
189
    self.assertTrue(qlang.MaybeFilter("foo==bar"))
190

  
191
    for i in set("()!~" + string.whitespace) | qlang.FILTER_DETECTION_CHARS:
192
      self.assertTrue(qlang.MaybeFilter(i),
193
                      msg="%r not recognized as filter" % i)
194

  
195
    self.assertFalse(qlang.MaybeFilter("node1"))
196
    self.assertFalse(qlang.MaybeFilter("n-o-d-e"))
197
    self.assertFalse(qlang.MaybeFilter("n_o_d_e"))
198
    self.assertFalse(qlang.MaybeFilter("node1.example.com"))
199
    self.assertFalse(qlang.MaybeFilter("node1.example.com."))
179
class TestMakeFilter(unittest.TestCase):
180
  def testNoNames(self):
181
    self.assertEqual(qlang.MakeFilter([], False), None)
182
    self.assertEqual(qlang.MakeFilter(None, False), None)
183

  
184
  def testPlainNames(self):
185
    self.assertEqual(qlang.MakeFilter(["web1", "web2"], False),
186
                     [qlang.OP_OR, [qlang.OP_EQUAL, "name", "web1"],
187
                                   [qlang.OP_EQUAL, "name", "web2"]])
188

  
189
  def testForcedFilter(self):
190
    for i in [None, [], ["1", "2"], ["", "", ""], ["a", "b", "c", "d"]]:
191
      self.assertRaises(errors.OpPrereqError, qlang.MakeFilter, i, True)
192

  
193
    # Glob pattern shouldn't parse as filter
194
    self.assertRaises(errors.QueryFilterParseError,
195
                      qlang.MakeFilter, ["*.site"], True)
196

  
197
    # Plain name parses as boolean filter
198
    self.assertEqual(qlang.MakeFilter(["web1"], True), [qlang.OP_TRUE, "web1"])
199

  
200
  def testFilter(self):
201
    self.assertEqual(qlang.MakeFilter(["foo/bar"], False),
202
                     [qlang.OP_TRUE, "foo/bar"])
203
    self.assertEqual(qlang.MakeFilter(["foo=='bar'"], False),
204
                     [qlang.OP_EQUAL, "foo", "bar"])
205
    self.assertEqual(qlang.MakeFilter(["field=*'*.site'"], False),
206
                     [qlang.OP_REGEXP, "field",
207
                      utils.DnsNameGlobPattern("*.site")])
208

  
209
    # Plain name parses as name filter, not boolean
210
    for name in ["node1", "n-o-d-e", "n_o_d_e", "node1.example.com",
211
                 "node1.example.com."]:
212
      self.assertEqual(qlang.MakeFilter([name], False),
213
                       [qlang.OP_OR, [qlang.OP_EQUAL, "name", name]])
214

  
215
    # Invalid filters
216
    for i in ["foo==bar", "foo+=1"]:
217
      self.assertRaises(errors.QueryFilterParseError,
218
                        qlang.MakeFilter, [i], False)
219

  
220
  def testGlob(self):
221
    self.assertEqual(qlang.MakeFilter(["*.site"], False),
222
                     [qlang.OP_OR, [qlang.OP_REGEXP, "name",
223
                                    utils.DnsNameGlobPattern("*.site")]])
224
    self.assertEqual(qlang.MakeFilter(["web?.example"], False),
225
                     [qlang.OP_OR, [qlang.OP_REGEXP, "name",
226
                                    utils.DnsNameGlobPattern("web?.example")]])
227
    self.assertEqual(qlang.MakeFilter(["*.a", "*.b", "?.c"], False),
228
                     [qlang.OP_OR,
229
                      [qlang.OP_REGEXP, "name",
230
                       utils.DnsNameGlobPattern("*.a")],
231
                      [qlang.OP_REGEXP, "name",
232
                       utils.DnsNameGlobPattern("*.b")],
233
                      [qlang.OP_REGEXP, "name",
234
                       utils.DnsNameGlobPattern("?.c")]])
200 235

  
201 236

  
202 237
if __name__ == "__main__":

Also available in: Unified diff