diff --git a/gso/migrations/versions/2025-06-24_285954f5ec04_add_l3_service_redeploy_workflow.py b/gso/migrations/versions/2025-06-24_285954f5ec04_add_l3_service_redeploy_workflow.py new file mode 100644 index 0000000000000000000000000000000000000000..05d80fc80df115f34a91b7dec88c4b0ad10737f0 --- /dev/null +++ b/gso/migrations/versions/2025-06-24_285954f5ec04_add_l3_service_redeploy_workflow.py @@ -0,0 +1,42 @@ +"""Add L3 service redeploy workflow. + +Revision ID: 285954f5ec04 +Revises: b2b5137ef0c7 +Create Date: 2025-06-24 16:49:06.495691 + +""" +from alembic import op +from orchestrator.migrations.helpers import ( + add_products_to_workflow_by_product_tag, + create_workflow, + delete_workflow, + remove_products_from_workflow_by_product_tag +) + +# revision identifiers, used by Alembic. +revision = '285954f5ec04' +down_revision = 'b2b5137ef0c7' +branch_labels = None +depends_on = None + +new_workflow = { + "name": "redeploy_l3_core_service", + "target": "MODIFY", + "description": "Redeploy Layer 3 service", + "product_type": "GeantIP" +} +additional_product_tags = ["IAS", "LHC", "COP", "RE_LHCONE", "RE_PEER"] + + +def upgrade() -> None: + conn = op.get_bind() + create_workflow(conn, new_workflow) + for product in additional_product_tags: + add_products_to_workflow_by_product_tag(conn, new_workflow["name"], product) + + +def downgrade() -> None: + conn = op.get_bind() + for product in additional_product_tags: + remove_products_from_workflow_by_product_tag(conn, new_workflow["name"], product) + delete_workflow(conn, new_workflow["name"]) diff --git a/gso/translations/en-GB.json b/gso/translations/en-GB.json index 54bcbc3236f11348a3533cc69ba9261711abf660..d2fa50f4813747a140061cda8f108434fe7c8ea4 100644 --- a/gso/translations/en-GB.json +++ b/gso/translations/en-GB.json @@ -159,6 +159,7 @@ "modify_r_and_e_lhcone": "Modify R&E LHCONE", "promote_p_to_pe": "Promote P to PE", "redeploy_base_config": "Redeploy base config", + "redeploy_l3_core_service": "Redeploy Layer 3 service", "redeploy_vrf": "Redeploy VRF router list", "task_check_site_connectivity": "Check NETCONF connectivity of a Site", "task_clean_old_tasks": "Remove old cleanup tasks", diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py index 827d2a07c5db07c4e000c94b27fb982a496a90f2..d37cd65309eb7fdd4da0edfe5c8af299c77a7890 100644 --- a/gso/workflows/__init__.py +++ b/gso/workflows/__init__.py @@ -120,6 +120,9 @@ LazyWorkflowInstance("gso.workflows.edge_port.create_imported_edge_port", "creat LazyWorkflowInstance("gso.workflows.edge_port.import_edge_port", "import_edge_port") LazyWorkflowInstance("gso.workflows.edge_port.migrate_edge_port", "migrate_edge_port") +# All L3 core services +LazyWorkflowInstance("gso.workflows.l3_core_service.redeploy_l3_core_service", "redeploy_l3_core_service") + # IAS workflows LazyWorkflowInstance("gso.workflows.l3_core_service.ias.create_ias", "create_ias") LazyWorkflowInstance("gso.workflows.l3_core_service.ias.modify_ias", "modify_ias") diff --git a/gso/workflows/l3_core_service/redeploy_l3_core_service.py b/gso/workflows/l3_core_service/redeploy_l3_core_service.py new file mode 100644 index 0000000000000000000000000000000000000000..cfaaacc7f75dff4462488fd6519b30adf07b9054 --- /dev/null +++ b/gso/workflows/l3_core_service/redeploy_l3_core_service.py @@ -0,0 +1,87 @@ +"""Base functionality for modifying an L3 Core Service subscription.""" + +from typing import TypeAlias, cast + +from orchestrator import workflow +from orchestrator.domain import SubscriptionModel +from orchestrator.forms import FormPage +from orchestrator.targets import Target +from orchestrator.workflow import StepList, begin, done +from orchestrator.workflows.steps import resync, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form +from pydantic import ConfigDict +from pydantic_forms.types import FormGenerator, UUIDstr +from pydantic_forms.validators import Choice + +from gso.products.product_blocks.l3_core_service import AccessPort +from gso.products.product_types.edge_port import EdgePort +from gso.services.lso_client import lso_interaction +from gso.services.partners import get_partner_by_id +from gso.utils.types.tt_number import TTNumber +from gso.workflows.l3_core_service.base_create_l3_core_service import ( + deploy_bgp_peers_dry, + deploy_bgp_peers_real, + provision_sbp_dry, + provision_sbp_real, +) + + +def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: + """Get input which Access Port should be re-deployed.""" + subscription = SubscriptionModel.from_subscription(subscription_id) + product_name = subscription.product.name + + def access_port_selector() -> TypeAlias: + """Generate a dropdown selector for choosing an Access Port in an input form.""" + access_ports = subscription.l3_core.ap_list # type: ignore[attr-defined] + options = { + str(access_port.subscription_instance_id): ( + f"{access_port.sbp.gs_id} on " + f"{EdgePort.from_subscription(access_port.sbp.edge_port.owner_subscription_id).description} " + f"({access_port.ap_type})" + ) + for access_port in access_ports + } + + return cast( + type[Choice], + Choice.__call__( + "Select an Access Port", + zip(options.keys(), options.items(), strict=True), + ), + ) + + class AccessPortSelectionForm(FormPage): + model_config = ConfigDict(title=f"Re-deploy {product_name} subscription") + + tt_number: TTNumber + access_port: access_port_selector() # type: ignore[valid-type] + + user_input = yield AccessPortSelectionForm + partner_name = get_partner_by_id(subscription.customer_id).name + access_port = AccessPort.from_db(user_input.access_port) + access_port_fqdn = EdgePort.from_subscription( + access_port.sbp.edge_port.owner_subscription_id + ).edge_port.node.router_fqdn + + return user_input.model_dump() | {"edge_port_fqdn_list": [access_port_fqdn], "partner_name": partner_name} + + +@workflow( + "Redeploy Layer 3 service", + initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), + target=Target.MODIFY, +) +def redeploy_l3_core_service() -> StepList: + """Redeploy a Layer 3 subscription.""" + return ( + begin + >> store_process_subscription(Target.MODIFY) + >> unsync + >> lso_interaction(provision_sbp_dry) + >> lso_interaction(provision_sbp_real) + >> lso_interaction(deploy_bgp_peers_dry) + >> lso_interaction(deploy_bgp_peers_real) + >> resync + >> done + ) diff --git a/test/workflows/l3_core_service/test_redeploy_l3_core_service.py b/test/workflows/l3_core_service/test_redeploy_l3_core_service.py new file mode 100644 index 0000000000000000000000000000000000000000..46b7c52b53b6568379aeb8d9737ba25689a84dbc --- /dev/null +++ b/test/workflows/l3_core_service/test_redeploy_l3_core_service.py @@ -0,0 +1,88 @@ +from copy import deepcopy + +import pytest +from orchestrator.domain import SubscriptionModel + +from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES +from test.workflows import assert_complete, assert_lso_interaction_success, extract_state, run_workflow + + +@pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES) +@pytest.mark.workflow() +def test_redeploy_l3_core_service_success(faker, l3_core_service_subscription_factory, product_name): + subscription = l3_core_service_subscription_factory(product_name=product_name) + old_subscription: SubscriptionModel = deepcopy(subscription) + access_port = subscription.l3_core.ap_list[0] + input_form_data = [ + {"subscription_id": str(subscription.subscription_id)}, + {"tt_number": faker.tt_number(), "access_port": str(access_port.subscription_instance_id)}, + ] + + result, process_stat, step_log = run_workflow("redeploy_l3_core_service", input_form_data) + + for _ in range(4): + result, step_log = assert_lso_interaction_success(result, process_stat, step_log) + + assert_complete(result) + state = extract_state(result) + subscription = SubscriptionModel.from_subscription(state["subscription_id"]) + ap_list = subscription.l3_core.ap_list + old_ap_list = old_subscription.l3_core.ap_list + + # Assertions that ensure the subscription is unchanged + for old_access_port, access_port in zip(old_ap_list, ap_list, strict=False): + assert access_port.sbp.gs_id == old_access_port.sbp.gs_id + assert access_port.sbp.is_tagged == old_access_port.sbp.is_tagged + assert access_port.sbp.vlan_id == old_access_port.sbp.vlan_id + assert str(access_port.sbp.ipv4_address) == str(old_access_port.sbp.ipv4_address) + assert access_port.sbp.ipv4_mask == old_access_port.sbp.ipv4_mask + assert str(access_port.sbp.ipv6_address) == str(old_access_port.sbp.ipv6_address) + assert access_port.sbp.ipv6_mask == old_access_port.sbp.ipv6_mask + assert access_port.sbp.custom_firewall_filters == old_access_port.sbp.custom_firewall_filters + + assert access_port.sbp.bgp_session_list[0].bfd_enabled == old_access_port.sbp.bgp_session_list[0].bfd_enabled + assert ( + access_port.sbp.bgp_session_list[0].has_custom_policies + == old_access_port.sbp.bgp_session_list[0].has_custom_policies + ) + assert ( + access_port.sbp.bgp_session_list[0].authentication_key + == old_access_port.sbp.bgp_session_list[0].authentication_key + ) + assert ( + access_port.sbp.bgp_session_list[0].multipath_enabled + == old_access_port.sbp.bgp_session_list[0].multipath_enabled + ) + assert ( + access_port.sbp.bgp_session_list[0].send_default_route + == old_access_port.sbp.bgp_session_list[0].send_default_route + ) + assert access_port.sbp.bgp_session_list[0].is_passive == old_access_port.sbp.bgp_session_list[0].is_passive + + assert access_port.sbp.bgp_session_list[1].bfd_enabled == old_access_port.sbp.bgp_session_list[1].bfd_enabled + assert ( + access_port.sbp.bgp_session_list[1].has_custom_policies + == old_access_port.sbp.bgp_session_list[1].has_custom_policies + ) + assert ( + access_port.sbp.bgp_session_list[1].authentication_key + == old_access_port.sbp.bgp_session_list[1].authentication_key + ) + assert ( + access_port.sbp.bgp_session_list[1].multipath_enabled + == old_access_port.sbp.bgp_session_list[1].multipath_enabled + ) + assert ( + access_port.sbp.bgp_session_list[1].send_default_route + == old_access_port.sbp.bgp_session_list[1].send_default_route + ) + assert access_port.sbp.bgp_session_list[1].is_passive == old_access_port.sbp.bgp_session_list[1].is_passive + + assert access_port.sbp.v4_bfd_settings.bfd_enabled == old_access_port.sbp.v4_bfd_settings.bfd_enabled + assert access_port.sbp.v4_bfd_settings.bfd_interval_rx == old_access_port.sbp.v4_bfd_settings.bfd_interval_rx + assert access_port.sbp.v4_bfd_settings.bfd_interval_tx == old_access_port.sbp.v4_bfd_settings.bfd_interval_tx + assert access_port.sbp.v4_bfd_settings.bfd_multiplier == old_access_port.sbp.v4_bfd_settings.bfd_multiplier + assert access_port.sbp.v6_bfd_settings.bfd_enabled == old_access_port.sbp.v6_bfd_settings.bfd_enabled + assert access_port.sbp.v6_bfd_settings.bfd_interval_rx == old_access_port.sbp.v6_bfd_settings.bfd_interval_rx + assert access_port.sbp.v6_bfd_settings.bfd_interval_tx == old_access_port.sbp.v6_bfd_settings.bfd_interval_tx + assert access_port.sbp.v6_bfd_settings.bfd_multiplier == old_access_port.sbp.v6_bfd_settings.bfd_multiplier