From 459af11f3d259b94aad6786bf357eefd25178b77 Mon Sep 17 00:00:00 2001
From: Karel van Klink <karel.vanklink@geant.org>
Date: Thu, 12 Oct 2023 16:48:52 +0200
Subject: [PATCH] update IPtrunk product block where the list of LAG members is
 a separate product block

---
 gso/migrations/env.py                         |   6 +-
 ...0-11_394dc60d5c02_modify_ip_trunk_model.py |  86 ++++++++++++++
 gso/products/product_blocks/iptrunk.py        |  13 +-
 gso/services/netbox_client.py                 |   4 +-
 gso/utils/types/imports.py                    |  37 ++----
 gso/workflows/iptrunk/create_iptrunk.py       |  83 +++++++------
 .../iptrunk/modify_trunk_interface.py         |  66 ++++++-----
 gso/workflows/iptrunk/utils.py                |  10 ++
 gso/workflows/router/create_router.py         |   2 +-
 gso/workflows/tasks/import_iptrunk.py         |  20 ++--
 pyproject.toml                                |   2 +
 test/fixtures.py                              |  13 +-
 test/imports/test_imports.py                  | 112 +++++++-----------
 test/workflows/iptrunk/test_create_iptrunk.py |  20 ++--
 .../iptrunk/test_modify_trunk_interface.py    |  36 +-----
 15 files changed, 279 insertions(+), 231 deletions(-)
 create mode 100644 gso/migrations/versions/2023-10-11_394dc60d5c02_modify_ip_trunk_model.py

diff --git a/gso/migrations/env.py b/gso/migrations/env.py
index 2f41e241..a1f9b9fc 100644
--- a/gso/migrations/env.py
+++ b/gso/migrations/env.py
@@ -1,11 +1,11 @@
 import logging
 import os
-from alembic import context
-from sqlalchemy import engine_from_config, pool
 
 import orchestrator
+from alembic import context
 from orchestrator.db.database import BaseModel
 from orchestrator.settings import app_settings
+from sqlalchemy import engine_from_config, pool
 
 # this is the Alembic Config object, which provides
 # access to the values within the .ini file in use.
@@ -61,7 +61,7 @@ def run_migrations_online() -> None:
     # this callback is used to prevent an auto-migration from being generated
     # when there are no changes to the schema
     # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
-    def process_revision_directives(context, revision, directives):  # type: ignore
+    def process_revision_directives(context, revision, directives):  # type: ignore[no-untyped-def]
         if getattr(config.cmd_opts, "autogenerate", False):
             script = directives[0]
             if script.upgrade_ops.is_empty():
diff --git a/gso/migrations/versions/2023-10-11_394dc60d5c02_modify_ip_trunk_model.py b/gso/migrations/versions/2023-10-11_394dc60d5c02_modify_ip_trunk_model.py
new file mode 100644
index 00000000..ce76bb6d
--- /dev/null
+++ b/gso/migrations/versions/2023-10-11_394dc60d5c02_modify_ip_trunk_model.py
@@ -0,0 +1,86 @@
+"""Modify IP trunk model.
+
+Revision ID: 394dc60d5c02
+Revises: 01e42c100448
+Create Date: 2023-10-11 17:55:38.289125
+
+"""
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = '394dc60d5c02'
+down_revision = '01e42c100448'
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    conn = op.get_bind()
+    conn.execute(sa.text("""
+DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkSideBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('iptrunk_side_ae_members_description'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkSideBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('iptrunk_side_ae_members_description'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkSideBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('iptrunk_side_ae_members'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkSideBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('iptrunk_side_ae_members'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM subscription_instance_values WHERE subscription_instance_values.resource_type_id IN (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('iptrunk_side_ae_members_description', 'iptrunk_side_ae_members'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM resource_types WHERE resource_types.resource_type IN ('iptrunk_side_ae_members_description', 'iptrunk_side_ae_members')
+    """))
+    conn.execute(sa.text("""
+INSERT INTO product_blocks (name, description, tag, status) VALUES ('IptrunkInterfaceBlock', 'Interface in a LAG as part of an IP trunk', 'IPTINT', 'active') RETURNING product_blocks.product_block_id
+    """))
+    conn.execute(sa.text("""
+INSERT INTO resource_types (resource_type, description) VALUES ('interface_description', 'Description of a LAG interface') RETURNING resource_types.resource_type_id
+    """))
+    conn.execute(sa.text("""
+INSERT INTO resource_types (resource_type, description) VALUES ('interface_name', 'Interface name of a LAG member') RETURNING resource_types.resource_type_id
+    """))
+    conn.execute(sa.text("""
+INSERT INTO product_block_relations (in_use_by_id, depends_on_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkSideBlock')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock')))
+    """))
+    conn.execute(sa.text("""
+INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_description')))
+    """))
+    conn.execute(sa.text("""
+INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_name')))
+    """))
+
+
+def downgrade() -> None:
+    conn = op.get_bind()
+    conn.execute(sa.text("""
+DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_description'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_description'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_name'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_name'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM subscription_instance_values WHERE subscription_instance_values.resource_type_id IN (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_description', 'interface_name'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM resource_types WHERE resource_types.resource_type IN ('interface_description', 'interface_name')
+    """))
+    conn.execute(sa.text("""
+DELETE FROM product_block_relations WHERE product_block_relations.in_use_by_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkSideBlock')) AND product_block_relations.depends_on_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM subscription_instances WHERE subscription_instances.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock')
+    """))
diff --git a/gso/products/product_blocks/iptrunk.py b/gso/products/product_blocks/iptrunk.py
index 787648b2..4f0a4ed5 100644
--- a/gso/products/product_blocks/iptrunk.py
+++ b/gso/products/product_blocks/iptrunk.py
@@ -6,7 +6,6 @@ from typing import TypeVar
 from orchestrator.domain.base import ProductBlockModel
 from orchestrator.forms.validators import UniqueConstrainedList
 from orchestrator.types import SubscriptionLifecycle, strEnum
-from pydantic import Field
 
 from gso.products.product_blocks.router import RouterBlock, RouterBlockInactive, RouterBlockProvisioning
 from gso.utils.types.phy_port import PhyPortCapacity
@@ -20,6 +19,10 @@ class IptrunkType(strEnum):
 T = TypeVar("T", covariant=True)
 
 
+class LAGMemberList(UniqueConstrainedList[T]):  # type: ignore[type-var]
+    pass
+
+
 class IptrunkInterfaceBlockInactive(
     ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="IptrunkInterfaceBlock"
 ):
@@ -49,21 +52,21 @@ class IptrunkSideBlockInactive(
     iptrunk_side_node: RouterBlockInactive
     iptrunk_side_ae_iface: str | None = None
     iptrunk_side_ae_geant_a_sid: str | None = None
-    iptrunk_side_ae_members: list[IptrunkInterfaceBlockInactive] = Field(default_factory=list)
+    iptrunk_side_ae_members: LAGMemberList[IptrunkInterfaceBlockInactive]
 
 
 class IptrunkSideBlockProvisioning(IptrunkSideBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
     iptrunk_side_node: RouterBlockProvisioning
     iptrunk_side_ae_iface: str | None = None
     iptrunk_side_ae_geant_a_sid: str | None = None
-    iptrunk_side_ae_members: list[IptrunkInterfaceBlockProvisioning] = Field(default_factory=list)
+    iptrunk_side_ae_members: LAGMemberList[IptrunkInterfaceBlockProvisioning]
 
 
 class IptrunkSideBlock(IptrunkSideBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
     iptrunk_side_node: RouterBlock
     iptrunk_side_ae_iface: str | None = None
     iptrunk_side_ae_geant_a_sid: str | None = None
-    iptrunk_side_ae_members: list[IptrunkInterfaceBlock] = Field(default_factory=list)
+    iptrunk_side_ae_members: LAGMemberList[IptrunkInterfaceBlock]
 
 
 class IptrunkBlockInactive(
@@ -79,7 +82,6 @@ class IptrunkBlockInactive(
     iptrunk_isis_metric: int | None = None
     iptrunk_ipv4_network: ipaddress.IPv4Network | None = None
     iptrunk_ipv6_network: ipaddress.IPv6Network | None = None
-    #
     iptrunk_sides: IptrunkSides[IptrunkSideBlockInactive]
 
 
@@ -94,7 +96,6 @@ class IptrunkBlockProvisioning(IptrunkBlockInactive, lifecycle=[SubscriptionLife
     iptrunk_isis_metric: int | None = None
     iptrunk_ipv4_network: ipaddress.IPv4Network | None = None
     iptrunk_ipv6_network: ipaddress.IPv6Network | None = None
-    #
     iptrunk_sides: IptrunkSides[IptrunkSideBlockProvisioning]
 
 
diff --git a/gso/services/netbox_client.py b/gso/services/netbox_client.py
index d583448c..28774d24 100644
--- a/gso/services/netbox_client.py
+++ b/gso/services/netbox_client.py
@@ -101,7 +101,9 @@ class NetboxClient:
 
         # First get manufacturer id
         manufacturer_id = int(self.netbox.dcim.manufacturers.get(name=manufacturer).id)
-        device_type = DeviceType(**{"manufacturer": manufacturer_id, "model": model, "slug": slug})  # type: ignore
+        device_type = DeviceType(
+            **{"manufacturer": manufacturer_id, "model": model, "slug": slug}  # type: ignore[arg-type]
+        )
         return self.netbox.dcim.device_types.create(dict(device_type))
 
     def create_device_role(self, name: str, slug: str) -> DeviceRole:
diff --git a/gso/utils/types/imports.py b/gso/utils/types/imports.py
index d0ce27c0..7c13292c 100644
--- a/gso/utils/types/imports.py
+++ b/gso/utils/types/imports.py
@@ -10,6 +10,7 @@ from gso.products.product_blocks.site import SiteTier
 from gso.services import subscriptions
 from gso.services.crm import CustomerNotFoundError, get_customer_by_name
 from gso.utils.types.phy_port import PhyPortCapacity
+from gso.workflows.iptrunk.utils import LAGMember
 
 
 class ImportResponseModel(BaseModel):
@@ -54,16 +55,14 @@ class IptrunkImportModel(BaseModel):
     iptrunk_description: str
     iptrunk_speed: PhyPortCapacity
     iptrunk_minimum_links: int
-    iptrunk_sideA_node_id: str
-    iptrunk_sideA_ae_iface: str
-    iptrunk_sideA_ae_geant_a_sid: str
-    iptrunk_sideA_ae_members: list[str]
-    iptrunk_sideA_ae_members_descriptions: list[str]
-    iptrunk_sideB_node_id: str
-    iptrunk_sideB_ae_iface: str
-    iptrunk_sideB_ae_geant_a_sid: str
-    iptrunk_sideB_ae_members: list[str]
-    iptrunk_sideB_ae_members_descriptions: list[str]
+    side_a_node_id: str
+    side_a_ae_iface: str
+    side_a_ae_geant_a_sid: str
+    side_a_ae_members: list[LAGMember]
+    side_b_node_id: str
+    side_b_ae_iface: str
+    side_b_ae_geant_a_sid: str
+    side_b_ae_members: list[LAGMember]
 
     iptrunk_ipv4_network: ipaddress.IPv4Network
     iptrunk_ipv6_network: ipaddress.IPv6Network
@@ -83,14 +82,14 @@ class IptrunkImportModel(BaseModel):
 
         return value
 
-    @validator("iptrunk_sideA_node_id", "iptrunk_sideB_node_id")
+    @validator("side_a_node_id", "side_b_node_id")
     def check_if_router_side_is_available(cls, value: str) -> str:
         if value not in cls._get_active_routers():
             raise ValueError("Router not found")
 
         return value
 
-    @validator("iptrunk_sideA_ae_members", "iptrunk_sideB_ae_members")
+    @validator("side_a_ae_members", "side_b_ae_members")
     def check_side_uniqueness(cls, value: list[str]) -> list[str]:
         if len(value) != len(set(value)):
             raise ValueError("Items must be unique")
@@ -100,26 +99,16 @@ class IptrunkImportModel(BaseModel):
     @root_validator
     def check_members(cls, values: dict[str, Any]) -> dict[str, Any]:
         min_links = values["iptrunk_minimum_links"]
-        side_a_members = values.get("iptrunk_sideA_ae_members", [])
-        side_a_descriptions = values.get("iptrunk_sideA_ae_members_descriptions", [])
-        side_b_members = values.get("iptrunk_sideB_ae_members", [])
-        side_b_descriptions = values.get("iptrunk_sideB_ae_members_descriptions", [])
+        side_a_members = values.get("side_a_ae_members", {})
+        side_b_members = values.get("side_b_ae_members", {})
 
         len_a = len(side_a_members)
-        len_a_desc = len(side_a_descriptions)
         len_b = len(side_b_members)
-        len_b_desc = len(side_b_descriptions)
 
         if len_a < min_links:
             raise ValueError(f"Side A members should be at least {min_links} (iptrunk_minimum_links)")
 
-        if len_a != len_a_desc:
-            raise ValueError("Mismatch in Side A members and their descriptions")
-
         if len_a != len_b:
             raise ValueError("Mismatch between Side A and B members")
 
-        if len_a != len_b_desc:
-            raise ValueError("Mismatch in Side B members and their descriptions")
-
         return values
diff --git a/gso/workflows/iptrunk/create_iptrunk.py b/gso/workflows/iptrunk/create_iptrunk.py
index 154c89dd..8501fc8b 100644
--- a/gso/workflows/iptrunk/create_iptrunk.py
+++ b/gso/workflows/iptrunk/create_iptrunk.py
@@ -1,3 +1,5 @@
+from uuid import uuid4
+
 from orchestrator.forms import FormPage
 from orchestrator.forms.validators import Choice, ChoiceList, UniqueConstrainedList
 from orchestrator.targets import Target
@@ -8,7 +10,7 @@ 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.iptrunk import IptrunkType
+from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlockInactive, 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
@@ -24,6 +26,7 @@ from gso.workflows.utils import (
     validate_router_in_netbox,
 )
 from gso.utils.types.phy_port import PhyPortCapacity
+from gso.workflows.iptrunk.utils import LAGMember
 
 
 def initial_input_form_generator(product_name: str) -> FormGenerator:
@@ -71,7 +74,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
         item_type = available_interfaces_choices(router_a, initial_user_input.iptrunk_speed)  # type: ignore
         unique_items = True
 
-    class JuniperAeMembers(UniqueConstrainedList[str]):
+    class JuniperAeMembers(UniqueConstrainedList[LAGMember]):
         min_items = initial_user_input.iptrunk_minimum_links
         unique_items = True
 
@@ -84,15 +87,15 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
         class Config:
             title = "Provide subscription details for side A of the trunk."
 
-        iptrunk_sideA_ae_iface: side_a_ae_iface  # type: ignore[valid-type]
-        iptrunk_sideA_ae_geant_a_sid: str
-        iptrunk_sideA_ae_members: ae_members_side_a  # type: ignore[valid-type]
-        iptrunk_sideA_ae_members_descriptions: AeMembersDescriptionListA
+        side_a_ae_iface: side_a_ae_iface  # 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_router_side_a.iptrunk_sideA_node_id.name))
-    router_enum_b = Choice("Select a router", zip(routers.keys(), routers.items()))  # type: ignore
+    routers.pop(str(user_input_side_a.side_a_node_id.name))
+    router_enum_b = Choice("Select a router", zip(routers.keys(), routers.items()))  # type: ignore[arg-type]
 
     class SelectRouterSideB(FormPage):
         class Config:
@@ -109,25 +112,25 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
     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)
+        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 = 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)
+    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)
 
     class CreateIptrunkSideBForm(FormPage):
         class Config:
             title = "Provide subscription details for side B of the trunk."
 
-        iptrunk_sideB_ae_iface: side_b_ae_iface  # type: ignore[valid-type]
-        iptrunk_sideB_ae_geant_a_sid: str
-        iptrunk_sideB_ae_members: ae_members_side_b  # type: ignore[valid-type]
-        iptrunk_sideB_ae_members_descriptions: AeMembersDescriptionListB
+        side_b_ae_iface: side_b_ae_iface  # 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
 
@@ -170,35 +173,37 @@ def initialize_subscription(
     iptrunk_description: str,
     iptrunk_speed: PhyPortCapacity,
     iptrunk_minimum_links: int,
-    iptrunk_sideA_node_id: str,
-    iptrunk_sideA_ae_iface: str,
-    iptrunk_sideA_ae_geant_a_sid: str,
-    iptrunk_sideA_ae_members: list[str],
-    iptrunk_sideA_ae_members_descriptions: list[str],
-    iptrunk_sideB_node_id: str,
-    iptrunk_sideB_ae_iface: str,
-    iptrunk_sideB_ae_geant_a_sid: str,
-    iptrunk_sideB_ae_members: list[str],
-    iptrunk_sideB_ae_members_descriptions: list[str],
+    side_a_node_id: str,
+    side_a_ae_iface: str,
+    side_a_ae_geant_a_sid: str,
+    side_a_ae_members: list[dict],
+    side_b_node_id: str,
+    side_b_ae_iface: str,
+    side_b_ae_geant_a_sid: str,
+    side_b_ae_members: list[dict],
 ) -> State:
     subscription.iptrunk.geant_s_sid = geant_s_sid
     subscription.iptrunk.iptrunk_description = iptrunk_description
     subscription.iptrunk.iptrunk_type = iptrunk_type
     subscription.iptrunk.iptrunk_speed = iptrunk_speed
-    subscription.iptrunk.iptrunk_isis_metric = 9000
+    subscription.iptrunk.iptrunk_isis_metric = 90000
     subscription.iptrunk.iptrunk_minimum_links = iptrunk_minimum_links
 
-    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node = Router.from_subscription(iptrunk_sideA_node_id).router
-    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_iface = iptrunk_sideA_ae_iface
-    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_geant_a_sid = iptrunk_sideA_ae_geant_a_sid
-    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members = iptrunk_sideA_ae_members
-    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members_description = iptrunk_sideA_ae_members_descriptions
-
-    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node = Router.from_subscription(iptrunk_sideB_node_id).router
-    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_iface = iptrunk_sideB_ae_iface
-    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_geant_a_sid = iptrunk_sideB_ae_geant_a_sid
-    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members = iptrunk_sideB_ae_members
-    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members_description = iptrunk_sideB_ae_members_descriptions
+    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node = Router.from_subscription(side_a_node_id).router
+    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_iface = side_a_ae_iface
+    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_geant_a_sid = side_a_ae_geant_a_sid
+    for member in side_a_ae_members:
+        subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members.append(
+            IptrunkInterfaceBlockInactive.new(subscription_id=uuid4(), **member)
+        )
+
+    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node = Router.from_subscription(side_b_node_id).router
+    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_iface = side_b_ae_iface
+    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_geant_a_sid = side_b_ae_geant_a_sid
+    for member in side_b_ae_members:
+        subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members.append(
+            IptrunkInterfaceBlockInactive.new(subscription_id=uuid4(), **member)
+        )
 
     subscription.description = f"IP trunk, geant_s_sid:{geant_s_sid}"
     subscription = IptrunkProvisioning.from_other_lifecycle(subscription, SubscriptionLifecycle.PROVISIONING)
diff --git a/gso/workflows/iptrunk/modify_trunk_interface.py b/gso/workflows/iptrunk/modify_trunk_interface.py
index 0dcdbe96..0b2b37f4 100644
--- a/gso/workflows/iptrunk/modify_trunk_interface.py
+++ b/gso/workflows/iptrunk/modify_trunk_interface.py
@@ -1,4 +1,5 @@
 import ipaddress
+from uuid import uuid4
 
 from orchestrator.forms import FormPage, ReadOnlyField
 from orchestrator.forms.validators import UniqueConstrainedList
@@ -8,11 +9,12 @@ from orchestrator.workflow import StepList, done, init, step, workflow
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
 
-from gso.products.product_blocks.iptrunk import IptrunkType
+from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock, IptrunkType
 from gso.products.product_types.iptrunk import Iptrunk
 from gso.services import provisioning_proxy
 from gso.services.provisioning_proxy import pp_interaction
 from gso.utils.types.phy_port import PhyPortCapacity
+from gso.workflows.iptrunk.utils import LAGMember
 
 
 def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
@@ -31,38 +33,32 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
     initial_user_input = yield ModifyIptrunkForm
 
-    class AeMembersListA(UniqueConstrainedList[str]):
+    class AeMembersListA(UniqueConstrainedList[LAGMember]):
         min_items = initial_user_input.iptrunk_minimum_links
 
     class ModifyIptrunkSideAForm(FormPage):
         class Config:
             title = "Provide subscription details for side A of the trunk."
 
-        iptrunk_sideA_node: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn)
-        iptrunk_sideA_ae_iface: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_iface)
-        iptrunk_sideA_ae_geant_a_sid: str = subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_geant_a_sid
-        iptrunk_sideA_ae_members: AeMembersListA = subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members
-        iptrunk_sideA_ae_members_descriptions: AeMembersListA = subscription.iptrunk.iptrunk_sides[
-            0
-        ].iptrunk_side_ae_members_description
+        side_a_node: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn)
+        side_a_ae_iface: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_iface)
+        side_a_ae_geant_a_sid: str = subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_geant_a_sid
+        side_a_ae_members: AeMembersListA = subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members
 
     user_input_side_a = yield ModifyIptrunkSideAForm
 
-    class AeMembersListB(UniqueConstrainedList[str]):
-        min_items = len(user_input_side_a.iptrunk_sideA_ae_members)
-        max_items = len(user_input_side_a.iptrunk_sideA_ae_members)
+    class AeMembersListB(UniqueConstrainedList[LAGMember]):
+        min_items = len(user_input_side_a.side_a_ae_members)
+        max_items = len(user_input_side_a.side_a_ae_members)
 
     class ModifyIptrunkSideBForm(FormPage):
         class Config:
             title = "Provide subscription details for side B of the trunk."
 
-        iptrunk_sideB_node: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn)
-        iptrunk_sideB_ae_iface: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_iface)
-        iptrunk_sideB_ae_geant_a_sid: str = subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_geant_a_sid
-        iptrunk_sideB_ae_members: AeMembersListB = subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members
-        iptrunk_sideB_ae_members_descriptions: AeMembersListB = subscription.iptrunk.iptrunk_sides[
-            1
-        ].iptrunk_side_ae_members_description
+        side_b_node: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn)
+        side_b_ae_iface: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_iface)
+        side_b_ae_geant_a_sid: str = subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_geant_a_sid
+        side_b_ae_members: AeMembersListB = subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members
 
     user_input_side_b = yield ModifyIptrunkSideBForm
 
@@ -77,12 +73,10 @@ def modify_iptrunk_subscription(
     iptrunk_description: str,
     iptrunk_speed: PhyPortCapacity,
     iptrunk_minimum_links: int,
-    iptrunk_sideA_ae_geant_a_sid: str,
-    iptrunk_sideA_ae_members: list[str],
-    iptrunk_sideA_ae_members_descriptions: list[str],
-    iptrunk_sideB_ae_geant_a_sid: str,
-    iptrunk_sideB_ae_members: list[str],
-    iptrunk_sideB_ae_members_descriptions: list[str],
+    side_a_ae_geant_a_sid: str,
+    side_a_ae_members: list[dict],
+    side_b_ae_geant_a_sid: str,
+    side_b_ae_members: list[dict],
 ) -> State:
     subscription.iptrunk.geant_s_sid = geant_s_sid
     subscription.iptrunk.iptrunk_description = iptrunk_description
@@ -90,13 +84,21 @@ def modify_iptrunk_subscription(
     subscription.iptrunk.iptrunk_speed = iptrunk_speed
     subscription.iptrunk.iptrunk_minimum_links = iptrunk_minimum_links
 
-    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_geant_a_sid = iptrunk_sideA_ae_geant_a_sid
-    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members = iptrunk_sideA_ae_members
-    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members_description = iptrunk_sideA_ae_members_descriptions
-
-    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_geant_a_sid = iptrunk_sideB_ae_geant_a_sid
-    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members = iptrunk_sideB_ae_members
-    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members_description = iptrunk_sideB_ae_members_descriptions
+    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_geant_a_sid = side_a_ae_geant_a_sid
+    #  Flush the old list of member interfaces
+    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members.clear()
+    #  And update the list to only include the new member interfaces
+    for member in side_a_ae_members:
+        subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members.append(
+            IptrunkInterfaceBlock.new(subscription_id=uuid4(), **member)
+        )
+
+    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_geant_a_sid = side_b_ae_geant_a_sid
+    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members.clear()
+    for member in side_b_ae_members:
+        subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members.append(
+            IptrunkInterfaceBlock.new(subscription_id=uuid4(), **member)
+        )
 
     subscription.description = f"IP trunk, geant_s_sid:{geant_s_sid}"
 
diff --git a/gso/workflows/iptrunk/utils.py b/gso/workflows/iptrunk/utils.py
index 690cfc58..57c11715 100644
--- a/gso/workflows/iptrunk/utils.py
+++ b/gso/workflows/iptrunk/utils.py
@@ -1,10 +1,20 @@
 from orchestrator import step
 from orchestrator.types import State, UUIDstr
+from pydantic import BaseModel
 
 from gso.products.product_types.iptrunk import Iptrunk
 from gso.services import provisioning_proxy
 
 
+class LAGMember(BaseModel):
+    #  TODO: validate interface name
+    interface_name: str
+    interface_description: str
+
+    def __hash__(self) -> int:
+        return hash((self.interface_name, self.interface_description))
+
+
 @step("[COMMIT] Set ISIS metric to 90000")
 def set_isis_to_90000(subscription: Iptrunk, process_id: UUIDstr, tt_number: str) -> State:
     old_isis_metric = subscription.iptrunk.iptrunk_isis_metric
diff --git a/gso/workflows/router/create_router.py b/gso/workflows/router/create_router.py
index 3d19684e..48011b04 100644
--- a/gso/workflows/router/create_router.py
+++ b/gso/workflows/router/create_router.py
@@ -160,7 +160,7 @@ 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
+            subscription.router.router_fqdn, subscription.router.router_site.site_tier  # type: ignore[arg-type, 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/gso/workflows/tasks/import_iptrunk.py b/gso/workflows/tasks/import_iptrunk.py
index f3769aac..8203a905 100644
--- a/gso/workflows/tasks/import_iptrunk.py
+++ b/gso/workflows/tasks/import_iptrunk.py
@@ -15,6 +15,7 @@ from gso.services import subscriptions
 from gso.services.crm import get_customer_by_name
 from gso.utils.types.phy_port import PhyPortCapacity
 from gso.workflows.iptrunk.create_iptrunk import initialize_subscription
+from gso.workflows.iptrunk.utils import LAGMember
 
 
 def _generate_routers() -> dict[str, str]:
@@ -42,17 +43,15 @@ def initial_input_form_generator() -> FormGenerator:
         iptrunk_speed: PhyPortCapacity
         iptrunk_minimum_links: int
 
-        iptrunk_sideA_node_id: RouterEnum  # type: ignore[valid-type]
-        iptrunk_sideA_ae_iface: str
-        iptrunk_sideA_ae_geant_a_sid: str
-        iptrunk_sideA_ae_members: UniqueConstrainedList[str]
-        iptrunk_sideA_ae_members_descriptions: UniqueConstrainedList[str]
+        side_a_node_id: RouterEnum  # type: ignore[valid-type]
+        side_a_ae_iface: str
+        side_a_ae_geant_a_sid: str
+        side_a_ae_members: UniqueConstrainedList[LAGMember]
 
-        iptrunk_sideB_node_id: RouterEnum  # type: ignore[valid-type]
-        iptrunk_sideB_ae_iface: str
-        iptrunk_sideB_ae_geant_a_sid: str
-        iptrunk_sideB_ae_members: UniqueConstrainedList[str]
-        iptrunk_sideB_ae_members_descriptions: UniqueConstrainedList[str]
+        side_b_node_id: RouterEnum  # type: ignore[valid-type]
+        side_b_ae_iface: str
+        side_b_ae_geant_a_sid: str
+        side_b_ae_members: UniqueConstrainedList[LAGMember]
 
         iptrunk_ipv4_network: ipaddress.IPv4Network
         iptrunk_ipv6_network: ipaddress.IPv6Network
@@ -82,6 +81,7 @@ def update_ipam_stub_for_subscription(
 ) -> State:
     subscription.iptrunk.iptrunk_ipv4_network = iptrunk_ipv4_network
     subscription.iptrunk.iptrunk_ipv6_network = iptrunk_ipv6_network
+    subscription.iptrunk.iptrunk_ipv6_network = iptrunk_ipv6_network
 
     return {"subscription": subscription}
 
diff --git a/pyproject.toml b/pyproject.toml
index 78360777..19d22785 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -46,6 +46,8 @@ show_error_codes = true
 show_column_numbers = true
 # Suppress "note: By default the bodies of untyped functions are not checked"
 disable_error_code = "annotation-unchecked"
+# Forbid the use of a generic "type: ignore" without specifying the exact error that is ignored
+enable_error_code = "ignore-without-code"
 
 [tool.ruff]
 exclude = [
diff --git a/test/fixtures.py b/test/fixtures.py
index 3b7df108..b7e95eb1 100644
--- a/test/fixtures.py
+++ b/test/fixtures.py
@@ -6,7 +6,7 @@ from orchestrator.domain import SubscriptionModel
 from orchestrator.types import SubscriptionLifecycle, UUIDstr
 
 from gso.products import ProductType
-from gso.products.product_blocks.iptrunk import IptrunkSideBlock, IptrunkType
+from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock, IptrunkSideBlock, IptrunkType
 from gso.products.product_blocks.router import RouterRole, RouterVendor
 from gso.products.product_blocks.site import SiteTier
 from gso.products.product_types.iptrunk import IptrunkInactive
@@ -140,10 +140,13 @@ def iptrunk_side_subscription_factory(router_subscription_factory, faker):
         iptrunk_side_node = Router.from_subscription(iptrunk_side_node_id).router
         iptrunk_side_ae_iface = iptrunk_side_ae_iface or faker.pystr()
         iptrunk_side_ae_geant_a_sid = iptrunk_side_ae_geant_a_sid or faker.geant_sid()
-        iptrunk_side_ae_members = iptrunk_side_ae_members or [faker.network_interface(), faker.network_interface()]
-        iptrunk_side_ae_members_description = iptrunk_side_ae_members_description or [
-            faker.sentence(),
-            faker.sentence(),
+        iptrunk_side_ae_members = iptrunk_side_ae_members or [
+            IptrunkInterfaceBlock.new(
+                faker.uuid4(), interface_name=faker.network_interface(), interface_description=faker.sentence()
+            ),
+            IptrunkInterfaceBlock.new(
+                faker.uuid4(), interface_name=faker.network_interface(), interface_description=faker.sentence()
+            ),
         ]
 
         return IptrunkSideBlock.new(
diff --git a/test/imports/test_imports.py b/test/imports/test_imports.py
index 639ace3e..b2bc58b6 100644
--- a/test/imports/test_imports.py
+++ b/test/imports/test_imports.py
@@ -26,16 +26,18 @@ def iptrunk_data(router_subscription_factory, faker):
         "iptrunk_description": faker.sentence(),
         "iptrunk_speed": PhyPortCapacity.HUNDRED_GIGABIT_PER_SECOND,
         "iptrunk_minimum_links": 5,
-        "iptrunk_sideA_node_id": router_side_a,
-        "iptrunk_sideA_ae_iface": faker.pystr(),
-        "iptrunk_sideA_ae_geant_a_sid": faker.pystr(),
-        "iptrunk_sideA_ae_members": [faker.pystr() for _ in range(5)],
-        "iptrunk_sideA_ae_members_descriptions": [faker.sentence() for _ in range(5)],
-        "iptrunk_sideB_node_id": router_side_b,
-        "iptrunk_sideB_ae_iface": faker.pystr(),
-        "iptrunk_sideB_ae_geant_a_sid": faker.pystr(),
-        "iptrunk_sideB_ae_members": [faker.pystr() for _ in range(5)],
-        "iptrunk_sideB_ae_members_descriptions": [faker.sentence() for _ in range(5)],
+        "side_a_node_id": router_side_a,
+        "side_a_ae_iface": faker.pystr(),
+        "side_a_ae_geant_a_sid": faker.pystr(),
+        "side_a_ae_members": [
+            {"interface_name": faker.network_interface(), "interface_description": faker.sentence()} for _ in range(5)
+        ],
+        "side_b_node_id": router_side_b,
+        "side_b_ae_iface": faker.pystr(),
+        "side_b_ae_geant_a_sid": faker.pystr(),
+        "side_b_ae_members": [
+            {"interface_name": faker.network_interface(), "interface_description": faker.sentence()} for _ in range(5)
+        ],
         "iptrunk_ipv4_network": str(faker.ipv4_network()),
         "iptrunk_ipv6_network": str(faker.ipv6_network()),
     }
@@ -43,13 +45,13 @@ def iptrunk_data(router_subscription_factory, faker):
 
 @pytest.fixture
 def mock_routers(iptrunk_data):
-    first_call = [iptrunk_data["iptrunk_sideA_node_id"], iptrunk_data["iptrunk_sideB_node_id"], str(uuid4())]
+    first_call = [iptrunk_data["side_a_node_id"], iptrunk_data["side_b_node_id"], str(uuid4())]
     side_effects = [
         first_call,
         first_call,
         [
-            (iptrunk_data["iptrunk_sideA_node_id"], "iptrunk_sideA_node_id description"),
-            (iptrunk_data["iptrunk_sideB_node_id"], "iptrunk_sideB_node_id description"),
+            (iptrunk_data["side_a_node_id"], "side_a_node_id description"),
+            (iptrunk_data["side_b_node_id"], "side_b_node_id description"),
             (str(uuid4()), "random description"),
         ],
     ]
@@ -203,26 +205,40 @@ def test_import_iptrunk_invalid_router_id_side_a_and_b(mock_start_process, test_
     assert response.status_code == 422
     assert response.json() == {
         "detail": [
-            {"loc": ["body", "iptrunk_sideA_node_id"], "msg": "Router not found", "type": "value_error"},
-            {"loc": ["body", "iptrunk_sideB_node_id"], "msg": "Router not found", "type": "value_error"},
+            {"loc": ["body", "side_a_node_id"], "msg": "Router not found", "type": "value_error"},
+            {"loc": ["body", "side_b_node_id"], "msg": "Router not found", "type": "value_error"},
         ]
     }
 
 
 @patch("gso.api.v1.imports._start_process")
-def test_import_iptrunk_non_unique_members_side_a(mock_start_process, test_client, iptrunk_data, mock_routers):
+def test_import_iptrunk_non_unique_members_side_a(mock_start_process, test_client, iptrunk_data, mock_routers, faker):
     mock_start_process.return_value = "123e4567-e89b-12d3-a456-426655440000"
 
-    iptrunk_data["iptrunk_sideA_ae_members"] = [5, 5, 5, 5, 5]
-    iptrunk_data["iptrunk_sideB_ae_members"] = [4, 4, 4, 5, 5]
+    repeat_interface_a = {"interface_name": faker.network_interface(), "interface_description": faker.sentence()}
+    repeat_interface_b = {"interface_name": faker.network_interface(), "interface_description": faker.sentence()}
+    iptrunk_data["side_a_ae_members"] = [
+        repeat_interface_a,
+        repeat_interface_a,
+        repeat_interface_a,
+        repeat_interface_a,
+        repeat_interface_a,
+    ]
+    iptrunk_data["side_b_ae_members"] = [
+        repeat_interface_b,
+        repeat_interface_a,
+        repeat_interface_a,
+        repeat_interface_b,
+        repeat_interface_b,
+    ]
 
     response = test_client.post(IPTRUNK_IMPORT_API_URL, json=iptrunk_data)
 
     assert response.status_code == 422
     assert response.json() == {
         "detail": [
-            {"loc": ["body", "iptrunk_sideA_ae_members"], "msg": "Items must be unique", "type": "value_error"},
-            {"loc": ["body", "iptrunk_sideB_ae_members"], "msg": "Items must be unique", "type": "value_error"},
+            {"loc": ["body", "side_a_ae_members"], "msg": "Items must be unique", "type": "value_error"},
+            {"loc": ["body", "side_b_ae_members"], "msg": "Items must be unique", "type": "value_error"},
             {
                 "loc": ["body", "__root__"],
                 "msg": "Side A members should be at least 5 (iptrunk_minimum_links)",
@@ -233,12 +249,12 @@ def test_import_iptrunk_non_unique_members_side_a(mock_start_process, test_clien
 
 
 @patch("gso.api.v1.imports._start_process")
-def test_iptrunk_import_fails_on_side_a_member_count_mismatch(
+def test_import_iptrunk_fails_on_side_a_member_count_mismatch(
     mock_start_process, test_client, iptrunk_data, mock_routers
 ):
     mock_start_process.return_value = "123e4567-e89b-12d3-a456-426655440000"
 
-    iptrunk_data["iptrunk_sideA_ae_members"].remove(iptrunk_data["iptrunk_sideA_ae_members"][0])
+    iptrunk_data["side_a_ae_members"].remove(iptrunk_data["side_a_ae_members"][0])
 
     response = test_client.post(IPTRUNK_IMPORT_API_URL, json=iptrunk_data)
 
@@ -255,36 +271,12 @@ def test_iptrunk_import_fails_on_side_a_member_count_mismatch(
 
 
 @patch("gso.api.v1.imports._start_process")
-def test_iptrunk_import_fails_on_side_a_member_description_mismatch(
-    mock_start_process, test_client, iptrunk_data, mock_routers
-):
-    mock_start_process.return_value = "123e4567-e89b-12d3-a456-426655440000"
-
-    iptrunk_data["iptrunk_sideA_ae_members_descriptions"].remove(
-        iptrunk_data["iptrunk_sideA_ae_members_descriptions"][0]
-    )
-
-    response = test_client.post(IPTRUNK_IMPORT_API_URL, json=iptrunk_data)
-
-    assert response.status_code == 422
-    assert response.json() == {
-        "detail": [
-            {
-                "loc": ["body", "__root__"],
-                "msg": "Mismatch in Side A members and their descriptions",
-                "type": "value_error",
-            }
-        ]
-    }
-
-
-@patch("gso.api.v1.imports._start_process")
-def test_iptrunk_import_fails_on_side_a_and_b_members_mismatch(
+def test_import_iptrunk_fails_on_side_a_and_b_members_mismatch(
     mock_start_process, test_client, iptrunk_data, mock_routers
 ):
     mock_start_process.return_value = "123e4567-e89b-12d3-a456-426655440000"
 
-    iptrunk_data["iptrunk_sideB_ae_members"].remove(iptrunk_data["iptrunk_sideB_ae_members"][0])
+    iptrunk_data["side_b_ae_members"].remove(iptrunk_data["side_b_ae_members"][0])
 
     response = test_client.post(IPTRUNK_IMPORT_API_URL, json=iptrunk_data)
 
@@ -292,27 +284,3 @@ def test_iptrunk_import_fails_on_side_a_and_b_members_mismatch(
     assert response.json() == {
         "detail": [{"loc": ["body", "__root__"], "msg": "Mismatch between Side A and B members", "type": "value_error"}]
     }
-
-
-@patch("gso.api.v1.imports._start_process")
-def test_iptrunk_import_fails_on_side_b_member_description_mismatch(
-    mock_start_process, test_client, iptrunk_data, mock_routers
-):
-    mock_start_process.return_value = "123e4567-e89b-12d3-a456-426655440000"
-
-    iptrunk_data["iptrunk_sideB_ae_members_descriptions"].remove(
-        iptrunk_data["iptrunk_sideB_ae_members_descriptions"][0]
-    )
-
-    response = test_client.post(IPTRUNK_IMPORT_API_URL, json=iptrunk_data)
-
-    assert response.status_code == 422
-    assert response.json() == {
-        "detail": [
-            {
-                "loc": ["body", "__root__"],
-                "msg": "Mismatch in Side B members and their descriptions",
-                "type": "value_error",
-            }
-        ]
-    }
diff --git a/test/workflows/iptrunk/test_create_iptrunk.py b/test/workflows/iptrunk/test_create_iptrunk.py
index b9a1930e..cff8bed9 100644
--- a/test/workflows/iptrunk/test_create_iptrunk.py
+++ b/test/workflows/iptrunk/test_create_iptrunk.py
@@ -94,18 +94,22 @@ def input_form_wizard_data(router_subscription_factory, faker):
     }
     create_ip_trunk_side_a_router_name = {"iptrunk_sideA_node_id": router_side_a}
     create_ip_trunk_side_a_step = {
-        "iptrunk_sideA_ae_iface": "LAG1",
-        "iptrunk_sideA_ae_geant_a_sid": faker.pystr(),
-        "iptrunk_sideA_ae_members": ["Interface1", "Interface2"],
-        "iptrunk_sideA_ae_members_descriptions": ["Interface1 Description", "Interface2 Description"],
+        "side_a_node_id": router_side_a,
+        "side_a_ae_iface": faker.pystr(),
+        "side_a_ae_geant_a_sid": faker.pystr(),
+        "side_a_ae_members": [
+            {"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_step = {
-        "iptrunk_sideB_ae_iface": "LAG1",
-        "iptrunk_sideB_ae_geant_a_sid": faker.pystr(),
-        "iptrunk_sideB_ae_members": ["Interface1", "Interface2"],
-        "iptrunk_sideB_ae_members_descriptions": ["Interface1 Description", "Interface2 Description"],
+        "side_b_node_id": router_side_b,
+        "side_b_ae_iface": faker.pystr(),
+        "side_b_ae_geant_a_sid": faker.pystr(),
+        "side_b_ae_members": [
+            {"interface_name": faker.network_interface(), "interface_description": faker.sentence()} for _ in range(5)
+        ],
     }
 
     return [
diff --git a/test/workflows/iptrunk/test_modify_trunk_interface.py b/test/workflows/iptrunk/test_modify_trunk_interface.py
index 452e7971..cf6c39a4 100644
--- a/test/workflows/iptrunk/test_modify_trunk_interface.py
+++ b/test/workflows/iptrunk/test_modify_trunk_interface.py
@@ -32,34 +32,12 @@ def test_iptrunk_modify_trunk_interface_success(
 
     new_side_a_sid = faker.geant_sid()
     new_side_a_ae_members = [
-        faker.network_interface(),
-        faker.network_interface(),
-        faker.network_interface(),
-        faker.network_interface(),
-        faker.network_interface(),
-    ]
-    new_side_a_ae_descriptions = [
-        faker.sentence(),
-        faker.sentence(),
-        faker.sentence(),
-        faker.sentence(),
-        faker.sentence(),
+        {"interface_name": faker.network_interface(), "interface_description": faker.sentence()} for _ in range(5)
     ]
 
     new_side_b_sid = faker.geant_sid()
     new_side_b_ae_members = [
-        faker.network_interface(),
-        faker.network_interface(),
-        faker.network_interface(),
-        faker.network_interface(),
-        faker.network_interface(),
-    ]
-    new_side_b_ae_descriptions = [
-        faker.sentence(),
-        faker.sentence(),
-        faker.sentence(),
-        faker.sentence(),
-        faker.sentence(),
+        {"interface_name": faker.network_interface(), "interface_description": faker.sentence()} for _ in range(5)
     ]
 
     #  Run workflow
@@ -74,14 +52,12 @@ def test_iptrunk_modify_trunk_interface_success(
             "iptrunk_minimum_links": new_link_count,
         },
         {
-            "iptrunk_sideA_ae_geant_a_sid": new_side_a_sid,
-            "iptrunk_sideA_ae_members": new_side_a_ae_members,
-            "iptrunk_sideA_ae_members_descriptions": new_side_a_ae_descriptions,
+            "side_a_ae_geant_a_sid": new_side_a_sid,
+            "side_a_ae_members": new_side_a_ae_members,
         },
         {
-            "iptrunk_sideB_ae_geant_a_sid": new_side_b_sid,
-            "iptrunk_sideB_ae_members": new_side_b_ae_members,
-            "iptrunk_sideB_ae_members_descriptions": new_side_b_ae_descriptions,
+            "side_b_ae_geant_a_sid": new_side_b_sid,
+            "side_b_ae_members": new_side_b_ae_members,
         },
     ]
 
-- 
GitLab