diff --git a/gso/utils/workflow_steps.py b/gso/utils/workflow_steps.py index 176b83258459d86dc51dbe92ba447c68892fca49..f1beae9ded934624936d7ffe61f5e5938521a479 100644 --- a/gso/utils/workflow_steps.py +++ b/gso/utils/workflow_steps.py @@ -12,7 +12,9 @@ from pydantic_forms.core import FormPage from pydantic_forms.types import FormGenerator from pydantic_forms.validators import Label +from gso.products.product_blocks.router import RouterRole from gso.products.product_types.iptrunk import Iptrunk +from gso.products.product_types.router import Router from gso.services import lso_client, subscriptions from gso.settings import load_oss_params @@ -160,3 +162,40 @@ def add_all_pe_to_p_real( inventory=subscription["router"]["router_fqdn"], extra_vars=extra_vars, ) + +@step("Calculate list of all active PE routers") +def calculate_pe_router_list() -> State: + """Calculate a list of all active PE routers in the network.""" + all_routers = [ + Router.from_subscription(r["subscription_id"]) for r in subscriptions.get_active_router_subscriptions() + ] + all_pe_routers = [router for router in all_routers if router.router.router_role == RouterRole.PE] + + return {"pe_router_list": all_pe_routers} + + +@step("Calculate list of all active P routers") +def calculate_p_router_list() -> State: + """Calculate a list of all active P routers in the network.""" + all_routers = [ + Router.from_subscription(r["subscription_id"]) for r in subscriptions.get_active_router_subscriptions() + ] + all_pe_routers = [router for router in all_routers if router.router.router_role == RouterRole.P] + + return {"pe_router_list": all_pe_routers} + + +def generate_inventory(router_list: list[Router]) -> dict[str, Any]: + """Generate an Ansible-compatible inventory for executing playbooks. Contains all active routers.""" + return { + "all": { + "hosts": { + router.router.router_fqdn: { + "lo4": str(router.router.router_lo_ipv4_address), + "lo6": str(router.router.router_lo_ipv6_address), + "vendor": str(router.router.vendor), + } + for router in router_list + } + }, + } diff --git a/gso/workflows/router/terminate_router.py b/gso/workflows/router/terminate_router.py index b9e521da831dbcc091286c21cb2d67862b963074..835837c7363c0a7278e6127d44bb577c93b30d98 100644 --- a/gso/workflows/router/terminate_router.py +++ b/gso/workflows/router/terminate_router.py @@ -19,10 +19,12 @@ from orchestrator.workflows.steps import ( from orchestrator.workflows.utils import wrap_modify_initial_input_form from gso.products.product_types.router import Router -from gso.services import infoblox +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.shared_enums import Vendor +from gso.utils.workflow_steps import calculate_p_router_list, calculate_pe_router_list, generate_inventory logger = logging.getLogger(__name__) @@ -44,7 +46,10 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: remove_configuration: bool = True user_input = yield TerminateForm - return user_input.model_dump() | {"router_is_nokia": router.router.vendor == Vendor.NOKIA} + 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") @@ -104,6 +109,145 @@ def remove_device_from_netbox(subscription: Router) -> dict[str, Router]: return {"subscription": subscription} +@step("[DRY RUN] Remove P router from the mesh of PE routers") +def remove_p_from_mesh_dry( + subscription: Router, callback_route: str, pe_router_list: list[Router], tt_number: str, process_id: UUIDstr +) -> None: + """Perform a dry run of removing the terminated router from the mesh of 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 iBGP mesh", + "verb": "remove_p_from_pe", + } + + lso_client.execute_playbook( + playbook_name="update_ibgp_mesh.yaml", + callback_route=callback_route, + inventory=generate_inventory(pe_router_list), + extra_vars=extra_vars, + ) + + +@step("[REAL RUN] Remove P router from the mesh of PE routers") +def remove_p_from_mesh_real( + subscription: Router, callback_route: str, pe_router_list: list[Router], tt_number: str, process_id: UUIDstr +) -> None: + """Perform a real run of removing the terminated router from the mesh of 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 iBGP mesh", + "verb": "remove_p_from_pe", + } + + lso_client.execute_playbook( + playbook_name="update_ibgp_mesh.yaml", + callback_route=callback_route, + inventory=generate_inventory(pe_router_list), + extra_vars=extra_vars, + ) + + +@step("[DRY RUN] Remove all PE routers from P router iBGP table") +def remove_all_pe_from_p_dry( + subscription: Router, callback_route: str, pe_router_list: list[Router], tt_number: str, process_id: UUIDstr +) -> None: + """Perform a dry run of removing all PE routers from P router iBGP table.""" + pe_router_list_excluding_current_router = [ + router for router in pe_router_list if router.router.router_fqdn != subscription.router.router_fqdn + ] + 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 iBGP mesh", + "verb": "remove_p_from_net", + } + + lso_client.execute_playbook( + playbook_name="update_ibgp_mesh.yaml", + callback_route=callback_route, + inventory=generate_inventory(pe_router_list_excluding_current_router), + extra_vars=extra_vars, + ) + + +@step("[REAL RUN] Remove all PE routers from P router iBGP table") +def remove_all_pe_from_p_real( + subscription: Router, callback_route: str, pe_router_list: list[Router], tt_number: str, process_id: UUIDstr +) -> None: + """Perform a real run of removing PE router from P router iBGP table.""" + pe_router_list_excluding_current_router = [ + router for router in pe_router_list if router.router.router_fqdn != subscription.router.router_fqdn + ] + 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(pe_router_list_excluding_current_router), + extra_vars=extra_vars, + ) + + +@step("[DRY RUN] Remove PE router from the mesh of P routers") +def remove_pe_from_mesh_dry( + subscription: Router, callback_route: str, p_router_list: list[Router], tt_number: str, process_id: UUIDstr +) -> None: + """Perform a dry run of removing PE router from the mesh of 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 iBGP mesh", + "verb": "remove_pe_from_p", + } + + lso_client.execute_playbook( + playbook_name="update_ibgp_mesh.yaml", + callback_route=callback_route, + inventory=generate_inventory(p_router_list), + extra_vars=extra_vars, + ) + + +@step("[REAL RUN] Remove PE router from the mesh of P routers") +def remove_pe_from_mesh_real( + subscription: Router, callback_route: str, p_router_list: list[Router], tt_number: str, process_id: UUIDstr +) -> None: + """Perform a dry run of removing PE router from the mesh of 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 iBGP mesh", + "verb": "remove_pe_from_p", + } + + lso_client.execute_playbook( + playbook_name="update_ibgp_mesh.yaml", + callback_route=callback_route, + inventory=generate_inventory(p_router_list), + 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), @@ -119,15 +263,26 @@ def terminate_router() -> StepList: """ run_config_steps = conditional(lambda state: state["remove_configuration"]) router_is_nokia = conditional(lambda state: state["router_is_nokia"]) + router_is_pe = conditional(lambda state: state["router_is_pe"] == "PE") + router_is_p = conditional(lambda state: state["router_is_p"] == "P") return ( begin >> store_process_subscription(Target.TERMINATE) >> unsync + >> calculate_pe_router_list + >> router_is_p(lso_interaction(remove_p_from_mesh_dry)) + >> router_is_p(lso_interaction(remove_p_from_mesh_real)) + >> router_is_pe(calculate_p_router_list) + >> router_is_pe(lso_interaction(remove_all_pe_from_p_dry)) + >> router_is_pe(lso_interaction(remove_all_pe_from_p_real)) + >> router_is_pe(lso_interaction(remove_pe_from_mesh_dry)) + >> router_is_pe(lso_interaction(remove_pe_from_mesh_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 diff --git a/gso/workflows/router/update_ibgp_mesh.py b/gso/workflows/router/update_ibgp_mesh.py index bf0d0416d9f4ae39fa4b18e2640e8be04444866f..ccecb823368492ceb27c9428e793a7d165a04e4b 100644 --- a/gso/workflows/router/update_ibgp_mesh.py +++ b/gso/workflows/router/update_ibgp_mesh.py @@ -13,11 +13,12 @@ from orchestrator.workflows.utils import wrap_modify_initial_input_form from pydantic import ConfigDict, model_validator from gso.products.product_types.router import Router -from gso.services import librenms_client, lso_client, subscriptions +from gso.services import librenms_client, lso_client from gso.services.lso_client import lso_interaction from gso.services.subscriptions import get_trunks_that_terminate_on_router from gso.utils.helpers import SNMPVersion from gso.utils.workflow_steps import add_all_pe_to_p_dry, add_all_pe_to_p_real +from gso.utils.workflow_steps import calculate_pe_router_list, generate_inventory def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: @@ -85,6 +86,62 @@ def add_p_to_mesh_real(subscription: dict[str, Any], callback_route: str, tt_num ) +@step("[DRY RUN] Add all PE routers to P router iBGP table") +def add_all_pe_to_p_dry( + subscription: dict[str, Any], pe_router_list: list[Router], callback_route: str, tt_number: str, process_id: UUIDstr +) -> 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": { + router.router.router_fqdn: { + "lo4": str(router.router.router_lo_ipv4_address), + "lo6": str(router.router.router_lo_ipv6_address), + "vendor": router.router.vendor, + } + for router in pe_router_list + }, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Update iBGP mesh", + "verb": "add_pe_to_p", + } + + lso_client.execute_playbook( + playbook_name="update_ibgp_mesh.yaml", + callback_route=callback_route, + inventory=subscription["router"]["router_fqdn"], + extra_vars=extra_vars, + ) + + +@step("[FOR REAL] Add all PE routers to P router iBGP table") +def add_all_pe_to_p_real( + subscription: dict[str, Any], pe_router_list: list[Router], callback_route: str, tt_number: str, process_id: UUIDstr +) -> None: + """Add the list of all PE routers to the new P router.""" + extra_vars = { + "dry_run": False, + "subscription": subscription, + "pe_router_list": { + router.router.router_fqdn: { + "lo4": str(router.router.router_lo_ipv4_address), + "lo6": str(router.router.router_lo_ipv6_address), + "vendor": router.router.vendor, + } + for router in pe_router_list + }, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Update iBGP mesh", + "verb": "add_pe_to_p", + } + + 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 iBGP session health") def check_ibgp_session(subscription: Router, callback_route: str) -> None: """Run a playbook using the provisioning proxy, to check the health of the new iBGP session."""