"""A workflow that terminates a router.""" import ipaddress import json import logging from orchestrator.forms import FormPage from orchestrator.forms.validators import Label from orchestrator.targets import Target from orchestrator.types import FormGenerator, SubscriptionLifecycle, UUIDstr from orchestrator.utils.json import json_dumps from orchestrator.workflow import StepList, begin, conditional, done, step, workflow from orchestrator.workflows.steps import ( resync, set_status, 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, lso_client from gso.services.librenms_client import LibreNMSClient from gso.services.lso_client import execute_playbook, lso_interaction from gso.services.netbox_client import NetboxClient from gso.utils.helpers import generate_inventory_for_active_routers from gso.utils.shared_enums import Vendor logger = logging.getLogger(__name__) def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: """Let the operator decide whether to delete configuration on the router, and clear up :term:`IPAM` resources.""" router = Router.from_subscription(subscription_id) class TerminateForm(FormPage): if router.status == SubscriptionLifecycle.INITIAL: info_label_2: Label = ( "This will immediately mark the subscription as terminated, preventing any other workflows from " "interacting with this product subscription." ) info_label_3: Label = "ONLY EXECUTE THIS WORKFLOW WHEN YOU ARE ABSOLUTELY SURE WHAT YOU ARE DOING." tt_number: str termination_label: Label = "Please confirm whether configuration should get removed from the router." remove_configuration: bool = True update_ibgp_mesh_label: Label = "Please confirm whether the iBGP mesh should get updated." update_ibgp_mesh: bool = True user_input = yield TerminateForm return user_input.model_dump() | { "router_is_nokia": router.router.vendor == Vendor.NOKIA, "router_role": router.router.router_role, } @step("Deprovision loopback IPs from IPAM") def deprovision_loopback_ips(subscription: Router) -> dict: """Clear up the loopback addresses from :term:`IPAM`.""" infoblox.delete_host_by_ip(ipaddress.IPv4Address(subscription.router.router_lo_ipv4_address)) return {"subscription": subscription} @step("[DRY RUN] Remove configuration from router") def remove_config_from_router_dry( subscription: Router, callback_route: str, process_id: UUIDstr, tt_number: str ) -> None: """Remove configuration from the router, first as a dry run.""" extra_vars = { "wfo_router_json": json.loads(json_dumps(subscription)), "dry_run": True, "verb": "terminate", "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Terminating " f"{subscription.router.router_fqdn}", } execute_playbook( playbook_name="base_config.yaml", callback_route=callback_route, inventory=subscription.router.router_fqdn, extra_vars=extra_vars, ) @step("[FOR REAL] Remove configuration from router") def remove_config_from_router_real( subscription: Router, callback_route: str, process_id: UUIDstr, tt_number: str ) -> None: """Remove configuration from the router.""" extra_vars = { "wfo_router_json": json.loads(json_dumps(subscription)), "dry_run": False, "verb": "terminate", "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Terminating " f"{subscription.router.router_fqdn}", } execute_playbook( playbook_name="base_config.yaml", callback_route=callback_route, inventory=subscription.router.router_fqdn, extra_vars=extra_vars, ) @step("Remove Device from Netbox") def remove_device_from_netbox(subscription: Router) -> dict[str, Router]: """Remove the device from Netbox.""" NetboxClient().delete_device(subscription.router.router_fqdn) return {"subscription": subscription} @step("[DRY RUN] Remove P router from all the PE routers") def remove_p_from_all_pe_dry( subscription: Router, callback_route: str, tt_number: str, process_id: UUIDstr ) -> None: """Perform a dry run of removing the terminated router from all the PE routers.""" extra_vars = { "dry_run": True, "subscription": subscription, "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " f"Remove {subscription.router.router_fqdn} from all the PE routers", "verb": "remove_p_from_pe", } lso_client.execute_playbook( playbook_name="update_ibgp_mesh.yaml", callback_route=callback_route, inventory=generate_inventory_for_active_routers(RouterRole.PE), extra_vars=extra_vars, ) @step("[REAL RUN] Remove P router from all the PE routers") def remove_p_from_all_pe_real( subscription: Router, callback_route: str, tt_number: str, process_id: UUIDstr ) -> None: """Perform a real run of removing the terminated router from all the PE routers.""" extra_vars = { "dry_run": False, "subscription": subscription, "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " f"Remove {subscription.router.router_fqdn} from all the PE routers", "verb": "remove_p_from_pe", } lso_client.execute_playbook( playbook_name="update_ibgp_mesh.yaml", callback_route=callback_route, inventory=generate_inventory_for_active_routers(RouterRole.PE), extra_vars=extra_vars, ) @step("[DRY RUN] Remove PE router from all the PE routers") def remove_pe_from_all_pe_dry( subscription: Router, callback_route: str, tt_number: str, process_id: UUIDstr ) -> None: """Perform a dry run of removing the terminated router from all the PE routers.""" extra_vars = { "dry_run": True, "subscription": subscription, "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " f"Remove {subscription.router.router_fqdn} from all the PE routers", "verb": "remove_p_from_net", } lso_client.execute_playbook( playbook_name="update_ibgp_mesh.yaml", callback_route=callback_route, inventory=generate_inventory_for_active_routers( RouterRole.PE, exclude_routers=[subscription.router.router_fqdn]), extra_vars=extra_vars, ) @step("[REAL RUN] Remove all PE routers from all the PE routers") def remove_pe_from_all_pe_real( subscription: Router, callback_route: str, tt_number: str, process_id: UUIDstr ) -> None: """Perform a real run of removing PE router from P router iBGP table.""" extra_vars = { "dry_run": False, "subscription": subscription, "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " f"Remove {subscription.router.router_fqdn} from iBGP mesh", "verb": "remove_p_from_net", } lso_client.execute_playbook( playbook_name="update_ibgp_mesh.yaml", callback_route=callback_route, inventory=generate_inventory_for_active_routers( RouterRole.PE, exclude_routers=[subscription.router.router_fqdn]), extra_vars=extra_vars, ) @step("[DRY RUN] Remove PE router from all the P routers") def remove_pe_from_all_p_dry( subscription: Router, callback_route: str, tt_number: str, process_id: UUIDstr ) -> None: """Perform a dry run of removing PE router from all P routers.""" extra_vars = { "dry_run": True, "subscription": subscription, "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " f"Remove {subscription.router.router_fqdn} from all the P routers", "verb": "remove_pe_from_p", } lso_client.execute_playbook( playbook_name="update_ibgp_mesh.yaml", callback_route=callback_route, inventory=generate_inventory_for_active_routers(RouterRole.P), extra_vars=extra_vars, ) @step("[REAL RUN] Remove PE router from all P routers") def remove_pe_from_all_p_real( subscription: Router, callback_route: str, tt_number: str, process_id: UUIDstr ) -> None: """Perform a dry run of removing PE router from all P routers.""" extra_vars = { "dry_run": False, "subscription": subscription, "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " f"Remove {subscription.router.router_fqdn} from all the P routers", "verb": "remove_pe_from_p", } lso_client.execute_playbook( playbook_name="update_ibgp_mesh.yaml", callback_route=callback_route, inventory=generate_inventory_for_active_routers(RouterRole.P), extra_vars=extra_vars, ) @step("Remove Device from Librenms") def remove_device_from_librenms(subscription: Router) -> dict[str, Router]: """Remove the device from LibreNMS.""" LibreNMSClient().remove_device(subscription.router.router_fqdn) return {"subscription": subscription} @workflow( "Terminate router", initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), target=Target.TERMINATE, ) def terminate_router() -> StepList: """Terminate a router subscription. * Let the operator decide whether to delete :term:`IPAM` resources, and remove configuration from the router * Clear up :term:`IPAM` resources, if selected by the operator * Disable and delete configuration on the router, if selected by the operator * Mark the subscription as terminated in the service database """ run_config_steps = conditional(lambda state: state["remove_configuration"]) update_ibgp_mesh = conditional(lambda state: state["update_ibgp_mesh"]) router_is_nokia = conditional(lambda state: state["router_is_nokia"]) router_is_pe = conditional(lambda state: state["router_role"] == RouterRole.PE) router_is_p = conditional(lambda state: state["router_role"] == RouterRole.P) return ( begin >> store_process_subscription(Target.TERMINATE) >> unsync >> update_ibgp_mesh(router_is_p(lso_interaction(remove_p_from_all_pe_dry))) >> update_ibgp_mesh(router_is_p(lso_interaction(remove_p_from_all_pe_real))) >> update_ibgp_mesh(router_is_pe(lso_interaction(remove_pe_from_all_pe_dry))) >> update_ibgp_mesh(router_is_pe(lso_interaction(remove_pe_from_all_pe_real))) >> update_ibgp_mesh(router_is_pe(lso_interaction(remove_pe_from_all_p_dry))) >> update_ibgp_mesh(router_is_pe(lso_interaction(remove_pe_from_all_p_real))) >> deprovision_loopback_ips >> run_config_steps(lso_interaction(remove_config_from_router_dry)) >> run_config_steps(lso_interaction(remove_config_from_router_real)) >> router_is_nokia(remove_device_from_netbox) >> remove_device_from_librenms >> set_status(SubscriptionLifecycle.TERMINATED) >> resync >> done )