From 1692073f2bd93e4dcd19c2502dfe5ddcb066cd5e Mon Sep 17 00:00:00 2001
From: Karel van Klink <karel.vanklink@geant.org>
Date: Thu, 11 Jan 2024 16:04:12 +0100
Subject: [PATCH] Add IP Trunk validation workflow

---
 ...7554c4_add_ip_trunk_validation_workflow.py | 39 +++++++++
 gso/workflows/__init__.py                     |  1 +
 gso/workflows/iptrunk/validate_iptrunk.py     | 81 +++++++++++++++++++
 3 files changed, 121 insertions(+)
 create mode 100644 gso/migrations/versions/2024-01-11_a1a69e7554c4_add_ip_trunk_validation_workflow.py
 create mode 100644 gso/workflows/iptrunk/validate_iptrunk.py

diff --git a/gso/migrations/versions/2024-01-11_a1a69e7554c4_add_ip_trunk_validation_workflow.py b/gso/migrations/versions/2024-01-11_a1a69e7554c4_add_ip_trunk_validation_workflow.py
new file mode 100644
index 00000000..02da4551
--- /dev/null
+++ b/gso/migrations/versions/2024-01-11_a1a69e7554c4_add_ip_trunk_validation_workflow.py
@@ -0,0 +1,39 @@
+"""Add IP Trunk validation workflow.
+
+Revision ID: a1a69e7554c4
+Revises: 75c63ad44cbb
+Create Date: 2024-01-11 15:57:57.534785
+
+"""
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = 'a1a69e7554c4'
+down_revision = '75c63ad44cbb'
+branch_labels = None
+depends_on = None
+
+
+from orchestrator.migrations.helpers import create_workflow, delete_workflow
+
+new_workflows = [
+    {
+        "name": "validate_iptrunk",
+        "target": "SYSTEM",
+        "description": "Validate IP trunk configuration",
+        "product_type": "Iptrunk"
+    }
+]
+
+
+def upgrade() -> None:
+    conn = op.get_bind()
+    for workflow in new_workflows:
+        create_workflow(conn, workflow)
+
+
+def downgrade() -> None:
+    conn = op.get_bind()
+    for workflow in new_workflows:
+        delete_workflow(conn, workflow["name"])
diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py
index 7e81eafc..45bd020c 100644
--- a/gso/workflows/__init__.py
+++ b/gso/workflows/__init__.py
@@ -32,6 +32,7 @@ LazyWorkflowInstance("gso.workflows.iptrunk.migrate_iptrunk", "migrate_iptrunk")
 LazyWorkflowInstance("gso.workflows.iptrunk.terminate_iptrunk", "terminate_iptrunk")
 LazyWorkflowInstance("gso.workflows.iptrunk.create_imported_iptrunk", "create_imported_iptrunk")
 LazyWorkflowInstance("gso.workflows.iptrunk.import_iptrunk", "import_iptrunk")
+LazyWorkflowInstance("gso.workflows.iptrunk.validate_iptrunk", "validate_iptrunk")
 
 #  Router workflows
 LazyWorkflowInstance("gso.workflows.router.activate_router", "activate_router")
diff --git a/gso/workflows/iptrunk/validate_iptrunk.py b/gso/workflows/iptrunk/validate_iptrunk.py
new file mode 100644
index 00000000..8d4f7be2
--- /dev/null
+++ b/gso/workflows/iptrunk/validate_iptrunk.py
@@ -0,0 +1,81 @@
+"""Router validation workflow. Used in a nightly schedule."""
+
+import json
+
+from orchestrator.targets import Target
+from orchestrator.utils.errors import ProcessFailureError
+from orchestrator.utils.json import json_dumps
+from orchestrator.workflow import StepList, done, init, step, workflow
+from orchestrator.workflows.steps import resync, store_process_subscription, unsync
+from orchestrator.workflows.utils import wrap_modify_initial_input_form
+
+from gso.products.product_types.iptrunk import Iptrunk
+from gso.services import infoblox
+from gso.services.provisioning_proxy import execute_playbook, pp_interaction
+
+
+@step("Validate IP trunk configuration")
+def validate_router_config(subscription: Iptrunk, callback_route: str) -> None:
+    """Run an Ansible playbook that validates the configuration that is present on an active IP trunk."""
+    extra_vars = {"wfo_trunk_json": json.loads(json_dumps(subscription)), "verb": "validate"}
+
+    execute_playbook(
+        playbook_name="base_config.yaml",
+        callback_route=callback_route,
+        inventory=f"{subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn}\n"
+        f"{subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn}\n",
+        extra_vars=extra_vars,
+    )
+
+
+@step("Verify IPAM resources for LAG interfaces")
+def verify_ipam_loopback(subscription: Iptrunk) -> None:
+    """Validate the :term:`IPAM` resources for the :term:`LAG` interfaces.
+
+    Raises an :class:`orchestrator.utils.errors.ProcessFailureError` if IPAM is configured incorrectly.
+    """
+    ipam_errors = []
+    ipam_v4_network = infoblox.find_network_by_cidr(subscription.iptrunk.iptrunk_ipv4_network)
+    ipam_v6_network = infoblox.find_network_by_cidr(subscription.iptrunk.iptrunk_ipv6_network)
+
+    if not ipam_v4_network or not ipam_v6_network:
+        ipam_errors += (
+            "Missing IP trunk IPAM records, found the following instead.\n"
+            f"IPv4 expected {subscription.iptrunk.iptrunk_ipv4_network}, actual: {ipam_v4_network}\n"
+            f"IPv6 expected {subscription.iptrunk.iptrunk_ipv6_network}, actual: {ipam_v6_network}"
+        )
+
+    #  Validate both sides of the trunk.
+    for trunk_side in subscription.iptrunk.iptrunk_sides:
+        lag_fqdn = f"{trunk_side.iptrunk_side_ae_iface}.{trunk_side.iptrunk_side_node.router_fqdn}"
+        #  Validate both IPv4 and IPv6 records.
+        for record in [infoblox.find_host_by_fqdn(lag_fqdn), infoblox.find_v6_host_by_fqdn(lag_fqdn)]:
+            if not record:
+                ipam_errors += f"Missing IPAM record for LAG interface {lag_fqdn}."
+            elif subscription.subscription_id not in record.comment:
+                ipam_errors += f"Found a misconfigured IPAM entry for {lag_fqdn}. Received: {record}"
+
+    if ipam_errors:
+        raise ProcessFailureError(str(ipam_errors))
+
+
+@workflow(
+    "Validate IP trunk configuration",
+    target=Target.SYSTEM,
+    initial_input_form=wrap_modify_initial_input_form(None),
+)
+def validate_iptrunk() -> StepList:
+    """Validate an existing, active IP Trunk subscription.
+
+    * Run an Ansible playbook to verify the configuration is intact.
+    * Verify that the LAG interfaces are correctly configured in IPAM.
+    """
+    return (
+        init
+        >> store_process_subscription(Target.SYSTEM)
+        >> unsync
+        >> pp_interaction(validate_router_config)
+        >> verify_ipam_loopback
+        >> resync
+        >> done
+    )
-- 
GitLab