From f86c1fc57f9b7e46e2966852bc330fc7bae67dd2 Mon Sep 17 00:00:00 2001
From: Karel van Klink <karel.vanklink@geant.org>
Date: Tue, 17 Oct 2023 13:23:21 +0200
Subject: [PATCH] integrate the iptrunk model rework and netbox functionality

---
 gso/utils/helpers.py                          |  9 --
 gso/workflows/iptrunk/create_iptrunk.py       | 98 +++++++++++--------
 gso/workflows/router/create_router.py         |  3 +-
 test/workflows/iptrunk/test_create_iptrunk.py | 26 ++---
 test/workflows/router/test_create_router.py   |  6 +-
 .../workflows/router/test_terminate_router.py |  2 +-
 6 files changed, 75 insertions(+), 69 deletions(-)

diff --git a/gso/utils/helpers.py b/gso/utils/helpers.py
index cd461b81..32d891ca 100644
--- a/gso/utils/helpers.py
+++ b/gso/utils/helpers.py
@@ -7,18 +7,9 @@ from orchestrator.types import UUIDstr
 
 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:
-    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:
     """Return a list of available interfaces for a given router and speed.
 
diff --git a/gso/workflows/iptrunk/create_iptrunk.py b/gso/workflows/iptrunk/create_iptrunk.py
index f1ee2cce..cd56d9da 100644
--- a/gso/workflows/iptrunk/create_iptrunk.py
+++ b/gso/workflows/iptrunk/create_iptrunk.py
@@ -1,7 +1,7 @@
 from uuid import uuid4
 
 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.types import FormGenerator, State, SubscriptionLifecycle, UUIDstr
 from orchestrator.workflow import StepList, done, init, step, workflow
@@ -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.router import Router
 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.netbox_client import NetboxClient, NotFoundError
 from gso.services.provisioning_proxy import pp_interaction
-from gso.workflows.utils import (
+from gso.utils.helpers import (
     available_interfaces_choices,
     available_lags_choices,
-    customer_selector,
     get_router_vendor,
     validate_router_in_netbox,
 )
-from gso.utils.types.phy_port import PhyPortCapacity
 from gso.workflows.iptrunk.utils import LAGMember
 
 
@@ -53,84 +51,100 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
 
     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 Config:
             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)
-        def validate_device_exists_in_netbox(cls, iptrunk_sideA_node_id: UUIDstr) -> str | None:
-            return validate_router_in_netbox(iptrunk_sideA_node_id)
+        @validator("side_a_node_id", allow_reuse=True)
+        def validate_device_exists_in_netbox(cls, side_a_node_id: UUIDstr) -> str | None:
+            return validate_router_in_netbox(side_a_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
+    router_a = user_input_router_side_a.side_a_node_id.name
 
-    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
+    if get_router_vendor(router_a) == RouterVendor.NOKIA:
+        available_interfaces = available_interfaces_choices(router_a, initial_user_input.iptrunk_speed)
+        if available_interfaces is None:
+            raise NotFoundError(f"Router {router_a} could not be found in Netbox.")
 
-    class JuniperAeMembers(UniqueConstrainedList[LAGMember]):
-        min_items = initial_user_input.iptrunk_minimum_links
-        unique_items = True
+        class NokiaLAGMember(LAGMember):
+            interface_name: Choice = available_interfaces  # type: ignore[assignment]
 
-    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]):
-        min_items = initial_user_input.iptrunk_minimum_links
+        class NokiaAeMembersA(UniqueConstrainedList[NokiaLAGMember]):
+            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 Config:
             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_members: ae_members_side_a  # type: ignore[valid-type]
-        side_a_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.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]
 
     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]
+        side_b_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 | None:
-            return validate_router_in_netbox(iptrunk_sideB_node_id)
+        @validator("side_b_node_id", allow_reuse=True)
+        def validate_device_exists_in_netbox(cls, side_b_node_id: UUIDstr) -> str | None:
+            return validate_router_in_netbox(side_b_node_id)
 
     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
+    router_b = user_input_router_side_b.side_b_node_id.name
+
+    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):
-        min_items = len(user_input_side_a.side_a_ae_members)
-        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 = NokiaAeMembersB
+    else:
 
-    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]):
-        min_items = len(user_input_side_a.side_a_ae_members)
-        max_items = len(user_input_side_a.side_a_ae_members)
+        ae_members_side_b = JuniperAeMembersB  # type: ignore[assignment]
 
     class CreateIptrunkSideBForm(FormPage):
         class Config:
             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_members: ae_members_side_b  # type: ignore[valid-type]
-        side_b_ae_members_descriptions: AeMembersDescriptionListB
 
     user_input_side_b = yield CreateIptrunkSideBForm
 
diff --git a/gso/workflows/router/create_router.py b/gso/workflows/router/create_router.py
index d559d245..79311e47 100644
--- a/gso/workflows/router/create_router.py
+++ b/gso/workflows/router/create_router.py
@@ -159,7 +159,8 @@ def provision_router_real(subscription: RouterProvisioning, process_id: UUIDstr,
 def create_netbox_device(subscription: RouterProvisioning) -> State:
     if subscription.router.router_vendor == RouterVendor.NOKIA:
         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": "Skipping NetBox device creation for Juniper router."}
diff --git a/test/workflows/iptrunk/test_create_iptrunk.py b/test/workflows/iptrunk/test_create_iptrunk.py
index 479fbb91..4f40c640 100644
--- a/test/workflows/iptrunk/test_create_iptrunk.py
+++ b/test/workflows/iptrunk/test_create_iptrunk.py
@@ -7,6 +7,7 @@ from gso.products import Iptrunk, ProductType
 from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity
 from gso.services.crm import customer_selector, get_customer_by_name
 from gso.services.subscriptions import get_product_id_by_name
+from gso.workflows.iptrunk.utils import LAGMember
 from test.workflows import (
     assert_aborted,
     assert_complete,
@@ -83,31 +84,30 @@ def input_form_wizard_data(router_subscription_factory, faker):
     router_side_b = router_subscription_factory()
 
     create_ip_trunk_step = {
-        "tt_number": faker.pystr(),
+        "tt_number": faker.tt_number(),
         "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_description": faker.sentence(),
         "iptrunk_speed": PhyPortCapacity.HUNDRED_GIGABIT_PER_SECOND,
         "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 = {
-        "side_a_node_id": router_side_a,
-        "side_a_ae_iface": faker.pystr(),
-        "side_a_ae_geant_a_sid": faker.pystr(),
+        "side_a_ae_iface": "LAG1",
+        "side_a_ae_geant_a_sid": faker.geant_sid(),
         "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 = {"iptrunk_sideB_node_id": router_side_b}
+    create_ip_trunk_side_b_router_name = {"side_b_node_id": router_side_b}
     create_ip_trunk_side_b_step = {
-        "side_b_node_id": router_side_b,
-        "side_b_ae_iface": faker.pystr(),
-        "side_b_ae_geant_a_sid": faker.pystr(),
+        "side_b_ae_iface": "LAG4",
+        "side_b_ae_geant_a_sid": faker.geant_sid(),
         "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)
         ],
     }
 
diff --git a/test/workflows/router/test_create_router.py b/test/workflows/router/test_create_router.py
index 7a8f65c5..5547dc65 100644
--- a/test/workflows/router/test_create_router.py
+++ b/test/workflows/router/test_create_router.py
@@ -29,7 +29,7 @@ def router_creation_input_form_data(site_subscription_factory, faker):
         "router_site": router_site,
         "hostname": faker.pystr(),
         "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],
         "is_ias_connected": True,
     }
@@ -37,7 +37,7 @@ def router_creation_input_form_data(site_subscription_factory, faker):
 
 @pytest.mark.workflow
 @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.find_network_by_cidr")
 @patch("gso.workflows.router.create_router.infoblox.find_host_by_fqdn")
@@ -134,7 +134,7 @@ def test_create_router_success(
 
 @pytest.mark.workflow
 @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.find_network_by_cidr")
 @patch("gso.workflows.router.create_router.infoblox.find_host_by_fqdn")
diff --git a/test/workflows/router/test_terminate_router.py b/test/workflows/router/test_terminate_router.py
index f5e88ca7..e580316b 100644
--- a/test/workflows/router/test_terminate_router.py
+++ b/test/workflows/router/test_terminate_router.py
@@ -12,7 +12,7 @@ def router_termination_input_form_data(site_subscription_factory, faker):
 
 
 @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_network")
 def test_terminate_router_success(
-- 
GitLab