"""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, 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_types.iptrunk import Iptrunk from gso.services import infoblox from gso.services.lso_client import anonymous_lso_interaction, execute_playbook from gso.services.netbox_client import NetboxClient from gso.utils.helpers import get_router_vendor from gso.utils.shared_enums import Vendor @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_records(subscription: Iptrunk) -> None: """Validate the :term:`IPAM` resources for the :term:`LAG` interfaces. Raises an :class:`orchestrator.utils.errors.ProcessFailureError` if :term:`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.append( "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}'" ) for index, side in enumerate(subscription.iptrunk.iptrunk_sides): lag_fqdn = f"{side.iptrunk_side_ae_iface}-0.{side.iptrunk_side_node.router_fqdn}" side_v4 = subscription.iptrunk.iptrunk_ipv4_network[index] side_v6 = subscription.iptrunk.iptrunk_ipv6_network[index + 1] # Validate IPv4 address allocation record = infoblox.find_host_by_fqdn(lag_fqdn) if not record: ipam_errors.append(f"No IPv4 host record found with FQDN '{lag_fqdn}'") else: # Allocation inside IPv4 network must be correct if str(side_v4) != record.ipv4addr: ipam_errors.append( f"Incorrectly allocated host record for FQDN '{lag_fqdn}'.\n" f"Expected '{side_v4}', actual: '{record.ipv4addr}'" ) # Allocated host record needs to be set correctly if record.comment != str(subscription.subscription_id): ipam_errors.append( f"Incorrect host record found for '{lag_fqdn}' at '{side_v4}'. Comment should have been equal " f"to subscription ID '{subscription.subscription_id}'." ) # Validate IPv6 address allocation record = infoblox.find_v6_host_by_fqdn(lag_fqdn) if not record: ipam_errors.append(f"No IPv6 host record found with FQDN '{lag_fqdn}'") else: # Allocation inside IPv6 network must be correct if str(side_v6) != record.ipv6addr: ipam_errors.append( f"Incorrectly allocated host record for FQDN '{lag_fqdn}'.\n" f"Expected '{side_v6}', actual: '{record.ipv6addr}'" ) # Allocated host record needs to be set correctly if record.comment != str(subscription.subscription_id): ipam_errors.append( f"Incorrect host record found for '{lag_fqdn}' at '{side_v6}'. Comment should have been equal " f"to subscription ID '{subscription.subscription_id}'." ) if ipam_errors: raise ProcessFailureError(message="IPAM misconfiguration(s) found", details=str(ipam_errors)) @step("Verify NetBox entries") def verify_netbox_entries(subscription: Iptrunk) -> None: """Validate required entries for an IP trunk in NetBox.""" nbclient = NetboxClient() netbox_errors = [] for side in subscription.iptrunk.iptrunk_sides: if get_router_vendor(side.iptrunk_side_node.owner_subscription_id) == Vendor.NOKIA: # Raises en exception when not found. interface = nbclient.get_interface_by_name_and_device( side.iptrunk_side_ae_iface, side.iptrunk_side_node.router_fqdn ) if interface.description != str(subscription.subscription_id): netbox_errors.append( f"Incorrect description for '{side.iptrunk_side_ae_iface}', expected " f"'{subscription.subscription_id}' but got '{interface.description}'" ) if not interface.enabled: netbox_errors.append(f"NetBox interface '{side.iptrunk_side_ae_iface}' is not enabled.") for member in side.iptrunk_side_ae_members: interface = nbclient.get_interface_by_name_and_device( member.interface_name, side.iptrunk_side_node.router_fqdn ) if interface.description != str(subscription.subscription_id): netbox_errors.append( f"Incorrect description for '{member.interface_name}', expected " f"'{subscription.subscription_id}' but got '{interface.description}'" ) if not interface.enabled: netbox_errors.append(f"NetBox interface '{side.iptrunk_side_ae_iface}' is not enabled.") if netbox_errors: raise ProcessFailureError(message="NetBox misconfiguration(s) found", details=str(netbox_errors)) @step("Verify configuration of IPtrunk") def verify_iptrunk_config(subscription: Iptrunk, callback_route: str) -> None: """Check for configuration drift on the relevant routers.""" execute_playbook( playbook_name="iptrunks.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={ "wfo_trunk_json": json.loads(json_dumps(subscription)), "verb": "deploy", "dry_run": "true", "config_object": "trunk_interface", "is_verification_workflow": "true", }, ) @step("Check ISIS configuration") def check_ip_trunk_isis(subscription: Iptrunk, callback_route: str) -> None: """Run an Ansible playbook to check for any :term:`ISIS` configuration drift.""" execute_playbook( playbook_name="iptrunks.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={ "wfo_trunk_json": json.loads(json_dumps(subscription)), "verb": "deploy", "dry_run": "true", "config_object": "isis_interface", "is_verification_workflow": "true", }, ) @step("Verify TWAMP configuration") def verify_twamp_config(subscription: Iptrunk, callback_route: str) -> None: """Check for configuration drift of TWAMP.""" execute_playbook( playbook_name="deploy_twamp.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={ "subscription": json.loads(json_dumps(subscription)), "verb": "deploy", "dry_run": "true", "is_verification_workflow": "true", }, ) @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. * Verify that the :term:`LAG` interfaces are correctly configured in :term:`IPAM`. * Check correct configuration of interfaces in NetBox. * Verify the configuration on both sides of the trunk is intact. * Check the ISIS metric of the trunk. * Verify that TWAMP configuration is correct. If a trunk has a Juniper router on both sides, it is considered legacy and does not require validation. """ skip_legacy_trunks = conditional( lambda state: all( side.iptrunk_side_node.vendor == Vendor.JUNIPER for side in Iptrunk.from_subscription(state["subscription_id"]).iptrunk.iptrunk_sides ) ) return ( begin >> store_process_subscription(Target.SYSTEM) >> skip_legacy_trunks(done) >> unsync >> verify_ipam_records >> verify_netbox_entries >> anonymous_lso_interaction(verify_iptrunk_config) >> anonymous_lso_interaction(check_ip_trunk_isis) >> anonymous_lso_interaction(verify_twamp_config) >> resync >> done )