diff --git a/gso/workflows/iptrunk/__init__.py b/gso/workflows/iptrunk/__init__.py
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..709c5458620c2110e8882ddffed5417d3f36cdd7 100644
--- a/gso/workflows/iptrunk/__init__.py
+++ b/gso/workflows/iptrunk/__init__.py
@@ -0,0 +1 @@
+"""All workflows that can be executed on IP trunks."""
diff --git a/gso/workflows/iptrunk/create_iptrunk.py b/gso/workflows/iptrunk/create_iptrunk.py
index 9c432a81b81b5c55d985d4126e59774939dee5c7..d159f53106182deef7793e4e5f417d1effdafbce 100644
--- a/gso/workflows/iptrunk/create_iptrunk.py
+++ b/gso/workflows/iptrunk/create_iptrunk.py
@@ -1,3 +1,5 @@
+"""A creation workflow that deploys a new IP trunk service."""
+
from uuid import uuid4
from orchestrator.forms import FormPage
@@ -33,6 +35,7 @@ from gso.utils.helpers import (
def initial_input_form_generator(product_name: str) -> FormGenerator:
+ """Gather input from the user in three steps. General information, and information on both sides of the trunk."""
# TODO: implement more strict validation:
# * interface names must be validated
@@ -160,6 +163,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
@step("Create subscription")
def create_subscription(product: UUIDstr, customer: UUIDstr) -> State:
+ """Create a new subscription object in the database."""
subscription = IptrunkInactive.from_product_id(product, customer)
return {
@@ -170,6 +174,7 @@ def create_subscription(product: UUIDstr, customer: UUIDstr) -> State:
@step("Get information from IPAM")
def get_info_from_ipam(subscription: IptrunkProvisioning) -> State:
+ """Allocate :term:`IP` resources in :term:`IPAM`."""
subscription.iptrunk.iptrunk_ipv4_network = infoblox.allocate_v4_network(
"TRUNK",
subscription.iptrunk.iptrunk_description,
@@ -199,6 +204,7 @@ def initialize_subscription(
side_b_ae_geant_a_sid: str,
side_b_ae_members: list[dict],
) -> State:
+ """Take all input from the user, and store it in the database."""
subscription.iptrunk.geant_s_sid = geant_s_sid
subscription.iptrunk.iptrunk_description = iptrunk_description
subscription.iptrunk.iptrunk_type = iptrunk_type
@@ -235,6 +241,7 @@ def provision_ip_trunk_iface_dry(
process_id: UUIDstr,
tt_number: str,
) -> State:
+ """Perform a dry run of deploying configuration on both sides of the trunk."""
provisioning_proxy.provision_ip_trunk(
subscription, process_id, callback_route, tt_number, "trunk_interface", dry_run=True,
)
@@ -249,6 +256,7 @@ def provision_ip_trunk_iface_real(
process_id: UUIDstr,
tt_number: str,
) -> State:
+ """Deploy IP trunk configuration on both sides."""
provisioning_proxy.provision_ip_trunk(
subscription, process_id, callback_route, tt_number, "trunk_interface", dry_run=False,
)
@@ -263,6 +271,7 @@ def check_ip_trunk_connectivity(
process_id: UUIDstr,
tt_number: str,
) -> State:
+ """Check successful connectivity across the new trunk."""
provisioning_proxy.check_ip_trunk(subscription, process_id, callback_route, tt_number, "ping")
return {"subscription": subscription}
@@ -275,6 +284,7 @@ def provision_ip_trunk_isis_iface_dry(
process_id: UUIDstr,
tt_number: str,
) -> State:
+ """Perform a dry run of deploying :term:`ISIS` configuration."""
provisioning_proxy.provision_ip_trunk(subscription, process_id, callback_route, tt_number, "isis_interface")
return {"subscription": subscription}
@@ -287,6 +297,7 @@ def provision_ip_trunk_isis_iface_real(
process_id: UUIDstr,
tt_number: str,
) -> State:
+ """Deploy :term:`ISIS` configuration on both sides."""
provisioning_proxy.provision_ip_trunk(
subscription, process_id, callback_route, tt_number, "isis_interface", dry_run=False,
)
@@ -301,6 +312,7 @@ def check_ip_trunk_isis(
process_id: UUIDstr,
tt_number: str,
) -> State:
+ """Run an Ansible playbook to confirm :term:`ISIS` adjacency."""
provisioning_proxy.check_ip_trunk(subscription, process_id, callback_route, tt_number, "isis")
return {"subscription": subscription}
@@ -360,6 +372,18 @@ def allocate_interfaces_in_netbox(subscription: IptrunkProvisioning) -> State:
target=Target.CREATE,
)
def create_iptrunk() -> StepList:
+ """Create a new IP trunk.
+
+ * Create the subscription object in the database
+ * Gather relevant information from Infoblox
+ * Reserve interfaces in Netbox
+ * Deploy configuration on the two sides of the trunk, first as a dry run
+ * Check connectivity on the new trunk
+ * Deploy the new :term:`ISIS` metric on the trunk, first as a dry run
+ * Verify :term:`ISIS` adjacency
+ * Allocate the interfaces in Netbox
+ * Set the subscription to active in the database
+ """
return (
init
>> create_subscription
diff --git a/gso/workflows/iptrunk/migrate_iptrunk.py b/gso/workflows/iptrunk/migrate_iptrunk.py
index ca6231a8bda43b04e985b84c6b2ab47423a10692..af2640dfd9d654513732e389f9f00dd81268a2f1 100644
--- a/gso/workflows/iptrunk/migrate_iptrunk.py
+++ b/gso/workflows/iptrunk/migrate_iptrunk.py
@@ -1,3 +1,9 @@
+"""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.
+"""
+
import copy
import re
from logging import getLogger
@@ -37,6 +43,7 @@ logger = getLogger(__name__)
def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+ """Gather input from the operator on the new router that the IP trunk should terminate on."""
subscription = Iptrunk.from_subscription(subscription_id)
form_title = (
f"Subscription {subscription.iptrunk.geant_s_sid} "
@@ -166,6 +173,7 @@ def disable_old_config_dry(
process_id: UUIDstr,
tt_number: str,
) -> State:
+ """Perform a dry run of disabling the old configuration on the routers."""
provisioning_proxy.migrate_ip_trunk(
subscription,
new_node,
@@ -195,6 +203,7 @@ def disable_old_config_real(
process_id: UUIDstr,
tt_number: str,
) -> State:
+ """Disable old configuration on the routers."""
provisioning_proxy.migrate_ip_trunk(
subscription,
new_node,
@@ -225,6 +234,10 @@ def deploy_new_config_dry(
process_id: UUIDstr,
tt_number: str,
) -> State:
+ """Perform a dry run of deploying configuration on the new router.
+
+ TODO: set the proper playbook verb
+ """
provisioning_proxy.migrate_ip_trunk(
subscription,
new_node,
@@ -256,6 +269,10 @@ def deploy_new_config_real(
process_id: UUIDstr,
tt_number: str,
) -> State:
+ """Deploy configuration on the new router.
+
+ TODO: set the proper playbook verb
+ """
provisioning_proxy.migrate_ip_trunk(
subscription,
new_node,
@@ -279,20 +296,21 @@ def deploy_new_config_real(
@inputstep("Wait for confirmation", assignee=Assignee.SYSTEM)
def confirm_continue_move_fiber() -> FormGenerator:
+ """Wait for confirmation from an operator that the physical fiber has been moved."""
class ProvisioningResultPage(FormPage):
class Config:
title = "Please confirm before continuing"
- info_label: Label = "New Trunk interface has been deployed, wait for the physical connection to be moved." # type: ignore[assignment]
+ info_label: Label = (
+ "New trunk interface has been deployed, "
+ "wait for the physical connection to be moved." # type: ignore[assignment]
+ )
yield ProvisioningResultPage
return {}
-# Interface checks go here
-
-
@step("Deploy ISIS configuration on new router")
def deploy_new_isis(
subscription: Iptrunk,
@@ -304,6 +322,10 @@ def deploy_new_isis(
process_id: UUIDstr,
tt_number: str,
) -> State:
+ """Deploy :term:`ISIS` configuration.
+
+ TODO: set the proper playbook verb.
+ """
provisioning_proxy.migrate_ip_trunk(
subscription,
new_node,
@@ -327,11 +349,14 @@ def deploy_new_isis(
@inputstep("Wait for confirmation", assignee=Assignee.SYSTEM)
def confirm_continue_restore_isis() -> FormGenerator:
+ """Wait for an operator to confirm that the old :term:`ISIS` metric should be restored."""
class ProvisioningResultPage(FormPage):
class Config:
title = "Please confirm before continuing"
- info_label: Label = "ISIS config has been deployed, confirm if you want to restore the old metric." # type: ignore[assignment]
+ info_label: Label = (
+ "ISIS config has been deployed, confirm if you want to restore the old metric." # type: ignore[assignment]
+ )
yield ProvisioningResultPage
@@ -346,6 +371,7 @@ def restore_isis_metric(
tt_number: str,
old_isis_metric: int,
) -> State:
+ """Restore the :term:`ISIS` metric to its original value."""
subscription.iptrunk.iptrunk_isis_metric = old_isis_metric
provisioning_proxy.provision_ip_trunk(
subscription, process_id, callback_route, tt_number, "isis_interface", dry_run=False,
@@ -365,6 +391,10 @@ def delete_old_config_dry(
process_id: UUIDstr,
tt_number: str,
) -> State:
+ """Perform a dry run of deleting the old configuration.
+
+ TODO: set the proper playbook verb
+ """
provisioning_proxy.migrate_ip_trunk(
subscription,
new_node,
@@ -394,6 +424,10 @@ def delete_old_config_real(
process_id: UUIDstr,
tt_number: str,
) -> State:
+ """Delete old configuration from the routers.
+
+ TODO: set the proper playbook verb
+ """
provisioning_proxy.migrate_ip_trunk(
subscription,
new_node,
@@ -415,6 +449,10 @@ def delete_old_config_real(
@step("Update IPAM")
def update_ipam(subscription: Iptrunk) -> State:
+ """Update :term:`IPAM` resources.
+
+ TODO: implement
+ """
return {"subscription": subscription}
@@ -426,6 +464,7 @@ def update_subscription_model(
new_lag_interface: str,
new_lag_member_interfaces: list[dict],
) -> State:
+ """Update the subscription model in the database."""
# Deep copy of subscription data
old_subscription = copy.deepcopy(subscription)
old_side_data = {
@@ -452,6 +491,7 @@ def reserve_interfaces_in_netbox(
new_lag_interface: str,
new_lag_member_interfaces: list[dict],
) -> State:
+ """Reserve new interfaces in Netbox."""
new_side = Router.from_subscription(new_node).router
nbclient = NetboxClient()
@@ -486,6 +526,7 @@ def update_netbox(
replace_index: int,
old_side_data: dict,
) -> State:
+ """Update Netbox, reallocating the old and new interfaces."""
new_side = subscription.iptrunk.iptrunk_sides[replace_index]
nbclient = NetboxClient()
if new_side.iptrunk_side_node.router_vendor == RouterVendor.NOKIA:
@@ -518,6 +559,23 @@ def update_netbox(
def migrate_iptrunk() -> StepList:
should_restore_isis_metric = conditional(lambda state: state["restore_isis_metric"])
+ """Migrate an IP trunk.
+
+ * Reserve new interfaces in Netbox
+ * Set the :term:`ISIS` metric of the current trunk to an arbitrarily high value to drain all traffic
+ * Disable - but don't 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 :term:`ISIS` interface between routers A and C
+ * Wait for operator confirmation that :term:`ISIS` is behaving as expected
+ * Restore the old :term:`ISIS` metric on the new trunk
+ * Delete the old, disabled configuration on the routers, first as a dry run
+ * Reflect the changes made in :term:`IPAM`
+ * Update the subscription model in the database accordingly
+ * Update the reserved interfaces in Netbox
+
+ TODO: add interface checks
+ """
return (
init
>> store_process_subscription(Target.MODIFY)
diff --git a/gso/workflows/iptrunk/modify_isis_metric.py b/gso/workflows/iptrunk/modify_isis_metric.py
index a7adf8a985d8253d2d87ccd3cd9ff0d11142af72..266df247a206a7de27ffab333805c8c8afed81fd 100644
--- a/gso/workflows/iptrunk/modify_isis_metric.py
+++ b/gso/workflows/iptrunk/modify_isis_metric.py
@@ -1,3 +1,5 @@
+"""A modification workflow for setting a new :term:`ISIS` metric for an IP trunk."""
+
from orchestrator.forms import FormPage
from orchestrator.targets import Target
from orchestrator.types import FormGenerator, State, UUIDstr
@@ -11,6 +13,7 @@ from gso.services.provisioning_proxy import pp_interaction
def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+ """Ask the operator for the new :term:`ISIS` metric."""
subscription = Iptrunk.from_subscription(subscription_id)
class ModifyIptrunkForm(FormPage):
@@ -24,6 +27,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
@step("Update subscription")
def modify_iptrunk_subscription(subscription: Iptrunk, isis_metric: int) -> State:
+ """Store the new :term:`ISIS` metric in the database by updating the subscription."""
subscription.iptrunk.iptrunk_isis_metric = isis_metric
return {"subscription": subscription}
@@ -36,6 +40,7 @@ def provision_ip_trunk_isis_iface_dry(
callback_route: str,
tt_number: str,
) -> State:
+ """Perform a dry run of deploying the new :term:`ISIS` metric on both sides of the trunk."""
provisioning_proxy.provision_ip_trunk(subscription, process_id, callback_route, tt_number, "isis_interface")
return {"subscription": subscription}
@@ -48,6 +53,7 @@ def provision_ip_trunk_isis_iface_real(
callback_route: str,
tt_number: str,
) -> State:
+ """Deploy the new :term:`ISIS` metric on both sides of the trunk."""
provisioning_proxy.provision_ip_trunk(
subscription, process_id, callback_route, tt_number, "isis_interface", dry_run=False,
)
@@ -61,6 +67,12 @@ def provision_ip_trunk_isis_iface_real(
target=Target.MODIFY,
)
def modify_isis_metric() -> StepList:
+ """Modify the :term:`ISIS` metric of an existing IP trunk.
+
+ * Modify the subscription model in the database
+ * Perform a dry run of setting the new :term:`ISIS` metric
+ * Deploy the new :term:`ISIS` metric on both sides of the trunk
+ """
return (
init
>> store_process_subscription(Target.MODIFY)
diff --git a/gso/workflows/iptrunk/modify_trunk_interface.py b/gso/workflows/iptrunk/modify_trunk_interface.py
index 9978f498a3cc4d3f99b31eb64a2b4f82e6d3aa07..d2326a92ba90cc63218726f7ffbebaaa40b07acb 100644
--- a/gso/workflows/iptrunk/modify_trunk_interface.py
+++ b/gso/workflows/iptrunk/modify_trunk_interface.py
@@ -1,3 +1,5 @@
+"""A modification workflow that updates the :term:`LAG` interfaces that are part of an existing IP trunk."""
+
import ipaddress
from uuid import uuid4
@@ -66,6 +68,7 @@ def initialize_ae_members(subscription: Iptrunk, initial_user_input: dict, side_
def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+ """Gather input from the operator on the interfaces that should be modified."""
subscription = Iptrunk.from_subscription(subscription_id)
class ModifyIptrunkForm(FormPage):
@@ -141,6 +144,7 @@ def modify_iptrunk_subscription(
side_b_ae_geant_a_sid: str,
side_b_ae_members: list[dict],
) -> State:
+ """Modify the subscription in the service database, reflecting the changes to the newly selected interfaces."""
# Prepare the list of removed AE members
previous_ae_members = {}
removed_ae_members = {}
@@ -197,14 +201,15 @@ def provision_ip_trunk_iface_dry(
tt_number: str,
removed_ae_members: list[str],
) -> State:
+ """Perform a dry run of deploying the updated IP trunk."""
provisioning_proxy.provision_ip_trunk(
subscription,
process_id,
callback_route,
tt_number,
"trunk_interface",
- True,
- removed_ae_members,
+ dry_run=True,
+ removed_ae_members=removed_ae_members,
)
return {"subscription": subscription}
@@ -218,14 +223,15 @@ def provision_ip_trunk_iface_real(
tt_number: str,
removed_ae_members: list[str],
) -> State:
+ """Provision the new IP trunk with updated interfaces."""
provisioning_proxy.provision_ip_trunk(
subscription,
process_id,
callback_route,
tt_number,
"trunk_interface",
- False,
- removed_ae_members,
+ dry_run=False,
+ removed_ae_members=removed_ae_members,
)
return {"subscription": subscription}
@@ -233,6 +239,7 @@ def provision_ip_trunk_iface_real(
@step("Update interfaces in Netbox. Reserving interfaces.")
def update_interfaces_in_netbox(subscription: Iptrunk, removed_ae_members: dict, previous_ae_members: dict) -> State:
+ """Update Netbox such that it contains the new interfaces."""
nbclient = NetboxClient()
for side in range(2):
if subscription.iptrunk.iptrunk_sides[side].iptrunk_side_node.router_vendor == RouterVendor.NOKIA:
@@ -269,7 +276,7 @@ def update_interfaces_in_netbox(subscription: Iptrunk, removed_ae_members: dict,
def allocate_interfaces_in_netbox(subscription: Iptrunk, previous_ae_members: dict) -> State:
"""Allocate the LAG interfaces in NetBox.
- attach the lag interfaces to the physical interfaces detach old ones from the LAG.
+ Attach the :term:`LAG` interfaces to the physical interfaces detach old ones from the :term:`LAG`.
"""
for side in range(2):
nbclient = NetboxClient()
@@ -299,6 +306,13 @@ def allocate_interfaces_in_netbox(subscription: Iptrunk, previous_ae_members: di
target=Target.MODIFY,
)
def modify_trunk_interface() -> StepList:
+ """Modify the interfaces that are part of an IP trunk.
+
+ * Update the subscription in the database
+ * Reserve new interfaces in Netbox
+ * Provision the updated version of the IP trunk, first as a dry run
+ * Allocate the previously reserved interfaces in Netbox
+ """
return (
init
>> store_process_subscription(Target.MODIFY)
diff --git a/gso/workflows/iptrunk/terminate_iptrunk.py b/gso/workflows/iptrunk/terminate_iptrunk.py
index 0e7004bd9c531e14837b3fa7573373fe23c34573..f0a2d166e94674dac34444869bc3eb2894b723b0 100644
--- a/gso/workflows/iptrunk/terminate_iptrunk.py
+++ b/gso/workflows/iptrunk/terminate_iptrunk.py
@@ -1,3 +1,5 @@
+"""A termination workflow for an active IP trunk."""
+
import ipaddress
from orchestrator.forms import FormPage
@@ -22,6 +24,7 @@ from gso.utils.helpers import set_isis_to_90000
def initial_input_form_generator() -> FormGenerator:
+ """Ask the operator to confirm whether router configuration and/or IPAM resources should be deleted."""
class TerminateForm(FormPage):
termination_label: Label = (
"Please confirm whether configuration should get removed from the A and B sides of the trunk, and whether "
@@ -42,6 +45,10 @@ def drain_traffic_from_ip_trunk(
callback_route: str,
tt_number: str,
) -> State:
+ """Drain all traffic from the trunk.
+
+ XXX: Should this not be done with the isis-90k-step?
+ """
provisioning_proxy.provision_ip_trunk(
subscription, process_id, callback_route, tt_number, "isis_interface", dry_run=False,
)
@@ -51,6 +58,7 @@ def drain_traffic_from_ip_trunk(
@step("Deprovision IP trunk [DRY RUN]")
def deprovision_ip_trunk_dry(subscription: Iptrunk, process_id: UUIDstr, callback_route: str, tt_number: str) -> State:
+ """Perform a dry run of deleting configuration from the routers."""
provisioning_proxy.deprovision_ip_trunk(subscription, process_id, callback_route, tt_number, dry_run=True)
return {"subscription": subscription}
@@ -58,13 +66,18 @@ def deprovision_ip_trunk_dry(subscription: Iptrunk, process_id: UUIDstr, callbac
@step("Deprovision IP trunk [FOR REAL]")
def deprovision_ip_trunk_real(subscription: Iptrunk, process_id: UUIDstr, callback_route: str, tt_number: str) -> State:
+ """Delete configuration from the routers."""
provisioning_proxy.deprovision_ip_trunk(subscription, process_id, callback_route, tt_number, dry_run=False)
return {"subscription": subscription}
-@step("Remove IP Trunk from NetBox")
+@step("Remove IP Trunk from Netbox")
def free_interfaces_in_netbox(subscription: Iptrunk) -> State:
+ """Mark used interfaces as free in Netbox.
+
+ TODO: decide on the conditionality of this step
+ """
for side in [0, 1]:
router = subscription.iptrunk.iptrunk_sides[side].iptrunk_side_node
router_fqdn = router.router_fqdn
@@ -84,6 +97,7 @@ def free_interfaces_in_netbox(subscription: Iptrunk) -> State:
@step("Deprovision IPv4 networks")
def deprovision_ip_trunk_ipv4(subscription: Iptrunk) -> dict:
+ """Clear up IPv4 resources in :term:`IPAM`."""
infoblox.delete_network(ipaddress.IPv4Network(subscription.iptrunk.iptrunk_ipv4_network))
return {"subscription": subscription}
@@ -91,6 +105,7 @@ def deprovision_ip_trunk_ipv4(subscription: Iptrunk) -> dict:
@step("Deprovision IPv6 networks")
def deprovision_ip_trunk_ipv6(subscription: Iptrunk) -> dict:
+ """Clear up IPv6 resources in :term:`IPAM`."""
infoblox.delete_network(ipaddress.IPv6Network(subscription.iptrunk.iptrunk_ipv6_network))
return {"subscription": subscription}
@@ -102,6 +117,15 @@ def deprovision_ip_trunk_ipv6(subscription: Iptrunk) -> dict:
target=Target.TERMINATE,
)
def terminate_iptrunk() -> StepList:
+ """Terminate an IP trunk.
+
+ * Let the operator decide whether to remove configuration from the routers, if so:
+ * Set the :term:`ISIS` metric of the IP trunk to an arbitrarily high value
+ * Disable and remove configuration from the routers, first as a dry run
+ * Mark the IP trunk interfaces as free in Netbox
+ * Clear IPAM resources, if selected by the operator
+ * Terminate the subscription in the service database
+ """
run_config_steps = conditional(lambda state: state["remove_configuration"])
run_ipam_steps = conditional(lambda state: state["clean_up_ipam"])