From 1527f73df73ea70ee9777beea03499e11b895c6d Mon Sep 17 00:00:00 2001
From: Karel van Klink <karel.vanklink@geant.org>
Date: Mon, 13 Nov 2023 16:55:51 +0000
Subject: [PATCH] add docstrings to the IP trunk workflows

---
 gso/workflows/iptrunk/__init__.py             |  1 +
 gso/workflows/iptrunk/create_iptrunk.py       | 24 +++++++
 gso/workflows/iptrunk/migrate_iptrunk.py      | 68 +++++++++++++++++--
 gso/workflows/iptrunk/modify_isis_metric.py   | 12 ++++
 .../iptrunk/modify_trunk_interface.py         | 24 +++++--
 gso/workflows/iptrunk/terminate_iptrunk.py    | 26 ++++++-
 6 files changed, 144 insertions(+), 11 deletions(-)

diff --git a/gso/workflows/iptrunk/__init__.py b/gso/workflows/iptrunk/__init__.py
index e69de29b..709c5458 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 9c432a81..d159f531 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 7492f081..2d599344 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} "
@@ -165,6 +172,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,
@@ -194,6 +202,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,
@@ -224,6 +233,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,
@@ -255,6 +268,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,
@@ -278,20 +295,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,
@@ -303,6 +321,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,
@@ -326,11 +348,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
 
@@ -345,6 +370,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,
@@ -364,6 +390,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,
@@ -393,6 +423,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,
@@ -414,6 +448,10 @@ def delete_old_config_real(
 
 @step("Update IPAM")
 def update_ipam(subscription: Iptrunk) -> State:
+    """Update :term:`IPAM` resources.
+
+    TODO: implement
+    """
     return {"subscription": subscription}
 
 
@@ -425,6 +463,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 = {
@@ -451,6 +490,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()
@@ -485,6 +525,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:
@@ -515,6 +556,23 @@ def update_netbox(
     target=Target.MODIFY,
 )
 def migrate_iptrunk() -> StepList:
+    """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 a7adf8a9..266df247 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 9978f498..d2326a92 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 0e7004bd..f0a2d166 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"])
 
-- 
GitLab