From 763916231f40a4ec3f9e724c8245896dfefd3849 Mon Sep 17 00:00:00 2001
From: Jeremy Stanley <fungi@yuggoth.org>
Date: Tue, 17 Nov 2020 17:59:11 +0000
Subject: [PATCH] validate-host: Options to require v4 and v6 routes

Make it possible for a site to demand that the validate-host role
finds IPv4 and/or IPv6 routes, making one or both explicitly
mandatory, instead of the default behavior of succeeding as long as
at least one is available. This allows a site to, for example,
discard nodes during a pre playbook if they lack IPv4 connectivity.

Change-Id: Icaa82212468a659a3756ed51cac442de33065b55
---
 roles/validate-host/README.rst                | 14 +++++++++++
 .../validate-host/library/zuul_debug_info.py  | 25 +++++++++++++++----
 roles/validate-host/tasks/main.yaml           |  2 ++
 3 files changed, 36 insertions(+), 5 deletions(-)

diff --git a/roles/validate-host/README.rst b/roles/validate-host/README.rst
index fcca72fe5..88edee30e 100644
--- a/roles/validate-host/README.rst
+++ b/roles/validate-host/README.rst
@@ -2,6 +2,20 @@ Log information about the build node
 
 **Role Variables**
 
+.. zuul:rolevar:: zuul_site_ipv4_route_required
+   :default: false
+
+   If true, fail when no IPv4 route to ``zuul_site_traceroute_host`` is
+   available. When false (default) a missing IPv4 route is acceptable
+   so long as there is still a viable IPv6 route.
+
+.. zuul:rolevar:: zuul_site_ipv6_route_required
+   :default: false
+
+   If true, fail when no IPv6 route to ``zuul_site_traceroute_host`` is
+   available. When false (default) a missing IPv6 route is acceptable
+   so long as there is still a viable IPv4 route.
+
 .. zuul:rolevar:: zuul_site_traceroute_host
 
    If defined, a host to run a traceroute against to verify build node
diff --git a/roles/validate-host/library/zuul_debug_info.py b/roles/validate-host/library/zuul_debug_info.py
index ce3be447f..65f57209e 100644
--- a/roles/validate-host/library/zuul_debug_info.py
+++ b/roles/validate-host/library/zuul_debug_info.py
@@ -42,12 +42,16 @@ def run_command(command):
 def main():
     module = AnsibleModule(
         argument_spec=dict(
+            ipv4_route_required=dict(required=False, type='bool'),
+            ipv6_route_required=dict(required=False, type='bool'),
             image_manifest=dict(required=False, type='str'),
             image_manifest_files=dict(required=False, type='list'),
             traceroute_host=dict(required=False, type='str'),
         )
     )
 
+    ipv4_route_required = module.params['ipv4_route_required']
+    ipv6_route_required = module.params['ipv6_route_required']
     image_manifest = module.params['image_manifest']
     traceroute_host = module.params['traceroute_host']
     image_manifest_files = module.params['image_manifest_files']
@@ -64,29 +68,40 @@ def main():
                 'content': open(image_manifest, 'r').read(),
             })
     if traceroute_host:
-        passed = False
+        v6_passed = False
         try:
             ret['traceroute_v6'] = run_command(
                 'traceroute6 -n {host}'.format(host=traceroute_host))
-            passed = True
+            v6_passed = True
         except (subprocess.CalledProcessError, OSError) as e:
             ret['traceroute_v6_exception'] = traceback.format_exc()
             ret['traceroute_v6_output'] = e.output
             ret['traceroute_v6_return'] = e.returncode
             pass
+        v4_passed = False
         try:
             ret['traceroute_v4'] = run_command(
                 'traceroute -n {host}'.format(host=traceroute_host))
-            passed = True
+            v4_passed = True
         except (subprocess.CalledProcessError, OSError) as e:
             ret['traceroute_v4_exception'] = traceback.format_exc()
             ret['traceroute_v4_output'] = e.output
             ret['traceroute_v4_return'] = e.returncode
             pass
+        if v6_passed or v4_passed:
+            # By default, only require one IP family to have a working route,
+            # either version will suffice
+            passed = True
+        if ipv6_route_required and not v6_passed:
+            # Override the result if IPv6 is explicitly required
+            passed = False
+        if ipv4_route_required and not v4_passed:
+            # Override the result if IPv4 is explicitly required
+            passed = False
         if not passed:
             module.fail_json(
-                msg="No viable v4 or v6 route found to {traceroute_host}."
-                    " The build node is assumed to be invalid.".format(
+                msg="The required v4 or v6 route to {traceroute_host} was not"
+                    " found. The build node is assumed to be invalid.".format(
                         traceroute_host=traceroute_host), **ret)
 
     for key, command in command_map.items():
diff --git a/roles/validate-host/tasks/main.yaml b/roles/validate-host/tasks/main.yaml
index 24a5dfef3..78b288bac 100644
--- a/roles/validate-host/tasks/main.yaml
+++ b/roles/validate-host/tasks/main.yaml
@@ -29,6 +29,8 @@
   block:
     - name: Collect information about zuul worker
       zuul_debug_info:
+        ipv4_route_required: "{{ zuul_site_ipv4_route_required|default(false) }}"
+        ipv6_route_required: "{{ zuul_site_ipv6_route_required|default(false) }}"
         image_manifest: "{{ zuul_site_image_manifest|default(omit) }}"
         image_manifest_files: "{{ zuul_site_image_manifest_files|default(omit) }}"
         traceroute_host: "{{ zuul_site_traceroute_host|default(omit) }}"