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.
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
)
|