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