From 16629d108e574b9d66ba8e2b823ec2351e5311e0 Mon Sep 17 00:00:00 2001
From: Michael Hanselmann <hansmi@google.com>
Date: Fri, 5 Aug 2011 15:38:41 +0200
Subject: [PATCH] Implement globbing operator for filters
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The operators β€œ=*” and β€œ!*” do globbing in filters, e.g.:

$ gnt-instance list --no-headers -o name 'name =* "*.site"'
inst1.site.example.com

Signed-off-by: Michael Hanselmann <hansmi@google.com>
Reviewed-by: Iustin Pop <iustin@google.com>
---
 lib/qlang.py                  | 19 +++++++++++++++++--
 man/ganeti.rst                |  7 +++++++
 test/ganeti.qlang_unittest.py |  6 ++++++
 3 files changed, 30 insertions(+), 2 deletions(-)

diff --git a/lib/qlang.py b/lib/qlang.py
index 5167d773a..0c5169e03 100644
--- a/lib/qlang.py
+++ b/lib/qlang.py
@@ -38,6 +38,7 @@ import pyparsing as pyp
 
 from ganeti import errors
 from ganeti import netutils
+from ganeti import utils
 
 
 # Logic operators with one or more operands, each of which is a filter on its
@@ -146,8 +147,10 @@ def BuildFilterParser():
   number = pyp.Combine(pyp.Optional(num_sign) + pyp.Word(pyp.nums))
   number.setParseAction(lambda toks: int(toks[0]))
 
+  quoted_string = pyp.quotedString.copy().setParseAction(pyp.removeQuotes)
+
   # Right-hand-side value
-  rval = (number | pyp.quotedString.setParseAction(pyp.removeQuotes))
+  rval = (number | quoted_string)
 
   # Boolean condition
   bool_cond = field_name.copy()
@@ -184,10 +187,22 @@ def BuildFilterParser():
   not_regexp_cond.setParseAction(lambda (field, value):
                                  [[OP_NOT, [OP_REGEXP, field, value]]])
 
+  # Globbing, e.g. name =* "*.site"
+  glob_cond = (field_name + pyp.Suppress("=*") + quoted_string)
+  glob_cond.setParseAction(lambda (field, value):
+                           [[OP_REGEXP, field,
+                             utils.DnsNameGlobPattern(value)]])
+
+  not_glob_cond = (field_name + pyp.Suppress("!*") + quoted_string)
+  not_glob_cond.setParseAction(lambda (field, value):
+                               [[OP_NOT, [OP_REGEXP, field,
+                                          utils.DnsNameGlobPattern(value)]]])
+
   # All possible conditions
   condition = (binary_cond ^ bool_cond ^
                in_cond ^ not_in_cond ^
-               regexp_cond ^ not_regexp_cond)
+               regexp_cond ^ not_regexp_cond ^
+               glob_cond ^ not_glob_cond)
 
   # Associativity operators
   filter_expr = pyp.operatorPrecedence(condition, [
diff --git a/man/ganeti.rst b/man/ganeti.rst
index 9b31845d7..2fdcb4481 100644
--- a/man/ganeti.rst
+++ b/man/ganeti.rst
@@ -265,6 +265,9 @@ Syntax in pseudo-BNF::
       */
       | <field> { =~ | !~ } m/<re>/<re-modifiers>
 
+      /* Globbing */
+      | <field> { =* | !* } <quoted-string>
+
       /* Boolean */
       | <field>
     }
@@ -283,6 +286,10 @@ Operators:
   Pattern match using regular expression
 *!~*
   Logically negated from *=~*
+*=\**
+  Globbing, see **glob**(7), though only * and ? are supported
+*!\**
+  Logically negated from *=\**
 *in*, *not in*
   Collection membership and negation
 
diff --git a/test/ganeti.qlang_unittest.py b/test/ganeti.qlang_unittest.py
index 74100d216..ed8f77f93 100755
--- a/test/ganeti.qlang_unittest.py
+++ b/test/ganeti.qlang_unittest.py
@@ -147,6 +147,12 @@ class TestParseFilter(unittest.TestCase):
       self._Test("notname =~ m%stest%s" % (i, i),
                  [qlang.OP_REGEXP, "notname", "test"])
 
+    self._Test("name =* '*.site'",
+               [qlang.OP_REGEXP, "name", utils.DnsNameGlobPattern("*.site")])
+    self._Test("field !* '*.example.*'",
+               [qlang.OP_NOT, [qlang.OP_REGEXP, "field",
+                               utils.DnsNameGlobPattern("*.example.*")]])
+
   def testAllFields(self):
     for name in frozenset(i for d in query.ALL_FIELD_LISTS for i in d.keys()):
       self._Test("%s == \"value\"" % name, [qlang.OP_EQUAL, name, "value"])
-- 
GitLab