Skip to content

Migrate edge port

A modification workflow that migrates an EdgePort to a different endpoint.

initial_input_form_generator(subscription_id)

Gather input from the operator on the new router that the EdgePort should connect to.

Source code in gso/workflows/edge_port/migrate_edge_port.py
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]
        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

    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 | {"subscription": subscription}

update_subscription_model(subscription, node, name, partner_name, ae_members, description=None)

Update the EdgePort subscription object in the service database with the new values.

Source code in gso/workflows/edge_port/migrate_edge_port.py
@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

    return {"subscription": subscription, "subscription_id": subscription.subscription_id}

reserve_interfaces_in_netbox(subscription)

Create the LAG interfaces in NetBox and attach the LAG interfaces to the physical interfaces.

Source code in gso/workflows/edge_port/migrate_edge_port.py
@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,
    }

create_edge_port_dry(subscription, tt_number, process_id, partner_name)

Create a new edge port in the network as a dry run.

Source code in gso/workflows/edge_port/migrate_edge_port.py
@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,
    }

create_edge_port_real(subscription, tt_number, process_id, partner_name)

Create a new edge port in the network for real.

Source code in gso/workflows/edge_port/migrate_edge_port.py
@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,
    }

allocate_interfaces_in_netbox(subscription)

Allocate the interfaces in NetBox.

Source code in gso/workflows/edge_port/migrate_edge_port.py
@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:
        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={"fqdn": fqdn, "interface_name": iface_name})

        NetboxClient().allocate_interface(device_name=fqdn, iface_name=iface_name)

confirm_continue_move_fiber()

Wait for confirmation from an operator that the physical fiber has been moved.

Source code in gso/workflows/edge_port/migrate_edge_port.py
@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(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."

    yield ProvisioningResultPage

    return {}

confirm_graphs_looks_good_in_moodi()

Wait for confirmation from an operator that the new Migration looks good so far.

Source code in gso/workflows/edge_port/migrate_edge_port.py
@inputstep("Wait for confirmation", assignee=Assignee.SYSTEM)
def confirm_graphs_looks_good_in_moodi() -> FormGenerator:
    """Wait for confirmation from an operator that the new Migration looks good so far."""

    class ProvisioningResultPage(SubmitFormPage):
        model_config = ConfigDict(title="Please confirm before continuing")

        info_label: Label = "Do you confirm that everything looks good in Moodi before continuing the workflow?"

    yield ProvisioningResultPage

    return {}

confirm_l3_core_service_migrations_are_complete()

Wait for confirmation from an operator that all L3 core services have been completed successfully.

Source code in gso/workflows/edge_port/migrate_edge_port.py
@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 {}

confirm_l2_circuit_migrations_are_complete()

Wait for confirmation from an operator that all L2 circuit migrations have been completed successfully.

Source code in gso/workflows/edge_port/migrate_edge_port.py
@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 {}

migrate_l3_core_services_to_new_node(subscription_id, tt_number)

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.

Source code in gso/workflows/edge_port/migrate_edge_port.py
@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_active_l3_services_linked_to_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,
                        "is_human_initiated_wf": False,
                        "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}

migrate_l2_circuits_to_new_node(subscription_id, tt_number)

Migrate Layer2 circuits from the old EdgePort to the new EdgePort.

Source code in gso/workflows/edge_port/migrate_edge_port.py
@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_active_l2_circuit_services_linked_to_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,
                        "is_human_initiated_wf": False,
                        "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}

disable_old_config_dry(subscription, process_id, tt_number)

Perform a dry run of disabling the old configuration on the routers.

Source code in gso/workflows/edge_port/migrate_edge_port.py
@step("[DRY RUN] Disable configuration on old router")
def disable_old_config_dry(
    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 {
        "playbook_name": "gap_ansible/playbooks/iptrunks_migration.yaml",
        "inventory": {
            "all": {
                "hosts": {
                    # TODO
                }
            }
        },
        "extra_vars": extra_vars,
    }

disable_old_config_real(subscription, process_id, tt_number)

Disable old configuration on the routers.

Source code in gso/workflows/edge_port/migrate_edge_port.py
@step("[FOR REAL] Disable configuration on old router")
def disable_old_config_real(
    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 {
        "playbook_name": "gap_ansible/playbooks/iptrunks_migration.yaml",
        "inventory": {
            "all": {
                "hosts": {
                    # TODO
                }
            }
        },
        "extra_vars": extra_vars,
    }

inform_operator_traffic_check()

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.

Source code in gso/workflows/edge_port/migrate_edge_port.py
@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 {}

migrate_edge_port()

Migrate an Edge Port.

Source code in gso/workflows/edge_port/migrate_edge_port.py
@workflow(
    "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 Edge Port."""
    return (
        begin
        >> store_process_subscription(Target.MODIFY)
        >> lso_interaction(disable_old_config_dry)
        >> lso_interaction(disable_old_config_real)
        >> inform_operator_traffic_check
        >> unsync
        >> update_subscription_model
        >> start_moodi()
        # 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_graphs_looks_good_in_moodi
        >> resync
        >> migrate_l3_core_services_to_new_node
        >> confirm_l3_core_service_migrations_are_complete
        >> confirm_graphs_looks_good_in_moodi
        >> migrate_l2_circuits_to_new_node
        >> confirm_l2_circuit_migrations_are_complete
        >> confirm_graphs_looks_good_in_moodi
        >> stop_moodi()
        >> done
    )