Skip to content

Migrate iptrunk

A modification workflow that migrates an IP trunk to a different endpoint.

For a trunk that originally connected endpoints A and B, this workflow introduces a new endpoint C. The trunk is then configured to run from A to C. B is then no longer associated with this IP trunk.

In the input form, the operator selects a new router where this trunk should terminate on.

initial_input_form_generator(subscription_id)

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

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
    """Gather input from the operator on the new router that the IP trunk should connect to."""
    subscription = Iptrunk.from_subscription(subscription_id)
    form_title = (
        f"Subscription {subscription.iptrunk.gs_id} "
        f" from {subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn}"
        f" to {subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn}"
    )
    sides_dict = {
        str(side.iptrunk_side_node.subscription.subscription_id): side.iptrunk_side_node.subscription.description
        for side in subscription.iptrunk.iptrunk_sides
    }

    replaced_side_enum = Choice(
        "Select the side of the IP trunk to be replaced",
        zip(sides_dict.keys(), sides_dict.items(), strict=True),  # type: ignore[arg-type]
    )

    class IPTrunkMigrateForm(FormPage):
        model_config = ConfigDict(title=form_title)

        tt_number: TTNumber
        replace_side: replaced_side_enum  # type: ignore[valid-type]
        warning_label: Label = "Are we moving to a different Site?"
        migrate_to_different_site: bool = False
        restore_isis_metric: bool = True

    migrate_form_input = yield IPTrunkMigrateForm

    current_routers = [
        side.iptrunk_side_node.subscription.subscription_id for side in subscription.iptrunk.iptrunk_sides
    ]

    routers = {}
    for router in get_active_router_subscriptions(includes=["subscription_id", "description"]):
        router_id = router["subscription_id"]
        if router_id not in current_routers:
            current_router_site = Router.from_subscription(router_id).router.router_site.subscription
            old_side_site = Router.from_subscription(migrate_form_input.replace_side).router.router_site
            if (
                not migrate_form_input.migrate_to_different_site
                and current_router_site.subscription_id != old_side_site.owner_subscription_id
            ):
                #  We want to stay on the same site, so all routers that are in different sites get skipped.
                continue
            #  If migrate_to_different_site is true, we can add *all* routers to the result map
            routers[str(router_id)] = router["description"]

    new_router_enum = Choice("Select a new router", zip(routers.keys(), routers.items(), strict=True))  # type: ignore[arg-type]

    class NewSideIPTrunkRouterForm(FormPage):
        model_config = ConfigDict(title=form_title)

        new_node: new_router_enum  # type: ignore[valid-type]

    new_side_iptrunk_router_input = yield NewSideIPTrunkRouterForm
    new_router = new_side_iptrunk_router_input.new_node
    side_a_ae_iface = available_lags_choices(new_router) or JuniperAEInterface

    new_side_is_nokia = get_router_vendor(new_router) == Vendor.NOKIA
    if new_side_is_nokia:

        class NokiaLAGMember(LAGMember):
            interface_name: available_interfaces_choices(  # type: ignore[valid-type]
                new_router,
                subscription.iptrunk.iptrunk_speed,
            )

        ae_members = Annotated[
            LAGMemberList[NokiaLAGMember],
            Len(
                min_length=len(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members),
                max_length=len(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members),
            ),
        ]
    else:
        ae_members = Annotated[  # type: ignore[assignment, misc]
            LAGMemberList[JuniperLAGMember],
            Len(
                min_length=len(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members),
                max_length=len(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members),
            ),
        ]

    replace_index = (
        0
        if str(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.subscription.subscription_id)
        == migrate_form_input.replace_side
        else 1
    )
    existing_lag_ae_members = [
        LAGMember(
            interface_name=iface.interface_name,
            interface_description=iface.interface_description,
        )
        for iface in subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_members
    ]

    class NewSideIPTrunkForm(SubmitFormPage):
        model_config = ConfigDict(title=form_title)

        new_lag_interface: side_a_ae_iface  # type: ignore[valid-type]
        existing_lag_interface: ReadOnlyField(existing_lag_ae_members, default_type=LAGMemberList[LAGMember])  # type: ignore[valid-type]
        new_lag_member_interfaces: ae_members

    new_side_input = yield NewSideIPTrunkForm
    return (
        migrate_form_input.model_dump()
        | new_side_iptrunk_router_input.model_dump()
        | new_side_input.model_dump()
        | {"replace_index": replace_index}
    )

netbox_reserve_interfaces(subscription, new_node, new_lag_interface, new_lag_member_interfaces)

Reserve new interfaces in Netbox, only when the new side's router is a Nokia router.

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@step("Netbox: Reserve new interfaces")
def netbox_reserve_interfaces(
    subscription: Iptrunk, new_node: UUIDstr, new_lag_interface: str, new_lag_member_interfaces: list[dict]
) -> State:
    """Reserve new interfaces in Netbox, only when the new side's router is a Nokia router."""
    new_side = Router.from_subscription(new_node).router
    nbclient = NetboxClient()
    # Create LAG interfaces
    lag_interface: Interfaces = nbclient.create_interface(
        iface_name=new_lag_interface,
        interface_type="lag",
        device_name=new_side.router_fqdn,
        description=str(subscription.subscription_id),
        enabled=True,
    )
    # Attach physical interfaces to LAG
    # Reserve interfaces
    for interface in new_lag_member_interfaces:
        nbclient.attach_interface_to_lag(
            device_name=new_side.router_fqdn,
            lag_name=lag_interface.name,
            iface_name=interface["interface_name"],
            description=str(subscription.subscription_id),
        )
        nbclient.reserve_interface(
            device_name=new_side.router_fqdn,
            iface_name=interface["interface_name"],
        )
    return {"subscription": subscription}

calculate_old_side_data(subscription, replace_index)

Store subscription information of the old side in the state of the workflow for later use.

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@step("Calculate old side data")
def calculate_old_side_data(subscription: Iptrunk, replace_index: int) -> State:
    """Store subscription information of the old side in the state of the workflow for later use."""
    old_subscription = copy.deepcopy(subscription)
    old_side_data = {
        "iptrunk_side_node": old_subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_node,
        "iptrunk_side_ae_iface": old_subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_iface,
        "iptrunk_side_ae_members": old_subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_members,
    }

    return {"old_side_data": old_side_data}

check_ip_trunk_optical_levels_pre(subscription)

Check Optical levels on the trunk before migration.

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@step("Check Optical PRE levels on the trunk endpoint")
def check_ip_trunk_optical_levels_pre(subscription: Iptrunk) -> LSOState:
    """Check Optical levels on the trunk before migration."""
    extra_vars = {"wfo_ip_trunk_json": json.loads(json_dumps(subscription)), "check": "optical_pre"}

    return {
        "playbook_name": "gap_ansible/playbooks/iptrunks_checks.yaml",
        "inventory": {
            "all": {
                "hosts": {
                    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn: None,
                    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn: None,
                }
            }
        },
        "extra_vars": extra_vars,
    }

check_ip_trunk_optical_levels_post(subscription, new_node, new_lag_member_interfaces, replace_index)

Check Optical POST levels on the trunk.

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@step("Check Optical POST levels on the trunk endpoint")
def check_ip_trunk_optical_levels_post(
    subscription: Iptrunk, new_node: Router, new_lag_member_interfaces: list[dict], replace_index: int
) -> LSOState:
    """Check Optical POST levels on the trunk."""
    extra_vars = {
        "wfo_ip_trunk_json": json.loads(json_dumps(subscription)),
        "new_node": json.loads(json_dumps(new_node)),
        "new_lag_member_interfaces": new_lag_member_interfaces,
        "replace_index": replace_index,
        "check": "optical_post",
    }

    return {
        "playbook_name": "gap_ansible/playbooks/iptrunks_checks.yaml",
        "inventory": {
            "all": {
                "hosts": {
                    subscription.iptrunk.iptrunk_sides[1 - replace_index].iptrunk_side_node.router_fqdn: None,
                    new_node.router.router_fqdn: None,
                }
            }
        },
        "extra_vars": extra_vars,
    }

check_ip_trunk_lldp(subscription, new_node, new_lag_member_interfaces, replace_index)

Check LLDP on the new trunk endpoints.

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@step("Check LLDP on the trunk endpoints")
def check_ip_trunk_lldp(
    subscription: Iptrunk, new_node: Router, new_lag_member_interfaces: list[dict], replace_index: int
) -> LSOState:
    """Check LLDP on the new trunk endpoints."""
    extra_vars = {
        "wfo_ip_trunk_json": json.loads(json_dumps(subscription)),
        "new_node": json.loads(json_dumps(new_node)),
        "new_lag_member_interfaces": new_lag_member_interfaces,
        "replace_index": replace_index,
        "check": "lldp",
    }

    return {
        "playbook_name": "gap_ansible/playbooks/iptrunks_checks.yaml",
        "inventory": {
            "all": {
                "hosts": {
                    subscription.iptrunk.iptrunk_sides[1 - replace_index].iptrunk_side_node.router_fqdn: None,
                    new_node.router.router_fqdn: None,
                }
            }
        },
        "extra_vars": extra_vars,
    }

disable_old_config_dry(subscription, new_node, new_lag_interface, new_lag_member_interfaces, replace_index, process_id, tt_number)

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

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@step("[DRY RUN] Disable configuration on old router")
def disable_old_config_dry(
    subscription: Iptrunk,
    new_node: Router,
    new_lag_interface: str,
    new_lag_member_interfaces: list[dict],
    replace_index: int,
    process_id: UUIDstr,
    tt_number: str,
) -> LSOState:
    """Perform a dry run of disabling the old configuration on the routers."""
    extra_vars = {
        "wfo_trunk_json": json.loads(json_dumps(subscription)),
        "new_node": json.loads(json_dumps(new_node)),
        "new_lag_interface": new_lag_interface,
        "new_lag_member_interfaces": new_lag_member_interfaces,
        "replace_index": replace_index,
        "verb": "deactivate",
        "config_object": "deactivate",
        "dry_run": True,
        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} "
        f"- Deploy config for {subscription.iptrunk.gs_id}",
    }

    return {
        "playbook_name": "gap_ansible/playbooks/iptrunks_migration.yaml",
        "inventory": {
            "all": {
                "hosts": {
                    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn: None,
                    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn: None,
                    new_node.router.router_fqdn: None,
                }
            }
        },
        "extra_vars": extra_vars,
    }

disable_old_config_real(subscription, new_node, new_lag_interface, new_lag_member_interfaces, replace_index, process_id, tt_number)

Disable old configuration on the routers.

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@step("[FOR REAL] Disable configuration on old router")
def disable_old_config_real(
    subscription: Iptrunk,
    new_node: Router,
    new_lag_interface: str,
    new_lag_member_interfaces: list[dict],
    replace_index: int,
    process_id: UUIDstr,
    tt_number: str,
) -> LSOState:
    """Disable old configuration on the routers."""
    extra_vars = {
        "wfo_trunk_json": json.loads(json_dumps(subscription)),
        "new_node": json.loads(json_dumps(new_node)),
        "new_lag_interface": new_lag_interface,
        "new_lag_member_interfaces": new_lag_member_interfaces,
        "replace_index": replace_index,
        "verb": "deactivate",
        "config_object": "deactivate",
        "dry_run": False,
        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} "
        f"- Deploy config for {subscription.iptrunk.gs_id}",
    }

    return {
        "playbook_name": "gap_ansible/playbooks/iptrunks_migration.yaml",
        "inventory": {
            "all": {
                "hosts": {
                    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn: None,
                    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn: None,
                    new_node.router.router_fqdn: None,
                }
            }
        },
        "extra_vars": extra_vars,
    }

deploy_new_config_dry(subscription, new_node, new_lag_interface, new_lag_member_interfaces, replace_index, process_id, tt_number)

Perform a dry run of deploying configuration on the new router.

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@step("[DRY RUN] Deploy configuration on new router")
def deploy_new_config_dry(
    subscription: Iptrunk,
    new_node: Router,
    new_lag_interface: str,
    new_lag_member_interfaces: list[dict],
    replace_index: int,
    process_id: UUIDstr,
    tt_number: str,
) -> LSOState:
    """Perform a dry run of deploying configuration on the new router."""
    extra_vars = {
        "wfo_trunk_json": json.loads(json_dumps(subscription)),
        "new_node": json.loads(json_dumps(new_node)),
        "new_lag_interface": new_lag_interface,
        "new_lag_member_interfaces": new_lag_member_interfaces,
        "replace_index": replace_index,
        "verb": "deploy",
        "config_object": "trunk_interface",
        "dry_run": True,
        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} "
        f"- Deploy config for {subscription.iptrunk.gs_id}",
    }

    return {
        "playbook_name": "gap_ansible/playbooks/iptrunks_migration.yaml",
        "inventory": {
            "all": {
                "hosts": {
                    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn: None,
                    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn: None,
                    new_node.router.router_fqdn: None,
                }
            }
        },
        "extra_vars": extra_vars,
    }

deploy_new_config_real(subscription, new_node, new_lag_interface, new_lag_member_interfaces, replace_index, process_id, tt_number)

Deploy configuration on the new router.

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@step("[FOR REAL] Deploy configuration on new router")
def deploy_new_config_real(
    subscription: Iptrunk,
    new_node: Router,
    new_lag_interface: str,
    new_lag_member_interfaces: list[dict],
    replace_index: int,
    process_id: UUIDstr,
    tt_number: str,
) -> LSOState:
    """Deploy configuration on the new router."""
    extra_vars = {
        "wfo_trunk_json": json.loads(json_dumps(subscription)),
        "new_node": json.loads(json_dumps(new_node)),
        "new_lag_interface": new_lag_interface,
        "new_lag_member_interfaces": new_lag_member_interfaces,
        "replace_index": replace_index,
        "verb": "deploy",
        "config_object": "trunk_interface",
        "dry_run": False,
        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} "
        f"- Deploy config for {subscription.iptrunk.gs_id}",
    }

    return {
        "playbook_name": "gap_ansible/playbooks/iptrunks_migration.yaml",
        "inventory": {
            "all": {
                "hosts": {
                    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn: None,
                    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn: None,
                    new_node.router.router_fqdn: None,
                }
            }
        },
        "extra_vars": extra_vars,
    }

update_remaining_side_bfd_dry(subscription, new_node, replace_index, process_id, tt_number)

Perform a dry run of updating configuration on the remaining router.

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@step("[DRY RUN] Update BFD on the remaining side")
def update_remaining_side_bfd_dry(
    subscription: Iptrunk, new_node: Router, replace_index: int, process_id: UUIDstr, tt_number: str
) -> LSOState:
    """Perform a dry run of updating configuration on the remaining router."""
    extra_vars = {
        "wfo_trunk_json": json.loads(json_dumps(subscription)),
        "new_node": json.loads(json_dumps(new_node)),
        "replace_index": replace_index,
        "verb": "update",
        "config_object": "bfd_update",
        "dry_run": True,
        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Update BFD config.",
    }

    return {
        "playbook_name": "gap_ansible/playbooks/iptrunks_migration.yaml",
        "inventory": {
            "all": {
                "hosts": {
                    subscription.iptrunk.iptrunk_sides[1 - replace_index].iptrunk_side_node.router_fqdn: None,
                }
            }
        },
        "extra_vars": extra_vars,
    }

update_remaining_side_bfd_real(subscription, new_node, replace_index, process_id, tt_number)

Update configuration on the remaining router.

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@step("[FOR REAL] Update BFD on the remaining side")
def update_remaining_side_bfd_real(
    subscription: Iptrunk, new_node: Router, replace_index: int, process_id: UUIDstr, tt_number: str
) -> LSOState:
    """Update configuration on the remaining router."""
    extra_vars = {
        "wfo_trunk_json": json.loads(json_dumps(subscription)),
        "new_node": json.loads(json_dumps(new_node)),
        "replace_index": replace_index,
        "verb": "update",
        "config_object": "bfd_update",
        "dry_run": False,
        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Update BFD config.",
    }

    return {
        "playbook_name": "gap_ansible/playbooks/iptrunks_migration.yaml",
        "inventory": {
            "all": {
                "hosts": {
                    subscription.iptrunk.iptrunk_sides[1 - replace_index].iptrunk_side_node.router_fqdn: None,
                }
            }
        },
        "extra_vars": extra_vars,
    }

check_ip_trunk_bfd(subscription, new_node, replace_index)

Check BFD session across the new trunk.

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@step("Check BFD session over trunk")
def check_ip_trunk_bfd(subscription: Iptrunk, new_node: Router, replace_index: int) -> LSOState:
    """Check BFD session across the new trunk."""
    extra_vars = {
        "wfo_ip_trunk_json": json.loads(json_dumps(subscription)),
        "new_node": json.loads(json_dumps(new_node)),
        "check": "bfd",
    }

    return {
        "playbook_name": "gap_ansible/playbooks/iptrunks_checks.yaml",
        "inventory": {
            "all": {
                "hosts": {
                    subscription.iptrunk.iptrunk_sides[1 - replace_index].iptrunk_side_node.router_fqdn: None,
                }
            }
        },
        "extra_vars": extra_vars,
    }

confirm_continue_move_fiber()

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

Source code in gso/workflows/iptrunk/migrate_iptrunk.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 trunk interface has been deployed, wait for the physical connection to be moved."

    yield ProvisioningResultPage

    return {}

check_ip_trunk_connectivity(subscription, replace_index)

Check successful connectivity across the new trunk.

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@step("Check IP connectivity of the trunk")
def check_ip_trunk_connectivity(subscription: Iptrunk, replace_index: int) -> LSOState:
    """Check successful connectivity across the new trunk."""
    extra_vars = {"wfo_ip_trunk_json": json.loads(json_dumps(subscription)), "check": "ping"}

    return {
        "playbook_name": "gap_ansible/playbooks/iptrunks_checks.yaml",
        "inventory": {
            "all": {
                "hosts": {
                    subscription.iptrunk.iptrunk_sides[1 - replace_index].iptrunk_side_node.router_fqdn: None,
                }
            }
        },
        "extra_vars": extra_vars,
    }

deploy_new_isis(subscription, new_node, new_lag_interface, new_lag_member_interfaces, replace_index, process_id, tt_number)

Deploy ISIS configuration.

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@step("[FOR REAL] Deploy ISIS configuration on new router")
def deploy_new_isis(
    subscription: Iptrunk,
    new_node: Router,
    new_lag_interface: str,
    new_lag_member_interfaces: list[dict],
    replace_index: int,
    process_id: UUIDstr,
    tt_number: str,
) -> LSOState:
    """Deploy ISIS configuration."""
    extra_vars = {
        "wfo_trunk_json": json.loads(json_dumps(subscription)),
        "new_node": json.loads(json_dumps(new_node)),
        "new_lag_interface": new_lag_interface,
        "new_lag_member_interfaces": new_lag_member_interfaces,
        "replace_index": replace_index,
        "verb": "deploy",
        "config_object": "isis_interface",
        "dry_run": False,
        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} "
        f"- Deploy config for {subscription.iptrunk.gs_id}",
    }

    return {
        "playbook_name": "gap_ansible/playbooks/iptrunks_migration.yaml",
        "inventory": {
            "all": {
                "hosts": {
                    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn: None,
                    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn: None,
                    new_node.router.router_fqdn: None,
                }
            }
        },
        "extra_vars": extra_vars,
    }

check_ip_trunk_isis(subscription, replace_index)

Run an Ansible playbook to confirm ISIS adjacency.

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@step("Check ISIS adjacency")
def check_ip_trunk_isis(subscription: Iptrunk, replace_index: int) -> LSOState:
    """Run an Ansible playbook to confirm ISIS adjacency."""
    extra_vars = {"wfo_ip_trunk_json": json.loads(json_dumps(subscription)), "check": "isis"}

    return {
        "playbook_name": "gap_ansible/playbooks/iptrunks_checks.yaml",
        "inventory": {
            "all": {
                "hosts": {
                    subscription.iptrunk.iptrunk_sides[1 - replace_index].iptrunk_side_node.router_fqdn: None,
                }
            }
        },
        "extra_vars": extra_vars,
    }

confirm_continue_restore_isis()

Wait for an operator to confirm that the old ISIS metric should be restored.

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@inputstep("Wait for confirmation", assignee=Assignee.SYSTEM)
def confirm_continue_restore_isis() -> FormGenerator:
    """Wait for an operator to confirm that the old ISIS metric should be restored."""

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

        info_label: Label = "ISIS config has been deployed, confirm if you want to restore the old metric."

    yield ProvisioningResultPage

    return {}

restore_isis_metric(subscription, process_id, tt_number, old_isis_metric)

Restore the ISIS metric to its original value.

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@step("[FOR REAL] Restore ISIS metric to original value")
def restore_isis_metric(
    subscription: Iptrunk,
    process_id: UUIDstr,
    tt_number: str,
    old_isis_metric: int,
) -> LSOState:
    """Restore the ISIS metric to its original value."""
    subscription.iptrunk.iptrunk_isis_metric = old_isis_metric
    extra_vars = {
        "wfo_trunk_json": json.loads(json_dumps(subscription)),
        "dry_run": False,
        "verb": "deploy",
        "config_object": "isis_interface",
        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Deploy config for "
        f"{subscription.iptrunk.gs_id}",
    }

    return {
        "subscription": subscription,
        "playbook_name": "gap_ansible/playbooks/iptrunks.yaml",
        "inventory": {
            "all": {
                "hosts": {
                    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn: None,
                    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn: None,
                }
            }
        },
        "extra_vars": extra_vars,
    }

delete_old_config_dry(subscription, new_node, new_lag_interface, new_lag_member_interfaces, replace_index, process_id, tt_number)

Perform a dry run of deleting the old configuration.

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@step("[DRY RUN] Delete configuration on old router")
def delete_old_config_dry(
    subscription: Iptrunk,
    new_node: Router,
    new_lag_interface: str,
    new_lag_member_interfaces: list[dict],
    replace_index: int,
    process_id: UUIDstr,
    tt_number: str,
) -> LSOState:
    """Perform a dry run of deleting the old configuration."""
    extra_vars = {
        "wfo_trunk_json": json.loads(json_dumps(subscription)),
        "new_node": json.loads(json_dumps(new_node)),
        "new_lag_interface": new_lag_interface,
        "new_lag_member_interfaces": new_lag_member_interfaces,
        "replace_index": replace_index,
        "verb": "delete",
        "config_object": "delete",
        "dry_run": True,
        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} "
        f"- Deploy config for {subscription.iptrunk.gs_id}",
    }

    return {
        "playbook_name": "gap_ansible/playbooks/iptrunks_migration.yaml",
        "inventory": {
            "all": {
                "hosts": {
                    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn: None,
                    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn: None,
                    new_node.router.router_fqdn: None,
                }
            }
        },
        "extra_vars": extra_vars,
    }

delete_old_config_real(subscription, new_node, new_lag_interface, new_lag_member_interfaces, replace_index, process_id, tt_number)

Delete old configuration from the routers.

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@step("[FOR REAL] Delete configuration on old router")
def delete_old_config_real(
    subscription: Iptrunk,
    new_node: Router,
    new_lag_interface: str,
    new_lag_member_interfaces: list[dict],
    replace_index: int,
    process_id: UUIDstr,
    tt_number: str,
) -> LSOState:
    """Delete old configuration from the routers."""
    extra_vars = {
        "wfo_trunk_json": json.loads(json_dumps(subscription)),
        "new_node": json.loads(json_dumps(new_node)),
        "new_lag_interface": new_lag_interface,
        "new_lag_member_interfaces": new_lag_member_interfaces,
        "replace_index": replace_index,
        "verb": "delete",
        "config_object": "delete",
        "dry_run": False,
        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} "
        f"- Deploy config for {subscription.iptrunk.gs_id}",
    }

    return {
        "playbook_name": "gap_ansible/playbooks/iptrunks_migration.yaml",
        "inventory": {
            "all": {
                "hosts": {
                    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn: None,
                    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn: None,
                    new_node.router.router_fqdn: None,
                }
            }
        },
        "extra_vars": extra_vars,
    }

update_ipam(subscription, replace_index, new_node, new_lag_interface)

Update IPAM resources.

Move the DNS record pointing to the old side of the trunk, to the new side.

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@step("Update IP records in IPAM")
def update_ipam(subscription: Iptrunk, replace_index: int, new_node: Router, new_lag_interface: str) -> State:
    """Update IPAM resources.

    Move the DNS record pointing to the old side of the trunk, to the new side.
    """
    v4_addr = subscription.iptrunk.iptrunk_ipv4_network[replace_index]
    # IPv6 networks start with an unused address we need to skip past.
    v6_addr = subscription.iptrunk.iptrunk_ipv6_network[replace_index + 1]

    #  Out with the old
    infoblox.delete_host_by_ip(subscription.iptrunk.iptrunk_ipv4_network[replace_index])
    #  And in with the new
    new_fqdn = f"{new_lag_interface}-0.{new_node.router.router_fqdn}"
    comment = str(subscription.subscription_id)
    infoblox.create_host_by_ip(new_fqdn, "TRUNK", comment, ipv4_address=v4_addr, ipv6_address=v6_addr)

    return {"subscription": subscription}

update_subscription_model(subscription, replace_index, new_node, new_lag_interface, new_lag_member_interfaces)

Update the subscription model in the database.

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@step("Update subscription model")
def update_subscription_model(
    subscription: Iptrunk,
    replace_index: int,
    new_node: UUIDstr,
    new_lag_interface: str,
    new_lag_member_interfaces: list[dict],
) -> State:
    """Update the subscription model in the database."""
    # Deep copy of subscription data
    subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_node = Router.from_subscription(new_node).router
    subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_iface = new_lag_interface
    subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_members.clear()
    #  And update the list to only include the new member interfaces
    for member in new_lag_member_interfaces:
        subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_members.append(
            IptrunkInterfaceBlock.new(subscription_id=uuid4(), **member),
        )

    # Take the new site names, and update the subscription description
    side_names = sorted([
        subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_site.site_name,
        subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_site.site_name,
    ])
    subscription.description = f"IP trunk {side_names[0]} {side_names[1]}, {subscription.iptrunk.gs_id}"

    return {"subscription": subscription}

netbox_remove_old_interfaces(old_side_data)

Remove the old LAG interface from Netbox, only relevant if the old side is a Nokia router.

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@step("Netbox: Remove old LAG interface")
def netbox_remove_old_interfaces(old_side_data: dict) -> State:
    """Remove the old LAG interface from Netbox, only relevant if the old side is a Nokia router."""
    nbclient = NetboxClient()

    for iface in old_side_data["iptrunk_side_ae_members"]:
        nbclient.free_interface(
            old_side_data["iptrunk_side_node"]["router_fqdn"],
            iface["interface_name"],
        )

    nbclient.delete_interface(
        old_side_data["iptrunk_side_node"]["router_fqdn"],
        old_side_data["iptrunk_side_ae_iface"],
    )

    return {}

netbox_allocate_new_interfaces(subscription, replace_index)

Allocate the new LAG interface in Netbox. Only relevant if the new router is a Nokia.

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@step("Netbox: Allocate new LAG member interfaces")
def netbox_allocate_new_interfaces(subscription: Iptrunk, replace_index: int) -> State:
    """Allocate the new LAG interface in Netbox. Only relevant if the new router is a Nokia."""
    nbclient = NetboxClient()
    new_side = subscription.iptrunk.iptrunk_sides[replace_index]

    for interface in new_side.iptrunk_side_ae_members:
        nbclient.allocate_interface(
            device_name=new_side.iptrunk_side_node.router_fqdn,
            iface_name=interface.interface_name,
        )

    return {"subscription": subscription}

create_new_sharepoint_checklist(subscription, tt_number, process_id)

Create a new checklist item in SharePoint for approving this migrated IPtrunk.

Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@step("Create a new SharePoint checklist item")
def create_new_sharepoint_checklist(subscription: Iptrunk, tt_number: str, process_id: UUIDstr) -> State:
    """Create a new checklist item in SharePoint for approving this migrated IPtrunk."""
    new_list_item_url = SharePointClient().add_list_item(
        list_name="ip_trunk",
        fields={
            "Title": f"{subscription.description} - {subscription.iptrunk.gs_id}",
            "TT_NUMBER": tt_number,
            "GAP_PROCESS_URL": f"{load_oss_params().GENERAL.public_hostname}/workflows/{process_id}",
            "ACTIVITY_TYPE": "Migration",
        },
    )

    return {"checklist_url": new_list_item_url}

migrate_iptrunk()

Migrate an IP trunk.

  • Reserve new interfaces in Netbox
  • Set the ISIS metric of the current trunk to an arbitrarily high value to drain all traffic
  • Disable - but do not delete - the old configuration on the routers, first as a dry run
  • Deploy the new configuration on the routers, first as a dry run
  • Wait for operator confirmation that the physical fiber has been moved before continuing
  • Deploy a new ISIS interface between routers A and C
  • Wait for operator confirmation that ISIS is behaving as expected
  • Restore the old ISIS metric on the new trunk
  • Delete the old configuration from the routers, first as a dry run
  • Reflect the changes made in IPAM
  • Update the subscription model in the database
  • Update the reserved interfaces in Netbox
Source code in gso/workflows/iptrunk/migrate_iptrunk.py
@workflow(
    "Migrate an IP Trunk",
    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
    target=Target.MODIFY,
)
def migrate_iptrunk() -> StepList:
    """Migrate an IP trunk.

    * Reserve new interfaces in Netbox
    * Set the ISIS metric of the current trunk to an arbitrarily high value to drain all traffic
    * Disable - but do not delete - the old configuration on the routers, first as a dry run
    * Deploy the new configuration on the routers, first as a dry run
    * Wait for operator confirmation that the physical fiber has been moved before continuing
    * Deploy a new ISIS interface between routers A and C
    * Wait for operator confirmation that ISIS is behaving as expected
    * Restore the old ISIS metric on the new trunk
    * Delete the old configuration from the routers, first as a dry run
    * Reflect the changes made in IPAM
    * Update the subscription model in the database
    * Update the reserved interfaces in Netbox
    """
    new_side_is_nokia = conditional(lambda state: get_router_vendor(state["new_node"]) == Vendor.NOKIA)
    old_side_is_nokia = conditional(
        lambda state: get_router_vendor(state["old_side_data"]["iptrunk_side_node"]["owner_subscription_id"])
        == Vendor.NOKIA
    )
    should_restore_isis_metric = conditional(lambda state: state["restore_isis_metric"])
    trunk_type_is_leased = conditional(
        lambda state: state["subscription"]["iptrunk"]["iptrunk_type"] == IptrunkType.LEASED
    )

    return (
        begin
        >> store_process_subscription(Target.MODIFY)
        >> unsync
        >> new_side_is_nokia(netbox_reserve_interfaces)
        >> calculate_old_side_data
        >> lso_interaction(set_isis_to_max)
        >> lso_interaction(check_ip_trunk_optical_levels_pre)
        >> lso_interaction(disable_old_config_dry)
        >> lso_interaction(disable_old_config_real)
        >> lso_interaction(deploy_new_config_dry)
        >> lso_interaction(deploy_new_config_real)
        >> trunk_type_is_leased(lso_interaction(update_remaining_side_bfd_dry))
        >> trunk_type_is_leased(lso_interaction(update_remaining_side_bfd_real))
        >> confirm_continue_move_fiber
        >> lso_interaction(check_ip_trunk_optical_levels_post)
        >> lso_interaction(check_ip_trunk_lldp)
        >> trunk_type_is_leased(lso_interaction(check_ip_trunk_bfd))
        >> lso_interaction(check_ip_trunk_connectivity)
        >> lso_interaction(deploy_new_isis)
        >> lso_interaction(check_ip_trunk_isis)
        >> lso_interaction(delete_old_config_dry)
        >> lso_interaction(delete_old_config_real)
        >> update_ipam
        >> update_subscription_model
        >> should_restore_isis_metric(confirm_continue_restore_isis)
        >> should_restore_isis_metric(lso_interaction(restore_isis_metric))
        >> old_side_is_nokia(netbox_remove_old_interfaces)
        >> new_side_is_nokia(netbox_allocate_new_interfaces)
        >> resync
        >> create_new_sharepoint_checklist
        >> prompt_sharepoint_checklist_url
        >> done
    )