From 3dfe7933fca1c0f4a0b63debc12fc903e6d5ddee Mon Sep 17 00:00:00 2001 From: Mohammad Torkashvand <mohammad.torkashvand@geant.org> Date: Thu, 6 Feb 2025 17:00:43 +0100 Subject: [PATCH 1/8] add bulk migration for an edge port --- gso/__init__.py | 26 +- ..._add_migration_workflow_for_an_edgeport.py | 39 ++ gso/oss-params-example.json | 2 +- gso/services/subscriptions.py | 59 ++- gso/tasks/__init__.py | 1 + gso/tasks/start_process.py | 11 + gso/translations/en-GB.json | 1 + gso/utils/workflow_steps.py | 4 +- gso/worker.py | 1 + gso/workflows/__init__.py | 1 + gso/workflows/edge_port/migrate_edge_port.py | 380 ++++++++++++++++++ .../l2_circuit/migrate_layer2_circuit.py | 20 + .../l2_circuit/modify_layer_2_circuit.py | 3 + .../l3_core_service/create_l3_core_service.py | 6 +- test/workflows/__init__.py | 17 + .../edge_port/test_create_edge_port.py | 13 +- .../edge_port/test_migrate_edge_port.py | 133 ++++++ .../test_create_l3_core_service.py | 7 +- 18 files changed, 698 insertions(+), 26 deletions(-) create mode 100644 gso/migrations/versions/2025-02-06_efebcde91f2f_add_migration_workflow_for_an_edgeport.py create mode 100644 gso/tasks/__init__.py create mode 100644 gso/tasks/start_process.py create mode 100644 gso/workflows/edge_port/migrate_edge_port.py create mode 100644 gso/workflows/l2_circuit/migrate_layer2_circuit.py create mode 100644 test/workflows/edge_port/test_migrate_edge_port.py diff --git a/gso/__init__.py b/gso/__init__.py index ba8f1bdc..610b1098 100644 --- a/gso/__init__.py +++ b/gso/__init__.py @@ -7,7 +7,6 @@ from orchestrator import OrchestratorCore, app_settings from orchestrator.cli.main import app as cli_app from orchestrator.graphql import SCALAR_OVERRIDES from orchestrator.services.tasks import initialise_celery -from orchestrator.settings import ExecutorType # noinspection PyUnresolvedReferences import gso.products @@ -37,18 +36,19 @@ def init_gso_app() -> OrchestratorCore: app.register_graphql(subscription_interface=custom_subscription_interface) app.include_router(api_router, prefix="/api") - if app_settings.EXECUTOR == ExecutorType.WORKER: - config = load_oss_params() - celery = Celery( - "geant-service-orchestrator", - broker=config.CELERY.broker_url, - backend=config.CELERY.result_backend, - include=["orchestrator.services.tasks"], - ) - celery.conf.update( - result_expires=config.CELERY.result_expires, - ) - gso_initialise_celery(celery) + oss_params = load_oss_params() + celery = Celery( + "geant-service-orchestrator", + broker=oss_params.CELERY.broker_url, + backend=oss_params.CELERY.result_backend, + include=["orchestrator.services.tasks", "gso.tasks.start_process"], + ) + celery.conf.update( + result_expires=oss_params.CELERY.result_expires, + task_always_eager=app_settings.TESTING, + task_eager_propagates=app_settings.TESTING, + ) + gso_initialise_celery(celery) return app diff --git a/gso/migrations/versions/2025-02-06_efebcde91f2f_add_migration_workflow_for_an_edgeport.py b/gso/migrations/versions/2025-02-06_efebcde91f2f_add_migration_workflow_for_an_edgeport.py new file mode 100644 index 00000000..1a9e9db7 --- /dev/null +++ b/gso/migrations/versions/2025-02-06_efebcde91f2f_add_migration_workflow_for_an_edgeport.py @@ -0,0 +1,39 @@ +"""Add a migration workflow for an EdgePort. + +Revision ID: efebcde91f2f +Revises: 8a65d0ed588e +Create Date: 2025-01-09 17:17:24.972289 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'efebcde91f2f' +down_revision = '16eef776a258' +branch_labels = None +depends_on = None + + +from orchestrator.migrations.helpers import create_workflow, delete_workflow + +new_workflows = [ + { + "name": "migrate_edge_port", + "target": "MODIFY", + "description": "Migrate an EdgePort", + "product_type": "EdgePort" + }, +] + + +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/oss-params-example.json b/gso/oss-params-example.json index ac0a29de..1a1495e4 100644 --- a/gso/oss-params-example.json +++ b/gso/oss-params-example.json @@ -133,6 +133,6 @@ }, "MOODI": { "host": "moodi.test.gap.geant.org", - "moodi_enabled": false + "moodi_enabled": true } } diff --git a/gso/services/subscriptions.py b/gso/services/subscriptions.py index e83f7831..c1352981 100644 --- a/gso/services/subscriptions.py +++ b/gso/services/subscriptions.py @@ -17,12 +17,12 @@ from orchestrator.db import ( db, ) from orchestrator.domain import SubscriptionModel -from orchestrator.services.subscriptions import query_in_use_by_subscriptions +from orchestrator.services.subscriptions import query_depends_on_subscriptions, query_in_use_by_subscriptions from orchestrator.types import SubscriptionLifecycle, UUIDstr -from sqlalchemy import text +from sqlalchemy import and_, text from sqlalchemy.exc import SQLAlchemyError -from gso.products import ProductName, ProductType +from gso.products import L2_CORE_SERVICE_PRODUCT_TYPE, L3_CORE_SERVICE_PRODUCT_TYPE, ProductName, ProductType from gso.products.product_types.site import Site SubscriptionType = dict[str, Any] @@ -185,6 +185,59 @@ def get_trunks_that_terminate_on_router( ) +def get_all_active_l3_core_services_on_top_of_edge_port(edge_port_id: UUIDstr) -> list[SubscriptionTable]: + """Retrieve all active l3 core services that are on top of the given edge port. + + Args: + edge_port_id: The ID of the edge port. + + Returns: + A list of active services that are on top of the edge port. + """ + return ( + query_in_use_by_subscriptions(UUID(edge_port_id)) + .join(ProductTable) + .filter( + and_( + ProductTable.product_type.in_([L3_CORE_SERVICE_PRODUCT_TYPE]), + SubscriptionTable.status == SubscriptionLifecycle.ACTIVE, + ) + ) + .all() + ) + + +def get_all_active_l2_core_services_on_top_of_edge_port(edge_port_id: UUIDstr) -> list[SubscriptionTable]: + """Retrieve all active l2 core services that are on top of the given edge port.""" + return ( + query_in_use_by_subscriptions(UUID(edge_port_id)) + .join(ProductTable) + .filter( + and_( + ProductTable.product_type.in_([L2_CORE_SERVICE_PRODUCT_TYPE]), + SubscriptionTable.status == SubscriptionLifecycle.ACTIVE, + ) + ) + .all() + ) + + +def get_edge_port_subscription_of_l3_core_service(edge_port_subscription_id: UUIDstr) -> list[SubscriptionModel]: + """Retrieve all active Edge Port subscriptions that are used by the given L3 Core Service.""" + query = ( + query_depends_on_subscriptions( + UUID(edge_port_subscription_id), filter_statuses=[SubscriptionLifecycle.ACTIVE.value] + ) + .join(ProductTable) + .filter( + ProductTable.product_type.in_([ProductType.EDGE_PORT]), + SubscriptionTable.status == SubscriptionLifecycle.ACTIVE, + ) + ) + + return [SubscriptionModel.from_subscription(subscription.subscription_id) for subscription in query.all()] + + def get_product_id_by_name(product_name: ProductName) -> UUID: """Retrieve the UUID of a product by its name. diff --git a/gso/tasks/__init__.py b/gso/tasks/__init__.py new file mode 100644 index 00000000..cc85efbe --- /dev/null +++ b/gso/tasks/__init__.py @@ -0,0 +1 @@ +"""celery tasks for gso.""" diff --git a/gso/tasks/start_process.py b/gso/tasks/start_process.py new file mode 100644 index 00000000..5760bffc --- /dev/null +++ b/gso/tasks/start_process.py @@ -0,0 +1,11 @@ +"""Celery task to start a process with the given workflow key and state.""" + +from celery import shared_task + + +@shared_task +def start_process_task(workflow_key: str, user_inputs: list[dict[str, str]]) -> None: + """Start a process with the given workflow key and state.""" + from orchestrator.services.processes import start_process # noqa: PLC0415 + + start_process(workflow_key, user_inputs=user_inputs) diff --git a/gso/translations/en-GB.json b/gso/translations/en-GB.json index 1c6561b2..c5d17a43 100644 --- a/gso/translations/en-GB.json +++ b/gso/translations/en-GB.json @@ -52,6 +52,7 @@ "create_site": "Create Site", "create_switch": "Create Switch", "create_edge_port": "Create Edge Port", + "migrate_edge_port": "Migrate Edge Port", "create_l3_core_service": "Create L3 Core Service", "create_lan_switch_interconnect": "Create LAN Switch Interconnect", "deploy_twamp": "Deploy TWAMP", diff --git a/gso/utils/workflow_steps.py b/gso/utils/workflow_steps.py index bfa88b49..a63d6a7e 100644 --- a/gso/utils/workflow_steps.py +++ b/gso/utils/workflow_steps.py @@ -22,6 +22,8 @@ from gso.settings import load_oss_params from gso.utils.helpers import generate_inventory_for_routers from gso.utils.shared_enums import Vendor +SKIP_MOODI_KEY = "__skip_moodi__" + def _deploy_base_config( subscription: dict[str, Any], @@ -394,7 +396,7 @@ def prompt_sharepoint_checklist_url(checklist_url: str) -> FormGenerator: return {} -_is_moodi_enabled = conditional(lambda _: load_oss_params().MOODI.moodi_enabled) +_is_moodi_enabled = conditional(lambda state: load_oss_params().MOODI.moodi_enabled and not state.get(SKIP_MOODI_KEY)) def start_moodi() -> StepList: diff --git a/gso/worker.py b/gso/worker.py index f7bfa083..3a28ba92 100644 --- a/gso/worker.py +++ b/gso/worker.py @@ -85,6 +85,7 @@ celery = OrchestratorWorker( "gso.schedules.send_email_notifications", "gso.schedules.clean_old_tasks", "orchestrator.services.tasks", + "gso.tasks.start_process", ], ) diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py index 92710b59..74f67c01 100644 --- a/gso/workflows/__init__.py +++ b/gso/workflows/__init__.py @@ -116,6 +116,7 @@ LazyWorkflowInstance("gso.workflows.edge_port.terminate_edge_port", "terminate_e LazyWorkflowInstance("gso.workflows.edge_port.validate_edge_port", "validate_edge_port") LazyWorkflowInstance("gso.workflows.edge_port.create_imported_edge_port", "create_imported_edge_port") LazyWorkflowInstance("gso.workflows.edge_port.import_edge_port", "import_edge_port") +LazyWorkflowInstance("gso.workflows.edge_port.migrate_edge_port", "migrate_edge_port") # L3 Core Service workflows LazyWorkflowInstance("gso.workflows.l3_core_service.create_l3_core_service", "create_l3_core_service") diff --git a/gso/workflows/edge_port/migrate_edge_port.py b/gso/workflows/edge_port/migrate_edge_port.py new file mode 100644 index 00000000..0e399f3f --- /dev/null +++ b/gso/workflows/edge_port/migrate_edge_port.py @@ -0,0 +1,380 @@ +"""A modification workflow that migrates an EdgePort to a different endpoint.""" + +import random +from typing import Annotated, Any +from uuid import uuid4 + +from annotated_types import Len +from orchestrator import step, workflow +from orchestrator.config.assignee import Assignee +from orchestrator.forms import FormPage +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 +from orchestrator.workflows.steps import resync, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form +from pydantic import AfterValidator, ConfigDict +from pydantic_forms.validators import Label, ReadOnlyField, validate_unique_list +from pynetbox.models.dcim import Interfaces + +from gso.products.product_blocks.edge_port import EdgePortAEMemberBlock +from gso.products.product_types.edge_port import EdgePort +from gso.products.product_types.router import Router +from gso.services.lso_client import LSOState, lso_interaction +from gso.services.netbox_client import NetboxClient +from gso.services.partners import get_partner_by_id +from gso.services.subscriptions import ( + get_all_active_l2_core_services_on_top_of_edge_port, + get_all_active_l3_core_services_on_top_of_edge_port, +) +from gso.tasks.start_process import start_process_task +from gso.utils.helpers import ( + active_pe_router_selector, + available_interfaces_choices, + available_service_lags_choices, +) +from gso.utils.types.interfaces import LAGMember +from gso.utils.types.tt_number import TTNumber +from gso.utils.workflow_steps import start_moodi, stop_moodi +from gso.workflows.shared import create_summary_form + + +def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: + """Gather input from the operator on the new router that the EdgePort should connect to.""" + subscription = EdgePort.from_subscription(subscription_id) + form_title = f"Migrating {subscription.edge_port.edge_port_description} " + + class MigrateEdgePortForm(FormPage): + model_config = ConfigDict(title=form_title) + + tt_number: TTNumber + partner_name: ReadOnlyField(get_partner_by_id(subscription.customer_id).name, default_type=str) # type: ignore[valid-type] + node: active_pe_router_selector(excludes=[subscription.edge_port.node.subscription.subscription_id]) # type: ignore[valid-type] + + initial_user_input = yield MigrateEdgePortForm + + class EdgePortLAGMember(LAGMember): + interface_name: available_interfaces_choices( # type: ignore[valid-type] + router_id=initial_user_input.node, speed=subscription.edge_port.member_speed + ) + + lag_ae_members = Annotated[ + list[EdgePortLAGMember], + AfterValidator(validate_unique_list), + Len( + min_length=len(subscription.edge_port.edge_port_ae_members), + max_length=len(subscription.edge_port.edge_port_ae_members), + ), + ] + + class SelectInterfaceForm(FormPage): + model_config = ConfigDict(title="Select Interfaces") + + name: available_service_lags_choices(router_id=initial_user_input.node) # type: ignore[valid-type] + description: str | None = None + ae_members: lag_ae_members + + interface_form_input_data = yield SelectInterfaceForm + + input_forms_data = initial_user_input.model_dump() | interface_form_input_data.model_dump() + summary_form_data = input_forms_data | { + "node": Router.from_subscription(initial_user_input.node).router.router_fqdn, + "partner_name": initial_user_input.partner_name, + "edge_port_name": input_forms_data["name"], + "edge_port_description": input_forms_data["description"], + "edge_port_ae_members": input_forms_data["ae_members"], + } + summary_fields = [ + "node", + "partner_name", + "edge_port_name", + "edge_port_description", + "edge_port_ae_members", + ] + yield from create_summary_form(summary_form_data, subscription.product.name, summary_fields) + return input_forms_data + + +@step("Update the EdgePort references") +def update_subscription_model( + subscription: EdgePort, + node: UUIDstr, + name: str, + partner_name: str, + ae_members: list[dict[str, Any]], + description: str | None = None, +) -> State: + """Update the EdgePort subscription object in the service database with the new values.""" + router = Router.from_subscription(node).router + subscription.edge_port.node = router + subscription.edge_port.edge_port_name = name + subscription.description = ( + f"Edge Port {name} on {router.router_fqdn}, {partner_name}, {subscription.edge_port.ga_id or ""}" + ) + subscription.edge_port.edge_port_description = description + edge_port_ae_members = [EdgePortAEMemberBlock.new(subscription_id=uuid4(), **member) for member in ae_members] + subscription.edge_port.edge_port_ae_members = ( + edge_port_ae_members # TODO: Clear the existing AE members and inactivate them and remove them from netbox? + ) + + return {"subscription": subscription, "subscription_id": subscription.subscription_id} + + +@step("Reserve interfaces in NetBox") +def reserve_interfaces_in_netbox(subscription: EdgePort) -> State: + """Create the LAG interfaces in NetBox and attach the LAG interfaces to the physical interfaces.""" + nbclient = NetboxClient() + edge_port = subscription.edge_port + # Create LAG interfaces + lag_interface: Interfaces = nbclient.create_interface( + iface_name=edge_port.edge_port_name, + interface_type="lag", + device_name=edge_port.node.router_fqdn, + description=str(subscription.subscription_id), + enabled=True, + ) + # Attach physical interfaces to LAG + # Update interface description to subscription ID + # Reserve interfaces + for interface in edge_port.edge_port_ae_members: + nbclient.attach_interface_to_lag( + device_name=edge_port.node.router_fqdn, + lag_name=lag_interface.name, + iface_name=interface.interface_name, + description=str(subscription.subscription_id), + ) + nbclient.reserve_interface( + device_name=edge_port.node.router_fqdn, + iface_name=interface.interface_name, + ) + return { + "subscription": subscription, + } + + +@step("[DRY RUN] Create edge port") +def create_edge_port_dry( + subscription: dict[str, Any], tt_number: str, process_id: UUIDstr, partner_name: str +) -> LSOState: + """Create a new edge port in the network as a dry run.""" + extra_vars = { + "dry_run": True, + "subscription": subscription, + "partner_name": partner_name, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Create Edge Port", + "verb": "create", + } + + return { + "playbook_name": "gap_ansible/playbooks/edge_port.yaml", + "inventory": {"all": {"hosts": {subscription["edge_port"]["node"]["router_fqdn"]: None}}}, + "extra_vars": extra_vars, + } + + +@step("[FOR REAL] Create edge port") +def create_edge_port_real( + subscription: dict[str, Any], tt_number: str, process_id: UUIDstr, partner_name: str +) -> LSOState: + """Create a new edge port in the network for real.""" + extra_vars = { + "dry_run": False, + "subscription": subscription, + "partner_name": partner_name, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Create Edge Port", + "verb": "create", + } + + return { + "playbook_name": "gap_ansible/playbooks/edge_port.yaml", + "inventory": {"all": {"hosts": {subscription["edge_port"]["node"]["router_fqdn"]: None}}}, + "extra_vars": extra_vars, + } + + +@step("Allocate interfaces in NetBox") +def allocate_interfaces_in_netbox(subscription: EdgePort) -> None: + """Allocate the interfaces in NetBox.""" + for interface in subscription.edge_port.edge_port_ae_members: + fqdn = subscription.edge_port.node.router_fqdn + iface_name = interface.interface_name + if not fqdn or not iface_name: + msg = "FQDN and/or interface name missing in subscription" + raise ProcessFailureError(msg, details=subscription.subscription_id) + + NetboxClient().allocate_interface(device_name=fqdn, iface_name=iface_name) + + +@inputstep("Wait for confirmation", assignee=Assignee.SYSTEM) +def confirm_continue_move_fiber() -> FormGenerator: + """Wait for confirmation from an operator that the physical fiber has been moved.""" + + class ProvisioningResultPage(FormPage): + model_config = ConfigDict(title="Please confirm before continuing") + + info_label: Label = "New EdgePort has been deployed, wait for the physical connection to be moved." + + yield ProvisioningResultPage + + return {} + + +@inputstep("Wait for confirmation", assignee=Assignee.SYSTEM) +def confirm_things_looks_good_in_moodi() -> FormGenerator: + """Wait for confirmation from an operator that the new Migration looks good so far.""" + + class ProvisioningResultPage(FormPage): + model_config = ConfigDict(title="Please confirm before continuing") + + info_label: Label = "Do you confirm that everything looks good in the Moodi before continuing?" + + yield ProvisioningResultPage + + return {} + + +@step("Migrate L3 core services to new node") +def migrate_l3_core_services_to_new_node(subscription_id: UUIDstr, tt_number: TTNumber) -> State: + """Migrate all L3 core services from the old EdgePort to the new EdgePort. + + This sub migrations do not modify the L3 core services. + The source and destination EdgePort remain the same for each service. + The migration playbook is executed once for each service to apply the configuration on the new node and as a result, + the service bindings port and bgp sessions related to this edge port of each service will be moved to the new node. + """ + l3_core_services = get_all_active_l3_core_services_on_top_of_edge_port(subscription_id) + edge_port = EdgePort.from_subscription(subscription_id) + + for l3_core_service in l3_core_services: + start_process_task.apply_async( # type: ignore[attr-defined] + args=[ + "migrate_l3_core_service", + [ + {"subscription_id": str(l3_core_service.subscription_id)}, + { + "tt_number": tt_number, + "skip_moodi": True, + "source_edge_port": str(edge_port.subscription_id), + "destination_edge_port": str(edge_port.subscription_id), + }, + ], + ], + countdown=random.choice([2, 3, 4, 5]), # noqa: S311 + ) + + return {"l3_core_services": l3_core_services} + + +@step("Migrate L2 circuits to new node") +def migrate_l2_circuits_to_new_node(subscription_id: UUIDstr, tt_number: TTNumber) -> State: + """Migrate Layer2 circuits from the old EdgePort to the new EdgePort.""" + layer2_circuits = get_all_active_l2_core_services_on_top_of_edge_port(subscription_id) + edge_port = EdgePort.from_subscription(subscription_id) + + for l2_core_service in layer2_circuits: + start_process_task.apply_async( # type: ignore[attr-defined] + args=[ + "migrate_layer_2_circuit", + [ + {"subscription_id": str(l2_core_service.subscription_id)}, + { + "tt_number": tt_number, + "skip_moodi": True, + "source_edge_port": str(edge_port.subscription_id), + "destination_edge_port": str(edge_port.subscription_id), + }, + ], + ], + countdown=random.choice([2, 3, 4, 5]), # noqa: S311 + ) + + return {"layer2_circuits": layer2_circuits} + + +@step("[DRY RUN] Disable configuration on old router") +def disable_old_config_dry( + # subscription: EdgePort, TODO + process_id: UUIDstr, + tt_number: str, +) -> LSOState: + """Perform a dry run of disabling the old configuration on the routers.""" + extra_vars = { + "verb": "deactivate", + "config_object": "deactivate", + "dry_run": True, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} " f"- Deploy config for #TODO", + } + + return { + "playbook_name": "gap_ansible/playbooks/iptrunks_migration.yaml", + "inventory": { + "all": { + "hosts": { + # TODO + } + } + }, + "extra_vars": extra_vars, + } + + +@step("[FOR REAL] Disable configuration on old router") +def disable_old_config_real( + # subscription: EdgePort, TODO + process_id: UUIDstr, + tt_number: str, +) -> LSOState: + """Disable old configuration on the routers.""" + extra_vars = { + "verb": "deactivate", + "config_object": "deactivate", + "dry_run": False, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} " f"- Deploy config for # TODO", + } + + return { + "playbook_name": "gap_ansible/playbooks/iptrunks_migration.yaml", + "inventory": { + "all": { + "hosts": { + # TODO + } + } + }, + "extra_vars": extra_vars, + } + + +@workflow( + "Migrate an EdgePort", + initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), + target=Target.MODIFY, +) +def migrate_edge_port() -> StepList: + """Migrate an EdgePort.""" + return ( + begin + >> store_process_subscription(Target.MODIFY) + >> unsync + >> lso_interaction(disable_old_config_dry) + >> lso_interaction(disable_old_config_real) + >> update_subscription_model + >> start_moodi() + # TODO: Neda mentioned if in the future we have nokia-to-nokia migration, then we need another step to + # dealocate the lag-- but this is not needed for now right? + >> reserve_interfaces_in_netbox + >> lso_interaction(create_edge_port_dry) + >> lso_interaction(create_edge_port_real) + >> confirm_continue_move_fiber + >> confirm_things_looks_good_in_moodi + >> resync + # TODO: Explain this step that at this point related subscriptions are unsynced and prevent + # any changes on l3/l2 services + >> migrate_l3_core_services_to_new_node + >> confirm_things_looks_good_in_moodi + >> migrate_l2_circuits_to_new_node + >> confirm_things_looks_good_in_moodi + >> stop_moodi() + >> done + ) diff --git a/gso/workflows/l2_circuit/migrate_layer2_circuit.py b/gso/workflows/l2_circuit/migrate_layer2_circuit.py new file mode 100644 index 00000000..9fdeb294 --- /dev/null +++ b/gso/workflows/l2_circuit/migrate_layer2_circuit.py @@ -0,0 +1,20 @@ +"""This workflow migrates an L2 Core Service to a new Edge Port. + +It can be triggered by an operator or automatically by the system during Edge Port migration which is a separate +workflow. + +System-triggered migration: +When the system migrates an Edge Port, it runs the workflow automatically. The source and destination Edge Ports are +set to the same values. Then here migration only applies the configuration to the router and fill the drift between +core DB as source of truth and the actual network since the intent of network has changed in the previous workflow +even though the L2 Circuit Service is not changed. + +Operator-triggered migration: +When an operator initiates the workflow, they are required to specify both the source and destination EdgePorts. +During the migration process, the system updates the related access_point.sbp.edge_port reference to replace the source +EdgePort with the destination EdgePort and applies the necessary configuration changes to the router. + +Important Note: +Since an L2 Circuit Service has multiple side, the workflow must be run separately for each side to fully +migrate the service. +""" diff --git a/gso/workflows/l2_circuit/modify_layer_2_circuit.py b/gso/workflows/l2_circuit/modify_layer_2_circuit.py index 6e378a77..57f0bf25 100644 --- a/gso/workflows/l2_circuit/modify_layer_2_circuit.py +++ b/gso/workflows/l2_circuit/modify_layer_2_circuit.py @@ -115,3 +115,6 @@ def modify_layer_2_circuit() -> StepList: >> resync >> done ) + + +# TODO: where is ansible playbook execution steps to reflect this changes on the network? diff --git a/gso/workflows/l3_core_service/create_l3_core_service.py b/gso/workflows/l3_core_service/create_l3_core_service.py index 3069bdac..fcd73b1a 100644 --- a/gso/workflows/l3_core_service/create_l3_core_service.py +++ b/gso/workflows/l3_core_service/create_l3_core_service.py @@ -201,14 +201,16 @@ def initialize_subscription( gs_id=sbp_gs_id, ) subscription.l3_core_service.ap_list.append( - AccessPortInactive.new( + AccessPortInactive.new( # TODO: Ask Simone why we don't active them somewhere? subscription_id=uuid4(), ap_type=edge_port["ap_type"], sbp=service_binding_port, custom_service_name=edge_port.get("custom_service_name"), ) ) - edge_port_fqdn_list.append(edge_port_subscription.edge_port.node.router_fqdn) + edge_port_fqdn_list.append( + edge_port_subscription.edge_port.node.router_fqdn + ) # TODO: why this is only one EP? what if we have multiple EP for an L3 Core Service partner_name = get_partner_by_id(subscription.customer_id).name subscription.description = f"{product_name} service for {partner_name}" diff --git a/test/workflows/__init__.py b/test/workflows/__init__.py index d01dde08..6d53a4d5 100644 --- a/test/workflows/__init__.py +++ b/test/workflows/__init__.py @@ -272,6 +272,16 @@ def resume_workflow( return result, step_log +def resume_suspend_workflow( + result, + process: ProcessStat, + step_log: list[tuple[Step, Process]], + input_data: State | list[State], +) -> tuple[Process, list]: + assert_suspended(result) + return resume_workflow(process, step_log, input_data) + + def assert_lso_success(result: Process, process_stat: ProcessStat, step_log: list): """Assert a successful LSO execution in a workflow.""" assert_awaiting_callback(result) @@ -304,3 +314,10 @@ def assert_lso_interaction_failure(result: Process, process_stat: ProcessStat, s assert_failed(result) return result, step_log + + +def assert_stop_moodi(result: Process, process_stat: ProcessStat, step_log: list): + """Assert a successful LSO execution in a workflow.""" + assert_suspended(result) + result, step_log = resume_workflow(process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) + return assert_lso_success(result, process_stat, step_log) diff --git a/test/workflows/edge_port/test_create_edge_port.py b/test/workflows/edge_port/test_create_edge_port.py index 4a4e9770..145dbbc0 100644 --- a/test/workflows/edge_port/test_create_edge_port.py +++ b/test/workflows/edge_port/test_create_edge_port.py @@ -14,6 +14,7 @@ from test.services.conftest import MockedNetboxClient from test.workflows import ( assert_complete, assert_lso_interaction_success, + assert_stop_moodi, extract_state, run_workflow, ) @@ -91,9 +92,11 @@ def test_successful_edge_port_creation( initial_data = [{"product": product_id}, *input_form_wizard_data] result, process_stat, step_log = run_workflow("create_edge_port", initial_data) - for _ in range(2): + for _ in range(3): result, step_log = assert_lso_interaction_success(result, process_stat, step_log) + result, step_log = assert_stop_moodi(result, process_stat, step_log) + assert_complete(result) state = extract_state(result) @@ -105,7 +108,7 @@ def test_successful_edge_port_creation( assert subscription.edge_port.ga_id == "GA-12345" assert subscription.description == f"Edge Port lag-21 on {router_fqdn}, GAAR, {subscription.edge_port.ga_id}" assert len(subscription.edge_port.edge_port_ae_members) == 2 - assert mock_execute_playbook.call_count == 2 + assert mock_execute_playbook.call_count == 4 @pytest.mark.workflow() @@ -123,9 +126,11 @@ def test_successful_edge_port_creation_with_auto_ga_id_creation( initial_data[1]["ga_id"] = None result, process_stat, step_log = run_workflow("create_edge_port", initial_data) - for _ in range(2): + for _ in range(3): result, step_log = assert_lso_interaction_success(result, process_stat, step_log) + result, step_log = assert_stop_moodi(result, process_stat, step_log) + assert_complete(result) state = extract_state(result) @@ -134,7 +139,7 @@ def test_successful_edge_port_creation_with_auto_ga_id_creation( assert subscription.status == "active" assert subscription.edge_port.ga_id.startswith("GA-5000") - assert mock_execute_playbook.call_count == 2 + assert mock_execute_playbook.call_count == 4 def test_edge_port_creation_with_invalid_input( diff --git a/test/workflows/edge_port/test_migrate_edge_port.py b/test/workflows/edge_port/test_migrate_edge_port.py new file mode 100644 index 00000000..1709b0b3 --- /dev/null +++ b/test/workflows/edge_port/test_migrate_edge_port.py @@ -0,0 +1,133 @@ +from unittest.mock import patch + +import pytest + +from gso.products import Layer2CircuitServiceType +from gso.products.product_types.edge_port import EdgePort +from gso.products.product_types.l3_core_service import L3CoreServiceType +from gso.products.product_types.router import Router +from gso.utils.shared_enums import Vendor +from test import USER_CONFIRM_EMPTY_FORM +from test.services.conftest import MockedNetboxClient +from test.workflows import ( + assert_complete, + assert_lso_interaction_success, + assert_stop_moodi, + extract_state, + resume_suspend_workflow, + run_workflow, +) + + +@pytest.fixture() +def _netbox_client_mock(): + with ( + patch("gso.services.netbox_client.NetboxClient.get_device_by_name") as mock_get_device_by_name, + patch("gso.services.netbox_client.NetboxClient.get_available_interfaces") as mock_get_available_interfaces, + patch("gso.services.netbox_client.NetboxClient.get_available_services_lags") as mock_available_services_lags, + patch("gso.services.netbox_client.NetboxClient.create_interface") as mock_create_interface, + patch("gso.services.netbox_client.NetboxClient.attach_interface_to_lag") as mock_attach_interface_to_lag, + patch("gso.services.netbox_client.NetboxClient.reserve_interface") as mock_reserve_interface, + patch("gso.services.netbox_client.NetboxClient.allocate_interface") as mock_allocate_interface, + ): + mock_get_device_by_name.return_value = MockedNetboxClient().get_device_by_name() + mock_get_available_interfaces.return_value = MockedNetboxClient().get_available_interfaces() + mock_available_services_lags.return_value = MockedNetboxClient().get_available_services_lags() + mock_create_interface.return_value = MockedNetboxClient().create_interface() + mock_attach_interface_to_lag.return_value = MockedNetboxClient().attach_interface_to_lag() + mock_reserve_interface.return_value = MockedNetboxClient().reserve_interface() + mock_allocate_interface.return_value = MockedNetboxClient().allocate_interface() + + yield + + +@pytest.fixture() +def partner(partner_factory, faker): + return partner_factory(name="GAAR", email=faker.email()) + + +@pytest.fixture() +def input_form_wizard_data(request, router_subscription_factory, partner, faker): + create_edge_port_step = { + "tt_number": faker.tt_number(), + "partner_name": partner["name"], + "node": str(router_subscription_factory(vendor=Vendor.NOKIA).subscription_id), + } + create_edge_port_interface_step = { + "name": "lag-21", + "description": faker.sentence(), + "ae_members": [ + { + "interface_name": f"Interface{interface}", + "interface_description": faker.sentence(), + } + for interface in range(2) + ], + } + summary_view_step = {} + + return [ + create_edge_port_step, + create_edge_port_interface_step, + summary_view_step, + ] + + +@pytest.mark.workflow() +@patch("gso.tasks.start_process.start_process_task.apply_async") +@patch("gso.services.lso_client._send_request") +def test_successful_edge_port_migration( + mock_execute_playbook, + start_process_task_apply_async, + input_form_wizard_data, + faker, + _netbox_client_mock, # noqa: PT019 + test_client, + edge_port_subscription_factory, + partner, + l3_core_service_subscription_factory, + layer_2_circuit_subscription_factory, +): + edge_port = edge_port_subscription_factory(partner=partner) + for service_type in [service_type for service_type in L3CoreServiceType if not service_type.startswith("IMPORTED")]: + l3_core_service = l3_core_service_subscription_factory(partner=partner, l3_core_service_type=service_type) + l3_core_service.l3_core_service.ap_list[0].sbp.edge_port = edge_port.edge_port + l3_core_service.save() + + for service_type in [ + service_type for service_type in Layer2CircuitServiceType if not service_type.startswith("IMPORTED") + ]: + layer_2_circuit_subscription_factory( + partner=partner, layer_2_circuit_side_a_edgeport=edge_port, layer_2_circuit_service_type=service_type + ) + + initial_data = [{"subscription_id": str(edge_port.subscription_id)}, *input_form_wizard_data] + result, process_stat, step_log = run_workflow("migrate_edge_port", initial_data) + + # Dry and Real run for disabling config and creating a new edge port + for _ in range(3): + result, step_log = assert_lso_interaction_success(result, process_stat, step_log) + + # confirm continue moved fiber + result, step_log = resume_suspend_workflow(result, process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) + + # confirm things in moodi looks good and start Migrate all L3 Core Services + result, step_log = resume_suspend_workflow(result, process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) + + # confirm things in moodi looks good and start migration of L2 core services + result, step_log = resume_suspend_workflow(result, process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) + result, step_log = resume_suspend_workflow(result, process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) + + result, step_log = assert_stop_moodi(result, process_stat, step_log) + + assert_complete(result) + state = extract_state(result) + subscription_id = state["subscription_id"] + subscription = EdgePort.from_subscription(subscription_id) + + assert subscription.status == "active" + router_fqdn = Router.from_subscription(input_form_wizard_data[0]["node"]).router.router_fqdn + assert subscription.edge_port.ga_id is not None + assert subscription.description == f"Edge Port lag-21 on {router_fqdn}, GAAR, {subscription.edge_port.ga_id}" + assert len(subscription.edge_port.edge_port_ae_members) == 2 + assert mock_execute_playbook.call_count == 4 diff --git a/test/workflows/l3_core_service/test_create_l3_core_service.py b/test/workflows/l3_core_service/test_create_l3_core_service.py index 4cbb754f..0d05e62d 100644 --- a/test/workflows/l3_core_service/test_create_l3_core_service.py +++ b/test/workflows/l3_core_service/test_create_l3_core_service.py @@ -12,6 +12,7 @@ from test.services.conftest import MockedSharePointClient from test.workflows import ( assert_complete, assert_lso_interaction_success, + assert_stop_moodi, assert_suspended, extract_state, resume_workflow, @@ -85,7 +86,7 @@ def test_create_l3_core_service_success( "v6_bgp_peer": base_bgp_peer_input() | {"add_v6_multicast": faker.boolean(), "peer_address": faker.ipv6()}, }, ] - lso_interaction_count = 6 + lso_interaction_count = 7 result, process_stat, step_log = run_workflow("create_l3_core_service", form_input_data) @@ -95,10 +96,12 @@ def test_create_l3_core_service_success( assert_suspended(result) result, step_log = resume_workflow(process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) + result, step_log = assert_stop_moodi(result, process_stat, step_log) + assert_complete(result) state = extract_state(result) subscription = L3CoreService.from_subscription(state["subscription_id"]) - assert mock_lso_client.call_count == lso_interaction_count + assert mock_lso_client.call_count == lso_interaction_count + 1 assert subscription.status == SubscriptionLifecycle.ACTIVE assert len(subscription.l3_core_service.ap_list) == 1 assert ( -- GitLab From fada3935947f3d98d8e1ee22df2c78daa456441b Mon Sep 17 00:00:00 2001 From: Mohammad Torkashvand <mohammad.torkashvand@geant.org> Date: Fri, 7 Feb 2025 22:55:34 +0100 Subject: [PATCH 2/8] addjust edgeport migration with new L3 core service migration wf --- gso/products/__init__.py | 6 +- gso/services/subscriptions.py | 28 +---- gso/tasks/__init__.py | 2 +- gso/utils/workflow_steps.py | 1 + gso/workflows/edge_port/migrate_edge_port.py | 105 ++++++++++++++---- .../l2_circuit/migrate_layer2_circuit.py | 2 +- .../l2_circuit/modify_layer_2_circuit.py | 3 - .../migrate_l3_core_service.py | 46 +++++--- test/workflows/__init__.py | 5 +- .../edge_port/test_migrate_edge_port.py | 17 ++- test/workflows/iptrunk/test_create_iptrunk.py | 9 +- .../workflows/iptrunk/test_migrate_iptrunk.py | 9 +- .../test_create_l3_core_service.py | 6 +- .../test_migrate_l3_core_service.py | 22 ++-- test/workflows/router/test_create_router.py | 4 +- test/workflows/switch/test_create_switch.py | 4 +- 16 files changed, 155 insertions(+), 114 deletions(-) diff --git a/gso/products/__init__.py b/gso/products/__init__.py index 7af22442..a1f5c193 100644 --- a/gso/products/__init__.py +++ b/gso/products/__init__.py @@ -81,7 +81,7 @@ class ProductName(strEnum): L3_CORE_SERVICE_PRODUCT_TYPE = L3CoreService.__name__ -L2_CORE_SERVICE_PRODUCT_TYPE = Layer2Circuit.__name__ +L2_CIRCUIT_PRODUCT_TYPE = Layer2Circuit.__name__ class ProductType(strEnum): @@ -116,9 +116,9 @@ class ProductType(strEnum): IMPORTED_LHCONE = ImportedL3CoreService.__name__ COPERNICUS = L3_CORE_SERVICE_PRODUCT_TYPE IMPORTED_COPERNICUS = ImportedL3CoreService.__name__ - GEANT_PLUS = L2_CORE_SERVICE_PRODUCT_TYPE + GEANT_PLUS = L2_CIRCUIT_PRODUCT_TYPE IMPORTED_GEANT_PLUS = ImportedLayer2Circuit.__name__ - EXPRESSROUTE = L2_CORE_SERVICE_PRODUCT_TYPE + EXPRESSROUTE = L2_CIRCUIT_PRODUCT_TYPE IMPORTED_EXPRESSROUTE = ImportedLayer2Circuit.__name__ VRF = VRF.__name__ diff --git a/gso/services/subscriptions.py b/gso/services/subscriptions.py index c1352981..8c378697 100644 --- a/gso/services/subscriptions.py +++ b/gso/services/subscriptions.py @@ -17,12 +17,12 @@ from orchestrator.db import ( db, ) from orchestrator.domain import SubscriptionModel -from orchestrator.services.subscriptions import query_depends_on_subscriptions, query_in_use_by_subscriptions +from orchestrator.services.subscriptions import query_in_use_by_subscriptions from orchestrator.types import SubscriptionLifecycle, UUIDstr from sqlalchemy import and_, text from sqlalchemy.exc import SQLAlchemyError -from gso.products import L2_CORE_SERVICE_PRODUCT_TYPE, L3_CORE_SERVICE_PRODUCT_TYPE, ProductName, ProductType +from gso.products import L2_CIRCUIT_PRODUCT_TYPE, L3_CORE_SERVICE_PRODUCT_TYPE, ProductName, ProductType from gso.products.product_types.site import Site SubscriptionType = dict[str, Any] @@ -185,7 +185,7 @@ def get_trunks_that_terminate_on_router( ) -def get_all_active_l3_core_services_on_top_of_edge_port(edge_port_id: UUIDstr) -> list[SubscriptionTable]: +def get_active_l3_services_linked_to_edge_port(edge_port_id: UUIDstr) -> list[SubscriptionTable]: """Retrieve all active l3 core services that are on top of the given edge port. Args: @@ -207,14 +207,14 @@ def get_all_active_l3_core_services_on_top_of_edge_port(edge_port_id: UUIDstr) - ) -def get_all_active_l2_core_services_on_top_of_edge_port(edge_port_id: UUIDstr) -> list[SubscriptionTable]: - """Retrieve all active l2 core services that are on top of the given edge port.""" +def get_active_l2_circuit_services_linked_to_edge_port(edge_port_id: UUIDstr) -> list[SubscriptionTable]: + """Retrieve all active l2 circuit services that are on top of the given edge port.""" return ( query_in_use_by_subscriptions(UUID(edge_port_id)) .join(ProductTable) .filter( and_( - ProductTable.product_type.in_([L2_CORE_SERVICE_PRODUCT_TYPE]), + ProductTable.product_type.in_([L2_CIRCUIT_PRODUCT_TYPE]), SubscriptionTable.status == SubscriptionLifecycle.ACTIVE, ) ) @@ -222,22 +222,6 @@ def get_all_active_l2_core_services_on_top_of_edge_port(edge_port_id: UUIDstr) - ) -def get_edge_port_subscription_of_l3_core_service(edge_port_subscription_id: UUIDstr) -> list[SubscriptionModel]: - """Retrieve all active Edge Port subscriptions that are used by the given L3 Core Service.""" - query = ( - query_depends_on_subscriptions( - UUID(edge_port_subscription_id), filter_statuses=[SubscriptionLifecycle.ACTIVE.value] - ) - .join(ProductTable) - .filter( - ProductTable.product_type.in_([ProductType.EDGE_PORT]), - SubscriptionTable.status == SubscriptionLifecycle.ACTIVE, - ) - ) - - return [SubscriptionModel.from_subscription(subscription.subscription_id) for subscription in query.all()] - - def get_product_id_by_name(product_name: ProductName) -> UUID: """Retrieve the UUID of a product by its name. diff --git a/gso/tasks/__init__.py b/gso/tasks/__init__.py index cc85efbe..6d945423 100644 --- a/gso/tasks/__init__.py +++ b/gso/tasks/__init__.py @@ -1 +1 @@ -"""celery tasks for gso.""" +"""celery tasks for GSO.""" diff --git a/gso/utils/workflow_steps.py b/gso/utils/workflow_steps.py index a63d6a7e..3f95229d 100644 --- a/gso/utils/workflow_steps.py +++ b/gso/utils/workflow_steps.py @@ -23,6 +23,7 @@ from gso.utils.helpers import generate_inventory_for_routers from gso.utils.shared_enums import Vendor SKIP_MOODI_KEY = "__skip_moodi__" +IS_HUMAN_INITIATED_WF_KEY = "__is_human_initiated_wf_key__" def _deploy_base_config( diff --git a/gso/workflows/edge_port/migrate_edge_port.py b/gso/workflows/edge_port/migrate_edge_port.py index 0e399f3f..20ec9709 100644 --- a/gso/workflows/edge_port/migrate_edge_port.py +++ b/gso/workflows/edge_port/migrate_edge_port.py @@ -1,5 +1,6 @@ """A modification workflow that migrates an EdgePort to a different endpoint.""" +import json import random from typing import Annotated, Any from uuid import uuid4 @@ -7,15 +8,16 @@ from uuid import uuid4 from annotated_types import Len from orchestrator import step, workflow from orchestrator.config.assignee import Assignee -from orchestrator.forms import FormPage +from orchestrator.forms import FormPage, SubmitFormPage from orchestrator.targets import Target from orchestrator.types import FormGenerator, State, UUIDstr from orchestrator.utils.errors import ProcessFailureError +from orchestrator.utils.json import json_dumps from orchestrator.workflow import StepList, begin, done, inputstep from orchestrator.workflows.steps import resync, store_process_subscription, unsync from orchestrator.workflows.utils import wrap_modify_initial_input_form -from pydantic import AfterValidator, ConfigDict -from pydantic_forms.validators import Label, ReadOnlyField, validate_unique_list +from pydantic import AfterValidator, ConfigDict, Field +from pydantic_forms.validators import Divider, Label, ReadOnlyField, validate_unique_list from pynetbox.models.dcim import Interfaces from gso.products.product_blocks.edge_port import EdgePortAEMemberBlock @@ -25,8 +27,8 @@ from gso.services.lso_client import LSOState, lso_interaction from gso.services.netbox_client import NetboxClient from gso.services.partners import get_partner_by_id from gso.services.subscriptions import ( - get_all_active_l2_core_services_on_top_of_edge_port, - get_all_active_l3_core_services_on_top_of_edge_port, + get_active_l2_circuit_services_linked_to_edge_port, + get_active_l3_services_linked_to_edge_port, ) from gso.tasks.start_process import start_process_task from gso.utils.helpers import ( @@ -50,6 +52,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: tt_number: TTNumber partner_name: ReadOnlyField(get_partner_by_id(subscription.customer_id).name, default_type=str) # type: ignore[valid-type] + divider: Divider = Field(None, exclude=True) node: active_pe_router_selector(excludes=[subscription.edge_port.node.subscription.subscription_id]) # type: ignore[valid-type] initial_user_input = yield MigrateEdgePortForm @@ -93,7 +96,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: "edge_port_ae_members", ] yield from create_summary_form(summary_form_data, subscription.product.name, summary_fields) - return input_forms_data + return input_forms_data | {"subscription": subscription} @step("Update the EdgePort references") @@ -114,9 +117,7 @@ def update_subscription_model( ) subscription.edge_port.edge_port_description = description edge_port_ae_members = [EdgePortAEMemberBlock.new(subscription_id=uuid4(), **member) for member in ae_members] - subscription.edge_port.edge_port_ae_members = ( - edge_port_ae_members # TODO: Clear the existing AE members and inactivate them and remove them from netbox? - ) + subscription.edge_port.edge_port_ae_members = edge_port_ae_members return {"subscription": subscription, "subscription_id": subscription.subscription_id} @@ -210,7 +211,7 @@ def allocate_interfaces_in_netbox(subscription: EdgePort) -> None: def confirm_continue_move_fiber() -> FormGenerator: """Wait for confirmation from an operator that the physical fiber has been moved.""" - class ProvisioningResultPage(FormPage): + class ProvisioningResultPage(SubmitFormPage): model_config = ConfigDict(title="Please confirm before continuing") info_label: Label = "New EdgePort has been deployed, wait for the physical connection to be moved." @@ -221,10 +222,10 @@ def confirm_continue_move_fiber() -> FormGenerator: @inputstep("Wait for confirmation", assignee=Assignee.SYSTEM) -def confirm_things_looks_good_in_moodi() -> FormGenerator: +def confirm_graphs_looks_good_in_moodi() -> FormGenerator: """Wait for confirmation from an operator that the new Migration looks good so far.""" - class ProvisioningResultPage(FormPage): + class ProvisioningResultPage(SubmitFormPage): model_config = ConfigDict(title="Please confirm before continuing") info_label: Label = "Do you confirm that everything looks good in the Moodi before continuing?" @@ -234,6 +235,34 @@ def confirm_things_looks_good_in_moodi() -> FormGenerator: return {} +@inputstep("Wait for confirmation", assignee=Assignee.SYSTEM) +def confirm_l3_core_service_migrations_are_complete() -> FormGenerator: + """Wait for confirmation from an operator that all L3 core services have been completed successfully.""" + + class ProvisioningResultPage(SubmitFormPage): + model_config = ConfigDict(title="Please confirm before continuing") + + info_label: Label = "Do you confirm that all L3 core service migrations have been completed successfully?" + + yield ProvisioningResultPage + + return {} + + +@inputstep("Wait for confirmation", assignee=Assignee.SYSTEM) +def confirm_l2_circuit_migrations_are_complete() -> FormGenerator: + """Wait for confirmation from an operator that all L2 circuit migrations have been completed successfully.""" + + class ProvisioningResultPage(SubmitFormPage): + model_config = ConfigDict(title="Please confirm before continuing") + + info_label: Label = "Do you confirm that all L2 circuit migrations have been completed successfully?" + + yield ProvisioningResultPage + + return {} + + @step("Migrate L3 core services to new node") def migrate_l3_core_services_to_new_node(subscription_id: UUIDstr, tt_number: TTNumber) -> State: """Migrate all L3 core services from the old EdgePort to the new EdgePort. @@ -241,9 +270,9 @@ def migrate_l3_core_services_to_new_node(subscription_id: UUIDstr, tt_number: TT This sub migrations do not modify the L3 core services. The source and destination EdgePort remain the same for each service. The migration playbook is executed once for each service to apply the configuration on the new node and as a result, - the service bindings port and bgp sessions related to this edge port of each service will be moved to the new node. + the service bindings port and BGP sessions related to this edge port of each service will be moved to the new node. """ - l3_core_services = get_all_active_l3_core_services_on_top_of_edge_port(subscription_id) + l3_core_services = get_active_l3_services_linked_to_edge_port(subscription_id) edge_port = EdgePort.from_subscription(subscription_id) for l3_core_service in l3_core_services: @@ -255,7 +284,10 @@ def migrate_l3_core_services_to_new_node(subscription_id: UUIDstr, tt_number: TT { "tt_number": tt_number, "skip_moodi": True, + "is_human_initiated_wf": False, "source_edge_port": str(edge_port.subscription_id), + }, + { "destination_edge_port": str(edge_port.subscription_id), }, ], @@ -269,7 +301,7 @@ def migrate_l3_core_services_to_new_node(subscription_id: UUIDstr, tt_number: TT @step("Migrate L2 circuits to new node") def migrate_l2_circuits_to_new_node(subscription_id: UUIDstr, tt_number: TTNumber) -> State: """Migrate Layer2 circuits from the old EdgePort to the new EdgePort.""" - layer2_circuits = get_all_active_l2_core_services_on_top_of_edge_port(subscription_id) + layer2_circuits = get_active_l2_circuit_services_linked_to_edge_port(subscription_id) edge_port = EdgePort.from_subscription(subscription_id) for l2_core_service in layer2_circuits: @@ -294,16 +326,21 @@ def migrate_l2_circuits_to_new_node(subscription_id: UUIDstr, tt_number: TTNumbe @step("[DRY RUN] Disable configuration on old router") def disable_old_config_dry( - # subscription: EdgePort, TODO + subscription: EdgePort, process_id: UUIDstr, tt_number: str, ) -> LSOState: """Perform a dry run of disabling the old configuration on the routers.""" + layer3_services = get_active_l3_services_linked_to_edge_port(str(subscription.subscription_id)) + layer2_circuits = get_active_l2_circuit_services_linked_to_edge_port(str(subscription.subscription_id)) + extra_vars = { "verb": "deactivate", "config_object": "deactivate", "dry_run": True, "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} " f"- Deploy config for #TODO", + "l3_core_services": [json.loads(json_dumps(layer3_service)) for layer3_service in layer3_services], + "l2_circuits": [json.loads(json_dumps(layer2_circuit)) for layer2_circuit in layer2_circuits], } return { @@ -346,6 +383,26 @@ def disable_old_config_real( } +@inputstep("Verify pre-check results", assignee=Assignee.SYSTEM) +def inform_operator_traffic_check() -> FormGenerator: + """Wait for confirmation from an operator that the results from the pre-checks look OK. + + In case the results look OK, the workflow can continue. If the results don't look OK, the workflow can still be + aborted at this time, without the subscription going out of sync. Moodi will also not start, and the subscription + model has not been updated yet. Effectively, this prevents any changes inside the orchestrator from occurring. The + one thing that must be rolled back manually, is the deactivated configuration that sits on the source device. + """ + + class PreCheckPage(SubmitFormPage): + model_config = ConfigDict(title="Please confirm before continuing") + + info_label_1: Label = "Please verify that traffic has moved as expected." + info_label_2: Label = "If traffic is misbehaving, this is your last chance to abort this workflow cleanly." + + yield PreCheckPage + return {} + + @workflow( "Migrate an EdgePort", initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), @@ -356,25 +413,25 @@ def migrate_edge_port() -> StepList: return ( begin >> store_process_subscription(Target.MODIFY) - >> unsync >> lso_interaction(disable_old_config_dry) >> lso_interaction(disable_old_config_real) + >> inform_operator_traffic_check + >> unsync >> update_subscription_model >> start_moodi() - # TODO: Neda mentioned if in the future we have nokia-to-nokia migration, then we need another step to - # dealocate the lag-- but this is not needed for now right? + # TODO: Add LAG de-allocation step for future Nokia-to-Nokia migration if needed. >> reserve_interfaces_in_netbox >> lso_interaction(create_edge_port_dry) >> lso_interaction(create_edge_port_real) >> confirm_continue_move_fiber - >> confirm_things_looks_good_in_moodi + >> confirm_graphs_looks_good_in_moodi >> resync - # TODO: Explain this step that at this point related subscriptions are unsynced and prevent - # any changes on l3/l2 services >> migrate_l3_core_services_to_new_node - >> confirm_things_looks_good_in_moodi + >> confirm_l3_core_service_migrations_are_complete + >> confirm_graphs_looks_good_in_moodi >> migrate_l2_circuits_to_new_node - >> confirm_things_looks_good_in_moodi + >> confirm_l2_circuit_migrations_are_complete + >> confirm_graphs_looks_good_in_moodi >> stop_moodi() >> done ) diff --git a/gso/workflows/l2_circuit/migrate_layer2_circuit.py b/gso/workflows/l2_circuit/migrate_layer2_circuit.py index 9fdeb294..e7d4d9b2 100644 --- a/gso/workflows/l2_circuit/migrate_layer2_circuit.py +++ b/gso/workflows/l2_circuit/migrate_layer2_circuit.py @@ -11,7 +11,7 @@ even though the L2 Circuit Service is not changed. Operator-triggered migration: When an operator initiates the workflow, they are required to specify both the source and destination EdgePorts. -During the migration process, the system updates the related access_point.sbp.edge_port reference to replace the source +During the migration process, the system updates the related edge_port reference to replace the source EdgePort with the destination EdgePort and applies the necessary configuration changes to the router. Important Note: diff --git a/gso/workflows/l2_circuit/modify_layer_2_circuit.py b/gso/workflows/l2_circuit/modify_layer_2_circuit.py index 57f0bf25..6e378a77 100644 --- a/gso/workflows/l2_circuit/modify_layer_2_circuit.py +++ b/gso/workflows/l2_circuit/modify_layer_2_circuit.py @@ -115,6 +115,3 @@ def modify_layer_2_circuit() -> StepList: >> resync >> done ) - - -# TODO: where is ansible playbook execution steps to reflect this changes on the network? diff --git a/gso/workflows/l3_core_service/migrate_l3_core_service.py b/gso/workflows/l3_core_service/migrate_l3_core_service.py index e10675aa..704190fb 100644 --- a/gso/workflows/l3_core_service/migrate_l3_core_service.py +++ b/gso/workflows/l3_core_service/migrate_l3_core_service.py @@ -17,7 +17,7 @@ from orchestrator.forms import FormPage, SubmitFormPage from orchestrator.targets import Target from orchestrator.utils.errors import ProcessFailureError from orchestrator.utils.json import json_dumps -from orchestrator.workflow import StepList, begin, done, inputstep, step +from orchestrator.workflow import StepList, begin, conditional, done, inputstep, step 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 @@ -29,7 +29,7 @@ from gso.products.product_types.l3_core_service import L3CoreService from gso.services.lso_client import LSOState, lso_interaction from gso.services.subscriptions import get_active_edge_port_subscriptions from gso.utils.types.tt_number import TTNumber -from gso.utils.workflow_steps import start_moodi, stop_moodi +from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, SKIP_MOODI_KEY, start_moodi, stop_moodi from gso.workflows.shared import create_summary_form @@ -53,6 +53,8 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: tt_number: TTNumber divider: Divider = Field(None, exclude=True) + skip_moodi: bool = False + is_human_initiated_wf: bool = True source_edge_port: source_edge_port_selector | str # type: ignore[valid-type] source_ep_user_input = yield L3CoreServiceSourceEdgePortSelectionForm @@ -77,21 +79,27 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: destination_ep_user_input = yield L3CoreServiceEdgePortSelectionForm - summary_input = { - "source_edge_port": EdgePort.from_subscription(source_ep_user_input.source_edge_port).description, - "destination_edge_port": EdgePort.from_subscription( - destination_ep_user_input.destination_edge_port - ).description, - } - yield from create_summary_form(summary_input, subscription.l3_core_service_type, list(summary_input.keys())) - user_input = ( + if source_ep_user_input.is_human_initiated_wf: + summary_input = { + "source_edge_port": EdgePort.from_subscription(source_ep_user_input.source_edge_port).description, + "destination_edge_port": EdgePort.from_subscription( + destination_ep_user_input.destination_edge_port + ).description, + } + yield from create_summary_form( + summary_input, subscription.l3_core_service_type.value, list(summary_input.keys()) + ) + + return ( {"subscription_id": subscription_id, "subscription": subscription} | source_ep_user_input.model_dump() | destination_ep_user_input.model_dump() + | { + IS_HUMAN_INITIATED_WF_KEY: source_ep_user_input.is_human_initiated_wf, + SKIP_MOODI_KEY: source_ep_user_input.skip_moodi, + } ) - return user_input - @step("Show BGP neighbors") def show_bgp_neighbors( @@ -320,15 +328,17 @@ def update_subscription_model( ) def migrate_l3_core_service() -> StepList: """Migrate a L3 Core Service to a destination Edge Port.""" + is_human_initiated_wf = conditional(lambda state: bool(state.get(IS_HUMAN_INITIATED_WF_KEY))) + return ( begin >> store_process_subscription(Target.MODIFY) - >> lso_interaction(show_bgp_neighbors) # TODO: send OTRS email with pre-check results - >> lso_interaction(deactivate_bgp_dry) - >> lso_interaction(deactivate_bgp_real) - >> lso_interaction(deactivate_sbp_dry) - >> lso_interaction(deactivate_sbp_real) - >> inform_operator_traffic_check + >> is_human_initiated_wf(lso_interaction(show_bgp_neighbors)) # TODO: send OTRS email with pre-check results + >> is_human_initiated_wf(lso_interaction(deactivate_bgp_dry)) + >> is_human_initiated_wf(lso_interaction(deactivate_bgp_real)) + >> is_human_initiated_wf(lso_interaction(deactivate_sbp_dry)) + >> is_human_initiated_wf(lso_interaction(deactivate_sbp_real)) + >> is_human_initiated_wf(inform_operator_traffic_check) >> unsync >> start_moodi() # TODO: include results from first LSO run >> generate_scoped_subscription_model diff --git a/test/workflows/__init__.py b/test/workflows/__init__.py index 6d53a4d5..4edb8470 100644 --- a/test/workflows/__init__.py +++ b/test/workflows/__init__.py @@ -272,7 +272,7 @@ def resume_workflow( return result, step_log -def resume_suspend_workflow( +def resume_suspended_workflow( result, process: ProcessStat, step_log: list[tuple[Step, Process]], @@ -318,6 +318,5 @@ def assert_lso_interaction_failure(result: Process, process_stat: ProcessStat, s def assert_stop_moodi(result: Process, process_stat: ProcessStat, step_log: list): """Assert a successful LSO execution in a workflow.""" - assert_suspended(result) - result, step_log = resume_workflow(process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) + result, step_log = resume_suspended_workflow(result, process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) return assert_lso_success(result, process_stat, step_log) diff --git a/test/workflows/edge_port/test_migrate_edge_port.py b/test/workflows/edge_port/test_migrate_edge_port.py index 1709b0b3..6c7c55d7 100644 --- a/test/workflows/edge_port/test_migrate_edge_port.py +++ b/test/workflows/edge_port/test_migrate_edge_port.py @@ -14,7 +14,7 @@ from test.workflows import ( assert_lso_interaction_success, assert_stop_moodi, extract_state, - resume_suspend_workflow, + resume_suspended_workflow, run_workflow, ) @@ -104,19 +104,16 @@ def test_successful_edge_port_migration( initial_data = [{"subscription_id": str(edge_port.subscription_id)}, *input_form_wizard_data] result, process_stat, step_log = run_workflow("migrate_edge_port", initial_data) + # confirm inform_operator_traffic_check + result, step_log = resume_suspended_workflow(result, process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) + # Dry and Real run for disabling config and creating a new edge port for _ in range(3): result, step_log = assert_lso_interaction_success(result, process_stat, step_log) - # confirm continue moved fiber - result, step_log = resume_suspend_workflow(result, process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) - - # confirm things in moodi looks good and start Migrate all L3 Core Services - result, step_log = resume_suspend_workflow(result, process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) - - # confirm things in moodi looks good and start migration of L2 core services - result, step_log = resume_suspend_workflow(result, process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) - result, step_log = resume_suspend_workflow(result, process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) + # all the steps in the workflow that needs user confirmation + for _ in range(6): + result, step_log = resume_suspended_workflow(result, process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) result, step_log = assert_stop_moodi(result, process_stat, step_log) diff --git a/test/workflows/iptrunk/test_create_iptrunk.py b/test/workflows/iptrunk/test_create_iptrunk.py index c9e5abdc..7e875e19 100644 --- a/test/workflows/iptrunk/test_create_iptrunk.py +++ b/test/workflows/iptrunk/test_create_iptrunk.py @@ -16,9 +16,8 @@ from test.workflows import ( assert_failed, assert_lso_interaction_failure, assert_lso_interaction_success, - assert_suspended, extract_state, - resume_workflow, + resume_suspended_workflow, run_workflow, ) @@ -134,8 +133,7 @@ def test_successful_iptrunk_creation_with_standard_lso_result( for _ in range(6): result, step_log = assert_lso_interaction_success(result, process_stat, step_log) - assert_suspended(result) - result, step_log = resume_workflow(process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) + result, step_log = resume_suspended_workflow(result, process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) assert_complete(result) @@ -227,8 +225,7 @@ def test_successful_iptrunk_creation_with_juniper_interface_names( for _ in range(6): result, step_log = assert_lso_interaction_success(result, process_stat, step_log) - assert_suspended(result) - result, step_log = resume_workflow(process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) + result, step_log = resume_suspended_workflow(result, process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) assert_complete(result) assert mock_execute_playbook.call_count == 6 diff --git a/test/workflows/iptrunk/test_migrate_iptrunk.py b/test/workflows/iptrunk/test_migrate_iptrunk.py index ce7d256e..e3c3ba5f 100644 --- a/test/workflows/iptrunk/test_migrate_iptrunk.py +++ b/test/workflows/iptrunk/test_migrate_iptrunk.py @@ -13,6 +13,7 @@ from test.workflows import ( assert_lso_interaction_success, assert_suspended, extract_state, + resume_suspended_workflow, resume_workflow, run_workflow, ) @@ -118,7 +119,7 @@ def interface_lists_are_equal(list1, list2): @patch("gso.services.netbox_client.NetboxClient.free_interface") @patch("gso.services.netbox_client.NetboxClient.delete_interface") @patch("gso.workflows.iptrunk.migrate_iptrunk.SharePointClient") -def test_migrate_iptrunk_success( # noqa: PLR0915 +def test_migrate_iptrunk_success( mock_sharepoint_client, mocked_delete_interface, mocked_free_interface, @@ -152,8 +153,7 @@ def test_migrate_iptrunk_success( # noqa: PLR0915 for _ in range(8): result, step_log = assert_lso_interaction_success(result, process_stat, step_log) - assert_suspended(result) - result, step_log = resume_workflow(process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) + result, step_log = resume_suspended_workflow(result, process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) for _ in range(8): result, step_log = assert_lso_interaction_success(result, process_stat, step_log) @@ -165,8 +165,7 @@ def test_migrate_iptrunk_success( # noqa: PLR0915 result, step_log = assert_lso_interaction_success(result, process_stat, step_log) # Continue workflow after it has displayed a checklist URL. - assert_suspended(result) - result, step_log = resume_workflow(process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) + result, step_log = resume_suspended_workflow(result, process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) assert_complete(result) diff --git a/test/workflows/l3_core_service/test_create_l3_core_service.py b/test/workflows/l3_core_service/test_create_l3_core_service.py index 0d05e62d..b3c43e59 100644 --- a/test/workflows/l3_core_service/test_create_l3_core_service.py +++ b/test/workflows/l3_core_service/test_create_l3_core_service.py @@ -13,9 +13,8 @@ from test.workflows import ( assert_complete, assert_lso_interaction_success, assert_stop_moodi, - assert_suspended, extract_state, - resume_workflow, + resume_suspended_workflow, run_workflow, ) @@ -93,8 +92,7 @@ def test_create_l3_core_service_success( for _ in range(lso_interaction_count): result, step_log = assert_lso_interaction_success(result, process_stat, step_log) - assert_suspended(result) - result, step_log = resume_workflow(process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) + result, step_log = resume_suspended_workflow(result, process_stat, step_log, USER_CONFIRM_EMPTY_FORM) result, step_log = assert_stop_moodi(result, process_stat, step_log) diff --git a/test/workflows/l3_core_service/test_migrate_l3_core_service.py b/test/workflows/l3_core_service/test_migrate_l3_core_service.py index eef3f281..04a69df9 100644 --- a/test/workflows/l3_core_service/test_migrate_l3_core_service.py +++ b/test/workflows/l3_core_service/test_migrate_l3_core_service.py @@ -9,9 +9,9 @@ from test import USER_CONFIRM_EMPTY_FORM from test.workflows import ( assert_complete, assert_lso_interaction_success, - assert_suspended, + assert_stop_moodi, extract_state, - resume_workflow, + resume_suspended_workflow, run_workflow, ) @@ -52,16 +52,17 @@ def test_migrate_l3_core_service_success( for _ in range(5): result, step_log = assert_lso_interaction_success(result, process_stat, step_log) - assert_suspended(result) - result, step_log = resume_workflow(process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) + result, step_log = resume_suspended_workflow(result, process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) - for _ in range(4): + for _ in range(5): result, step_log = assert_lso_interaction_success(result, process_stat, step_log) + result, step_log = assert_stop_moodi(result, process_stat, step_log) + assert_complete(result) state = extract_state(result) subscription = L3CoreService.from_subscription(state["subscription_id"]) - assert mock_execute_playbook.call_count == 9 + assert mock_execute_playbook.call_count == 11 assert subscription.insync assert len(subscription.l3_core_service.ap_list) == 1 assert str(subscription.l3_core_service.ap_list[0].sbp.edge_port.owner_subscription_id) == destination_edge_port @@ -113,8 +114,7 @@ def test_migrate_l3_core_service_scoped_emission( for _ in range(4): result, step_log = assert_lso_interaction_success(result, process_stat, step_log) - assert_suspended(result) - result, step_log = resume_workflow(process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) + result, step_log = resume_suspended_workflow(result, process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) result, step_log = assert_lso_interaction_success(result, process_stat, step_log) @@ -130,13 +130,15 @@ def test_migrate_l3_core_service_scoped_emission( state["subscription"]["l3_core_service"] == state["__old_subscriptions__"][subscription_id]["l3_core_service"] ) # Subscription is unchanged for now - for _ in range(3): + for _ in range(4): result, step_log = assert_lso_interaction_success(result, process_stat, step_log) + result, step_log = assert_stop_moodi(result, process_stat, step_log) + assert_complete(result) state = extract_state(result) subscription = L3CoreService.from_subscription(state["subscription_id"]) - assert mock_execute_playbook.call_count == 9 + assert mock_execute_playbook.call_count == 11 assert subscription.insync assert len(subscription.l3_core_service.ap_list) == 5 assert str(subscription.l3_core_service.ap_list[3].sbp.edge_port.owner_subscription_id) == destination_edge_port diff --git a/test/workflows/router/test_create_router.py b/test/workflows/router/test_create_router.py index 249b268d..67d317b1 100644 --- a/test/workflows/router/test_create_router.py +++ b/test/workflows/router/test_create_router.py @@ -17,6 +17,7 @@ from test.workflows import ( assert_lso_interaction_success, assert_suspended, extract_state, + resume_suspended_workflow, resume_workflow, run_workflow, ) @@ -102,8 +103,7 @@ def test_create_nokia_router_success( result, step_log = assert_lso_interaction_success(result, process_stat, step_log) - assert_suspended(result) - result, step_log = resume_workflow(process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) + result, step_log = resume_suspended_workflow(result, process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) assert_complete(result) diff --git a/test/workflows/switch/test_create_switch.py b/test/workflows/switch/test_create_switch.py index 1209b48a..a6e02d83 100644 --- a/test/workflows/switch/test_create_switch.py +++ b/test/workflows/switch/test_create_switch.py @@ -13,6 +13,7 @@ from test.workflows import ( assert_lso_interaction_success, assert_suspended, extract_state, + resume_suspended_workflow, resume_workflow, run_workflow, ) @@ -54,8 +55,7 @@ def test_create_switch_success( result, step_log = assert_lso_interaction_success(result, process_stat, step_log) # Sharepoint list created - assert_suspended(result) - result, step_log = resume_workflow(process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) + result, step_log = resume_suspended_workflow(result, process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) assert_complete(result) -- GitLab From 39e36de02a21ae8ead2ac8839942e6d1d5246b09 Mon Sep 17 00:00:00 2001 From: Mohammad Torkashvand <mohammad.torkashvand@geant.org> Date: Tue, 11 Feb 2025 08:27:41 +0000 Subject: [PATCH 3/8] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Karel van Klink <karel.vanklink@geant.org> --- gso/workflows/edge_port/migrate_edge_port.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gso/workflows/edge_port/migrate_edge_port.py b/gso/workflows/edge_port/migrate_edge_port.py index 20ec9709..b2a7e6d6 100644 --- a/gso/workflows/edge_port/migrate_edge_port.py +++ b/gso/workflows/edge_port/migrate_edge_port.py @@ -313,6 +313,7 @@ def migrate_l2_circuits_to_new_node(subscription_id: UUIDstr, tt_number: TTNumbe { "tt_number": tt_number, "skip_moodi": True, + "is_human_initiated_wf": False, "source_edge_port": str(edge_port.subscription_id), "destination_edge_port": str(edge_port.subscription_id), }, -- GitLab From 36b0e742d8d67ae61a59ad331866a109b9041492 Mon Sep 17 00:00:00 2001 From: Mohammad Torkashvand <mohammad.torkashvand@geant.org> Date: Tue, 11 Feb 2025 08:28:05 +0000 Subject: [PATCH 4/8] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Karel van Klink <karel.vanklink@geant.org> --- gso/workflows/edge_port/migrate_edge_port.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gso/workflows/edge_port/migrate_edge_port.py b/gso/workflows/edge_port/migrate_edge_port.py index b2a7e6d6..5930935e 100644 --- a/gso/workflows/edge_port/migrate_edge_port.py +++ b/gso/workflows/edge_port/migrate_edge_port.py @@ -228,7 +228,7 @@ def confirm_graphs_looks_good_in_moodi() -> FormGenerator: class ProvisioningResultPage(SubmitFormPage): model_config = ConfigDict(title="Please confirm before continuing") - info_label: Label = "Do you confirm that everything looks good in the Moodi before continuing?" + info_label: Label = "Do you confirm that everything looks good in Moodi before continuing the workflow?" yield ProvisioningResultPage -- GitLab From 3c633283e184dc53cb13acb9d717cee75bbe3cdc Mon Sep 17 00:00:00 2001 From: Mohammad Torkashvand <mohammad.torkashvand@geant.org> Date: Tue, 11 Feb 2025 08:28:19 +0000 Subject: [PATCH 5/8] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Karel van Klink <karel.vanklink@geant.org> --- gso/workflows/edge_port/migrate_edge_port.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gso/workflows/edge_port/migrate_edge_port.py b/gso/workflows/edge_port/migrate_edge_port.py index 5930935e..94e867c8 100644 --- a/gso/workflows/edge_port/migrate_edge_port.py +++ b/gso/workflows/edge_port/migrate_edge_port.py @@ -202,7 +202,7 @@ def allocate_interfaces_in_netbox(subscription: EdgePort) -> None: iface_name = interface.interface_name if not fqdn or not iface_name: msg = "FQDN and/or interface name missing in subscription" - raise ProcessFailureError(msg, details=subscription.subscription_id) + raise ProcessFailureError(msg, details={"fqdn": fqdn, "interface_name": iface_name}) NetboxClient().allocate_interface(device_name=fqdn, iface_name=iface_name) -- GitLab From e9bf99a154cfacf387a6c6935e1f375ce44831c2 Mon Sep 17 00:00:00 2001 From: Mohammad Torkashvand <mohammad.torkashvand@geant.org> Date: Tue, 11 Feb 2025 08:28:42 +0000 Subject: [PATCH 6/8] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Karel van Klink <karel.vanklink@geant.org> --- gso/workflows/edge_port/migrate_edge_port.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gso/workflows/edge_port/migrate_edge_port.py b/gso/workflows/edge_port/migrate_edge_port.py index 94e867c8..1b31c94d 100644 --- a/gso/workflows/edge_port/migrate_edge_port.py +++ b/gso/workflows/edge_port/migrate_edge_port.py @@ -197,8 +197,8 @@ def create_edge_port_real( @step("Allocate interfaces in NetBox") def allocate_interfaces_in_netbox(subscription: EdgePort) -> None: """Allocate the interfaces in NetBox.""" + fqdn = subscription.edge_port.node.router_fqdn for interface in subscription.edge_port.edge_port_ae_members: - fqdn = subscription.edge_port.node.router_fqdn iface_name = interface.interface_name if not fqdn or not iface_name: msg = "FQDN and/or interface name missing in subscription" -- GitLab From 985c5eb150c987b534225bbce406039e07263b7e Mon Sep 17 00:00:00 2001 From: Mohammad Torkashvand <mohammad.torkashvand@geant.org> Date: Tue, 11 Feb 2025 08:28:46 +0000 Subject: [PATCH 7/8] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Karel van Klink <karel.vanklink@geant.org> --- ...02-06_efebcde91f2f_add_migration_workflow_for_an_edgeport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gso/migrations/versions/2025-02-06_efebcde91f2f_add_migration_workflow_for_an_edgeport.py b/gso/migrations/versions/2025-02-06_efebcde91f2f_add_migration_workflow_for_an_edgeport.py index 1a9e9db7..6005b58b 100644 --- a/gso/migrations/versions/2025-02-06_efebcde91f2f_add_migration_workflow_for_an_edgeport.py +++ b/gso/migrations/versions/2025-02-06_efebcde91f2f_add_migration_workflow_for_an_edgeport.py @@ -21,7 +21,7 @@ new_workflows = [ { "name": "migrate_edge_port", "target": "MODIFY", - "description": "Migrate an EdgePort", + "description": "Migrate an Edge Port", "product_type": "EdgePort" }, ] -- GitLab From 6f16ed7e768e21a44f243bd78f972e3768259a2d Mon Sep 17 00:00:00 2001 From: Mohammad Torkashvand <mohammad.torkashvand@geant.org> Date: Tue, 11 Feb 2025 09:29:06 +0100 Subject: [PATCH 8/8] apply review suggestions --- gso/utils/workflow_steps.py | 4 ++-- gso/workflows/edge_port/migrate_edge_port.py | 11 ++++++++--- .../l3_core_service/create_l3_core_service.py | 6 ++---- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/gso/utils/workflow_steps.py b/gso/utils/workflow_steps.py index 3f95229d..fd5eac9a 100644 --- a/gso/utils/workflow_steps.py +++ b/gso/utils/workflow_steps.py @@ -22,8 +22,8 @@ from gso.settings import load_oss_params from gso.utils.helpers import generate_inventory_for_routers from gso.utils.shared_enums import Vendor -SKIP_MOODI_KEY = "__skip_moodi__" -IS_HUMAN_INITIATED_WF_KEY = "__is_human_initiated_wf_key__" +SKIP_MOODI_KEY = "__skip_moodi" +IS_HUMAN_INITIATED_WF_KEY = "__is_human_initiated_wf" def _deploy_base_config( diff --git a/gso/workflows/edge_port/migrate_edge_port.py b/gso/workflows/edge_port/migrate_edge_port.py index 1b31c94d..4602d72d 100644 --- a/gso/workflows/edge_port/migrate_edge_port.py +++ b/gso/workflows/edge_port/migrate_edge_port.py @@ -359,16 +359,21 @@ def disable_old_config_dry( @step("[FOR REAL] Disable configuration on old router") def disable_old_config_real( - # subscription: EdgePort, TODO + subscription: EdgePort, process_id: UUIDstr, tt_number: str, ) -> LSOState: """Disable old configuration on the routers.""" + layer3_services = get_active_l3_services_linked_to_edge_port(str(subscription.subscription_id)) + layer2_circuits = get_active_l2_circuit_services_linked_to_edge_port(str(subscription.subscription_id)) + extra_vars = { "verb": "deactivate", "config_object": "deactivate", "dry_run": False, "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} " f"- Deploy config for # TODO", + "l3_core_services": [json.loads(json_dumps(layer3_service)) for layer3_service in layer3_services], + "l2_circuits": [json.loads(json_dumps(layer2_circuit)) for layer2_circuit in layer2_circuits], } return { @@ -405,12 +410,12 @@ def inform_operator_traffic_check() -> FormGenerator: @workflow( - "Migrate an EdgePort", + "Migrate an Edge Port", initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), target=Target.MODIFY, ) def migrate_edge_port() -> StepList: - """Migrate an EdgePort.""" + """Migrate an Edge Port.""" return ( begin >> store_process_subscription(Target.MODIFY) diff --git a/gso/workflows/l3_core_service/create_l3_core_service.py b/gso/workflows/l3_core_service/create_l3_core_service.py index fcd73b1a..3069bdac 100644 --- a/gso/workflows/l3_core_service/create_l3_core_service.py +++ b/gso/workflows/l3_core_service/create_l3_core_service.py @@ -201,16 +201,14 @@ def initialize_subscription( gs_id=sbp_gs_id, ) subscription.l3_core_service.ap_list.append( - AccessPortInactive.new( # TODO: Ask Simone why we don't active them somewhere? + AccessPortInactive.new( subscription_id=uuid4(), ap_type=edge_port["ap_type"], sbp=service_binding_port, custom_service_name=edge_port.get("custom_service_name"), ) ) - edge_port_fqdn_list.append( - edge_port_subscription.edge_port.node.router_fqdn - ) # TODO: why this is only one EP? what if we have multiple EP for an L3 Core Service + edge_port_fqdn_list.append(edge_port_subscription.edge_port.node.router_fqdn) partner_name = get_partner_by_id(subscription.customer_id).name subscription.description = f"{product_name} service for {partner_name}" -- GitLab