Skip to content
Snippets Groups Projects
Verified Commit 1527f73d authored by Karel van Klink's avatar Karel van Klink :smiley_cat:
Browse files

add docstrings to the IP trunk workflows

parent f94f214e
No related branches found
No related tags found
1 merge request!111Feature/ruff everything party hat emoji
"""All workflows that can be executed on IP trunks."""
"""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
......
"""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)
......
"""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)
......
"""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)
......
"""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"])
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment