terminate_iptrunk.py 7.21 KiB
"""A termination workflow for an active IP trunk."""
import ipaddress
import json
from orchestrator.forms import FormPage
from orchestrator.forms.validators import Label
from orchestrator.targets import Target
from orchestrator.types import FormGenerator, State, SubscriptionLifecycle, UUIDstr
from orchestrator.utils.json import json_dumps
from orchestrator.workflow import StepList, begin, conditional, done, step, workflow
from orchestrator.workflows.steps import (
resync,
set_status,
store_process_subscription,
unsync,
)
from orchestrator.workflows.utils import wrap_modify_initial_input_form
from pydantic import field_validator
from gso.products.product_blocks.iptrunk import IptrunkSideBlock
from gso.products.product_types.iptrunk import Iptrunk
from gso.services import infoblox
from gso.services.lso_client import execute_playbook, lso_interaction
from gso.services.netbox_client import NetboxClient
from gso.utils.helpers import get_router_vendor, validate_tt_number
from gso.utils.shared_enums import Vendor
from gso.utils.workflow_steps import set_isis_to_max
def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
"""Ask the operator to confirm whether router configuration and :term:`IPAM` resources should be deleted."""
iptrunk = Iptrunk.from_subscription(subscription_id)
class TerminateForm(FormPage):
if iptrunk.status == SubscriptionLifecycle.INITIAL:
info_label_2: Label = (
"This will immediately mark the subscription as terminated, preventing any other workflows from "
"interacting with this product subscription."
)
info_label_3: Label = "ONLY EXECUTE THIS WORKFLOW WHEN YOU ARE ABSOLUTELY SURE WHAT YOU ARE DOING."
tt_number: str
termination_label: Label = (
"Please confirm whether configuration should get removed from the A and B sides of the trunk."
)
remove_configuration: bool = True
@field_validator("tt_number")
def validate_tt_number(cls, tt_number: str) -> str:
return validate_tt_number(tt_number)
user_input = yield TerminateForm
return user_input.dict()
@step("[DRY RUN] Deprovision IP trunk")
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."""
extra_vars = {
"wfo_trunk_json": json.loads(json_dumps(subscription)),
"dry_run": True,
"verb": "terminate",
"config_object": "trunk_deprovision",
"commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} "
f"- Remove config for {subscription.iptrunk.geant_s_sid}",
}
execute_playbook(
playbook_name="iptrunks.yaml",
inventory=f"{subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn}\n"
f"{subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn}\n",
extra_vars=extra_vars,
callback_route=callback_route,
)
return {"subscription": subscription}
@step("[FOR REAL] Deprovision IP trunk")
def deprovision_ip_trunk_real(subscription: Iptrunk, process_id: UUIDstr, callback_route: str, tt_number: str) -> State:
"""Delete configuration from the routers."""
extra_vars = {
"wfo_trunk_json": json.loads(json_dumps(subscription)),
"dry_run": False,
"verb": "terminate",
"config_object": "trunk_deprovision",
"commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} "
f"- Remove config for {subscription.iptrunk.geant_s_sid}",
}
execute_playbook(
playbook_name="iptrunks.yaml",
inventory=f"{subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn}\n"
f"{subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn}\n",
extra_vars=extra_vars,
callback_route=callback_route,
)
return {"subscription": subscription}
def _free_up_interfaces_from_netbox(side_block: IptrunkSideBlock) -> None:
nbclient = NetboxClient()
for member in side_block.iptrunk_side_ae_members:
nbclient.free_interface(side_block.iptrunk_side_node.router_fqdn, member.interface_name)
if side_block.iptrunk_side_ae_iface:
nbclient.delete_interface(side_block.iptrunk_side_node.router_fqdn, side_block.iptrunk_side_ae_iface)
@step("Netbox: Remove interfaces on side A")
def netbox_clean_up_side_a(subscription: Iptrunk) -> State:
"""Mark used interfaces on side A as free in Netbox."""
_free_up_interfaces_from_netbox(subscription.iptrunk.iptrunk_sides[0])
return {"subscription": subscription}
@step("Netbox: Remove interfaces on side B")
def netbox_clean_up_side_b(subscription: Iptrunk) -> State:
"""Mark used interfaces on side B as free in Netbox."""
_free_up_interfaces_from_netbox(subscription.iptrunk.iptrunk_sides[1])
return {"subscription": subscription}
@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}
@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}
@workflow(
"Terminate IPtrunk",
initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
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 :term:`IPAM` resources
* Terminate the subscription in the service database
"""
run_config_steps = conditional(lambda state: state["remove_configuration"])
side_a_is_nokia = conditional(
lambda state: get_router_vendor(
state["subscription"]["iptrunk"]["iptrunk_sides"][0]["iptrunk_side_node"]["owner_subscription_id"]
)
== Vendor.NOKIA
)
side_b_is_nokia = conditional(
lambda state: get_router_vendor(
state["subscription"]["iptrunk"]["iptrunk_sides"][1]["iptrunk_side_node"]["owner_subscription_id"]
)
== Vendor.NOKIA
)
config_steps = (
begin
>> lso_interaction(set_isis_to_max)
>> lso_interaction(deprovision_ip_trunk_dry)
>> lso_interaction(deprovision_ip_trunk_real)
)
return (
begin
>> store_process_subscription(Target.TERMINATE)
>> unsync
>> run_config_steps(config_steps)
>> side_a_is_nokia(netbox_clean_up_side_a)
>> side_b_is_nokia(netbox_clean_up_side_b)
>> deprovision_ip_trunk_ipv4
>> deprovision_ip_trunk_ipv6
>> set_status(SubscriptionLifecycle.TERMINATED)
>> resync
>> done
)