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

integrate the iptrunk model rework and netbox functionality

parent 93683d92
No related branches found
No related tags found
1 merge request!83Clean up the repo a bit, and add some unit tests
...@@ -7,18 +7,9 @@ from orchestrator.types import UUIDstr ...@@ -7,18 +7,9 @@ from orchestrator.types import UUIDstr
from gso.products.product_blocks.router import RouterVendor from gso.products.product_blocks.router import RouterVendor
from gso.products.product_types.router import Router from gso.products.product_types.router import Router
from gso.services.crm import all_customers
from gso.services.netbox_client import NetboxClient from gso.services.netbox_client import NetboxClient
def customer_selector() -> Choice:
customers = {}
for customer in all_customers():
customers[customer["id"]] = customer["name"]
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: def available_interfaces_choices(router_id: UUID, speed: str) -> Choice | None:
"""Return a list of available interfaces for a given router and speed. """Return a list of available interfaces for a given router and speed.
......
from uuid import uuid4 from uuid import uuid4
from orchestrator.forms import FormPage from orchestrator.forms import FormPage
from orchestrator.forms.validators import Choice, ChoiceList, UniqueConstrainedList from orchestrator.forms.validators import Choice, UniqueConstrainedList
from orchestrator.targets import Target from orchestrator.targets import Target
from orchestrator.types import FormGenerator, State, SubscriptionLifecycle, UUIDstr from orchestrator.types import FormGenerator, State, SubscriptionLifecycle, UUIDstr
from orchestrator.workflow import StepList, done, init, step, workflow from orchestrator.workflow import StepList, done, init, step, workflow
...@@ -15,17 +15,15 @@ from gso.products.product_blocks.router import RouterVendor ...@@ -15,17 +15,15 @@ from gso.products.product_blocks.router import RouterVendor
from gso.products.product_types.iptrunk import IptrunkInactive, IptrunkProvisioning from gso.products.product_types.iptrunk import IptrunkInactive, IptrunkProvisioning
from gso.products.product_types.router import Router from gso.products.product_types.router import Router
from gso.services import infoblox, provisioning_proxy, subscriptions from gso.services import infoblox, provisioning_proxy, subscriptions
from gso.services.netbox_client import NetboxClient
from gso.services.crm import customer_selector from gso.services.crm import customer_selector
from gso.services.netbox_client import NetboxClient, NotFoundError
from gso.services.provisioning_proxy import pp_interaction from gso.services.provisioning_proxy import pp_interaction
from gso.workflows.utils import ( from gso.utils.helpers import (
available_interfaces_choices, available_interfaces_choices,
available_lags_choices, available_lags_choices,
customer_selector,
get_router_vendor, get_router_vendor,
validate_router_in_netbox, validate_router_in_netbox,
) )
from gso.utils.types.phy_port import PhyPortCapacity
from gso.workflows.iptrunk.utils import LAGMember from gso.workflows.iptrunk.utils import LAGMember
...@@ -53,84 +51,100 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: ...@@ -53,84 +51,100 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
initial_user_input = yield CreateIptrunkForm initial_user_input = yield CreateIptrunkForm
router_enum_a = Choice("Select a router", zip(routers.keys(), routers.items())) # type: ignore router_enum_a = Choice("Select a router", zip(routers.keys(), routers.items())) # type: ignore[arg-type]
class SelectRouterSideA(FormPage): class SelectRouterSideA(FormPage):
class Config: class Config:
title = "Select a router for side A of the trunk." title = "Select a router for side A of the trunk."
iptrunk_sideA_node_id: router_enum_a # type: ignore[valid-type] side_a_node_id: router_enum_a # type: ignore[valid-type]
@validator("iptrunk_sideA_node_id", allow_reuse=True) @validator("side_a_node_id", allow_reuse=True)
def validate_device_exists_in_netbox(cls, iptrunk_sideA_node_id: UUIDstr) -> str | None: def validate_device_exists_in_netbox(cls, side_a_node_id: UUIDstr) -> str | None:
return validate_router_in_netbox(iptrunk_sideA_node_id) return validate_router_in_netbox(side_a_node_id)
user_input_router_side_a = yield SelectRouterSideA user_input_router_side_a = yield SelectRouterSideA
router_a = user_input_router_side_a.iptrunk_sideA_node_id.name router_a = user_input_router_side_a.side_a_node_id.name
side_a_ae_iface = available_lags_choices(router_a) or str
class AeMembersListA(ChoiceList): if get_router_vendor(router_a) == RouterVendor.NOKIA:
min_items = initial_user_input.iptrunk_minimum_links available_interfaces = available_interfaces_choices(router_a, initial_user_input.iptrunk_speed)
item_type = available_interfaces_choices(router_a, initial_user_input.iptrunk_speed) # type: ignore if available_interfaces is None:
unique_items = True raise NotFoundError(f"Router {router_a} could not be found in Netbox.")
class JuniperAeMembers(UniqueConstrainedList[LAGMember]): class NokiaLAGMember(LAGMember):
min_items = initial_user_input.iptrunk_minimum_links interface_name: Choice = available_interfaces # type: ignore[assignment]
unique_items = True
ae_members_side_a = AeMembersListA if get_router_vendor(router_a) == RouterVendor.NOKIA else JuniperAeMembers def __hash__(self) -> int:
return hash((self.interface_name, self.interface_description))
class AeMembersDescriptionListA(UniqueConstrainedList[str]): class NokiaAeMembersA(UniqueConstrainedList[NokiaLAGMember]):
min_items = initial_user_input.iptrunk_minimum_links min_items = initial_user_input.iptrunk_minimum_links
ae_members_side_a = NokiaAeMembersA
else:
class JuniperAeMembersA(UniqueConstrainedList[LAGMember]):
min_items = initial_user_input.iptrunk_minimum_links
ae_members_side_a = JuniperAeMembersA # type: ignore[assignment]
class CreateIptrunkSideAForm(FormPage): class CreateIptrunkSideAForm(FormPage):
class Config: class Config:
title = "Provide subscription details for side A of the trunk." title = "Provide subscription details for side A of the trunk."
side_a_ae_iface: side_a_ae_iface # type: ignore[valid-type] side_a_ae_iface: available_lags_choices(router_a) or str # type: ignore[valid-type]
side_a_ae_geant_a_sid: str side_a_ae_geant_a_sid: str
side_a_ae_members: ae_members_side_a # type: ignore[valid-type] side_a_ae_members: ae_members_side_a # type: ignore[valid-type]
side_a_ae_members_descriptions: AeMembersDescriptionListA
user_input_side_a = yield CreateIptrunkSideAForm user_input_side_a = yield CreateIptrunkSideAForm
# Remove the selected router for side A, to prevent any loops # Remove the selected router for side A, to prevent any loops
routers.pop(str(user_input_side_a.side_a_node_id.name)) routers.pop(str(router_a))
router_enum_b = Choice("Select a router", zip(routers.keys(), routers.items())) # type: ignore[arg-type] router_enum_b = Choice("Select a router", zip(routers.keys(), routers.items())) # type: ignore[arg-type]
class SelectRouterSideB(FormPage): class SelectRouterSideB(FormPage):
class Config: class Config:
title = "Select a router for side B of the trunk." title = "Select a router for side B of the trunk."
iptrunk_sideB_node_id: router_enum_b # type: ignore[valid-type] side_b_node_id: router_enum_b # type: ignore[valid-type]
@validator("iptrunk_sideB_node_id", allow_reuse=True) @validator("side_b_node_id", allow_reuse=True)
def validate_device_exists_in_netbox(cls, iptrunk_sideB_node_id: UUIDstr) -> str | None: def validate_device_exists_in_netbox(cls, side_b_node_id: UUIDstr) -> str | None:
return validate_router_in_netbox(iptrunk_sideB_node_id) return validate_router_in_netbox(side_b_node_id)
user_input_router_side_b = yield SelectRouterSideB user_input_router_side_b = yield SelectRouterSideB
router_b = user_input_router_side_b.iptrunk_sideB_node_id.name router_b = user_input_router_side_b.side_b_node_id.name
side_b_ae_iface = available_lags_choices(router_b) or str
if get_router_vendor(router_b) == RouterVendor.NOKIA:
available_interfaces = available_interfaces_choices(router_b, initial_user_input.iptrunk_speed)
if available_interfaces is None:
raise NotFoundError(f"Router {router_b} could not be found in Netbox.")
class NokiaLAGMember(LAGMember): # type: ignore[no-redef]
interface_name: Choice = available_interfaces # type: ignore[assignment]
def __hash__(self) -> int:
return hash((self.interface_name, self.interface_description))
class NokiaAeMembersB(UniqueConstrainedList):
min_items = len(user_input_side_a.side_a_ae_members)
max_items = len(user_input_side_a.side_a_ae_members)
item_type = NokiaLAGMember
class AeMembersListB(ChoiceList): ae_members_side_b = NokiaAeMembersB
min_items = len(user_input_side_a.side_a_ae_members) else:
max_items = len(user_input_side_a.side_a_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 JuniperAeMembersB(UniqueConstrainedList[LAGMember]):
min_items = len(user_input_side_a.side_a_ae_members)
class AeMembersDescriptionListB(UniqueConstrainedList[LAGMember]): ae_members_side_b = JuniperAeMembersB # type: ignore[assignment]
min_items = len(user_input_side_a.side_a_ae_members)
max_items = len(user_input_side_a.side_a_ae_members)
class CreateIptrunkSideBForm(FormPage): class CreateIptrunkSideBForm(FormPage):
class Config: class Config:
title = "Provide subscription details for side B of the trunk." title = "Provide subscription details for side B of the trunk."
side_b_ae_iface: side_b_ae_iface # type: ignore[valid-type] side_b_ae_iface: available_lags_choices(router_b) or str # type: ignore[valid-type]
side_b_ae_geant_a_sid: str side_b_ae_geant_a_sid: str
side_b_ae_members: ae_members_side_b # type: ignore[valid-type] side_b_ae_members: ae_members_side_b # type: ignore[valid-type]
side_b_ae_members_descriptions: AeMembersDescriptionListB
user_input_side_b = yield CreateIptrunkSideBForm user_input_side_b = yield CreateIptrunkSideBForm
......
...@@ -159,7 +159,8 @@ def provision_router_real(subscription: RouterProvisioning, process_id: UUIDstr, ...@@ -159,7 +159,8 @@ def provision_router_real(subscription: RouterProvisioning, process_id: UUIDstr,
def create_netbox_device(subscription: RouterProvisioning) -> State: def create_netbox_device(subscription: RouterProvisioning) -> State:
if subscription.router.router_vendor == RouterVendor.NOKIA: if subscription.router.router_vendor == RouterVendor.NOKIA:
NetboxClient().create_device( NetboxClient().create_device(
subscription.router.router_fqdn, subscription.router.router_site.site_tier # type: ignore[arg-type, union-attr] subscription.router.router_fqdn,
str(subscription.router.router_site.site_tier), # type: ignore[union-attr]
) )
return {"subscription": subscription, "label_text": "Creating NetBox device"} return {"subscription": subscription, "label_text": "Creating NetBox device"}
return {"subscription": subscription, "label_text": "Skipping NetBox device creation for Juniper router."} return {"subscription": subscription, "label_text": "Skipping NetBox device creation for Juniper router."}
......
...@@ -7,6 +7,7 @@ from gso.products import Iptrunk, ProductType ...@@ -7,6 +7,7 @@ from gso.products import Iptrunk, ProductType
from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity
from gso.services.crm import customer_selector, get_customer_by_name from gso.services.crm import customer_selector, get_customer_by_name
from gso.services.subscriptions import get_product_id_by_name from gso.services.subscriptions import get_product_id_by_name
from gso.workflows.iptrunk.utils import LAGMember
from test.workflows import ( from test.workflows import (
assert_aborted, assert_aborted,
assert_complete, assert_complete,
...@@ -83,31 +84,30 @@ def input_form_wizard_data(router_subscription_factory, faker): ...@@ -83,31 +84,30 @@ def input_form_wizard_data(router_subscription_factory, faker):
router_side_b = router_subscription_factory() router_side_b = router_subscription_factory()
create_ip_trunk_step = { create_ip_trunk_step = {
"tt_number": faker.pystr(), "tt_number": faker.tt_number(),
"customer": getattr(customer_selector(), get_customer_by_name("GÉANT")["id"]), "customer": getattr(customer_selector(), get_customer_by_name("GÉANT")["id"]),
"geant_s_sid": faker.pystr(), "geant_s_sid": faker.geant_sid(),
"iptrunk_type": IptrunkType.DARK_FIBER, "iptrunk_type": IptrunkType.DARK_FIBER,
"iptrunk_description": faker.sentence(), "iptrunk_description": faker.sentence(),
"iptrunk_speed": PhyPortCapacity.HUNDRED_GIGABIT_PER_SECOND, "iptrunk_speed": PhyPortCapacity.HUNDRED_GIGABIT_PER_SECOND,
"iptrunk_minimum_links": 2, "iptrunk_minimum_links": 2,
} }
create_ip_trunk_side_a_router_name = {"iptrunk_sideA_node_id": router_side_a} create_ip_trunk_side_a_router_name = {"side_a_node_id": router_side_a}
create_ip_trunk_side_a_step = { create_ip_trunk_side_a_step = {
"side_a_node_id": router_side_a, "side_a_ae_iface": "LAG1",
"side_a_ae_iface": faker.pystr(), "side_a_ae_geant_a_sid": faker.geant_sid(),
"side_a_ae_geant_a_sid": faker.pystr(),
"side_a_ae_members": [ "side_a_ae_members": [
{"interface_name": faker.network_interface(), "interface_description": faker.sentence()} for _ in range(5) LAGMember(interface_name=faker.network_interface(), interface_description=faker.sentence())
for _ in range(5)
], ],
} }
create_ip_trunk_side_b_router_name = {"side_b_node_id": router_side_b}
create_ip_trunk_side_b_router_name = {"iptrunk_sideB_node_id": router_side_b}
create_ip_trunk_side_b_step = { create_ip_trunk_side_b_step = {
"side_b_node_id": router_side_b, "side_b_ae_iface": "LAG4",
"side_b_ae_iface": faker.pystr(), "side_b_ae_geant_a_sid": faker.geant_sid(),
"side_b_ae_geant_a_sid": faker.pystr(),
"side_b_ae_members": [ "side_b_ae_members": [
{"interface_name": faker.network_interface(), "interface_description": faker.sentence()} for _ in range(5) LAGMember(interface_name=faker.network_interface(), interface_description=faker.sentence())
for _ in range(5)
], ],
} }
......
...@@ -29,7 +29,7 @@ def router_creation_input_form_data(site_subscription_factory, faker): ...@@ -29,7 +29,7 @@ def router_creation_input_form_data(site_subscription_factory, faker):
"router_site": router_site, "router_site": router_site,
"hostname": faker.pystr(), "hostname": faker.pystr(),
"ts_port": faker.pyint(), "ts_port": faker.pyint(),
"router_vendor": faker.random_choices(elements=(RouterVendor.NOKIA, RouterVendor.JUNIPER), length=1)[0], "router_vendor": RouterVendor.NOKIA,
"router_role": faker.random_choices(elements=(RouterRole.P, RouterRole.PE, RouterRole.AMT), length=1)[0], "router_role": faker.random_choices(elements=(RouterRole.P, RouterRole.PE, RouterRole.AMT), length=1)[0],
"is_ias_connected": True, "is_ias_connected": True,
} }
...@@ -37,7 +37,7 @@ def router_creation_input_form_data(site_subscription_factory, faker): ...@@ -37,7 +37,7 @@ def router_creation_input_form_data(site_subscription_factory, faker):
@pytest.mark.workflow @pytest.mark.workflow
@patch("gso.workflows.router.create_router.provisioning_proxy.provision_router") @patch("gso.workflows.router.create_router.provisioning_proxy.provision_router")
@patch("gso.workflows.router.create_router.NetBoxClient.create_device") @patch("gso.workflows.router.create_router.NetboxClient.create_device")
@patch("gso.workflows.router.create_router.infoblox.hostname_available") @patch("gso.workflows.router.create_router.infoblox.hostname_available")
@patch("gso.workflows.router.create_router.infoblox.find_network_by_cidr") @patch("gso.workflows.router.create_router.infoblox.find_network_by_cidr")
@patch("gso.workflows.router.create_router.infoblox.find_host_by_fqdn") @patch("gso.workflows.router.create_router.infoblox.find_host_by_fqdn")
...@@ -134,7 +134,7 @@ def test_create_router_success( ...@@ -134,7 +134,7 @@ def test_create_router_success(
@pytest.mark.workflow @pytest.mark.workflow
@patch("gso.workflows.router.create_router.provisioning_proxy.provision_router") @patch("gso.workflows.router.create_router.provisioning_proxy.provision_router")
@patch("gso.workflows.router.create_router.NetBoxClient.create_device") @patch("gso.workflows.router.create_router.NetboxClient.create_device")
@patch("gso.workflows.router.create_router.infoblox.hostname_available") @patch("gso.workflows.router.create_router.infoblox.hostname_available")
@patch("gso.workflows.router.create_router.infoblox.find_network_by_cidr") @patch("gso.workflows.router.create_router.infoblox.find_network_by_cidr")
@patch("gso.workflows.router.create_router.infoblox.find_host_by_fqdn") @patch("gso.workflows.router.create_router.infoblox.find_host_by_fqdn")
......
...@@ -12,7 +12,7 @@ def router_termination_input_form_data(site_subscription_factory, faker): ...@@ -12,7 +12,7 @@ def router_termination_input_form_data(site_subscription_factory, faker):
@pytest.mark.workflow @pytest.mark.workflow
@patch("gso.workflows.router.terminate_router.NetBoxClient.delete_device") @patch("gso.workflows.router.terminate_router.NetboxClient.delete_device")
@patch("gso.workflows.router.terminate_router.infoblox.delete_host_by_ip") @patch("gso.workflows.router.terminate_router.infoblox.delete_host_by_ip")
@patch("gso.workflows.router.terminate_router.infoblox.delete_network") @patch("gso.workflows.router.terminate_router.infoblox.delete_network")
def test_terminate_router_success( def test_terminate_router_success(
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment