"""Router validation workflow. Used in a nightly schedule.""" from typing import Any from orchestrator.targets import Target from orchestrator.types import State, UUIDstr from orchestrator.utils.errors import ProcessFailureError 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 gso.products.product_blocks.router import RouterRole from gso.products.product_types.router import Router from gso.services import infoblox from gso.services.kentik_client import KentikClient from gso.services.librenms_client import LibreNMSClient from gso.services.lso_client import LSOState, anonymous_lso_interaction from gso.services.netbox_client import NetboxClient from gso.utils.helpers import generate_inventory_for_routers 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]) -> LSOState: """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": generate_inventory_for_routers(RouterRole.PE)["all"]["hosts"], "verb": "verify_p_ibgp", "is_verification_workflow": "true", } return { "playbook_name": "gap_ansible/playbooks/update_ibgp_mesh.yaml", "inventory": {"all": {"hosts": {subscription["router"]["router_fqdn"]: None}}}, "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("Verify Kentik entry for PE router") def check_kentik_entry_exists(subscription: Router) -> None: """Validate the Kentik entry for a PE Router. Raises an HTTP error 404 when the device is not present in Kentik. """ client = KentikClient() device = client.get_device_by_name(subscription.router.router_fqdn) if not device: raise ProcessFailureError( message="Device not found in Kentik", details={"device": subscription.router.router_fqdn} ) @step("Check base config for drift") def verify_base_config(subscription: dict[str, Any]) -> LSOState: """Workflow step for running a playbook that checks whether base config has drifted.""" return { "playbook_name": "gap_ansible/playbooks/base_config.yaml", "inventory": {"all": {"hosts": {subscription["router"]["router_fqdn"]: None}}}, "extra_vars": { "wfo_router_json": 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) is_pe_router = conditional(lambda state: state["subscription"]["router"]["router_role"] == RouterRole.PE) 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 >> is_pe_router(check_kentik_entry_exists) >> anonymous_lso_interaction(verify_base_config) >> anonymous_lso_interaction(verify_p_ibgp) >> resync >> done )