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 0000000000000000000000000000000000000000..02da45515e633290df61124d3e8be03a14923e19 --- /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 7e81eafcd82ff3e4f4fb73f9de5678c69184119c..45bd020c95403aeb3a9a618006bdf4ecca0625a6 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 0000000000000000000000000000000000000000..8d4f7be25f25b4548c37e1b6b1ff4cd28bac607f --- /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 + )