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

Integrated migrate workflow with Netbox.

parent b9496100
Branches
Tags
1 merge request!92Feature/nat 314 integrate iptrunk modification with netbox
...@@ -53,6 +53,14 @@ class NetboxClient: ...@@ -53,6 +53,14 @@ class NetboxClient:
def get_all_devices(self) -> list[Devices]: def get_all_devices(self) -> list[Devices]:
return list(self.netbox.dcim.devices.all()) return list(self.netbox.dcim.devices.all())
def get_allocated_interfaces_by_gso_subscription(self, device_name: str, subscription_id: UUID) -> list[Interfaces]:
"""Return all allocated interfaces of a device by name."""
device = self.get_device_by_name(device_name)
return self.netbox.dcim.interfaces.filter(
device_id=device.id, enabled=True, mark_connected=True, description=subscription_id
)
def get_device_by_name(self, device_name: str) -> Devices: def get_device_by_name(self, device_name: str) -> Devices:
"""Return the device object by name from netbox, or ``None`` if not found.""" """Return the device object by name from netbox, or ``None`` if not found."""
return self.netbox.dcim.devices.get(name=device_name) return self.netbox.dcim.devices.get(name=device_name)
...@@ -84,6 +92,13 @@ class NetboxClient: ...@@ -84,6 +92,13 @@ class NetboxClient:
description=description, description=description,
) )
def delete_interface(self, device_name: str, iface_name: str) -> None:
"""Delete an interface from a device by name."""
device = self.get_device_by_name(device_name)
interface = self.netbox.dcim.interfaces.get(device_id=device.id, name=iface_name)
return interface.delete()
def create_device_type(self, manufacturer: str, model: str, slug: str) -> DeviceTypes: def create_device_type(self, manufacturer: str, model: str, slug: str) -> DeviceTypes:
"""Create a new device type in Netbox.""" """Create a new device type in Netbox."""
...@@ -224,6 +239,43 @@ class NetboxClient: ...@@ -224,6 +239,43 @@ class NetboxClient:
return interface return interface
def free_interface(self, device_name: str, iface_name: str) -> Interfaces:
"""Free interface by marking disconnect and disable it."""
device = self.get_device_by_name(device_name)
interface = self.netbox.dcim.interfaces.get(device_id=device.id, name=iface_name)
# Check if interface is available
if interface is None:
raise NotFoundError(f"Interface: {iface_name} on device: {device_name} not found.")
interface.mark_connected = False
interface.enabled = False
interface.description = ""
interface.save()
return interface
def deallocate_interface(self, device_name: str, iface_name: str) -> Interfaces:
"""Allocate an interface by marking it as connected."""
device = self.get_device_by_name(device_name)
interface = self.netbox.dcim.interfaces.get(device_id=device.id, name=iface_name)
# Check if interface is available
if interface is None:
raise NotFoundError(f"Interface: {iface_name} on device: {device_name} not found.")
# Check if interface is reserved
if interface.mark_connected:
raise WorkflowStateError(f"The interface: {iface_name} on device: {device_name} is already allocated.")
# allocate interface by mark as connected
interface.mark_connected = False
interface.save()
return interface
def get_available_lags(self, router_id: UUID) -> list[str]: def get_available_lags(self, router_id: UUID) -> list[str]:
"""Return all available :term:`LAG`s not assigned to a device.""" """Return all available :term:`LAG`s not assigned to a device."""
......
...@@ -4,7 +4,6 @@ from typing import NoReturn ...@@ -4,7 +4,6 @@ from typing import NoReturn
from orchestrator import step, workflow from orchestrator import step, workflow
from orchestrator.config.assignee import Assignee from orchestrator.config.assignee import Assignee
from orchestrator.db import ProductTable, SubscriptionTable
from orchestrator.forms import FormPage from orchestrator.forms import FormPage
from orchestrator.forms.validators import Choice, Label, UniqueConstrainedList from orchestrator.forms.validators import Choice, Label, UniqueConstrainedList
from orchestrator.targets import Target from orchestrator.targets import Target
...@@ -13,108 +12,118 @@ from orchestrator.workflow import StepList, done, init, inputstep ...@@ -13,108 +12,118 @@ 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 pynetbox.models.dcim import Interfaces
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
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 set_isis_to_90000 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
logger = getLogger(__name__) logger = getLogger(__name__)
def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
subscription = Iptrunk.from_subscription(subscription_id) subscription = Iptrunk.from_subscription(subscription_id)
form_title = (
f"Subscription {subscription.iptrunk.geant_s_sid} "
f" from {subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn}"
f" to {subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn}"
)
sides_dict = { sides_dict = {
str(side.iptrunk_side_node.subscription.subscription_id): side.iptrunk_side_node.subscription.description str(side.iptrunk_side_node.subscription.subscription_id): side.iptrunk_side_node.subscription.description
for side in subscription.iptrunk.iptrunk_sides for side in subscription.iptrunk.iptrunk_sides
} }
ReplacedSide = Choice( replaced_side_enum = Choice(
"Select the side of the IP trunk to be replaced", "Select the side of the IP trunk to be replaced",
zip(sides_dict.keys(), sides_dict.items()), # type: ignore[arg-type] zip(sides_dict.keys(), sides_dict.items()), # type: ignore[arg-type]
) )
class OldSideIptrunkForm(FormPage): class IPTrunkMigrateForm(FormPage):
class Config: class Config:
title = ( title = form_title
f"Subscription {subscription.iptrunk.geant_s_sid} from "
f"{subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn}"
f" to "
f"{subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn}"
)
tt_number: str tt_number: str
replace_side: ReplacedSide # type: ignore[valid-type] replace_side: replaced_side_enum # type: ignore[valid-type]
warning_label: Label = "Are we moving to a different Site?" # type: ignore[assignment] warning_label: Label = "Are we moving to a different Site?" # type: ignore[assignment]
migrate_to_different_site: bool | None = False migrate_to_different_site: bool = False
old_side_input = yield OldSideIptrunkForm 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,
]
routers = {} routers = {}
for router_id, router_description in ( for router_id, router_description in get_active_router_subscriptions(fields=["subscription_id", "description"]):
SubscriptionTable.query.join(ProductTable) if router_id not in current_routers:
.filter( current_router_site = Router.from_subscription(router_id).router.router_site.subscription
ProductTable.product_type == "Router", old_side_site = Router.from_subscription(migrate_form_input.replace_side).router.router_site
SubscriptionTable.status == "active",
)
.with_entities(SubscriptionTable.subscription_id, SubscriptionTable.description)
.all()
):
if router_id not in [
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.subscription.subscription_id,
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.subscription.subscription_id,
]:
current_router = Router.from_subscription(router_id)
old_side_site_id = Router.from_subscription(old_side_input.replace_side).router.router_site
if ( if (
not old_side_input.migrate_to_different_site migrate_form_input.migrate_to_different_site
and current_router.router.router_site.subscription.subscription_id != old_side_site_id and current_router_site.subscription_id == old_side_site.owner_subscription_id
): ):
continue continue
routers[str(router_id)] = router_description routers[str(router_id)] = router_description
NewRouterEnum = Choice("Select a new router", zip(routers.keys(), routers.items())) # type: ignore[arg-type] new_router_enum = Choice("Select a new router", zip(routers.keys(), routers.items())) # type: ignore[arg-type]
class NewSideIPTrunkRouterForm(FormPage):
class Config:
title = form_title
new_node: new_router_enum # type: ignore[valid-type]
new_side_iptrunk_router_input = yield NewSideIPTrunkRouterForm
new_router = new_side_iptrunk_router_input.new_node
side_a_ae_iface = available_lags_choices(new_router) or str
class LagMemberList(UniqueConstrainedList[str]): class LagMemberList(UniqueConstrainedList[str]):
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 NewSideIptrunkForm(FormPage): class JuniperLagMemberList(UniqueConstrainedList[str]):
class Config: min_items = len(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members)
title = ( max_items = len(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members)
f"Subscription {subscription.iptrunk.geant_s_sid} from " unique_items = True
f"{subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn} to "
f"{subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn}"
)
new_node: NewRouterEnum # type: ignore[valid-type] ae_members_side_a = LagMemberList if get_router_vendor(new_router) == RouterVendor.NOKIA else JuniperLagMemberList
new_lag_interface: str
new_lag_member_interfaces: LagMemberList
@validator("new_lag_interface", allow_reuse=True, pre=True, always=True) class NewSideIPTrunkForm(FormPage):
def lag_interface_proper_name(cls, new_lag_name: str) -> str | NoReturn: class Config:
nokia_lag_re = re.compile("^lag-\\d+$") title = form_title
juniper_lag_re = re.compile("^ae\\d{1,2}$")
if nokia_lag_re.match(new_lag_name) or juniper_lag_re.match(new_lag_name): new_lag_interface: side_a_ae_iface # type: ignore[valid-type]
return new_lag_name new_lag_member_interfaces: ae_members_side_a # type: ignore[valid-type]
raise ValueError("Invalid LAG name, please try again.") @validator("new_lag_interface", allow_reuse=True, pre=True, always=True)
def lag_interface_proper_name(cls, new_lag_interface: str) -> str | NoReturn:
if get_router_vendor(new_router) == RouterVendor.JUNIPER:
juniper_lag_re = re.compile("^ae\\d{1,2}$")
if not juniper_lag_re.match(new_lag_interface):
raise ValueError("Invalid LAG name, please try again.")
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: def _find_updated_side_of_trunk(trunk: Iptrunk, new_side: str) -> int:
sides = trunk.iptrunk.iptrunk_sides for side in trunk.iptrunk.iptrunk_sides:
if str(sides[0].iptrunk_side_node.subscription.subscription_id) == new_side: if str(side.iptrunk_side_node.subscription.subscription_id) == new_side:
return 0 return trunk.iptrunk.iptrunk_sides.index(side)
elif str(sides[1].iptrunk_side_node.subscription.subscription_id) == new_side: # noqa: RET505
return 1
raise ValueError("Invalid Router id provided to be replaced!") raise ValueError("Invalid Router id provided to be replaced!")
replace_index = _find_updated_side_of_trunk(subscription, old_side_input.replace_side) return (
migrate_form_input.dict()
return old_side_input.dict() | new_side_input.dict() | {"replace_index": replace_index} | new_side_iptrunk_router_input.dict()
| new_side_input.dict()
| {"replace_index": _find_updated_side_of_trunk(subscription, migrate_form_input.replace_side)}
)
@step("[DRY RUN] Disable configuration on old router") @step("[DRY RUN] Disable configuration on old router")
...@@ -385,10 +394,76 @@ def update_subscription_model( ...@@ -385,10 +394,76 @@ def update_subscription_model(
new_lag_interface: str, new_lag_interface: str,
new_lag_member_interfaces: list[str], new_lag_member_interfaces: list[str],
) -> State: ) -> State:
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,
}
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 = new_lag_member_interfaces
return {"subscription": subscription, "old_side_data": old_side_data}
@step("Reserve interfaces in Netbox")
def reserve_interfaces_in_netbox(
subscription: Iptrunk,
new_node: UUIDstr,
new_lag_interface: str,
new_lag_member_interfaces: list[str],
) -> State:
new_side = Router.from_subscription(new_node).router
nbclient = NetboxClient()
if new_side.router_vendor == RouterVendor.NOKIA:
# Create LAG interfaces
lag_interface: Interfaces = nbclient.create_interface(
iface_name=new_lag_interface,
type="lag",
device_name=new_side.router_fqdn,
description=str(subscription.subscription_id),
enabled=True,
)
# Attach physical interfaces to LAG
# Reserve interfaces
for interface in new_lag_member_interfaces:
nbclient.attach_interface_to_lag(
device_name=new_side.router_fqdn,
lag_name=lag_interface.name,
iface_name=interface,
description=str(subscription.subscription_id),
)
nbclient.reserve_interface(
device_name=new_side.router_fqdn,
iface_name=interface,
)
return {"subscription": subscription}
@step("Update Netbox.")
def update_netbox(
subscription: Iptrunk,
replace_index: int,
old_side_data: dict,
) -> State:
new_side = subscription.iptrunk.iptrunk_sides[replace_index]
nbclient = NetboxClient()
if new_side.iptrunk_side_node.router_vendor == RouterVendor.NOKIA:
for interface in new_side.iptrunk_side_ae_members:
nbclient.allocate_interface(
device_name=new_side.iptrunk_side_node.router_fqdn,
iface_name=interface,
)
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)
# Delete LAG interfaces
nbclient.delete_interface(
old_side_data["iptrunk_side_node"]["router_fqdn"], old_side_data["iptrunk_side_ae_iface"]
)
return {"subscription": subscription} return {"subscription": subscription}
...@@ -402,6 +477,7 @@ def migrate_iptrunk() -> StepList: ...@@ -402,6 +477,7 @@ def migrate_iptrunk() -> StepList:
init init
>> store_process_subscription(Target.MODIFY) >> store_process_subscription(Target.MODIFY)
>> unsync >> unsync
>> reserve_interfaces_in_netbox
>> pp_interaction(set_isis_to_90000, 3) >> pp_interaction(set_isis_to_90000, 3)
>> pp_interaction(disable_old_config_dry, 3) >> pp_interaction(disable_old_config_dry, 3)
>> pp_interaction(disable_old_config_real, 3) >> pp_interaction(disable_old_config_real, 3)
...@@ -415,6 +491,7 @@ def migrate_iptrunk() -> StepList: ...@@ -415,6 +491,7 @@ def migrate_iptrunk() -> StepList:
>> pp_interaction(delete_old_config_real, 3) >> pp_interaction(delete_old_config_real, 3)
>> update_ipam >> update_ipam
>> update_subscription_model >> update_subscription_model
>> update_netbox
>> resync >> resync
>> done >> done
) )
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment