Skip to content
Snippets Groups Projects
terminate_router.py 11.11 KiB
"""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_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.shared_enums import Vendor
from gso.utils.workflow_steps import calculate_p_router_list, calculate_pe_router_list, generate_inventory

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

    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 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),
    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"])
    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
    )