"""A workflow that terminates a router.""" import ipaddress import json import logging from typing import Any 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.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, 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.kentik_client import KentikClient 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.settings import load_oss_params from gso.utils.helpers import generate_inventory_for_active_routers from gso.utils.shared_enums import Vendor from gso.utils.types import TTNumber 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: TTNumber 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", # FIX: need to use correct playbook. 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": json.loads(json_dumps(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": json.loads(json_dumps(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 PE router from the PE router mesh.""" extra_vars = { "dry_run": True, "subscription": json.loads(json_dumps(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_pe_from_pe", } 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 terminated PE router from PE the router mesh.""" extra_vars = { "dry_run": False, "subscription": json.loads(json_dumps(subscription)), "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " f"Remove {subscription.router.router_fqdn} from iBGP mesh", "verb": "remove_pe_from_pe", } 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": json.loads(json_dumps(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 real run of removing PE router from all P routers.""" extra_vars = { "dry_run": False, "subscription": json.loads(json_dumps(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) -> None: """Remove the device from LibreNMS.""" LibreNMSClient().remove_device(subscription.router.router_fqdn) @step("Apply the archiving license in Kentik") def kentik_apply_archive_license(subscription: Router) -> dict[str, dict[str, Any]]: """Apply the archiving license to a PE router in Kentik. This includes setting the flow rate to one flow per second. """ kentik_client = KentikClient() kentik_archive_plan_id = kentik_client.get_plan_by_name(load_oss_params().KENTIK.archive_license_key)["id"] kentik_device = kentik_client.get_device_by_name(subscription.router.router_fqdn) if "id" not in kentik_device: msg = "Failed to find Kentik device by name" raise ProcessFailureError(msg, details=kentik_device) updated_device = {"device": {"plan_id": kentik_archive_plan_id, "device_sample_rate": 1}} kentik_device = kentik_client.update_device(kentik_device["id"], updated_device) return {"kentik_device": kentik_device} @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 >> router_is_pe(kentik_apply_archive_license) >> set_status(SubscriptionLifecycle.TERMINATED) >> resync >> done )