diff --git a/gso/services/netbox_client.py b/gso/services/netbox_client.py index aac06eea66fcd21281e030486c30e3c647fbbf5e..96d0cc8743e66a2c463cb6adbade8d197168de2a 100644 --- a/gso/services/netbox_client.py +++ b/gso/services/netbox_client.py @@ -3,6 +3,7 @@ from uuid import UUID import pydantic import pynetbox +from infoblox_client.objects import Interface from pynetbox.models.dcim import Devices, DeviceTypes, Interfaces from gso.products.product_types.router import Router @@ -256,6 +257,17 @@ class NetboxClient: 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: """Allocate an interface by marking it as connected.""" @@ -310,3 +322,13 @@ class NetboxClient: return self.netbox.dcim.interfaces.filter( 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.") diff --git a/gso/services/provisioning_proxy.py b/gso/services/provisioning_proxy.py index 5a6ba11d79fedcfa8431d1dc0c8a283e23c9d73e..c29d942915a537e1a9d64ad0ad449449dd2ae9e0 100644 --- a/gso/services/provisioning_proxy.py +++ b/gso/services/provisioning_proxy.py @@ -103,7 +103,12 @@ def provision_router( 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: """Provision an IP trunk service using :term:`LSO`. @@ -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`. :type dry_run: bool :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 = { "subscription": json.loads(json_dumps(subscription)), @@ -124,6 +131,7 @@ def provision_ip_trunk( "tt_number": tt_number, "process_id": process_id, "object": config_object, + "removed_ae_members": removed_ae_members, } _send_request("ip_trunk", parameters, process_id, CUDOperation.POST) @@ -175,7 +183,7 @@ def migrate_ip_trunk( subscription: Iptrunk, new_node: Router, new_lag_interface: str, - new_lag_member_interfaces: list[str], + new_lag_member_interfaces: list[dict], replace_index: int, process_id: UUIDstr, tt_number: str, diff --git a/gso/utils/helpers.py b/gso/utils/helpers.py index ff37395b92b3c3d7a8c9b5c61e392afa7b7bbe31..fabfddecfd69aa90151d331bf0c0a205eedda26b 100644 --- a/gso/utils/helpers.py +++ b/gso/utils/helpers.py @@ -7,6 +7,7 @@ from orchestrator.types import State, UUIDstr from pydantic import BaseModel 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_types.iptrunk import Iptrunk from gso.products.product_types.router import Router @@ -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] +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: """Return a list of available lags for a given router. diff --git a/gso/workflows/iptrunk/migrate_iptrunk.py b/gso/workflows/iptrunk/migrate_iptrunk.py index b1e48b57091f0f0ca971b8eab07e94401b064dd0..e31d0011e5968a51de9276a17626f33e98715c99 100644 --- a/gso/workflows/iptrunk/migrate_iptrunk.py +++ b/gso/workflows/iptrunk/migrate_iptrunk.py @@ -1,6 +1,8 @@ +import copy import re from logging import getLogger from typing import NoReturn +from uuid import uuid4 from orchestrator import step, workflow from orchestrator.config.assignee import Assignee @@ -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.utils import wrap_modify_initial_input_form from pydantic import validator +from pydantic_forms.core import ReadOnlyField 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_types.iptrunk import Iptrunk from gso.products.product_types.router import Router @@ -21,7 +25,13 @@ from gso.services import provisioning_proxy from gso.services.netbox_client import NetboxClient from gso.services.provisioning_proxy import pp_interaction 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__) @@ -55,9 +65,9 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: migrate_form_input = yield IPTrunkMigrateForm current_routers = [ - subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.subscription.subscription_id, - subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.subscription.subscription_id, + side.iptrunk_side_node.subscription.subscription_id for side in subscription.iptrunk.iptrunk_sides ] + routers = {} for router_id, router_description in get_active_router_subscriptions(fields=["subscription_id", "description"]): if router_id not in current_routers: @@ -82,25 +92,44 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: new_router = new_side_iptrunk_router_input.new_node side_a_ae_iface = available_lags_choices(new_router) or str - class LagMemberList(UniqueConstrainedList[str]): - min_items = len(subscription.iptrunk.iptrunk_sides[0].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 + 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) + max_items = len(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members) + + ae_members = NokiaAeMembers + else: - class JuniperLagMemberList(UniqueConstrainedList[str]): - min_items = len(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members) - max_items = len(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members) - unique_items = True + class JuniperLagMember(UniqueConstrainedList[LAGMember]): + min_items = len(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members) + max_items = len(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members) - 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 Config: title = form_title 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) 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: return new_lag_interface 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 ( migrate_form_input.dict() | new_side_iptrunk_router_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( subscription: Iptrunk, new_node: Router, new_lag_interface: str, - new_lag_member_interfaces: list[str], + new_lag_member_interfaces: list[dict], replace_index: int, process_id: UUIDstr, tt_number: str, @@ -159,7 +181,7 @@ def disable_old_config_real( subscription: Iptrunk, new_node: Router, new_lag_interface: str, - new_lag_member_interfaces: list[str], + new_lag_member_interfaces: list[dict], replace_index: int, process_id: UUIDstr, tt_number: str, @@ -188,7 +210,7 @@ def deploy_new_config_dry( subscription: Iptrunk, new_node: Router, new_lag_interface: str, - new_lag_member_interfaces: list[str], + new_lag_member_interfaces: list[dict], replace_index: int, process_id: UUIDstr, tt_number: str, @@ -218,7 +240,7 @@ def deploy_new_config_real( subscription: Iptrunk, new_node: Router, new_lag_interface: str, - new_lag_member_interfaces: list[str], + new_lag_member_interfaces: list[dict], replace_index: int, process_id: UUIDstr, tt_number: str, @@ -268,7 +290,7 @@ def deploy_new_isis( subscription: Iptrunk, new_node: Router, new_lag_interface: str, - new_lag_member_interfaces: list[str], + new_lag_member_interfaces: list[dict], replace_index: int, process_id: UUIDstr, tt_number: str, @@ -322,7 +344,7 @@ def delete_old_config_dry( subscription: Iptrunk, new_node: Router, new_lag_interface: str, - new_lag_member_interfaces: list[str], + new_lag_member_interfaces: list[dict], replace_index: int, process_id: UUIDstr, tt_number: str, @@ -353,7 +375,7 @@ def delete_old_config_real( subscription: Iptrunk, new_node: Router, new_lag_interface: str, - new_lag_member_interfaces: list[str], + new_lag_member_interfaces: list[dict], replace_index: int, process_id: UUIDstr, tt_number: str, @@ -392,16 +414,23 @@ def update_subscription_model( replace_index: int, new_node: UUIDstr, new_lag_interface: str, - new_lag_member_interfaces: list[str], + new_lag_member_interfaces: list[dict], ) -> State: + # Deep copy of subscription data + old_subscription = copy.deepcopy(subscription) old_side_data = { - "iptrunk_side_node": 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_members": subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_members, + "iptrunk_side_node": old_subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_node, + "iptrunk_side_ae_iface": old_subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_iface, + "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_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} @@ -411,7 +440,7 @@ def reserve_interfaces_in_netbox( subscription: Iptrunk, new_node: UUIDstr, new_lag_interface: str, - new_lag_member_interfaces: list[str], + new_lag_member_interfaces: list[dict], ) -> State: new_side = Router.from_subscription(new_node).router @@ -431,17 +460,17 @@ def reserve_interfaces_in_netbox( nbclient.attach_interface_to_lag( device_name=new_side.router_fqdn, lag_name=lag_interface.name, - iface_name=interface, + iface_name=interface["interface_name"], description=str(subscription.subscription_id), ) nbclient.reserve_interface( device_name=new_side.router_fqdn, - iface_name=interface, + iface_name=interface["interface_name"], ) return {"subscription": subscription} -@step("Update Netbox.") +@step("Update Netbox. Allocate new interfaces and deallocate old ones.") def update_netbox( subscription: Iptrunk, replace_index: int, @@ -453,12 +482,12 @@ def update_netbox( for interface in new_side.iptrunk_side_ae_members: nbclient.allocate_interface( 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: # Set interfaces to free 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 nbclient.delete_interface( diff --git a/gso/workflows/iptrunk/modify_trunk_interface.py b/gso/workflows/iptrunk/modify_trunk_interface.py index 284602504eb7af3d7d7563728fbe401a1ce0f424..a2448438769d53077773d79586f9c1fbdc586a0e 100644 --- a/gso/workflows/iptrunk/modify_trunk_interface.py +++ b/gso/workflows/iptrunk/modify_trunk_interface.py @@ -1,4 +1,5 @@ import ipaddress +from typing import List, Type from uuid import uuid4 from orchestrator.forms import FormPage, ReadOnlyField @@ -8,12 +9,50 @@ from orchestrator.types import FormGenerator, State, UUIDstr from orchestrator.workflow import StepList, done, init, step, workflow from orchestrator.workflows.steps import resync, store_process_subscription, unsync 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.router import RouterVendor from gso.products.product_types.iptrunk import Iptrunk from gso.services import provisioning_proxy +from gso.services.netbox_client import NetboxClient 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: @@ -24,6 +63,10 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: geant_s_sid: str = subscription.iptrunk.geant_s_sid iptrunk_description: str = subscription.iptrunk.iptrunk_description 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_minimum_links: int = subscription.iptrunk.iptrunk_minimum_links iptrunk_isis_metric: int = ReadOnlyField(subscription.iptrunk.iptrunk_isis_metric) @@ -31,9 +74,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: iptrunk_ipv6_network: ipaddress.IPv6Network = ReadOnlyField(subscription.iptrunk.iptrunk_ipv6_network) initial_user_input = yield ModifyIptrunkForm - - class AeMembersListA(UniqueConstrainedList[LAGMember]): - min_items = initial_user_input.iptrunk_minimum_links + ae_members_side_a = initialize_ae_members(subscription, initial_user_input.dict(), 0) class ModifyIptrunkSideAForm(FormPage): class Config: @@ -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_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_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]): - min_items = len(user_input_side_a.side_a_ae_members) - max_items = len(user_input_side_a.side_a_ae_members) + user_input_side_a = yield ModifyIptrunkSideAForm + ae_members_side_b = initialize_ae_members(subscription, initial_user_input.dict(), 1) class ModifyIptrunkSideBForm(FormPage): class Config: @@ -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_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_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 @@ -77,6 +131,20 @@ def modify_iptrunk_subscription( side_b_ae_geant_a_sid: str, side_b_ae_members: list[dict], ) -> 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.iptrunk_description = iptrunk_description subscription.iptrunk.iptrunk_type = iptrunk_type @@ -101,12 +169,20 @@ def modify_iptrunk_subscription( 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]") -def provision_ip_trunk_iface_dry(subscription: Iptrunk, process_id: UUIDstr, tt_number: str) -> State: - provisioning_proxy.provision_ip_trunk(subscription, process_id, tt_number, "trunk_interface") +def provision_ip_trunk_iface_dry( + 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 { "subscription": subscription, @@ -115,8 +191,12 @@ def provision_ip_trunk_iface_dry(subscription: Iptrunk, process_id: UUIDstr, tt_ @step("Provision IP trunk interface [FOR REAL]") -def provision_ip_trunk_iface_real(subscription: Iptrunk, process_id: UUIDstr, tt_number: str) -> State: - provisioning_proxy.provision_ip_trunk(subscription, process_id, tt_number, "trunk_interface", False) +def provision_ip_trunk_iface_real( + 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 { "subscription": subscription, @@ -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( "Modify IP Trunk interface", initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), @@ -135,8 +278,10 @@ def modify_trunk_interface() -> StepList: >> store_process_subscription(Target.MODIFY) >> unsync >> modify_iptrunk_subscription + >> update_interfaces_in_netbox >> pp_interaction(provision_ip_trunk_iface_dry, 3) >> pp_interaction(provision_ip_trunk_iface_real, 3) + >> allocate_interfaces_in_netbox >> resync >> done )