diff --git a/gso/migrations/versions/2023-12-18_bacd55c26106_add_ibgp_mesh_workflow.py b/gso/migrations/versions/2023-12-18_bacd55c26106_add_ibgp_mesh_workflow.py new file mode 100644 index 0000000000000000000000000000000000000000..27610f3160c006f17269072623fab4667869a3ba --- /dev/null +++ b/gso/migrations/versions/2023-12-18_bacd55c26106_add_ibgp_mesh_workflow.py @@ -0,0 +1,39 @@ +"""Add iBGP mesh workflow. + +Revision ID: bacd55c26106 +Revises: 815033570ad7 +Create Date: 2023-12-18 17:58:29.581963 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'bacd55c26106' +down_revision = '815033570ad7' +branch_labels = None +depends_on = None + + +from orchestrator.migrations.helpers import create_workflow, delete_workflow + +new_workflows = [ + { + "name": "update_ibgp_mesh", + "target": "MODIFY", + "description": "Update iBGP mesh", + "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/translations/en-GB.json b/gso/translations/en-GB.json index ef849f987e5670543adea2eb5b41f7a0ba9d0c29..c8f8d2410f4b94719e127739b2a6e711e01e2efe 100644 --- a/gso/translations/en-GB.json +++ b/gso/translations/en-GB.json @@ -41,6 +41,7 @@ "migrate_iptrunk": "Migrate IP Trunk", "modify_isis_metric": "Modify the ISIS metric", "modify_trunk_interface": "Modify IP Trunk interface", - "redeploy_base_config": "Redeploy base config" + "redeploy_base_config": "Redeploy base config", + "update_ibgp_mesh": "Update iBGP mesh" } } diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py index 28ba2b51525cee346a26401e7f17c89e18229845..56f8a191946a79a786bc8764f70c03c77d9abd1b 100644 --- a/gso/workflows/__init__.py +++ b/gso/workflows/__init__.py @@ -11,6 +11,7 @@ LazyWorkflowInstance("gso.workflows.iptrunk.terminate_iptrunk", "terminate_iptru LazyWorkflowInstance("gso.workflows.router.create_router", "create_router") LazyWorkflowInstance("gso.workflows.router.redeploy_base_config", "redeploy_base_config") LazyWorkflowInstance("gso.workflows.router.terminate_router", "terminate_router") +LazyWorkflowInstance("gso.workflows.router.update_ibgp_mesh", "update_ibgp_mesh") LazyWorkflowInstance("gso.workflows.site.create_site", "create_site") LazyWorkflowInstance("gso.workflows.site.modify_site", "modify_site") LazyWorkflowInstance("gso.workflows.site.terminate_site", "terminate_site") diff --git a/gso/workflows/router/update_ibgp_mesh.py b/gso/workflows/router/update_ibgp_mesh.py new file mode 100644 index 0000000000000000000000000000000000000000..c6bd0e81193960f17ea8c1695523e05d438535d4 --- /dev/null +++ b/gso/workflows/router/update_ibgp_mesh.py @@ -0,0 +1,224 @@ +from typing import Any + +from orchestrator.forms import FormPage +from orchestrator.targets import Target +from orchestrator.types import FormGenerator, State, UUIDstr +from orchestrator.workflow import StepList, done, init, 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 root_validator + +from gso.products.product_blocks.router import RouterRole +from gso.products.product_types.router import Router +from gso.services import provisioning_proxy, subscriptions +from gso.services.provisioning_proxy import pp_interaction, indifferent_pp_interaction +from gso.services.subscriptions import get_active_trunks_that_terminate_on_router + + +def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: + subscription = Router.from_subscription(subscription_id) + + class AddBGPSessionForm(FormPage): + class Config: + title = f"Add {subscription.router.router_fqdn} to the iBGP mesh?" + + @root_validator + def router_has_a_trunk(cls, values: dict[str, Any]) -> dict[str, Any]: + if len(get_active_trunks_that_terminate_on_router(subscription_id)) == 0: + msg = "Selected router does not terminate any active IP trunks." + raise ValueError(msg) + + return values + + yield AddBGPSessionForm + + return {"subscription": subscription} + + +@step("Calculate list of all active PE routers") +def calculate_pe_router_list() -> State: + 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} + + +def _generate_pe_inventory(pe_router_list: list[Router]) -> dict[str, Any]: + return { + "_meta": { + "vars": { + router.router.router_fqdn: { + "lo4": router.router.router_lo_ipv4_address, + "lo6": router.router.router_lo_ipv6_address, + "vendor": router.router.vendor, + } + } for router in pe_router_list + }, + "all": { + "hosts": { + router.router.router_fqdn: None for router in pe_router_list + } + } + } + + +@step("[DRY RUN] Add P router to iBGP mesh") +def add_p_to_mesh_dry(subscription: Router, callback_route: str, pe_router_list: list[Router]) -> State: + extra_vars = { + "dry_run": True, + "subscription": subscription + } + + provisioning_proxy.execute_playbook( + playbook_name="update_ibgp_mesh.yaml", + callback_route=callback_route, + inventory=_generate_pe_inventory(pe_router_list), + extra_vars=extra_vars, + ) + + return {"subscription": subscription} + + +@step("[FOR REAL] Add P router to iBGP mesh") +def add_p_to_mesh_real(subscription: Router, callback_route: str, pe_router_list: list[Router]) -> State: + extra_vars = { + "dry_run": False, + "subscription": subscription + } + + provisioning_proxy.execute_playbook( + playbook_name="update_ibgp_mesh.yaml", + callback_route=callback_route, + inventory=_generate_pe_inventory(pe_router_list), + extra_vars=extra_vars, + ) + + return {"subscription": subscription} + + +@step("[DRY RUN] Add all PE routers to P router iBGP table") +def add_all_pe_to_p_dry(subscription: Router, pe_router_list: list[Router], callback_route: str) -> State: + extra_vars = { + "dry_run": True, + "pe_router_list": { + router.router.router_fqdn: { + "lo4": router.router.router_lo_ipv4_address, + "lo6": router.router.router_lo_ipv6_address, + "vendor": router.router.vendor, + } for router in pe_router_list + }, + } + + inventory = { + "all": { + "hosts": { + router.router.router_fqdn: None + } for router in pe_router_list + } + } + + provisioning_proxy.execute_playbook( + playbook_name="update_ibgp_mesh.yaml", + callback_route=callback_route, + inventory=inventory, + extra_vars=extra_vars, + ) + + return {"subscription": subscription} + + +@step("[FOR REAL] Add all PE routers to P router iBGP table") +def add_all_pe_to_p_real(subscription: Router, pe_router_list: list[Router], callback_route: str) -> State: + extra_vars = { + "dry_run": False, + "pe_router_list": { + router.router.router_fqdn: { + "lo4": router.router.router_lo_ipv4_address, + "lo6": router.router.router_lo_ipv6_address, + "vendor": router.router.vendor, + } for router in pe_router_list + }, + } + + inventory = { + "all": { + "hosts": { + router.router.router_fqdn: None + } for router in pe_router_list + } + } + + provisioning_proxy.execute_playbook( + playbook_name="update_ibgp_mesh.yaml", + callback_route=callback_route, + inventory=inventory, + extra_vars=extra_vars, + ) + + return {"subscription": subscription} + + +@step("Verify iBGP session health") +def check_ibgp_session(subscription: Router, callback_route: str) -> State: + inventory = { + "all": { + "hosts": { + subscription.router.router_fqdn: None + } + } + } + + provisioning_proxy.execute_playbook( + playbook_name="check_ibgp.yaml", + callback_route=callback_route, + inventory=inventory, + extra_vars={}, + ) + + return {"subscription": subscription} + + +@step("Add the router to LibreNMS") +def add_device_to_librenms(subscription: Router) -> State: + librenms_client.add_device(subscription) + + return {"subscription": subscription} + + +@step("Update subscription model") +def update_subscription_model(subscription: Router) -> State: + subscription.router.router_access_via_ts = False + + return {"subscription": subscription} + + +@workflow( + "Update iBGP mesh", + initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), + target=Target.MODIFY, +) +def update_ibgp_mesh() -> StepList: + """Update the iBGP mesh with a new P router + + * Add the new P-router to all other PE-routers in the network, including a dry run. + * Add all PE-routers to the P-router, including a dry run. + * Verify that the iBGP session is up. + * Add the new P-router to LibreNMS. + * Update the subscription model. + """ + return ( + init + >> store_process_subscription(Target.MODIFY) + >> unsync + >> calculate_pe_router_list + >> pp_interaction(add_p_to_mesh_dry) + >> pp_interaction(add_p_to_mesh_real) + >> pp_interaction(add_all_pe_to_p_dry) + >> pp_interaction(add_all_pe_to_p_real) + >> indifferent_pp_interaction(check_ibgp_session) + >> add_device_to_librenms + >> update_subscription_model + >> resync + >> done + )