diff --git a/gso/migrations/versions/2024-08-07_88dd5a44150d_add_promote_p_to_pe_workflows.py b/gso/migrations/versions/2024-08-07_88dd5a44150d_add_promote_p_to_pe_workflows.py new file mode 100644 index 0000000000000000000000000000000000000000..3ad1235c2c676c1c7164647d5fb8b483247ea919 --- /dev/null +++ b/gso/migrations/versions/2024-08-07_88dd5a44150d_add_promote_p_to_pe_workflows.py @@ -0,0 +1,39 @@ +"""Add promote P to PE workflows.. + +Revision ID: 88dd5a44150d +Revises: 41fd1ae225aq +Create Date: 2024-08-07 13:54:44.362435 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '88dd5a44150d' +down_revision = '41fd1ae225aq' +branch_labels = None +depends_on = None + + +from orchestrator.migrations.helpers import create_workflow, delete_workflow + +new_workflows = [ + { + "name": "promote_p_to_pe", + "target": "MODIFY", + "description": "Promote P router to PE router", + "product_type": "Router" + } +] + + +def upgrade() -> None: + conn = op.get_bind() + for workflow in new_workflows: + create_workflow(conn, workflow) + + +def downgrade() -> None: + conn = op.get_bind() + for workflow in new_workflows: + delete_workflow(conn, workflow["name"]) diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py index 56561838557882cac784faed6745c1d80661a0cd..e788472232d4625afa14ae9fbfb932cd3573b012 100644 --- a/gso/workflows/__init__.py +++ b/gso/workflows/__init__.py @@ -44,6 +44,7 @@ LazyWorkflowInstance("gso.workflows.router.modify_connection_strategy", "modify_ LazyWorkflowInstance("gso.workflows.router.import_router", "import_router") LazyWorkflowInstance("gso.workflows.router.create_imported_router", "create_imported_router") LazyWorkflowInstance("gso.workflows.router.validate_router", "validate_router") +LazyWorkflowInstance("gso.workflows.router.promote_p_to_pe", "promote_p_to_pe") # Site workflows LazyWorkflowInstance("gso.workflows.site.create_site", "create_site") diff --git a/gso/workflows/router/promote_p_to_pe.py b/gso/workflows/router/promote_p_to_pe.py new file mode 100644 index 0000000000000000000000000000000000000000..d893c057167a20a7971adccdfa6d9a374b4f9924 --- /dev/null +++ b/gso/workflows/router/promote_p_to_pe.py @@ -0,0 +1,532 @@ +"""Promote a P router to a PE router.""" + +from typing import Any + +from orchestrator.config.assignee import Assignee +from orchestrator.forms import FormPage +from orchestrator.forms.validators import Label +from orchestrator.targets import Target +from orchestrator.types import FormGenerator, State, UUIDstr +from orchestrator.utils.errors import ProcessFailureError +from orchestrator.workflow import StepList, begin, done, inputstep, step, workflow +from orchestrator.workflows.steps import resync, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form +from pydantic import ConfigDict, field_validator + +from gso.products.product_blocks.router import RouterRole +from gso.products.product_types.router import Router, RouterInactive +from gso.services import lso_client +from gso.services.kentik_client import KentikClient, NewKentikDevice +from gso.services.lso_client import lso_interaction +from gso.utils.helpers import generate_inventory_for_active_routers, validate_tt_number + + +def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: + """Promote P router to PE router.""" + subscription = Router.from_subscription(subscription_id) + + class PromotePToPEForm(FormPage): + tt_number: str + + @field_validator("tt_number") + def validate_tt_number(cls, tt_number: str) -> str: + return validate_tt_number(tt_number) + + user_input = yield PromotePToPEForm + + return user_input.model_dump() + + +@step("[DRY RUN] Evacuate the router by setting isis_overload") +def set_isis_overload_dry(subscription: dict[str, Any], callback_route: str, tt_number: str, + process_id: UUIDstr) -> None: + """Perform a dry run of evacuating the router by setting isis overload.""" + extra_vars = { + "dry_run": True, + "subscription": subscription, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Set ISIS overload", + "verb": "set_isis_overload", + } + + lso_client.execute_playbook( + playbook_name="promote_p_to_pe.yaml", + callback_route=callback_route, + inventory=subscription["router"]["router_fqdn"], + extra_vars=extra_vars, + ) + + +@step("[FOR REAL] Evacuate the router by setting isis_overload") +def set_isis_overload_real(subscription: dict[str, Any], callback_route: str, tt_number: str, + process_id: UUIDstr) -> None: + """Perform a real run of evacuating the router by setting isis overload.""" + extra_vars = { + "dry_run": False, + "subscription": subscription, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Set ISIS overload", + "verb": "set_isis_overload", + } + + lso_client.execute_playbook( + playbook_name="promote_p_to_pe.yaml", + callback_route=callback_route, + inventory=subscription["router"]["router_fqdn"], + extra_vars=extra_vars, + ) + + +@step("[DRY RUN] Deploy PE base config") +def deploy_pe_base_config_dry(subscription: dict[str, Any], callback_route: str, tt_number: str, + process_id: UUIDstr) -> None: + """Perform a dry run of adding the base config to the router.""" + extra_vars = { + "dry_run": True, + "subscription": subscription, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - deploy PE base config", + "verb": "deploy_pe_base_config", + } + + lso_client.execute_playbook( + playbook_name="promote_p_to_pe.yaml", + callback_route=callback_route, + inventory=subscription["router"]["router_fqdn"], + extra_vars=extra_vars, + ) + + +@step("[FOR REAL] Deploy PE base config") +def deploy_pe_base_config_real(subscription: dict[str, Any], callback_route: str, tt_number: str, + process_id: UUIDstr) -> None: + """Perform a real run of adding the base config to the router.""" + extra_vars = { + "dry_run": False, + "subscription": subscription, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - deploy PE base config", + "verb": "deploy_pe_base_config", + } + + lso_client.execute_playbook( + playbook_name="promote_p_to_pe.yaml", + callback_route=callback_route, + inventory=subscription["router"]["router_fqdn"], + extra_vars=extra_vars, + ) + + +@inputstep("Prompt EARL insertion", assignee=Assignee.SYSTEM) +def prompt_insert_in_earl(subscription: dict[str, Any]) -> FormGenerator: + """Wait for confirmation from an operator that the router has been inserted in EARL.""" + + class EARLPrompt(FormPage): + model_config = ConfigDict(title="Update RADIUS clients") + + info_label: Label = ( + f"Please add the router {subscription["router"]["router_fqdn"]} to EARL." + ) + + yield EARLPrompt + + return {} + + +@step("Create Kentik device") +def create_kentik_device(subscription: RouterInactive) -> State: + """Create a new device in Kentik.""" + if not ( + subscription.router.router_site + and subscription.router.router_site.site_name + and subscription.router.router_site.site_tier + and subscription.router.router_fqdn + ): + msg = "Router object is missing required properties." + raise ProcessFailureError(msg) + + kentik_client = KentikClient() + kentik_site = kentik_client.get_site_by_name(subscription.router.router_site.site_name) + + if not kentik_site: + msg = f"Site could not be found in Kentik: {subscription.router.router_site.site_name}" + raise ProcessFailureError(msg) + + site_tier = subscription.router.router_site.site_tier + new_device = NewKentikDevice( + device_name=subscription.router.router_fqdn, + device_description=str(subscription.subscription_id), + sending_ips=[str(subscription.router.router_lo_ipv4_address)], + site_tier=site_tier, + site_id=kentik_site["id"], + device_snmp_ip=str(subscription.router.router_lo_ipv4_address), + device_bgp_flowspec=False, + device_bgp_neighbor_ip=str(subscription.router.router_lo_ipv4_address), + device_bgp_neighbor_ip6=str(subscription.router.router_lo_ipv6_address), + ) + kentik_device = kentik_client.create_device(new_device) + + if "error" in kentik_device or "kentik_error" in kentik_device: + raise ProcessFailureError(str(kentik_device)) + + kentik_device.pop("custom_column_data", None) + return {"kentik_device": kentik_device} + + +@step("[DRY RUN] Remove PE from P") +def remove_pe_from_p_dry(subscription: dict[str, Any], callback_route: str, tt_number: str, + process_id: UUIDstr) -> None: + """Perform a dry run of removing the PE routers from the P router.""" + extra_vars = { + "dry_run": True, + "subscription": subscription, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " + f"Depopulate P-only neighbour list from the promoted router", + "verb": "remove_pe_from_p", + "pe_router_list": generate_inventory_for_active_routers(RouterRole.PE) + } + + 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] Remove PE from P") +def remove_pe_from_p_real(subscription: dict[str, Any], callback_route: str, tt_number: str, + process_id: UUIDstr) -> None: + """Remove the PE routers from the P router.""" + extra_vars = { + "dry_run": False, + "subscription": subscription, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " + f"Depopulate P-only neighbour list from the promoted router", + "verb": "remove_pe_from_p", + "pe_router_list": generate_inventory_for_active_routers(RouterRole.PE) + } + + lso_client.execute_playbook( + playbook_name="update_ibgp_mesh.yaml", + callback_route=callback_route, + inventory=subscription["router"]["router_fqdn"], + extra_vars=extra_vars, + ) + + +@step("[DRY RUN] Add PE mesh to PE") +def add_pe_mesh_to_pe_dry(subscription: dict[str, Any], callback_route: str, tt_number: str, + process_id: UUIDstr) -> None: + """Perform a dry run of adding list of PE routers into iGEANT/iGEANT6 of promoted router.""" + extra_vars = { + "dry_run": True, + "subscription": subscription, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " + f"Add list of PE routers into iGEANT/iGEANT6 of promoted router", + "verb": "add_pe_mesh_to_pe", + "pe_router_list": generate_inventory_for_active_routers(RouterRole.PE) + } + + 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 PE mesh to PE") +def add_pe_mesh_to_pe_real(subscription: dict[str, Any], callback_route: str, tt_number: str, + process_id: UUIDstr) -> None: + """Perform a real run of adding list of PE routers into iGEANT/iGEANT6 of promoted router.""" + extra_vars = { + "dry_run": False, + "subscription": subscription, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " + f"Add list of PE routers into iGEANT/iGEANT6 of promoted router", + "verb": "add_pe_mesh_to_pe", + "pe_router_list": generate_inventory_for_active_routers(RouterRole.PE) + } + + lso_client.execute_playbook( + playbook_name="update_ibgp_mesh.yaml", + callback_route=callback_route, + inventory=subscription["router"]["router_fqdn"], + extra_vars=extra_vars, + ) + + +@step("[DRY RUN] Add PE to PE mesh") +def add_pe_to_pe_mesh_dry(subscription: dict[str, Any], callback_route: str, tt_number: str, + process_id: UUIDstr) -> None: + """Perform a dry run of adding the promoted router to all PE routers in iGEANT/iGEANT6.""" + extra_vars = { + "dry_run": True, + "subscription": subscription, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " + f"Add promoted router to all PE routers in iGEANT/iGEANT.", + "verb": "add_pe_to_pe_mesh", + } + + 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("[FOR REAL] Add PE to PE mesh") +def add_pe_to_pe_mesh_real(subscription: dict[str, Any], callback_route: str, tt_number: str, + process_id: UUIDstr) -> None: + """Perform a real run of adding the promoted router to all PE routers in iGEANT/iGEANT6.""" + extra_vars = { + "dry_run": False, + "subscription": subscription, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " + f"Add promoted router to all PE routers in iGEANT/iGEANT.", + "verb": "add_pe_to_pe_mesh", + } + + 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] Check iBGP session") +def check_pe_ibgp_dry(subscription: dict[str, Any], callback_route: str, tt_number: str, + process_id: UUIDstr) -> None: + """Perform a dry run of checking the iBGP session.""" + extra_vars = { + "dry_run": True, + "subscription": subscription, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Check iBGP session", + "verb": "check_pe_ibgp", + } + + lso_client.execute_playbook( + playbook_name="check_ibgp.yaml", + callback_route=callback_route, + inventory=subscription["router"]["router_fqdn"], + extra_vars=extra_vars, + ) + + +@step("[FOR REAL] Check iBGP session") +def check_pe_ibgp_real(subscription: dict[str, Any], callback_route: str, tt_number: str, + process_id: UUIDstr) -> None: + """Perform a real run of checking the iBGP session.""" + extra_vars = { + "dry_run": False, + "subscription": subscription, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Check iBGP session", + "verb": "check_pe_ibgp", + } + + lso_client.execute_playbook( + playbook_name="check_ibgp.yaml", + callback_route=callback_route, + inventory=subscription["router"]["router_fqdn"], + extra_vars=extra_vars, + ) + + +@step("[DRY RUN] Deploy routing instances") +def deploy_routing_instances_dry(subscription: dict[str, Any], callback_route: str, tt_number: str, + process_id: UUIDstr) -> None: + """Perform a dry run of deploying routing instances.""" + extra_vars = { + "dry_run": True, + "subscription": subscription, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Deploy PE base config", + "verb": "deploy_routing_instances", + } + + lso_client.execute_playbook( + playbook_name="promote_p_to_pe.yaml", + callback_route=callback_route, + inventory=subscription["router"]["router_fqdn"], + extra_vars=extra_vars, + ) + + +@step("[FOR REAL] Deploy routing instances") +def deploy_routing_instances_real(subscription: dict[str, Any], callback_route: str, tt_number: str, + process_id: UUIDstr) -> None: + """Perform a real run of deploying routing instances.""" + extra_vars = { + "dry_run": False, + "subscription": subscription, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Deploy PE base config", + "verb": "deploy_routing_instances", + } + + lso_client.execute_playbook( + playbook_name="promote_p_to_pe.yaml", + callback_route=callback_route, + inventory=subscription["router"]["router_fqdn"], + extra_vars=extra_vars, + ) + + +@step("[DRY RUN] Check L3 services") +def check_l3_services_dry(subscription: dict[str, Any], callback_route: str, tt_number: str, + process_id: UUIDstr) -> None: + """Perform a dry run of checking L3 services.""" + extra_vars = { + "dry_run": True, + "subscription": subscription, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Check L3 services", + "verb": "check_base_ris", + } + + lso_client.execute_playbook( + playbook_name="check_l3_services.yaml", + callback_route=callback_route, + inventory=subscription["router"]["router_fqdn"], + extra_vars=extra_vars, + ) + + +@step("[FOR REAL] Check L3 services") +def check_l3_services_real(subscription: dict[str, Any], callback_route: str, tt_number: str, + process_id: UUIDstr) -> None: + """Perform a real run of checking L3 services.""" + extra_vars = { + "dry_run": False, + "subscription": subscription, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Check L3 services", + "verb": "check_base_ris", + } + + lso_client.execute_playbook( + playbook_name="check_l3_services.yaml", + callback_route=callback_route, + inventory=subscription["router"]["router_fqdn"], + extra_vars=extra_vars, + ) + + +@step("[DRY RUN] Remove isis overload") +def remove_isis_overload_dry(subscription: dict[str, Any], callback_route: str, tt_number: str, + process_id: UUIDstr) -> None: + """Perform a dry run of removing isis overload.""" + extra_vars = { + "dry_run": True, + "subscription": subscription, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Remove ISIS overload", + "verb": "remove_isis_overload", + } + + lso_client.execute_playbook( + playbook_name="promote_p_to_pe.yaml", + callback_route=callback_route, + inventory=subscription["router"]["router_fqdn"], + extra_vars=extra_vars, + ) + + +@step("[FOR REAL] Remove isis overload") +def remove_isis_overload_real(subscription: dict[str, Any], callback_route: str, tt_number: str, + process_id: UUIDstr) -> None: + """Perform a real run of removing isis overload.""" + extra_vars = { + "dry_run": False, + "subscription": subscription, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Remove ISIS overload", + "verb": "remove_isis_overload", + } + + lso_client.execute_playbook( + playbook_name="promote_p_to_pe.yaml", + callback_route=callback_route, + inventory=subscription["router"]["router_fqdn"], + extra_vars=extra_vars, + ) + + +@step("Set router role to PE (Update subscription model)") +def update_subscription_model(subscription: Router) -> State: + """Update the subscription model to set router role to PE.""" + subscription.router.router_role = RouterRole.PE + + return {"subscription": subscription} + + +@step("[DRY RUN] Delete default routes") +def delete_default_routes_dry(subscription: dict[str, Any], callback_route: str, tt_number: str, + process_id: UUIDstr) -> None: + """Perform a dry run of deleting the default routes.""" + extra_vars = { + "dry_run": True, + "subscription": subscription, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " + f"Delete static default routes (part of P base-config)", + "verb": "delete_default_routes", + } + + lso_client.execute_playbook( + playbook_name="promote_p_to_pe.yaml", + callback_route=callback_route, + inventory=subscription["router"]["router_fqdn"], + extra_vars=extra_vars, + ) + + +@step("[FOR REAL] Delete default routes") +def delete_default_routes_real(subscription: dict[str, Any], callback_route: str, tt_number: str, + process_id: UUIDstr) -> None: + """Perform a real run of deleting the default routes.""" + extra_vars = { + "dry_run": False, + "subscription": subscription, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " + f"Delete static default routes (part of P base-config)", + "verb": "delete_default_routes", + } + + lso_client.execute_playbook( + playbook_name="promote_p_to_pe.yaml", + callback_route=callback_route, + inventory=subscription["router"]["router_fqdn"], + extra_vars=extra_vars, + ) + + +@workflow( + "Promote P router to PE router", + initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), + target=Target.MODIFY, +) +def promote_p_to_pe() -> StepList: + """Promote a P router to a PE router.""" + return ( + begin + >> store_process_subscription(Target.MODIFY) + >> unsync + >> lso_interaction(set_isis_overload_dry) + >> lso_interaction(set_isis_overload_real) + >> lso_interaction(deploy_pe_base_config_dry) + >> lso_interaction(deploy_pe_base_config_real) + >> prompt_insert_in_earl + >> create_kentik_device + >> lso_interaction(remove_pe_from_p_dry) + >> lso_interaction(remove_pe_from_p_real) + >> lso_interaction(add_pe_mesh_to_pe_dry) + >> lso_interaction(add_pe_mesh_to_pe_real) + >> lso_interaction(add_pe_to_pe_mesh_dry) + >> lso_interaction(add_pe_to_pe_mesh_real) + >> lso_interaction(check_pe_ibgp_dry) + >> lso_interaction(check_pe_ibgp_real) + >> lso_interaction(deploy_routing_instances_dry) + >> lso_interaction(deploy_routing_instances_real) + >> lso_interaction(check_l3_services_dry) + >> lso_interaction(check_l3_services_real) + >> lso_interaction(remove_isis_overload_dry) + >> lso_interaction(remove_isis_overload_real) + >> update_subscription_model + >> lso_interaction(delete_default_routes_dry) + >> lso_interaction(delete_default_routes_real) + >> resync + >> done + )