Skip to content
Snippets Groups Projects
Commit 047d4901 authored by Neda Moeini's avatar Neda Moeini Committed by Neda Moeini
Browse files

Integreate create IP Trunk workflow with netbox

parent 1d28d7fd
Branches
Tags
1 merge request!82IP-TRUNK-CREATE-WORKFLOW-NETBOX-INTEGRATION
......@@ -67,7 +67,9 @@ class NetBoxClient:
self.netbox.dcim.interfaces.filter(device_id=device.id, enabled=False, mark_connected=False, speed=speed)
)
def create_interface(self, iface_name: str, type: str, speed: str, device_name: str) -> Interfaces:
def create_interface(
self, iface_name: str, type: str, device_name: str, description: str | None = None, enabled: bool = False
) -> Interfaces:
"""Create new interface on a device, where device is defined by name.
The type parameter can be 1000base-t, 10gbase-t, lag, etc.
......@@ -76,7 +78,12 @@ class NetBoxClient:
"""
device = self.get_device_by_name(device_name)
return self.netbox.dcim.interfaces.create(
name=iface_name, type=type, speed=speed, enabled=False, mark_connected=False, device=device.id
name=iface_name,
type=type,
enabled=enabled,
mark_connected=False,
device=device.id,
description=description,
)
def create_device_type(self, manufacturer: str, model: str, slug: str) -> DeviceTypes:
......@@ -110,7 +117,7 @@ class NetBoxClient:
device_role = self.netbox.dcim.device_roles.get(name="router")
# Get site id
device_site = self.netbox.dcim.sites.get(name="Amsterdam")
device_site = self.netbox.dcim.sites.get(name="GEANT")
# Create new device
device = self.netbox.dcim.devices.create(
......@@ -118,40 +125,59 @@ class NetBoxClient:
)
module_bays = list(self.netbox.dcim.module_bays.filter(device_id=device.id))
card_type = self.netbox.dcim.module_types.get(model=tier_info.module_type)
# TODo: Use the module_bays_slots to create the modules
for module_bay in module_bays:
self.netbox.dcim.modules.create(
device=device.id,
module_bay=module_bay.id,
module_type=card_type.id,
status="active",
enabled=False,
comments="Installed via pynetbox",
)
for interface in self.netbox.dcim.interfaces.filter(device_id=device.id):
speed = None
type_parts = interface.type.value.split("-")
if "gbase" in type_parts[0]:
# Extract the numeric part and convert to bits per second
speed = int("".join(filter(str.isdigit, type_parts[0]))) * 1000000
interface.speed = speed
interface.enabled = False
interface.save()
return device
def delete_device(self, router_name: str) -> None:
self.netbox.dcim.devices.get(name=router_name).delete()
return
def attach_interface_to_lag(self, device_name: str, lag_name: str, iface_name: str) -> Interfaces:
"""Assign a given interface to a lag.
def attach_interface_to_lag(
self, device_name: str, lag_name: str, iface_name: str, description: str | None = None
) -> Interfaces:
"""Assign a given interface to a LAG.
Returns the lag object with the assignend interfaces
Returns the interface object after assignment.
"""
# Get device id
device = self.get_device_by_name(device_name)
# Now get interface for device
# Get interface for device
iface = self.netbox.dcim.interfaces.get(name=iface_name, device_id=device.id)
# Get lag
# Get LAG
lag = self.netbox.dcim.interfaces.get(name=lag_name, device_id=device.id)
# Assign interface to lag
# Assign interface to LAG
iface.lag = lag.id
# Update interface
return self.netbox.dcim.interfaces.update(iface)
# Set description if provided
if description:
iface.description = description
iface.save()
return iface
def reserve_interface(self, device_name: str, iface_name: str) -> Interfaces:
"""Reserve an interface by enabling it."""
......@@ -210,3 +236,10 @@ class NetBoxClient:
# Return available lags not assigned to the device
return [lag for lag in all_feasible_lags if lag not in lag_interface_names]
def get_available_interfaces(self, router_id: UUID, speed: str) -> Interfaces:
router = Router.from_subscription(router_id).router.router_fqdn
device = self.get_device_by_name(router)
return self.netbox.dcim.interfaces.filter(
device=device.name, enabled=False, mark_connected=False, speed=int(speed.split("G")[0]) * 1000000
)
......@@ -12,16 +12,16 @@ class ModuleInfo(BaseModel):
class TierInfo:
def __init__(self) -> None:
self.Tier1 = ModuleInfo(
device_type="7750-SR7s",
device_type="7750 SR-7s",
module_bays_slots=[1, 2],
module_type="XCM2s-XMA2s-36p-800g",
module_type="XMA2-s-36p-400g",
breakout_interfaces_per_slot=[36, 35, 34, 33],
total_10g_interfaces=80,
)
self.Tier2 = ModuleInfo(
device_type="7750-SR7s",
module_bays_slots=[1, 2],
module_type="XCM2s-XMA2s-36p-400g",
module_type="XMA2-s-36p-400g",
breakout_interfaces_per_slot=[36, 35, 34, 33],
total_10g_interfaces=60,
)
......@@ -30,4 +30,5 @@ class TierInfo:
return getattr(self, name)
FEASIBLE_IP_TRUNK_LAG_RANGE = range(1, 10)
# The range includes values from 1 to 10 (11 is not included)
FEASIBLE_IP_TRUNK_LAG_RANGE = range(1, 11)
from typing import NoReturn
from orchestrator.forms import FormPage
from orchestrator.forms.validators import Choice, UniqueConstrainedList
from orchestrator.forms.validators import Choice, ChoiceList, UniqueConstrainedList
from orchestrator.targets import Target
from orchestrator.types import FormGenerator, State, SubscriptionLifecycle, UUIDstr
from orchestrator.workflow import StepList, done, init, step, workflow
from orchestrator.workflows.steps import resync, set_status, store_process_subscription
from orchestrator.workflows.utils import wrap_create_initial_input_form
from pydantic import validator
from pynetbox.models.dcim import Interfaces
from gso.products.product_blocks import PhyPortCapacity
from gso.products.product_blocks.iptrunk import IptrunkType
from gso.products.product_blocks.router import RouterVendor
from gso.products.product_types.iptrunk import IptrunkInactive, IptrunkProvisioning
from gso.products.product_types.router import Router
from gso.services import infoblox, provisioning_proxy, subscriptions
from gso.services.netbox_client import NetBoxClient
from gso.services.provisioning_proxy import pp_interaction
from gso.workflows.utils import customer_selector
from gso.workflows.utils import (
available_interfaces_choices,
available_lags_choices,
customer_selector,
get_router_vendor,
)
def initial_input_form_generator(product_name: str) -> FormGenerator:
......@@ -39,28 +50,83 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
initial_user_input = yield CreateIptrunkForm
class AeMembersListA(UniqueConstrainedList[str]):
router_enum_a = Choice("Select a router", zip(routers.keys(), routers.items())) # type: ignore
class SelectRouterSideA(FormPage):
class Config:
title = "Select a router for side A of the trunk."
iptrunk_sideA_node_id: router_enum_a # type: ignore[valid-type]
@validator("iptrunk_sideA_node_id", allow_reuse=True)
def validate_device_exists_in_netbox(cls, iptrunk_sideA_node_id: UUIDstr) -> str | NoReturn:
router = Router.from_subscription(iptrunk_sideA_node_id).router
if router.router_vendor == RouterVendor.NOKIA:
device = NetBoxClient().get_device_by_name(router.router_fqdn)
if not device:
raise ValueError("The selected router does not exist in Netbox.")
return iptrunk_sideA_node_id
user_input_router_side_a = yield SelectRouterSideA
router_a = user_input_router_side_a.iptrunk_sideA_node_id.name
side_a_ae_iface = available_lags_choices(router_a) or str
class AeMembersListA(ChoiceList):
min_items = initial_user_input.iptrunk_minimum_links
item_type = available_interfaces_choices(router_a, initial_user_input.iptrunk_speed) # type: ignore
unique_items = True
RouterEnumA = Choice("Select a router", zip(routers.keys(), routers.items())) # type: ignore[arg-type]
class JuniperAeMembers(UniqueConstrainedList[str]):
min_items = initial_user_input.iptrunk_minimum_links
unique_items = True
ae_members_side_a = AeMembersListA if get_router_vendor(router_a) == RouterVendor.NOKIA else JuniperAeMembers
class AeMembersDescriptionListA(UniqueConstrainedList[str]):
min_items = initial_user_input.iptrunk_minimum_links
class CreateIptrunkSideAForm(FormPage):
class Config:
title = "Provide subscription details for side A of the trunk."
iptrunk_sideA_node_id: RouterEnumA # type: ignore[valid-type]
iptrunk_sideA_ae_iface: str
iptrunk_sideA_ae_iface: side_a_ae_iface # type: ignore[valid-type]
iptrunk_sideA_ae_geant_a_sid: str
iptrunk_sideA_ae_members: AeMembersListA
iptrunk_sideA_ae_members_descriptions: AeMembersListA
iptrunk_sideA_ae_members: ae_members_side_a # type: ignore[valid-type]
iptrunk_sideA_ae_members_descriptions: AeMembersDescriptionListA
user_input_side_a = yield CreateIptrunkSideAForm
# Remove the selected router for side A, to prevent any loops
routers.pop(str(user_input_side_a.iptrunk_sideA_node_id.name))
RouterEnumB = Choice("Select a router", zip(routers.keys(), routers.items())) # type: ignore[arg-type]
routers.pop(str(user_input_router_side_a.iptrunk_sideA_node_id.name))
router_enum_b = Choice("Select a router", zip(routers.keys(), routers.items())) # type: ignore
class SelectRouterSideB(FormPage):
class Config:
title = "Select a router for side B of the trunk."
iptrunk_sideB_node_id: router_enum_b # type: ignore[valid-type]
@validator("iptrunk_sideB_node_id", allow_reuse=True)
def validate_device_exists_in_netbox(cls, iptrunk_sideB_node_id: UUIDstr) -> str | NoReturn:
router = Router.from_subscription(iptrunk_sideB_node_id).router
if router.router_vendor == RouterVendor.NOKIA:
device = NetBoxClient().get_device_by_name(router.router_fqdn)
if not device:
raise ValueError("The selected router does not exist in Netbox.")
return iptrunk_sideB_node_id
class AeMembersListB(UniqueConstrainedList[str]):
user_input_router_side_b = yield SelectRouterSideB
router_b = user_input_router_side_b.iptrunk_sideB_node_id.name
side_b_ae_iface = available_lags_choices(router_b) or str
class AeMembersListB(ChoiceList):
min_items = len(user_input_side_a.iptrunk_sideA_ae_members)
max_items = len(user_input_side_a.iptrunk_sideA_ae_members)
item_type = available_interfaces_choices(router_b, initial_user_input.iptrunk_speed) # type: ignore
unique_items = True
ae_members_side_b = AeMembersListB if get_router_vendor(router_b) == RouterVendor.NOKIA else JuniperAeMembers
class AeMembersDescriptionListB(UniqueConstrainedList[str]):
min_items = len(user_input_side_a.iptrunk_sideA_ae_members)
max_items = len(user_input_side_a.iptrunk_sideA_ae_members)
......@@ -68,15 +134,20 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
class Config:
title = "Provide subscription details for side B of the trunk."
iptrunk_sideB_node_id: RouterEnumB # type: ignore[valid-type]
iptrunk_sideB_ae_iface: str
iptrunk_sideB_ae_iface: side_b_ae_iface # type: ignore[valid-type]
iptrunk_sideB_ae_geant_a_sid: str
iptrunk_sideB_ae_members: AeMembersListB
iptrunk_sideB_ae_members_descriptions: AeMembersListB
iptrunk_sideB_ae_members: ae_members_side_b # type: ignore[valid-type]
iptrunk_sideB_ae_members_descriptions: AeMembersDescriptionListB
user_input_side_b = yield CreateIptrunkSideBForm
return initial_user_input.dict() | user_input_side_a.dict() | user_input_side_b.dict()
return (
initial_user_input.dict()
| user_input_side_a.dict()
| user_input_side_b.dict()
| user_input_router_side_a.dict()
| user_input_router_side_b.dict()
)
@step("Create subscription")
......@@ -161,7 +232,7 @@ def provision_ip_trunk_iface_real(subscription: IptrunkProvisioning, process_id:
return {
"subscription": subscription,
"label_text": "[COMMIT] Provisioning a trunk interface, please refresh to get the results of the playbook.",
"label_text": "Provisioning a trunk interface, please refresh to get the results of the playbook.",
}
......@@ -205,6 +276,57 @@ def check_ip_trunk_isis(subscription: IptrunkProvisioning, process_id: UUIDstr,
}
@step("NextBox integration")
def reserve_interfaces_in_netbox(subscription: IptrunkProvisioning) -> State:
"""Create the LAG interfaces in NetBox and attach the lag interfaces to the physical interfaces."""
nbclient = NetBoxClient()
for side in range(0, 2):
if subscription.iptrunk.iptrunk_sides[side].iptrunk_side_node.router_vendor == RouterVendor.NOKIA:
# Create LAG interfaces
lag_interface: Interfaces = nbclient.create_interface(
iface_name=subscription.iptrunk.iptrunk_sides[side].iptrunk_side_ae_iface,
type="lag",
device_name=subscription.iptrunk.iptrunk_sides[side].iptrunk_side_node.router_fqdn,
description=str(subscription.subscription_id),
enabled=True,
)
# 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:
nbclient.attach_interface_to_lag(
device_name=subscription.iptrunk.iptrunk_sides[side].iptrunk_side_node.router_fqdn,
lag_name=lag_interface.name,
iface_name=interface,
description=str(subscription.subscription_id),
)
nbclient.reserve_interface(
device_name=subscription.iptrunk.iptrunk_sides[side].iptrunk_side_node.router_fqdn,
iface_name=interface,
)
return {
"subscription": subscription,
"label_text": "NextBox integration: Reserved interfaces.",
}
@step("Allocate interfaces in Netbox")
def allocate_interfaces_in_netbox(subscription: IptrunkProvisioning) -> State:
"""Allocate the LAG interfaces in NetBox and attach the lag interfaces to the physical interfaces."""
for side in range(0, 2):
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:
NetBoxClient().allocate_interface(
device_name=subscription.iptrunk.iptrunk_sides[side].iptrunk_side_node.router_fqdn,
iface_name=interface,
)
return {
"subscription": subscription,
"label_text": "NextBox integration: Allocated interfaces.",
}
@workflow(
"Create IP trunk",
initial_input_form=wrap_create_initial_input_form(initial_input_form_generator),
......@@ -217,12 +339,14 @@ def create_iptrunk() -> StepList:
>> store_process_subscription(Target.CREATE)
>> initialize_subscription
>> get_info_from_ipam
>> reserve_interfaces_in_netbox
>> pp_interaction(provision_ip_trunk_iface_dry, 3)
>> pp_interaction(provision_ip_trunk_iface_real, 3)
>> pp_interaction(check_ip_trunk_connectivity, 2, False)
>> pp_interaction(provision_ip_trunk_isis_iface_dry, 3)
>> pp_interaction(provision_ip_trunk_isis_iface_real, 3)
>> pp_interaction(check_ip_trunk_isis, 2, False)
>> allocate_interfaces_in_netbox
>> set_status(SubscriptionLifecycle.ACTIVE)
>> resync
>> done
......
......@@ -157,9 +157,10 @@ def provision_router_real(subscription: RouterProvisioning, process_id: UUIDstr,
@step("Create NetBox Device")
def create_netbox_device(subscription: RouterProvisioning) -> State:
NetBoxClient().create_device(
subscription.router.router_fqdn, subscription.router.router_site.site_tier # type: ignore
)
if subscription.router.router_vendor == RouterVendor.NOKIA:
NetBoxClient().create_device(
subscription.router.router_fqdn, subscription.router.router_site.site_tier # type: ignore
)
return {"subscription": subscription}
......
......@@ -9,6 +9,7 @@ from orchestrator.workflow import StepList, conditional, done, init, step, workf
from orchestrator.workflows.steps import resync, set_status, store_process_subscription, unsync
from orchestrator.workflows.utils import wrap_modify_initial_input_form
from gso.products.product_blocks.router import RouterVendor
from gso.products.product_types.router import Router
from gso.services import infoblox
from gso.services.netbox_client import NetBoxClient
......@@ -61,7 +62,8 @@ def remove_config_from_router() -> None:
@step("Remove Device from NetBox")
def remove_device_from_netbox(subscription: Router) -> dict[str, Router]:
NetBoxClient().delete_device(subscription.router.router_fqdn)
if subscription.router.router_vendor == RouterVendor.NOKIA:
NetBoxClient().delete_device(subscription.router.router_fqdn)
return {"subscription": subscription}
......
import re
from ipaddress import IPv4Address
from uuid import UUID
from orchestrator.forms.validators import Choice
from gso.products.product_blocks.router import RouterVendor
from gso.products.product_types.router import Router
from gso.services.crm import all_customers
from gso.services.netbox_client import NetBoxClient
def customer_selector() -> Choice:
......@@ -14,6 +18,48 @@ def customer_selector() -> Choice:
return Choice("Select a customer", zip(customers.keys(), customers.items())) # type: ignore[arg-type]
def available_interfaces_choices(router_id: UUID, speed: str) -> Choice | None:
"""Return a list of available interfaces for a given router and speed.
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
interfaces = {
interface["name"]: f"{interface['name']} - {interface['module']['display']} - {interface['description']}"
for interface in NetBoxClient().get_available_interfaces(router_id, speed)
}
return Choice("ae member", zip(interfaces.keys(), interfaces.items())) # type: ignore[arg-type]
def available_lags_choices(router_id: UUID) -> Choice | None:
"""Return a list of available lags for a given router.
For Nokia routers, return a list of available lags.
For Juniper routers, return a string.
"""
if Router.from_subscription(router_id).router.router_vendor != RouterVendor.NOKIA:
return None
side_a_ae_iface_list = NetBoxClient().get_available_lags(router_id)
return Choice("ae iface", zip(side_a_ae_iface_list, side_a_ae_iface_list)) # type: ignore[arg-type]
def get_router_vendor(router_id: UUID) -> str:
"""Retrieve the vendor of a router.
Args:
----
router_id (UUID): The {term}`UUID` of the router.
Returns:
-------
str: The vendor of the router.
"""
return Router.from_subscription(router_id).router.router_vendor
def iso_from_ipv4(ipv4_address: IPv4Address) -> str:
"""Calculate an :term:`ISO` address, based on an IPv4 address.
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment