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
No related branches found
No related tags found
1 merge request!92Feature/nat 314 integrate iptrunk modification with netbox
......@@ -53,6 +53,14 @@ class NetboxClient:
def get_all_devices(self) -> list[Devices]:
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:
"""Return the device object by name from netbox, or ``None`` if not found."""
return self.netbox.dcim.devices.get(name=device_name)
......@@ -84,6 +92,13 @@ class NetboxClient:
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:
"""Create a new device type in Netbox."""
......@@ -224,6 +239,43 @@ class NetboxClient:
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]:
"""Return all available :term:`LAG`s not assigned to a device."""
......
......@@ -4,7 +4,6 @@ from typing import NoReturn
from orchestrator import step, workflow
from orchestrator.config.assignee import Assignee
from orchestrator.db import ProductTable, SubscriptionTable
from orchestrator.forms import FormPage
from orchestrator.forms.validators import Choice, Label, UniqueConstrainedList
from orchestrator.targets import Target
......@@ -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.utils import wrap_modify_initial_input_form
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.router import Router
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 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__)
def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
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 = {
str(side.iptrunk_side_node.subscription.subscription_id): side.iptrunk_side_node.subscription.description
for side in subscription.iptrunk.iptrunk_sides
}
ReplacedSide = Choice(
replaced_side_enum = Choice(
"Select the side of the IP trunk to be replaced",
zip(sides_dict.keys(), sides_dict.items()), # type: ignore[arg-type]
)
class OldSideIptrunkForm(FormPage):
class IPTrunkMigrateForm(FormPage):
class Config:
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}"
)
title = form_title
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]
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 = {}
for router_id, router_description in (
SubscriptionTable.query.join(ProductTable)
.filter(
ProductTable.product_type == "Router",
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
for router_id, router_description in get_active_router_subscriptions(fields=["subscription_id", "description"]):
if router_id not in current_routers:
current_router_site = Router.from_subscription(router_id).router.router_site.subscription
old_side_site = Router.from_subscription(migrate_form_input.replace_side).router.router_site
if (
not old_side_input.migrate_to_different_site
and current_router.router.router_site.subscription.subscription_id != old_side_site_id
migrate_form_input.migrate_to_different_site
and current_router_site.subscription_id == old_side_site.owner_subscription_id
):
continue
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]):
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
class NewSideIptrunkForm(FormPage):
class Config:
title = (
f"Subscription {subscription.iptrunk.geant_s_sid} from "
f"{subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn} to "
f"{subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn}"
)
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
new_node: NewRouterEnum # type: ignore[valid-type]
new_lag_interface: str
new_lag_member_interfaces: LagMemberList
ae_members_side_a = LagMemberList if get_router_vendor(new_router) == RouterVendor.NOKIA else JuniperLagMemberList
@validator("new_lag_interface", allow_reuse=True, pre=True, always=True)
def lag_interface_proper_name(cls, new_lag_name: str) -> str | NoReturn:
nokia_lag_re = re.compile("^lag-\\d+$")
juniper_lag_re = re.compile("^ae\\d{1,2}$")
class NewSideIPTrunkForm(FormPage):
class Config:
title = form_title
if nokia_lag_re.match(new_lag_name) or juniper_lag_re.match(new_lag_name):
return new_lag_name
new_lag_interface: side_a_ae_iface # type: ignore[valid-type]
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:
sides = trunk.iptrunk.iptrunk_sides
if str(sides[0].iptrunk_side_node.subscription.subscription_id) == new_side:
return 0
elif str(sides[1].iptrunk_side_node.subscription.subscription_id) == new_side: # noqa: RET505
return 1
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!")
replace_index = _find_updated_side_of_trunk(subscription, old_side_input.replace_side)
return old_side_input.dict() | new_side_input.dict() | {"replace_index": replace_index}
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)}
)
@step("[DRY RUN] Disable configuration on old router")
......@@ -385,10 +394,76 @@ def update_subscription_model(
new_lag_interface: str,
new_lag_member_interfaces: list[str],
) -> 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_ae_iface = new_lag_interface
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}
......@@ -402,6 +477,7 @@ def migrate_iptrunk() -> StepList:
init
>> store_process_subscription(Target.MODIFY)
>> unsync
>> reserve_interfaces_in_netbox
>> pp_interaction(set_isis_to_90000, 3)
>> pp_interaction(disable_old_config_dry, 3)
>> pp_interaction(disable_old_config_real, 3)
......@@ -415,6 +491,7 @@ def migrate_iptrunk() -> StepList:
>> pp_interaction(delete_old_config_real, 3)
>> update_ipam
>> update_subscription_model
>> update_netbox
>> resync
>> done
)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment