Commit f8638e28 authored by Michael Hanselmann's avatar Michael Hanselmann
Browse files

Detect globbing patterns as query arguments

Short: this patch enables the use of “gnt-instance list '*.site'”.

Detailed description: This patch changes the command line interface code
to try to deduce the kind of filter from the arguments to a “list”
command. If it's a list of plain names an old-style name filter is used.
If filtering is forced or the single argument is potentially a filter,
it is parsed as a query filter string. Any name looking like a globbing
pattern (e.g. “*.site” or “web?”) is treated as such.
Signed-off-by: default avatarMichael Hanselmann <>
Reviewed-by: default avatarIustin Pop <>
parent 1fcd3b81
......@@ -2661,18 +2661,7 @@ def GenericList(resource, fields, names, unit, separator, header, cl=None,
if not names:
names = None
if (force_filter or
(names and len(names) == 1 and qlang.MaybeFilter(names[0]))):
(filter_text, ) = names
except ValueError:
raise errors.OpPrereqError("Exactly one argument must be given as a"
" filter")
logging.debug("Parsing '%s' as filter", filter_text)
filter_ = qlang.ParseFilter(filter_text)
filter_ = qlang.MakeSimpleFilter("name", names)
filter_ = qlang.MakeFilter(names, force_filter)
response = cl.Query(resource, fields, filter_)
......@@ -33,12 +33,14 @@ converted to callable functions by L{query._CompileFilter}.
import re
import string # pylint: disable-msg=W0402
import logging
import pyparsing as pyp
from ganeti import errors
from ganeti import netutils
from ganeti import utils
from ganeti import compat
# Logic operators with one or more operands, each of which is a filter on its
......@@ -61,7 +63,10 @@ OP_CONTAINS = "=[]"
#: Characters used for detecting user-written filters (see L{MaybeFilter})
FILTER_DETECTION_CHARS = frozenset("()=/!~" + string.whitespace)
FILTER_DETECTION_CHARS = frozenset("()=/!~'\"\\" + string.whitespace)
#: Characters used to detect globbing filters (see L{MaybeGlobbing})
GLOB_DETECTION_CHARS = frozenset("*?")
def MakeSimpleFilter(namefield, values):
......@@ -233,6 +238,8 @@ def ParseFilter(text, parser=None):
@rtype: list
logging.debug("Parsing as query filter: %s", text)
if parser is None:
parser = BuildFilterParser()
......@@ -243,25 +250,75 @@ def ParseFilter(text, parser=None):
" '%s': %s" % (text, err), err)
def MaybeFilter(text):
"""Try to determine if a string is a filter or a name.
def _IsHostname(text):
"""Checks if a string could be a hostname.
If in doubt, this function treats a text as a name.
@type text: string
@param text: String to be examined
@rtype: bool
# Quick check for punctuation and whitespace
if frozenset(text) & FILTER_DETECTION_CHARS:
return True
except errors.OpPrereqError:
# Not a valid hostname, treat as filter
return False
return True
# Most probably a name
return False
def _CheckFilter(text):
"""CHecks if a string could be a filter.
@rtype: bool
return bool(frozenset(text) & FILTER_DETECTION_CHARS)
def _CheckGlobbing(text):
"""Checks if a string could be a globbing pattern.
@rtype: bool
return bool(frozenset(text) & GLOB_DETECTION_CHARS)
def _MakeFilterPart(namefield, text):
"""Generates filter for one argument.
if _CheckGlobbing(text):
return [OP_REGEXP, namefield, utils.DnsNameGlobPattern(text)]
return [OP_EQUAL, namefield, text]
def MakeFilter(args, force_filter):
"""Try to make a filter from arguments to a command.
If the name could be a filter it is parsed as such. If it's just a globbing
pattern, e.g. "*.site", such a filter is constructed. As a last resort the
names are treated just as a plain name filter.
@type args: list of string
@param args: Arguments to command
@type force_filter: bool
@param force_filter: Whether to force treatment as a full-fledged filter
@rtype: list
@return: Query filter
if (force_filter or
(args and len(args) == 1 and _CheckFilter(args[0]))):
(filter_text, ) = args
except (TypeError, ValueError):
raise errors.OpPrereqError("Exactly one argument must be given as a"
" filter")
result = ParseFilter(filter_text)
elif args:
result = [OP_OR] + map(compat.partial(_MakeFilterPart, "name"), args)
result = None
return result
......@@ -54,13 +54,7 @@ class TestParseFilter(unittest.TestCase):
self.parser = qlang.BuildFilterParser()
def _Test(self, filter_, expected, expect_filter=True):
if expect_filter:
msg="'%s' was not recognized as a filter" % filter_)
msg=("'%s' should not be recognized as a filter" %
self.assertEqual(qlang.MakeFilter([filter_], not expect_filter), expected)
self.assertEqual(qlang.ParseFilter(filter_, parser=self.parser), expected)
def test(self):
......@@ -182,21 +176,62 @@ class TestParseFilter(unittest.TestCase):"Invalid filter '%s' did not raise exception" % filter_)
class TestMaybeFilter(unittest.TestCase):
def test(self):
for i in set("()!~" + string.whitespace) | qlang.FILTER_DETECTION_CHARS:
msg="%r not recognized as filter" % i)
class TestMakeFilter(unittest.TestCase):
def testNoNames(self):
self.assertEqual(qlang.MakeFilter([], False), None)
self.assertEqual(qlang.MakeFilter(None, False), None)
def testPlainNames(self):
self.assertEqual(qlang.MakeFilter(["web1", "web2"], False),
[qlang.OP_OR, [qlang.OP_EQUAL, "name", "web1"],
[qlang.OP_EQUAL, "name", "web2"]])
def testForcedFilter(self):
for i in [None, [], ["1", "2"], ["", "", ""], ["a", "b", "c", "d"]]:
self.assertRaises(errors.OpPrereqError, qlang.MakeFilter, i, True)
# Glob pattern shouldn't parse as filter
qlang.MakeFilter, ["*.site"], True)
# Plain name parses as boolean filter
self.assertEqual(qlang.MakeFilter(["web1"], True), [qlang.OP_TRUE, "web1"])
def testFilter(self):
self.assertEqual(qlang.MakeFilter(["foo/bar"], False),
[qlang.OP_TRUE, "foo/bar"])
self.assertEqual(qlang.MakeFilter(["foo=='bar'"], False),
[qlang.OP_EQUAL, "foo", "bar"])
self.assertEqual(qlang.MakeFilter(["field=*'*.site'"], False),
[qlang.OP_REGEXP, "field",
# Plain name parses as name filter, not boolean
for name in ["node1", "n-o-d-e", "n_o_d_e", "",
self.assertEqual(qlang.MakeFilter([name], False),
[qlang.OP_OR, [qlang.OP_EQUAL, "name", name]])
# Invalid filters
for i in ["foo==bar", "foo+=1"]:
qlang.MakeFilter, [i], False)
def testGlob(self):
self.assertEqual(qlang.MakeFilter(["*.site"], False),
[qlang.OP_OR, [qlang.OP_REGEXP, "name",
self.assertEqual(qlang.MakeFilter(["web?.example"], False),
[qlang.OP_OR, [qlang.OP_REGEXP, "name",
self.assertEqual(qlang.MakeFilter(["*.a", "*.b", "?.c"], False),
[qlang.OP_REGEXP, "name",
[qlang.OP_REGEXP, "name",
[qlang.OP_REGEXP, "name",
if __name__ == "__main__":
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment