From d19b35d668184fdac00a8ef56ff58c0748f22816 Mon Sep 17 00:00:00 2001
From: John Estabrook <jestabro@vyos.io>
Date: Thu, 12 Aug 2021 11:09:58 -0500
Subject: T3574: add constraintGroup for combining validators with logical AND

(cherry picked from commit 591b8bcadd8b6bbd46c61484193d2bf7e16bd1ae
                    commit 31553283aaa929da63147082e85513e8d4dacf0e
                    commit 59a4aadfe419eca16e6288b37d6c51acd9789903)
---
 python/vyos/xml/load.py         |  3 ++
 schema/interface_definition.rnc | 11 ++++++
 schema/interface_definition.rng | 21 ++++++++++++
 scripts/build-command-templates | 76 +++++++++++++++++++++++++----------------
 4 files changed, 81 insertions(+), 30 deletions(-)

diff --git a/python/vyos/xml/load.py b/python/vyos/xml/load.py
index 0965d4220..37479c6e1 100644
--- a/python/vyos/xml/load.py
+++ b/python/vyos/xml/load.py
@@ -225,6 +225,9 @@ def _format_node(inside, conf, xml):
                         else:
                             _fatal(constraint)
 
+                elif 'constraintGroup' in properties:
+                    properties.pop('constraintGroup')
+
                 elif 'constraintErrorMessage' in properties:
                     r[kw.error] = properties.pop('constraintErrorMessage')
 
diff --git a/schema/interface_definition.rnc b/schema/interface_definition.rnc
index 6647f5e11..d7fc4966c 100644
--- a/schema/interface_definition.rnc
+++ b/schema/interface_definition.rnc
@@ -93,6 +93,7 @@ properties = element properties
 {
     help? &
     constraint? &
+    constraintGroup* &
     valueHelp* &
     (element constraintErrorMessage { text })? &
     completionHelp* &
@@ -140,6 +141,16 @@ constraint = element constraint
       validator )+
 }
 
+# Tag and leaf nodes may have constraintGroups on their names and
+# values (respectively).
+# When multiple constraints are listed within a group, they work as
+# logical AND
+constraintGroup = element constraintGroup
+{
+    ( (element regex { text }) |
+      validator )+
+}
+
 # A constraint may also use an external validator rather than regex
 validator = element validator
 {
diff --git a/schema/interface_definition.rng b/schema/interface_definition.rng
index 22e886006..3ff60cf18 100644
--- a/schema/interface_definition.rng
+++ b/schema/interface_definition.rng
@@ -160,6 +160,9 @@
         <optional>
           <ref name="constraint"/>
         </optional>
+        <zeroOrMore>
+          <ref name="constraintGroup"/>
+        </zeroOrMore>
         <zeroOrMore>
           <ref name="valueHelp"/>
         </zeroOrMore>
@@ -244,6 +247,24 @@
       </oneOrMore>
     </element>
   </define>
+  <!--
+    Tag and leaf nodes may have constraintGroups on their names and
+    values (respectively).
+    When multiple constraints are listed within a group, they work as
+    logical AND
+  -->
+  <define name="constraintGroup">
+    <element name="constraintGroup">
+      <oneOrMore>
+        <choice>
+          <element name="regex">
+            <text/>
+          </element>
+          <ref name="validator"/>
+        </choice>
+      </oneOrMore>
+    </element>
+  </define>
   <!-- A constraint may also use an external validator rather than regex -->
   <define name="validator">
     <element name="validator">
diff --git a/scripts/build-command-templates b/scripts/build-command-templates
index 452c420eb..d8abb0a13 100755
--- a/scripts/build-command-templates
+++ b/scripts/build-command-templates
@@ -86,6 +86,37 @@ def make_path(l):
         print(path)
     return path
 
+def collect_validators(ve):
+    regexes = []
+    regex_elements = ve.findall("regex")
+    if regex_elements is not None:
+        regexes = list(map(lambda e: e.text.strip().replace('\\','\\\\'), regex_elements))
+    if "" in regexes:
+        print("Warning: empty regex, node will be accepting any value")
+
+    validator_elements = ve.findall("validator")
+    validators = []
+    if validator_elements is not None:
+        for v in validator_elements:
+            v_name = os.path.join(validator_dir, v.get("name"))
+
+            # XXX: lxml returns None for empty arguments
+            v_argument = None
+            try:
+                v_argument = v.get("argument")
+            except:
+                pass
+            if v_argument is None:
+                v_argument = ""
+
+            validators.append("{0} {1}".format(v_name, v_argument))
+
+
+    regex_args = " ".join(map(lambda s: "--regex \\\'{0}\\\'".format(s), regexes))
+    validator_args = " ".join(map(lambda s: "--exec \\\"{0}\\\"".format(s), validators))
+
+    return regex_args + " " + validator_args
+
 def get_properties(p):
     props = {}
 
@@ -108,7 +139,8 @@ def get_properties(p):
     except:
         props["val_help"] = []
 
-    # Get the constraint statements
+    # Get the constraint and constraintGroup statements
+
     error_msg = default_constraint_err_msg
     # Get the error message if it's there
     try:
@@ -117,40 +149,24 @@ def get_properties(p):
         pass
 
     vce = p.find("constraint")
-    vc = []
+
+    distinct_validator_string = ""
     if vce is not None:
         # The old backend doesn't support multiple validators in OR mode
         # so we emulate it
 
-        regexes = []
-        regex_elements = vce.findall("regex")
-        if regex_elements is not None:
-            regexes = list(map(lambda e: e.text.strip().replace('\\','\\\\'), regex_elements))
-        if "" in regexes:
-            print("Warning: empty regex, node will be accepting any value")
-
-        validator_elements = vce.findall("validator")
-        validators = []
-        if validator_elements is not None:
-            for v in validator_elements:
-                v_name = os.path.join(validator_dir, v.get("name"))
-
-                # XXX: lxml returns None for empty arguments
-                v_argument = None
-                try:
-                    v_argument = v.get("argument")
-                except:
-                    pass
-                if v_argument is None:
-                    v_argument = ""
-
-                validators.append("{0} {1}".format(v_name, v_argument))
-
-
-        regex_args = " ".join(map(lambda s: "--regex \\\'{0}\\\'".format(s), regexes))
-        validator_args = " ".join(map(lambda s: "--exec \\\"{0}\\\"".format(s), validators))
+        distinct_validator_string = collect_validators(vce)
+
+    vcge = p.findall("constraintGroup")
+
+    group_validator_string = ""
+    if len(vcge):
+        for vcg in vcge:
+            group_validator_string = group_validator_string + " --grp " + collect_validators(vcg)
+
+    if vce is not None or len(vcge):
         validator_script = '${vyos_libexec_dir}/validate-value'
-        validator_string = "exec \"{0} {1} {2} --value \\\'$VAR(@)\\\'\"; \"{3}\"".format(validator_script, regex_args, validator_args, error_msg)
+        validator_string = "exec \"{0} {1} {2} --value \\\'$VAR(@)\\\'\"; \"{3}\"".format(validator_script, distinct_validator_string, group_validator_string, error_msg)
 
         props["constraint"] = validator_string
 
-- 
cgit v1.2.3