Skip to content
Snippets Groups Projects
Commit c3caa765 authored by Neda Moeini's avatar Neda Moeini Committed by Karel van Klink
Browse files

Updated modification/migrations IP trunk workflow to integrate it with Netbox.

parent d84a50a3
No related branches found
No related tags found
1 merge request!92Feature/nat 314 integrate iptrunk modification with netbox
...@@ -3,6 +3,7 @@ from uuid import UUID ...@@ -3,6 +3,7 @@ from uuid import UUID
import pydantic import pydantic
import pynetbox import pynetbox
from infoblox_client.objects import Interface
from pynetbox.models.dcim import Devices, DeviceTypes, Interfaces from pynetbox.models.dcim import Devices, DeviceTypes, Interfaces
from gso.products.product_types.router import Router from gso.products.product_types.router import Router
...@@ -256,6 +257,17 @@ class NetboxClient: ...@@ -256,6 +257,17 @@ class NetboxClient:
return interface return interface
def detach_interfaces_from_lag(self, device_name: str, lag_name: str) -> None:
"""Detach all interfaces from a LAG."""
device = self.get_device_by_name(device_name)
lag = self.netbox.dcim.interfaces.get(device_id=device.id, name=lag_name)
for interface in self.netbox.dcim.interfaces.filter(
device_id=device.id, lag_id=lag.id, enabled=False, mark_connected=False
):
interface.lag = None
interface.save()
return
def deallocate_interface(self, device_name: str, iface_name: str) -> Interfaces: def deallocate_interface(self, device_name: str, iface_name: str) -> Interfaces:
"""Allocate an interface by marking it as connected.""" """Allocate an interface by marking it as connected."""
...@@ -310,3 +322,13 @@ class NetboxClient: ...@@ -310,3 +322,13 @@ class NetboxClient:
return self.netbox.dcim.interfaces.filter( return self.netbox.dcim.interfaces.filter(
device=device.name, enabled=False, mark_connected=False, speed=speed_bps device=device.name, enabled=False, mark_connected=False, speed=speed_bps
) )
def get_interface_by_name_and_device(self, router_id: UUID, interface_name: str) -> Interface:
"""Return the interface object by name and device from netbox, or ``None`` if not found."""
router = Router.from_subscription(router_id).router.router_fqdn
device = self.get_device_by_name(router)
try:
return self.netbox.dcim.interfaces.get(device=device.name, name=interface_name)
except pynetbox.RequestError:
raise NotFoundError(f"Interface: {interface_name} on device: {device.name} not found.")
...@@ -103,7 +103,12 @@ def provision_router( ...@@ -103,7 +103,12 @@ def provision_router(
def provision_ip_trunk( def provision_ip_trunk(
subscription: IptrunkProvisioning, process_id: UUIDstr, tt_number: str, config_object: str, dry_run: bool = True subscription: IptrunkProvisioning,
process_id: UUIDstr,
tt_number: str,
config_object: str,
dry_run: bool = True,
removed_ae_members: list[str] | None = None,
) -> None: ) -> None:
"""Provision an IP trunk service using :term:`LSO`. """Provision an IP trunk service using :term:`LSO`.
...@@ -116,6 +121,8 @@ def provision_ip_trunk( ...@@ -116,6 +121,8 @@ def provision_ip_trunk(
:param dry_run: A boolean indicating whether this should be a dry run or not, defaults to `True`. :param dry_run: A boolean indicating whether this should be a dry run or not, defaults to `True`.
:type dry_run: bool :type dry_run: bool
:rtype: None :rtype: None
:param removed_ae_members: A list of interfaces that are removed from the :term:`LAG`, defaults to `None`.
it's only used when we removed some interfaces from the LAG in modify_ip_trunk.
""" """
parameters = { parameters = {
"subscription": json.loads(json_dumps(subscription)), "subscription": json.loads(json_dumps(subscription)),
...@@ -124,6 +131,7 @@ def provision_ip_trunk( ...@@ -124,6 +131,7 @@ def provision_ip_trunk(
"tt_number": tt_number, "tt_number": tt_number,
"process_id": process_id, "process_id": process_id,
"object": config_object, "object": config_object,
"removed_ae_members": removed_ae_members,
} }
_send_request("ip_trunk", parameters, process_id, CUDOperation.POST) _send_request("ip_trunk", parameters, process_id, CUDOperation.POST)
...@@ -175,7 +183,7 @@ def migrate_ip_trunk( ...@@ -175,7 +183,7 @@ def migrate_ip_trunk(
subscription: Iptrunk, subscription: Iptrunk,
new_node: Router, new_node: Router,
new_lag_interface: str, new_lag_interface: str,
new_lag_member_interfaces: list[str], new_lag_member_interfaces: list[dict],
replace_index: int, replace_index: int,
process_id: UUIDstr, process_id: UUIDstr,
tt_number: str, tt_number: str,
......
...@@ -7,6 +7,7 @@ from orchestrator.types import State, UUIDstr ...@@ -7,6 +7,7 @@ from orchestrator.types import State, UUIDstr
from pydantic import BaseModel from pydantic import BaseModel
from pydantic_forms.validators import Choice from pydantic_forms.validators import Choice
from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock
from gso.products.product_blocks.router import RouterVendor from gso.products.product_blocks.router import RouterVendor
from gso.products.product_types.iptrunk import Iptrunk from gso.products.product_types.iptrunk import Iptrunk
from gso.products.product_types.router import Router from gso.products.product_types.router import Router
...@@ -51,6 +52,30 @@ def available_interfaces_choices(router_id: UUID, speed: str) -> Choice | None: ...@@ -51,6 +52,30 @@ def available_interfaces_choices(router_id: UUID, speed: str) -> Choice | None:
return Choice("ae member", zip(interfaces.keys(), interfaces.items())) # type: ignore[arg-type] return Choice("ae member", zip(interfaces.keys(), interfaces.items())) # type: ignore[arg-type]
def available_interfaces_choices_including_current_members(
router_id: UUID, speed: str, interfaces: list[IptrunkInterfaceBlock]
) -> Choice | None:
"""Return a list of available interfaces for a given router and speed including the current members.
For Nokia routers, return a list of available interfaces.
For Juniper routers, return a string.
"""
if Router.from_subscription(router_id).router.router_vendor != RouterVendor.NOKIA:
return None
available_interfaces = list(NetboxClient().get_available_interfaces(router_id, speed))
available_interfaces.extend(
[
NetboxClient().get_interface_by_name_and_device(router_id, interface.interface_name)
for interface in interfaces
]
)
options = {
interface["name"]: f"{interface['name']} - {interface['module']['display']} - {interface['description']}"
for interface in available_interfaces
}
return Choice("ae member", zip(options.keys(), options.items())) # type: ignore[arg-type]
def available_lags_choices(router_id: UUID) -> Choice | None: def available_lags_choices(router_id: UUID) -> Choice | None:
"""Return a list of available lags for a given router. """Return a list of available lags for a given router.
......
import copy
import re import re
from logging import getLogger from logging import getLogger
from typing import NoReturn from typing import NoReturn
from uuid import uuid4
from orchestrator import step, workflow from orchestrator import step, workflow
from orchestrator.config.assignee import Assignee from orchestrator.config.assignee import Assignee
...@@ -12,8 +14,10 @@ from orchestrator.workflow import StepList, done, init, inputstep ...@@ -12,8 +14,10 @@ from orchestrator.workflow import StepList, done, init, inputstep
from orchestrator.workflows.steps import resync, store_process_subscription, unsync from orchestrator.workflows.steps import resync, store_process_subscription, unsync
from orchestrator.workflows.utils import wrap_modify_initial_input_form from orchestrator.workflows.utils import wrap_modify_initial_input_form
from pydantic import validator from pydantic import validator
from pydantic_forms.core import ReadOnlyField
from pynetbox.models.dcim import Interfaces from pynetbox.models.dcim import Interfaces
from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock
from gso.products.product_blocks.router import RouterVendor from gso.products.product_blocks.router import RouterVendor
from gso.products.product_types.iptrunk import Iptrunk from gso.products.product_types.iptrunk import Iptrunk
from gso.products.product_types.router import Router from gso.products.product_types.router import Router
...@@ -21,7 +25,13 @@ from gso.services import provisioning_proxy ...@@ -21,7 +25,13 @@ from gso.services import provisioning_proxy
from gso.services.netbox_client import NetboxClient from gso.services.netbox_client import NetboxClient
from gso.services.provisioning_proxy import pp_interaction from gso.services.provisioning_proxy import pp_interaction
from gso.services.subscriptions import get_active_router_subscriptions from gso.services.subscriptions import get_active_router_subscriptions
from gso.utils.helpers import available_interfaces_choices, available_lags_choices, get_router_vendor, set_isis_to_90000 from gso.utils.helpers import (
LAGMember,
available_interfaces_choices,
available_lags_choices,
get_router_vendor,
set_isis_to_90000,
)
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -55,9 +65,9 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: ...@@ -55,9 +65,9 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
migrate_form_input = yield IPTrunkMigrateForm migrate_form_input = yield IPTrunkMigrateForm
current_routers = [ current_routers = [
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.subscription.subscription_id, side.iptrunk_side_node.subscription.subscription_id for side in subscription.iptrunk.iptrunk_sides
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.subscription.subscription_id,
] ]
routers = {} routers = {}
for router_id, router_description in get_active_router_subscriptions(fields=["subscription_id", "description"]): for router_id, router_description in get_active_router_subscriptions(fields=["subscription_id", "description"]):
if router_id not in current_routers: if router_id not in current_routers:
...@@ -82,25 +92,44 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: ...@@ -82,25 +92,44 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
new_router = new_side_iptrunk_router_input.new_node new_router = new_side_iptrunk_router_input.new_node
side_a_ae_iface = available_lags_choices(new_router) or str side_a_ae_iface = available_lags_choices(new_router) or str
class LagMemberList(UniqueConstrainedList[str]): if get_router_vendor(new_router) == RouterVendor.NOKIA:
class NokiaLAGMember(LAGMember):
interface_name: available_interfaces_choices( # type: ignore[valid-type]
new_router, subscription.iptrunk.iptrunk_speed
)
class NokiaAeMembers(UniqueConstrainedList[NokiaLAGMember]):
min_items = len(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members) min_items = len(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members)
max_items = len(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members) max_items = len(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members)
item_type = available_interfaces_choices(new_router, subscription.iptrunk.iptrunk_speed) # type: ignore
unique_items = True
class JuniperLagMemberList(UniqueConstrainedList[str]): ae_members = NokiaAeMembers
else:
class JuniperLagMember(UniqueConstrainedList[LAGMember]):
min_items = len(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members) min_items = len(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members)
max_items = len(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members) max_items = len(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members)
unique_items = True
ae_members_side_a = LagMemberList if get_router_vendor(new_router) == RouterVendor.NOKIA else JuniperLagMemberList ae_members = JuniperLagMember # type: ignore[assignment]
replace_index = (
0
if str(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.subscription.subscription_id)
== migrate_form_input.replace_side
else 1
)
existing_lag_ae_members = [
{"interface_name": iface.interface_name, "interface_description": iface.interface_description}
for iface in subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_members
]
class NewSideIPTrunkForm(FormPage): class NewSideIPTrunkForm(FormPage):
class Config: class Config:
title = form_title title = form_title
new_lag_interface: side_a_ae_iface # type: ignore[valid-type] new_lag_interface: side_a_ae_iface # type: ignore[valid-type]
new_lag_member_interfaces: ae_members_side_a # type: ignore[valid-type] existing_lag_interface: list[LAGMember] = ReadOnlyField(existing_lag_ae_members)
new_lag_member_interfaces: ae_members # type: ignore[valid-type]
@validator("new_lag_interface", allow_reuse=True, pre=True, always=True) @validator("new_lag_interface", allow_reuse=True, pre=True, always=True)
def lag_interface_proper_name(cls, new_lag_interface: str) -> str | NoReturn: def lag_interface_proper_name(cls, new_lag_interface: str) -> str | NoReturn:
...@@ -111,18 +140,11 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: ...@@ -111,18 +140,11 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
return new_lag_interface return new_lag_interface
new_side_input = yield NewSideIPTrunkForm new_side_input = yield NewSideIPTrunkForm
def _find_updated_side_of_trunk(trunk: Iptrunk, new_side: str) -> int:
for side in trunk.iptrunk.iptrunk_sides:
if str(side.iptrunk_side_node.subscription.subscription_id) == new_side:
return trunk.iptrunk.iptrunk_sides.index(side)
raise ValueError("Invalid Router id provided to be replaced!")
return ( return (
migrate_form_input.dict() migrate_form_input.dict()
| new_side_iptrunk_router_input.dict() | new_side_iptrunk_router_input.dict()
| new_side_input.dict() | new_side_input.dict()
| {"replace_index": _find_updated_side_of_trunk(subscription, migrate_form_input.replace_side)} | {"replace_index": replace_index}
) )
...@@ -131,7 +153,7 @@ def disable_old_config_dry( ...@@ -131,7 +153,7 @@ def disable_old_config_dry(
subscription: Iptrunk, subscription: Iptrunk,
new_node: Router, new_node: Router,
new_lag_interface: str, new_lag_interface: str,
new_lag_member_interfaces: list[str], new_lag_member_interfaces: list[dict],
replace_index: int, replace_index: int,
process_id: UUIDstr, process_id: UUIDstr,
tt_number: str, tt_number: str,
...@@ -159,7 +181,7 @@ def disable_old_config_real( ...@@ -159,7 +181,7 @@ def disable_old_config_real(
subscription: Iptrunk, subscription: Iptrunk,
new_node: Router, new_node: Router,
new_lag_interface: str, new_lag_interface: str,
new_lag_member_interfaces: list[str], new_lag_member_interfaces: list[dict],
replace_index: int, replace_index: int,
process_id: UUIDstr, process_id: UUIDstr,
tt_number: str, tt_number: str,
...@@ -188,7 +210,7 @@ def deploy_new_config_dry( ...@@ -188,7 +210,7 @@ def deploy_new_config_dry(
subscription: Iptrunk, subscription: Iptrunk,
new_node: Router, new_node: Router,
new_lag_interface: str, new_lag_interface: str,
new_lag_member_interfaces: list[str], new_lag_member_interfaces: list[dict],
replace_index: int, replace_index: int,
process_id: UUIDstr, process_id: UUIDstr,
tt_number: str, tt_number: str,
...@@ -218,7 +240,7 @@ def deploy_new_config_real( ...@@ -218,7 +240,7 @@ def deploy_new_config_real(
subscription: Iptrunk, subscription: Iptrunk,
new_node: Router, new_node: Router,
new_lag_interface: str, new_lag_interface: str,
new_lag_member_interfaces: list[str], new_lag_member_interfaces: list[dict],
replace_index: int, replace_index: int,
process_id: UUIDstr, process_id: UUIDstr,
tt_number: str, tt_number: str,
...@@ -268,7 +290,7 @@ def deploy_new_isis( ...@@ -268,7 +290,7 @@ def deploy_new_isis(
subscription: Iptrunk, subscription: Iptrunk,
new_node: Router, new_node: Router,
new_lag_interface: str, new_lag_interface: str,
new_lag_member_interfaces: list[str], new_lag_member_interfaces: list[dict],
replace_index: int, replace_index: int,
process_id: UUIDstr, process_id: UUIDstr,
tt_number: str, tt_number: str,
...@@ -322,7 +344,7 @@ def delete_old_config_dry( ...@@ -322,7 +344,7 @@ def delete_old_config_dry(
subscription: Iptrunk, subscription: Iptrunk,
new_node: Router, new_node: Router,
new_lag_interface: str, new_lag_interface: str,
new_lag_member_interfaces: list[str], new_lag_member_interfaces: list[dict],
replace_index: int, replace_index: int,
process_id: UUIDstr, process_id: UUIDstr,
tt_number: str, tt_number: str,
...@@ -353,7 +375,7 @@ def delete_old_config_real( ...@@ -353,7 +375,7 @@ def delete_old_config_real(
subscription: Iptrunk, subscription: Iptrunk,
new_node: Router, new_node: Router,
new_lag_interface: str, new_lag_interface: str,
new_lag_member_interfaces: list[str], new_lag_member_interfaces: list[dict],
replace_index: int, replace_index: int,
process_id: UUIDstr, process_id: UUIDstr,
tt_number: str, tt_number: str,
...@@ -392,16 +414,23 @@ def update_subscription_model( ...@@ -392,16 +414,23 @@ def update_subscription_model(
replace_index: int, replace_index: int,
new_node: UUIDstr, new_node: UUIDstr,
new_lag_interface: str, new_lag_interface: str,
new_lag_member_interfaces: list[str], new_lag_member_interfaces: list[dict],
) -> State: ) -> State:
# Deep copy of subscription data
old_subscription = copy.deepcopy(subscription)
old_side_data = { old_side_data = {
"iptrunk_side_node": subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_node, "iptrunk_side_node": old_subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_node,
"iptrunk_side_ae_iface": subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_iface, "iptrunk_side_ae_iface": old_subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_iface,
"iptrunk_side_ae_members": subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_members, "iptrunk_side_ae_members": old_subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_members,
} }
subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_node = Router.from_subscription(new_node).router subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_node = Router.from_subscription(new_node).router
subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_iface = new_lag_interface subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_iface = new_lag_interface
subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_members = new_lag_member_interfaces subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_members.clear()
# And update the list to only include the new member interfaces
for member in new_lag_member_interfaces:
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members.append(
IptrunkInterfaceBlock.new(subscription_id=uuid4(), **member)
)
return {"subscription": subscription, "old_side_data": old_side_data} return {"subscription": subscription, "old_side_data": old_side_data}
...@@ -411,7 +440,7 @@ def reserve_interfaces_in_netbox( ...@@ -411,7 +440,7 @@ def reserve_interfaces_in_netbox(
subscription: Iptrunk, subscription: Iptrunk,
new_node: UUIDstr, new_node: UUIDstr,
new_lag_interface: str, new_lag_interface: str,
new_lag_member_interfaces: list[str], new_lag_member_interfaces: list[dict],
) -> State: ) -> State:
new_side = Router.from_subscription(new_node).router new_side = Router.from_subscription(new_node).router
...@@ -431,17 +460,17 @@ def reserve_interfaces_in_netbox( ...@@ -431,17 +460,17 @@ def reserve_interfaces_in_netbox(
nbclient.attach_interface_to_lag( nbclient.attach_interface_to_lag(
device_name=new_side.router_fqdn, device_name=new_side.router_fqdn,
lag_name=lag_interface.name, lag_name=lag_interface.name,
iface_name=interface, iface_name=interface["interface_name"],
description=str(subscription.subscription_id), description=str(subscription.subscription_id),
) )
nbclient.reserve_interface( nbclient.reserve_interface(
device_name=new_side.router_fqdn, device_name=new_side.router_fqdn,
iface_name=interface, iface_name=interface["interface_name"],
) )
return {"subscription": subscription} return {"subscription": subscription}
@step("Update Netbox.") @step("Update Netbox. Allocate new interfaces and deallocate old ones.")
def update_netbox( def update_netbox(
subscription: Iptrunk, subscription: Iptrunk,
replace_index: int, replace_index: int,
...@@ -453,12 +482,12 @@ def update_netbox( ...@@ -453,12 +482,12 @@ def update_netbox(
for interface in new_side.iptrunk_side_ae_members: for interface in new_side.iptrunk_side_ae_members:
nbclient.allocate_interface( nbclient.allocate_interface(
device_name=new_side.iptrunk_side_node.router_fqdn, device_name=new_side.iptrunk_side_node.router_fqdn,
iface_name=interface, iface_name=interface.interface_name,
) )
if old_side_data["iptrunk_side_node"]["router_vendor"] == RouterVendor.NOKIA: if old_side_data["iptrunk_side_node"]["router_vendor"] == RouterVendor.NOKIA:
# Set interfaces to free # Set interfaces to free
for iface in old_side_data["iptrunk_side_ae_members"]: for iface in old_side_data["iptrunk_side_ae_members"]:
nbclient.free_interface(old_side_data["iptrunk_side_node"]["router_fqdn"], iface) nbclient.free_interface(old_side_data["iptrunk_side_node"]["router_fqdn"], iface["interface_name"])
# Delete LAG interfaces # Delete LAG interfaces
nbclient.delete_interface( nbclient.delete_interface(
......
import ipaddress import ipaddress
from typing import List, Type
from uuid import uuid4 from uuid import uuid4
from orchestrator.forms import FormPage, ReadOnlyField from orchestrator.forms import FormPage, ReadOnlyField
...@@ -8,12 +9,50 @@ from orchestrator.types import FormGenerator, State, UUIDstr ...@@ -8,12 +9,50 @@ from orchestrator.types import FormGenerator, State, UUIDstr
from orchestrator.workflow import StepList, done, init, step, workflow from orchestrator.workflow import StepList, done, init, step, workflow
from orchestrator.workflows.steps import resync, store_process_subscription, unsync from orchestrator.workflows.steps import resync, store_process_subscription, unsync
from orchestrator.workflows.utils import wrap_modify_initial_input_form from orchestrator.workflows.utils import wrap_modify_initial_input_form
from pydantic import validator
from pydantic_forms.validators import Label
from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock, IptrunkType, PhyPortCapacity from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock, IptrunkType, PhyPortCapacity
from gso.products.product_blocks.router import RouterVendor
from gso.products.product_types.iptrunk import Iptrunk from gso.products.product_types.iptrunk import Iptrunk
from gso.services import provisioning_proxy from gso.services import provisioning_proxy
from gso.services.netbox_client import NetboxClient
from gso.services.provisioning_proxy import pp_interaction from gso.services.provisioning_proxy import pp_interaction
from gso.utils.helpers import LAGMember from gso.utils.helpers import (
LAGMember,
available_interfaces_choices,
available_interfaces_choices_including_current_members,
validate_iptrunk_unique_interface,
)
def initialize_ae_members(subscription: Iptrunk, initial_user_input: dict, side_index: int) -> Type[LAGMember]:
"""Initialize the list of AE members."""
router = subscription.iptrunk.iptrunk_sides[side_index].iptrunk_side_node
iptrunk_minimum_link = initial_user_input["iptrunk_minimum_links"]
if router.router_vendor == RouterVendor.NOKIA:
iptrunk_speed = initial_user_input["iptrunk_speed"]
class NokiaLAGMember(LAGMember):
interface_name: available_interfaces_choices_including_current_members( # type: ignore[valid-type]
router.owner_subscription_id,
iptrunk_speed,
subscription.iptrunk.iptrunk_sides[side_index].iptrunk_side_ae_members,
) if iptrunk_speed == subscription.iptrunk.iptrunk_speed else (
available_interfaces_choices(router.owner_subscription_id, initial_user_input["iptrunk_speed"])
)
class NokiaAeMembers(UniqueConstrainedList[NokiaLAGMember]):
min_items = iptrunk_minimum_link
ae_members = NokiaAeMembers
else:
class JuniperAeMembers(UniqueConstrainedList[LAGMember]):
min_items = iptrunk_minimum_link
ae_members = JuniperAeMembers # type: ignore[assignment]
return ae_members # type: ignore[return-value]
def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
...@@ -24,6 +63,10 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: ...@@ -24,6 +63,10 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
geant_s_sid: str = subscription.iptrunk.geant_s_sid geant_s_sid: str = subscription.iptrunk.geant_s_sid
iptrunk_description: str = subscription.iptrunk.iptrunk_description iptrunk_description: str = subscription.iptrunk.iptrunk_description
iptrunk_type: IptrunkType = subscription.iptrunk.iptrunk_type iptrunk_type: IptrunkType = subscription.iptrunk.iptrunk_type
warning_label: Label = (
"Changing the PhyPortCapacity will result in the deletion of all AE members. "
"You will need to add the new AE members in the next steps." # type: ignore[assignment]
)
iptrunk_speed: PhyPortCapacity = subscription.iptrunk.iptrunk_speed iptrunk_speed: PhyPortCapacity = subscription.iptrunk.iptrunk_speed
iptrunk_minimum_links: int = subscription.iptrunk.iptrunk_minimum_links iptrunk_minimum_links: int = subscription.iptrunk.iptrunk_minimum_links
iptrunk_isis_metric: int = ReadOnlyField(subscription.iptrunk.iptrunk_isis_metric) iptrunk_isis_metric: int = ReadOnlyField(subscription.iptrunk.iptrunk_isis_metric)
...@@ -31,9 +74,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: ...@@ -31,9 +74,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
iptrunk_ipv6_network: ipaddress.IPv6Network = ReadOnlyField(subscription.iptrunk.iptrunk_ipv6_network) iptrunk_ipv6_network: ipaddress.IPv6Network = ReadOnlyField(subscription.iptrunk.iptrunk_ipv6_network)
initial_user_input = yield ModifyIptrunkForm initial_user_input = yield ModifyIptrunkForm
ae_members_side_a = initialize_ae_members(subscription, initial_user_input.dict(), 0)
class AeMembersListA(UniqueConstrainedList[LAGMember]):
min_items = initial_user_input.iptrunk_minimum_links
class ModifyIptrunkSideAForm(FormPage): class ModifyIptrunkSideAForm(FormPage):
class Config: class Config:
...@@ -42,13 +83,18 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: ...@@ -42,13 +83,18 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
side_a_node: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn) side_a_node: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn)
side_a_ae_iface: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_iface) side_a_ae_iface: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_iface)
side_a_ae_geant_a_sid: str = subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_geant_a_sid side_a_ae_geant_a_sid: str = subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_geant_a_sid
side_a_ae_members: AeMembersListA = subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members side_a_ae_members: ae_members_side_a = ( # type: ignore[valid-type]
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members
if initial_user_input.iptrunk_speed == subscription.iptrunk.iptrunk_speed
else []
)
user_input_side_a = yield ModifyIptrunkSideAForm @validator("side_a_ae_members", allow_reuse=True)
def validate_iptrunk_unique_interface_side_a(cls, side_a_ae_members: list[LAGMember]) -> list[LAGMember]:
return validate_iptrunk_unique_interface(side_a_ae_members)
class AeMembersListB(UniqueConstrainedList[LAGMember]): user_input_side_a = yield ModifyIptrunkSideAForm
min_items = len(user_input_side_a.side_a_ae_members) ae_members_side_b = initialize_ae_members(subscription, initial_user_input.dict(), 1)
max_items = len(user_input_side_a.side_a_ae_members)
class ModifyIptrunkSideBForm(FormPage): class ModifyIptrunkSideBForm(FormPage):
class Config: class Config:
...@@ -57,7 +103,15 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: ...@@ -57,7 +103,15 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
side_b_node: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn) side_b_node: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn)
side_b_ae_iface: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_iface) side_b_ae_iface: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_iface)
side_b_ae_geant_a_sid: str = subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_geant_a_sid side_b_ae_geant_a_sid: str = subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_geant_a_sid
side_b_ae_members: AeMembersListB = subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members side_b_ae_members: ae_members_side_b = ( # type: ignore[valid-type]
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members
if initial_user_input.iptrunk_speed == subscription.iptrunk.iptrunk_speed
else []
)
@validator("side_b_ae_members", allow_reuse=True)
def validate_iptrunk_unique_interface_side_b(cls, side_b_ae_members: list[LAGMember]) -> list[LAGMember]:
return validate_iptrunk_unique_interface(side_b_ae_members)
user_input_side_b = yield ModifyIptrunkSideBForm user_input_side_b = yield ModifyIptrunkSideBForm
...@@ -77,6 +131,20 @@ def modify_iptrunk_subscription( ...@@ -77,6 +131,20 @@ def modify_iptrunk_subscription(
side_b_ae_geant_a_sid: str, side_b_ae_geant_a_sid: str,
side_b_ae_members: list[dict], side_b_ae_members: list[dict],
) -> State: ) -> State:
# Prepare the list of removed AE members
previous_ae_members = {}
removed_ae_members = {}
for side_index in range(2):
previous_ae_members[side_index] = [
{"interface_name": member.interface_name, "interface_description": member.interface_description}
for member in subscription.iptrunk.iptrunk_sides[side_index].iptrunk_side_ae_members
]
for side_index in range(2):
previous_members = previous_ae_members[side_index]
current_members = side_a_ae_members if side_index == 0 else side_b_ae_members
removed_ae_members[side_index] = [
ae_member for ae_member in previous_members if ae_member not in current_members
]
subscription.iptrunk.geant_s_sid = geant_s_sid subscription.iptrunk.geant_s_sid = geant_s_sid
subscription.iptrunk.iptrunk_description = iptrunk_description subscription.iptrunk.iptrunk_description = iptrunk_description
subscription.iptrunk.iptrunk_type = iptrunk_type subscription.iptrunk.iptrunk_type = iptrunk_type
...@@ -101,12 +169,20 @@ def modify_iptrunk_subscription( ...@@ -101,12 +169,20 @@ def modify_iptrunk_subscription(
subscription.description = f"IP trunk, geant_s_sid:{geant_s_sid}" subscription.description = f"IP trunk, geant_s_sid:{geant_s_sid}"
return {"subscription": subscription} return {
"subscription": subscription,
"removed_ae_members": removed_ae_members,
"previous_ae_members": previous_ae_members,
}
@step("Provision IP trunk interface [DRY RUN]") @step("Provision IP trunk interface [DRY RUN]")
def provision_ip_trunk_iface_dry(subscription: Iptrunk, process_id: UUIDstr, tt_number: str) -> State: def provision_ip_trunk_iface_dry(
provisioning_proxy.provision_ip_trunk(subscription, process_id, tt_number, "trunk_interface") subscription: Iptrunk, process_id: UUIDstr, tt_number: str, removed_ae_members: List[str]
) -> State:
provisioning_proxy.provision_ip_trunk(
subscription, process_id, tt_number, "trunk_interface", True, removed_ae_members
)
return { return {
"subscription": subscription, "subscription": subscription,
...@@ -115,8 +191,12 @@ def provision_ip_trunk_iface_dry(subscription: Iptrunk, process_id: UUIDstr, tt_ ...@@ -115,8 +191,12 @@ def provision_ip_trunk_iface_dry(subscription: Iptrunk, process_id: UUIDstr, tt_
@step("Provision IP trunk interface [FOR REAL]") @step("Provision IP trunk interface [FOR REAL]")
def provision_ip_trunk_iface_real(subscription: Iptrunk, process_id: UUIDstr, tt_number: str) -> State: def provision_ip_trunk_iface_real(
provisioning_proxy.provision_ip_trunk(subscription, process_id, tt_number, "trunk_interface", False) subscription: Iptrunk, process_id: UUIDstr, tt_number: str, removed_ae_members: List[str]
) -> State:
provisioning_proxy.provision_ip_trunk(
subscription, process_id, tt_number, "trunk_interface", False, removed_ae_members
)
return { return {
"subscription": subscription, "subscription": subscription,
...@@ -124,6 +204,69 @@ def provision_ip_trunk_iface_real(subscription: Iptrunk, process_id: UUIDstr, tt ...@@ -124,6 +204,69 @@ def provision_ip_trunk_iface_real(subscription: Iptrunk, process_id: UUIDstr, tt
} }
@step("Update interfaces in Netbox. Reserving interfaces.")
def update_interfaces_in_netbox(subscription: Iptrunk, removed_ae_members: dict, previous_ae_members: dict) -> State:
nbclient = NetboxClient()
for side in range(0, 2):
if subscription.iptrunk.iptrunk_sides[side].iptrunk_side_node.router_vendor == RouterVendor.NOKIA:
lag_interface = subscription.iptrunk.iptrunk_sides[side].iptrunk_side_ae_iface
router_name = subscription.iptrunk.iptrunk_sides[side].iptrunk_side_node.router_fqdn
# Free removed interfaces
for member in removed_ae_members[str(side)]:
nbclient.free_interface(router_name, member["interface_name"])
# Attach physical interfaces to LAG
# Update interface description to subscription ID
# Reserve interfaces
for interface in subscription.iptrunk.iptrunk_sides[side].iptrunk_side_ae_members:
if any(
ae_member.get("interface_name") == interface.interface_name
for ae_member in previous_ae_members[str(side)]
):
continue
nbclient.attach_interface_to_lag(
device_name=subscription.iptrunk.iptrunk_sides[side].iptrunk_side_node.router_fqdn,
lag_name=lag_interface,
iface_name=interface.interface_name,
description=str(subscription.subscription_id),
)
nbclient.reserve_interface(
device_name=subscription.iptrunk.iptrunk_sides[side].iptrunk_side_node.router_fqdn,
iface_name=interface.interface_name,
)
return {
"subscription": subscription,
}
@step("Allocate interfaces in Netbox")
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.
"""
for side in range(0, 2):
nbclient = NetboxClient()
if subscription.iptrunk.iptrunk_sides[side].iptrunk_side_node.router_vendor == RouterVendor.NOKIA:
for interface in subscription.iptrunk.iptrunk_sides[side].iptrunk_side_ae_members:
if any(
ae_member.get("interface_name") == interface.interface_name
for ae_member in previous_ae_members[str(side)]
):
continue
nbclient.allocate_interface(
device_name=subscription.iptrunk.iptrunk_sides[side].iptrunk_side_node.router_fqdn,
iface_name=interface.interface_name,
)
# detach the old interfaces from lag
nbclient.detach_interfaces_from_lag(
device_name=subscription.iptrunk.iptrunk_sides[side].iptrunk_side_node.router_fqdn,
lag_name=subscription.iptrunk.iptrunk_sides[side].iptrunk_side_ae_iface,
)
return {"subscription": subscription}
@workflow( @workflow(
"Modify IP Trunk interface", "Modify IP Trunk interface",
initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
...@@ -135,8 +278,10 @@ def modify_trunk_interface() -> StepList: ...@@ -135,8 +278,10 @@ def modify_trunk_interface() -> StepList:
>> store_process_subscription(Target.MODIFY) >> store_process_subscription(Target.MODIFY)
>> unsync >> unsync
>> modify_iptrunk_subscription >> modify_iptrunk_subscription
>> update_interfaces_in_netbox
>> pp_interaction(provision_ip_trunk_iface_dry, 3) >> pp_interaction(provision_ip_trunk_iface_dry, 3)
>> pp_interaction(provision_ip_trunk_iface_real, 3) >> pp_interaction(provision_ip_trunk_iface_real, 3)
>> allocate_interfaces_in_netbox
>> resync >> resync
>> done >> done
) )
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment