"""Router validation workflow. Used in a nightly schedule.""" import json from typing import Any from orchestrator.targets import Target from orchestrator.utils.errors import ProcessFailureError from orchestrator.utils.json import json_dumps from orchestrator.workflow import StepList, begin, conditional, done, step, workflow from orchestrator.workflows.steps import resync, store_process_subscription, unsync from orchestrator.workflows.utils import wrap_modify_initial_input_form from pydantic_forms.types import State, UUIDstr from gso.products.product_types.router import Router from gso.services import infoblox, lso_client, subscriptions from gso.services.librenms_client import LibreNMSClient from gso.services.lso_client import anonymous_lso_interaction, execute_playbook from gso.services.netbox_client import NetboxClient from gso.utils.shared_enums import Vendor @step("Prepare required keys in state") def prepare_state(subscription_id: UUIDstr) -> State: """Add required keys to the state for the workflow to run successfully.""" router = Router.from_subscription(subscription_id) return {"subscription": router} @step("Verify IPAM resources for loopback interface") def verify_ipam_loopback(subscription: Router) -> None: """Validate the :term:`IPAM` resources for the loopback interface. Raises an :class:`orchestrator.utils.errors.ProcessFailureError` if :term:`IPAM` is configured incorrectly. """ host_record = infoblox.find_host_by_fqdn(f"lo0.{subscription.router.router_fqdn}") if not host_record or str(subscription.subscription_id) not in host_record.comment: msg = "Loopback record is incorrectly configured in IPAM, please investigate this manually!" raise ProcessFailureError(msg) @step("Verify correct Netbox entry") def check_netbox_entry_exists(subscription: Router) -> None: """Validate the Netbox entry for a Router. This will only ensure existence of the node itself in Netbox. Validation of separate interfaces takes places in other subscriptions' validation workflows. """ client = NetboxClient() # Try and fetch the host, which will raise an exception on failure. client.get_device_by_name(subscription.router.router_fqdn) @step("Verify BGP configuration on P router") def verify_p_ibgp(subscription: dict[str, Any], callback_route: str) -> None: """Perform a dry run of adding the list of all PE routers to the new P router.""" extra_vars = { "dry_run": True, "subscription": subscription, "pe_router_list": subscriptions.get_active_pe_router_dict(), "verb": "verify_p_ibgp", "is_verification_workflow": "true", } lso_client.execute_playbook( playbook_name="update_ibgp_mesh.yaml", callback_route=callback_route, inventory=subscription["router"]["router_fqdn"], extra_vars=extra_vars, ) @step("Verify correct LibreNMS entry") def check_librenms_entry_exists(subscription: Router) -> None: """Validate the LibreNMS entry for a Router. Raises an HTTP error 404 when the device is not present in LibreNMS. """ client = LibreNMSClient() errors = client.validate_device(subscription.router.router_fqdn) if errors: raise ProcessFailureError(message="LibreNMS configuration error", details=errors) @step("Check base config for drift") def verify_base_config(subscription: Router, callback_route: str) -> None: """Workflow step for running a playbook that checks whether base config has drifted.""" execute_playbook( playbook_name="base_config.yaml", callback_route=callback_route, inventory=subscription.router.router_fqdn, extra_vars={ "wfo_router_json": json.loads(json_dumps(subscription)), "verb": "deploy", "dry_run": "true", "is_verification_workflow": "true", }, ) @workflow( "Validate router configuration", target=Target.SYSTEM, initial_input_form=wrap_modify_initial_input_form(None) ) def validate_router() -> StepList: """Validate an existing, active Router subscription. * Verify that the loopback interface is correctly configured in :term:`IPAM`. * Verify that the router is correctly configured in Netbox. * Verify that the router is correctly configured in LibreNMS. * Redeploy base config to verify the configuration is intact. * Validate configuration of the iBGP mesh """ is_juniper_router = conditional(lambda state: state["subscription"]["router"]["vendor"] == Vendor.JUNIPER) return ( begin >> store_process_subscription(Target.SYSTEM) >> prepare_state >> is_juniper_router(done) >> unsync >> verify_ipam_loopback >> check_netbox_entry_exists >> check_librenms_entry_exists >> anonymous_lso_interaction(verify_base_config) >> anonymous_lso_interaction(verify_p_ibgp) >> resync >> done )