Skip to content

Migrate layer 2 circuit

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.

Info

Since an L2 Circuit Service has two sides, the workflow must be run separately for each side to fully migrate the service.

input_form_generator(subscription_id)

Generate an input form for migrating a Layer 2 Circuit.

Source code in gso/workflows/l2_circuit/migrate_layer_2_circuit.py
def input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
    """Generate an input form for migrating a Layer 2 Circuit."""
    subscription = Layer2Circuit.from_subscription(subscription_id)

    def _source_edge_port_selector() -> TypeAlias:
        sides_dict = {
            str(side.sbp.edge_port.owner_subscription_id): EdgePort.from_subscription(
                side.sbp.edge_port.owner_subscription_id
            ).description
            for side in subscription.layer_2_circuit.layer_2_circuit_sides
        }
        return cast(
            type[Choice],
            Choice.__call__("Select source Edge Port", zip(sides_dict.keys(), sides_dict.items(), strict=True)),
        )

    class MigrateL2CircuitForm(FormPage):
        model_config = ConfigDict(title="Migrating Layer 2 Circuit")

        tt_number: TTNumber
        is_human_initiated_wf: bool = True
        source_edge_port: _source_edge_port_selector()  # type: ignore[valid-type]
        divider: Divider = Field(None, exclude=True)

        label_a: Label = Field("Are we migrating to a different site?", exclude=True)
        migrate_to_different_site: bool = False

        label_b: Label = Field("Execute Ansible playbooks on the OLD side of the circuit?", exclude=True)
        run_old_side_ansible: bool = True
        label_c: Label = Field("Execute Ansible playbooks on the NEW side of the circuit?", exclude=True)
        run_new_side_ansible: bool = True

    initial_user_input = yield MigrateL2CircuitForm

    source_edge_port = EdgePort.from_subscription(initial_user_input.source_edge_port)
    label_to_filter = None
    if subscription.layer_2_circuit_service_type == Layer2CircuitServiceType.EXPRESSROUTE:
        label_to_filter = ProductName.EXPRESSROUTE_EDGE
    active_edge_ports = get_active_edge_port_subscriptions(
        partner_id=source_edge_port.customer_id, port_label_name=label_to_filter
    )

    #  For the destination Edge Port options, we take the XOR of ``migrate_to_different_site`` and two Edge Ports being
    #  located in the same site. This way, we either get a list of all Edge Port that belong to this partner that are
    #  all in the same site, OR all the remaining Edge Ports that are not at the same site. This is to avoid presenting
    #  the operator with a list of **all** Edge Ports that a partner has.
    destination_edge_port_options = {
        str(edge_port.subscription_id): edge_port.description
        for edge_port in active_edge_ports
        if initial_user_input.migrate_to_different_site
        ^ (
            edge_port.edge_port.node.router_site.owner_subscription_id
            == source_edge_port.edge_port.node.router_site.owner_subscription_id
        )
    }
    destination_edge_port_selector = Choice.__call__(
        "Select a destination Edge Port",
        zip(destination_edge_port_options.keys(), destination_edge_port_options.items(), strict=True),
    )

    class SelectNewEdgePortForm(SubmitFormPage):
        model_config = ConfigDict(title="Migrating Layer 2 Circuit")

        destination_edge_port: destination_edge_port_selector  # type: ignore[valid-type]
        generate_new_vc_id: bool = False

    user_input = yield SelectNewEdgePortForm

    return {
        "tt_number": initial_user_input.tt_number,
        "run_old_side_ansible": initial_user_input.run_old_side_ansible,
        "run_new_side_ansible": initial_user_input.run_new_side_ansible,
        "subscription": subscription,
        "subscription_id": subscription_id,
        "source_edge_port": initial_user_input.source_edge_port,
        "destination_edge_port": user_input.destination_edge_port,
        "generate_new_vc_id": user_input.generate_new_vc_id,
        IS_HUMAN_INITIATED_WF_KEY: initial_user_input.is_human_initiated_wf,
    }

update_subscription_model(subscription, source_edge_port, destination_edge_port, generate_new_vc_id)

Replace the old Edge Port with the newly selected one in the subscription model.

Source code in gso/workflows/l2_circuit/migrate_layer_2_circuit.py
@step("Update subscription model")
def update_subscription_model(
    subscription: Layer2Circuit,
    source_edge_port: EdgePort,
    destination_edge_port: EdgePort,
    generate_new_vc_id: bool,  # noqa: FBT001
) -> State:
    """Replace the old Edge Port with the newly selected one in the subscription model."""
    replace_index = (
        0
        if subscription.layer_2_circuit.layer_2_circuit_sides[0].sbp.edge_port.owner_subscription_id
        == source_edge_port.subscription_id
        else 1
    )
    subscription.layer_2_circuit.layer_2_circuit_sides[replace_index].sbp.edge_port = destination_edge_port.edge_port

    if generate_new_vc_id:
        vc_id = generate_unique_vc_id(l2c_type=subscription.layer_2_circuit.layer_2_circuit_type)
        if not vc_id:
            msg = "Failed to generate unique Virtual Circuit ID."
            raise ProcessFailureError(msg)
        subscription.layer_2_circuit.virtual_circuit_id = vc_id

    return {"subscription": subscription}

generate_scoped_subscription(subscription, source_edge_port, destination_edge_port)

Calculate what the updated subscription model will look like, but don't update the actual subscription yet.

The new subscription is used for Moodi, but the updated subscription model is not stored yet, to avoid issues recovering when the workflow is aborted.

Source code in gso/workflows/l2_circuit/migrate_layer_2_circuit.py
@step("Generate updated subscription model for moodi")
def generate_scoped_subscription(
    subscription: Layer2Circuit,
    source_edge_port: EdgePort,
    destination_edge_port: EdgePort,
) -> State:
    """Calculate what the updated subscription model will look like, but don't update the actual subscription yet.

    The new subscription is used for Moodi, but the updated subscription model is not
    stored yet, to avoid issues recovering when the workflow is aborted.
    """
    scoped_service = generate_scoped_subscription_for_l2_service(subscription, source_edge_port, destination_edge_port)
    return {_MONITORED_OBJECTS_KEY: scoped_service}

migrate_layer_2_circuit()

Migrate a Layer 2 Circuit.

Source code in gso/workflows/l2_circuit/migrate_layer_2_circuit.py
@modify_workflow("Migrate Layer 2 Circuit", initial_input_form=input_form_generator)
def migrate_layer_2_circuit() -> StepList:
    """Migrate a Layer 2 Circuit."""
    run_old_side_ansible = conditional(operator.itemgetter("run_old_side_ansible"))
    run_new_side_ansible = conditional(operator.itemgetter("run_new_side_ansible"))
    should_run_moodi = conditional(lambda state: state["run_old_side_ansible"] or state["run_new_side_ansible"])
    is_human_initiated_wf = conditional(lambda state: bool(state.get(IS_HUMAN_INITIATED_WF_KEY)))
    is_not_human_initiated_wf = conditional(lambda state: not bool(state.get(IS_HUMAN_INITIATED_WF_KEY)))

    return (
        begin
        >> generate_scoped_subscription
        >> should_run_moodi(start_moodi(monitored_objects_key=_MONITORED_OBJECTS_KEY))
        >> run_old_side_ansible(generate_fqdn_list)
        >> run_old_side_ansible(annotate_edge_ports_with_partner_names)
        >> is_human_initiated_wf(run_old_side_ansible(lso_interaction(terminate_l2circuit_dry)))
        >> is_human_initiated_wf(run_old_side_ansible(lso_interaction(terminate_l2circuit_real)))
        >> is_not_human_initiated_wf(run_old_side_ansible(anonymous_lso_interaction(terminate_l2circuit_real)))
        >> update_subscription_model
        >> run_new_side_ansible(generate_fqdn_list)
        >> run_new_side_ansible(annotate_edge_ports_with_partner_names)
        >> is_human_initiated_wf(run_new_side_ansible(lso_interaction(provision_l2circuit_dry)))
        >> is_human_initiated_wf(run_new_side_ansible(lso_interaction(provision_l2circuit_real)))
        >> is_not_human_initiated_wf(run_new_side_ansible(anonymous_lso_interaction(provision_l2circuit_real)))
        >> should_run_moodi(stop_moodi())
    )