From 8de14282d1e9fc2b81e52ea71b27a66917fe6e31 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Thu, 18 Jul 2024 13:34:18 +0200
Subject: [PATCH] Added more steps in router termination including: - Remove
 the terminated router from iGEANT/iGEANT6 on all the PEs - Remove the
 terminated router from iGEANT_P_ONLY/iGEANT_P_ONLY_6 on all Ps - Remove
 router from LibreNMS

---
 gso/utils/workflow_steps.py              |  39 ++++++
 gso/workflows/router/terminate_router.py | 159 ++++++++++++++++++++++-
 gso/workflows/router/update_ibgp_mesh.py |  59 ++++++++-
 3 files changed, 254 insertions(+), 3 deletions(-)

diff --git a/gso/utils/workflow_steps.py b/gso/utils/workflow_steps.py
index 176b8325..f1beae9d 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 b9e521da..835837c7 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 bf0d0416..ccecb823 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."""
-- 
GitLab