From 697821a01f59430b874ef933f836ae818651163c Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Mon, 10 Mar 2025 16:00:29 +0100
Subject: [PATCH 01/87] Add IAS product and remove it from the fixed input

---
 gso/products/__init__.py                      |  5 ++-
 gso/products/product_blocks/ias.py            | 28 +++++++++++++
 gso/products/product_types/ias.py             | 42 +++++++++++++++++++
 gso/products/product_types/l3_core_service.py |  5 ---
 4 files changed, 73 insertions(+), 7 deletions(-)
 create mode 100644 gso/products/product_blocks/ias.py
 create mode 100644 gso/products/product_types/ias.py

diff --git a/gso/products/__init__.py b/gso/products/__init__.py
index 2d73ad898..f8c02b1fe 100644
--- a/gso/products/__init__.py
+++ b/gso/products/__init__.py
@@ -21,6 +21,7 @@ from gso.products.product_types.site import ImportedSite, Site
 from gso.products.product_types.super_pop_switch import ImportedSuperPopSwitch, SuperPopSwitch
 from gso.products.product_types.switch import ImportedSwitch, Switch
 from gso.products.product_types.vrf import VRF
+from gso.products.product_types.ias import IAS, ImportedIAS
 
 
 class ProductName(strEnum):
@@ -108,8 +109,6 @@ class ProductType(strEnum):
     IMPORTED_EDGE_PORT = ImportedEdgePort.__name__
     GEANT_IP = L3_CORE_SERVICE_PRODUCT_TYPE
     IMPORTED_GEANT_IP = ImportedL3CoreService.__name__
-    IAS = L3_CORE_SERVICE_PRODUCT_TYPE
-    IMPORTED_IAS = ImportedL3CoreService.__name__
     GWS = L3_CORE_SERVICE_PRODUCT_TYPE
     IMPORTED_GWS = ImportedL3CoreService.__name__
     LHCONE = L3_CORE_SERVICE_PRODUCT_TYPE
@@ -121,6 +120,8 @@ class ProductType(strEnum):
     EXPRESSROUTE = L2_CIRCUIT_PRODUCT_TYPE
     IMPORTED_EXPRESSROUTE = ImportedLayer2Circuit.__name__
     VRF = VRF.__name__
+    IAS = IAS.__name__
+    IMPORTED_IAS = ImportedIAS.__name__
 
 
 SUBSCRIPTION_MODEL_REGISTRY.update(
diff --git a/gso/products/product_blocks/ias.py b/gso/products/product_blocks/ias.py
new file mode 100644
index 000000000..abc7a2c86
--- /dev/null
+++ b/gso/products/product_blocks/ias.py
@@ -0,0 +1,28 @@
+"""Product blocks for IAS products."""
+
+from orchestrator.types import SubscriptionLifecycle
+
+from gso.products.product_blocks.l3_core_service import L3CoreServiceBlock
+
+
+class IASProductBlockInactive(L3CoreServiceBlock, lifecycle=[SubscriptionLifecycle.INITIAL],
+                              product_block_name="IASProductBlock"):
+    """An inactive IAS product block. See `IASProductBlock`."""
+
+    ias_flavor: str | None = None
+
+
+class IASProductBlockProvisioning(IASProductBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
+    """A provisioning IAS product block. See `IASProductBlock`."""
+
+    ias_flavor: str | None = None
+
+
+class IASProductBlock(IASProductBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
+    """An active IAS product block.
+
+    Attributes:
+        ap_list: The list of Access Points where this service is present.
+    """
+
+    ias_flavor: str | None = None
diff --git a/gso/products/product_types/ias.py b/gso/products/product_types/ias.py
new file mode 100644
index 000000000..5a130ff05
--- /dev/null
+++ b/gso/products/product_types/ias.py
@@ -0,0 +1,42 @@
+"""Product type for IAS."""
+
+from orchestrator.domain.base import SubscriptionModel
+from orchestrator.types import SubscriptionLifecycle
+
+from gso.products.product_blocks.ias import (
+    IASProductBlock,
+    IASProductBlockInactive,
+    IASProductBlockProvisioning,
+)
+
+
+class IASInactive(SubscriptionModel, is_base=True):
+    """An IAS product that is inactive."""
+
+    ias: IASProductBlockInactive
+
+
+class IASProvisioning(IASInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
+    """An IAS product that is being provisioned."""
+
+    ias: IASProductBlockProvisioning
+
+
+class IAS(IASProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
+    """An IAS product that is active."""
+
+    ias: IASProductBlock
+
+
+class ImportedIASInactive(SubscriptionModel, is_base=True):
+    """An imported IAS product that is inactive."""
+
+    ias: IASProductBlockInactive
+
+
+class ImportedIAS(
+    ImportedIASInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE]
+):
+    """An imported IAS product that is active."""
+
+    ias: IASProductBlock
\ No newline at end of file
diff --git a/gso/products/product_types/l3_core_service.py b/gso/products/product_types/l3_core_service.py
index 34f9beccd..b07d6bb03 100644
--- a/gso/products/product_types/l3_core_service.py
+++ b/gso/products/product_types/l3_core_service.py
@@ -20,9 +20,6 @@ class L3CoreServiceType(strEnum):
     GEANT_IP = "GÉANT IP"
     """GÉANT IP."""
     IMPORTED_GEANT_IP = "IMPORTED GÉANT IP"
-    IAS = "IAS"
-    """Internet Access Serive."""
-    IMPORTED_IAS = "IMPORTED IAS"
     GWS = "GWS"
     """GÉANT Web Services."""
     IMPORTED_GWS = "IMPORTED GWS"
@@ -36,14 +33,12 @@ class L3CoreServiceType(strEnum):
 
 L3_CORE_SERVICE_TYPES = [
     L3CoreServiceType.GEANT_IP,
-    L3CoreServiceType.IAS,
     L3CoreServiceType.GWS,
     L3CoreServiceType.LHCONE,
     L3CoreServiceType.COPERNICUS,
 ]
 IMPORTED_L3_CORE_SERVICE_TYPES = [
     L3CoreServiceType.IMPORTED_GEANT_IP,
-    L3CoreServiceType.IMPORTED_IAS,
     L3CoreServiceType.IMPORTED_GWS,
     L3CoreServiceType.IMPORTED_LHCONE,
     L3CoreServiceType.IMPORTED_COPERNICUS,
-- 
GitLab


From e670e0a964fa9554c89bef38633a6dc65f95a5d3 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 13 Mar 2025 15:29:19 +0100
Subject: [PATCH 02/87] fix IAS in SUBSCRIPTION_MODEL_REGISTRY

---
 gso/products/__init__.py           | 6 +++---
 gso/products/product_blocks/ias.py | 4 ++--
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/gso/products/__init__.py b/gso/products/__init__.py
index f8c02b1fe..ad6714523 100644
--- a/gso/products/__init__.py
+++ b/gso/products/__init__.py
@@ -9,6 +9,7 @@ from orchestrator.domain import SUBSCRIPTION_MODEL_REGISTRY
 from pydantic_forms.types import strEnum
 
 from gso.products.product_types.edge_port import EdgePort, ImportedEdgePort
+from gso.products.product_types.ias import IAS, ImportedIAS
 from gso.products.product_types.iptrunk import ImportedIptrunk, Iptrunk
 from gso.products.product_types.l3_core_service import ImportedL3CoreService, L3CoreService
 from gso.products.product_types.lan_switch_interconnect import ImportedLanSwitchInterconnect, LanSwitchInterconnect
@@ -21,7 +22,6 @@ from gso.products.product_types.site import ImportedSite, Site
 from gso.products.product_types.super_pop_switch import ImportedSuperPopSwitch, SuperPopSwitch
 from gso.products.product_types.switch import ImportedSwitch, Switch
 from gso.products.product_types.vrf import VRF
-from gso.products.product_types.ias import IAS, ImportedIAS
 
 
 class ProductName(strEnum):
@@ -147,8 +147,8 @@ SUBSCRIPTION_MODEL_REGISTRY.update(
         ProductName.IMPORTED_EDGE_PORT.value: ImportedEdgePort,
         ProductName.GEANT_IP.value: L3CoreService,
         ProductName.IMPORTED_GEANT_IP.value: ImportedL3CoreService,
-        ProductName.IAS.value: L3CoreService,
-        ProductName.IMPORTED_IAS.value: ImportedL3CoreService,
+        ProductName.IAS.value: IAS,
+        ProductName.IMPORTED_IAS.value: ImportedIAS,
         ProductName.GWS.value: L3CoreService,
         ProductName.IMPORTED_GWS.value: ImportedL3CoreService,
         ProductName.LHCONE.value: L3CoreService,
diff --git a/gso/products/product_blocks/ias.py b/gso/products/product_blocks/ias.py
index abc7a2c86..724ae4418 100644
--- a/gso/products/product_blocks/ias.py
+++ b/gso/products/product_blocks/ias.py
@@ -2,10 +2,10 @@
 
 from orchestrator.types import SubscriptionLifecycle
 
-from gso.products.product_blocks.l3_core_service import L3CoreServiceBlock
+from gso.products.product_blocks.l3_core_service import L3CoreServiceBlockInactive
 
 
-class IASProductBlockInactive(L3CoreServiceBlock, lifecycle=[SubscriptionLifecycle.INITIAL],
+class IASProductBlockInactive(L3CoreServiceBlockInactive, lifecycle=[SubscriptionLifecycle.INITIAL],
                               product_block_name="IASProductBlock"):
     """An inactive IAS product block. See `IASProductBlock`."""
 
-- 
GitLab


From 7f5b30f0b6d3fee27143bb33df5313fb29678758 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Tue, 18 Mar 2025 10:47:31 +0100
Subject: [PATCH 03/87] Generalize l3 services creation common functionality

---
 .../base_create_l3_core_service.py            | 354 ++++++++++++++++++
 gso/workflows/l3_core_service/ias/__init__.py |   1 +
 2 files changed, 355 insertions(+)
 create mode 100644 gso/workflows/l3_core_service/base_create_l3_core_service.py
 create mode 100644 gso/workflows/l3_core_service/ias/__init__.py

diff --git a/gso/workflows/l3_core_service/base_create_l3_core_service.py b/gso/workflows/l3_core_service/base_create_l3_core_service.py
new file mode 100644
index 000000000..ae76ab718
--- /dev/null
+++ b/gso/workflows/l3_core_service/base_create_l3_core_service.py
@@ -0,0 +1,354 @@
+"""Base workflow for creating a new L3 Core Service."""
+
+from typing import Any
+from uuid import uuid4
+
+from orchestrator.forms import FormPage, SubmitFormPage
+from orchestrator.forms.validators import Label
+from orchestrator.types import State
+from orchestrator.workflow import step
+from pydantic import BaseModel, ConfigDict, Field, NonNegativeInt, computed_field, model_validator
+from pydantic_forms.types import FormGenerator, UUIDstr
+from pydantic_forms.validators import Divider
+
+from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPTypes
+from gso.products.product_blocks.l3_core_service import AccessPortInactive
+from gso.products.product_blocks.service_binding_port import BFDSettings, ServiceBindingPortInactive
+from gso.products.product_types.edge_port import EdgePort
+from gso.products.product_types.ias import IAS, IASInactive
+from gso.products.product_types.l3_core_service import L3CoreService, L3CoreServiceInactive
+from gso.services.lso_client import LSOState
+from gso.services.partners import get_partner_by_id
+from gso.services.sharepoint import SharePointClient
+from gso.services.subscriptions import generate_unique_id
+from gso.settings import load_oss_params
+from gso.utils.helpers import (
+    active_edge_port_selector,
+    partner_choice,
+)
+from gso.utils.shared_enums import APType, SBPType
+from gso.utils.types.geant_ids import IMPORTED_GS_ID
+from gso.utils.types.ip_address import IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask
+from gso.utils.types.tt_number import TTNumber
+from gso.utils.types.virtual_identifiers import VLAN_ID
+
+
+def initial_input_form_generator(product_name: str) -> FormGenerator:
+    """Gather input from the operator to build a new subscription object."""
+
+    class CreateL3CoreServiceForm(FormPage):
+        model_config = ConfigDict(title=f"{product_name} - Select partner")
+
+        tt_number: TTNumber
+        partner: partner_choice()  # type: ignore[valid-type]
+
+    initial_user_input = yield CreateL3CoreServiceForm
+
+    class EdgePortSelection(BaseModel):
+        edge_port: active_edge_port_selector(partner_id=initial_user_input.partner)  # type: ignore[valid-type]
+        ap_type: APType
+        custom_service_name: str | None = None
+
+    class EdgePortSelectionForm(FormPage):
+        model_config = ConfigDict(title=f"{product_name} - Select Edge Ports")
+        info_label: Label = Field(
+            f"Please select the Edge Ports where this {product_name} service will terminate", exclude=True
+        )
+
+        edge_port: EdgePortSelection
+
+    selected_edge_port = yield EdgePortSelectionForm
+
+    class BFDSettingsForm(BaseModel):
+        bfd_enabled: bool = False
+        bfd_interval_rx: int | None = Field(default=None, examples=["BFD RX defaults"])
+        bfd_interval_tx: int | None = None
+        bfd_multiplier: int | None = None
+
+    class IPv4BGPPeer(BaseModel):
+        peer_address: IPv4AddressType
+        authentication_key: str | None = None
+        has_custom_policies: bool = False
+        bfd_enabled: bool = False
+        multipath_enabled: bool = False
+        prefix_limit: NonNegativeInt | None = None
+        is_passive: bool = False
+        add_v4_multicast: bool = Field(default=False, exclude=True)
+        send_default_route: bool = False
+
+        @computed_field  # type: ignore[prop-decorator]
+        @property
+        def families(self) -> list[IPFamily]:
+            return [IPFamily.V4UNICAST, IPFamily.V4MULTICAST] if self.add_v4_multicast else [IPFamily.V4UNICAST]
+
+        @computed_field  # type: ignore[prop-decorator]
+        @property
+        def ip_type(self) -> IPTypes:
+            return IPTypes.IPV4
+
+    class IPv6BGPPeer(BaseModel):
+        peer_address: IPv6AddressType
+        authentication_key: str | None = None
+        has_custom_policies: bool = False
+        bfd_enabled: bool = False
+        multipath_enabled: bool = False
+        prefix_limit: NonNegativeInt | None = None
+        is_passive: bool = False
+        add_v6_multicast: bool = Field(default=False, exclude=True)
+        send_default_route: bool = False
+
+        @computed_field  # type: ignore[prop-decorator]
+        @property
+        def families(self) -> list[IPFamily]:
+            return [IPFamily.V6UNICAST, IPFamily.V6MULTICAST] if self.add_v6_multicast else [IPFamily.V6UNICAST]
+
+        @computed_field  # type: ignore[prop-decorator]
+        @property
+        def ip_type(self) -> IPTypes:
+            return IPTypes.IPV6
+
+    class BindingPortInputForm(SubmitFormPage):
+        model_config = ConfigDict(title=f"{product_name} - Configure Edge Port")
+        info_label: Label = Field("Please configure the Service Binding Ports for the Edge Port.", exclude=True)
+        current_ep_label: Label = Field(
+            f"Currently configuring on {EdgePort.from_subscription(selected_edge_port.edge_port.edge_port).description}"
+            f" (Access Port type: {selected_edge_port.edge_port.ap_type})",
+            exclude=True,
+        )
+
+        generate_gs_id: bool = True
+        gs_id: IMPORTED_GS_ID | None = None
+        is_tagged: bool = False
+        vlan_id: VLAN_ID
+        custom_firewall_filters: bool = False
+        divider: Divider = Field(None, exclude=True)
+        v4_label: Label = Field("IPV4 SBP interface params", exclude=True)
+        ipv4_address: IPv4AddressType
+        ipv4_mask: IPv4Netmask
+        v4_bfd_settings: BFDSettingsForm
+        v4_bgp_peer: IPv4BGPPeer
+        divider2: Divider = Field(None, exclude=True)
+        v6_label: Label = Field("IPV6 SBP interface params", exclude=True)
+        ipv6_address: IPv6AddressType
+        ipv6_mask: IPv6Netmask
+        v6_bfd_settings: BFDSettingsForm
+        v6_bgp_peer: IPv6BGPPeer
+
+        @model_validator(mode="before")
+        def validate_gs_id(cls, input_data: dict[str, Any]) -> dict[str, Any]:
+            gs_id = input_data.get("gs_id")
+            generate_gs_id = input_data.get("generate_gs_id", True)
+
+            if generate_gs_id and gs_id:
+                error_message = (
+                    "You cannot provide a GS ID manually while the 'Auto-generate GS ID' option is enabled."
+                    "Please either uncheck 'Auto-generate GS ID' or remove the manual GS ID."
+                )
+                raise ValueError(error_message)
+            return input_data
+
+    binding_port_input_form = yield BindingPortInputForm
+    binding_port_input = binding_port_input_form.model_dump() | {
+        "bgp_peers": [
+            binding_port_input_form.v4_bgp_peer.model_dump(),
+            binding_port_input_form.v6_bgp_peer.model_dump(),
+        ]
+    }
+
+    return (
+        initial_user_input.model_dump()
+        | selected_edge_port.model_dump()
+        | {"binding_port_input": binding_port_input, "product_name": product_name}
+    )
+
+
+def initialize_service_binding(
+    subscription: IASInactive | L3CoreServiceInactive,
+    edge_port: dict,
+    binding_port_input: dict,
+    product_name: str,
+    service_type_attr: str,
+) -> dict:
+    """Initialize a service binding port for a given service type in the subscription model."""
+    edge_port_fqdn_list = []
+    edge_port_subscription = EdgePort.from_subscription(edge_port["edge_port"])
+
+    sbp_bgp_session_list = [
+        BGPSession.new(subscription_id=uuid4(), rtbh_enabled=True, is_multi_hop=True, **session)
+        for session in binding_port_input["bgp_peers"]
+    ]
+
+    sbp_gs_id = (
+        generate_unique_id(prefix="GS")
+        if binding_port_input.pop("generate_gs_id", False)
+        else binding_port_input.pop("gs_id", None)
+    )
+
+    binding_port_input.pop("gs_id", None)
+
+    service_binding_port = ServiceBindingPortInactive.new(
+        subscription_id=uuid4(),
+        v4_bfd_settings=BFDSettings.new(subscription_id=uuid4(), **(binding_port_input.pop("v4_bfd_settings"))),
+        v6_bfd_settings=BFDSettings.new(subscription_id=uuid4(), **(binding_port_input.pop("v6_bfd_settings"))),
+        **binding_port_input,
+        bgp_session_list=sbp_bgp_session_list,
+        sbp_type=SBPType.L3,  # Adjust based on your requirement
+        edge_port=edge_port_subscription.edge_port,
+        gs_id=sbp_gs_id,
+    )
+
+    service_attr = getattr(subscription, service_type_attr, None)
+    if service_attr is not None:
+        service_attr.ap_list.append(
+            AccessPortInactive.new(
+                subscription_id=uuid4(),
+                ap_type=edge_port["ap_type"],
+                sbp=service_binding_port,
+                custom_service_name=edge_port.get("custom_service_name"),
+            )
+        )
+
+    edge_port_fqdn_list.append(edge_port_subscription.edge_port.node.router_fqdn)
+
+    partner_name = get_partner_by_id(subscription.customer_id).name
+    subscription.description = f"{product_name} service for {partner_name}"
+
+    return {"subscription": subscription, "edge_port_fqdn_list": edge_port_fqdn_list, "partner_name": partner_name}
+
+
+@step("[DRY RUN] Deploy service binding port")
+def provision_sbp_dry(
+    subscription: dict[str, Any], process_id: UUIDstr, tt_number: str, edge_port_fqdn_list: list[str], partner_name: str
+) -> LSOState:
+    """Perform a dry run of deploying Service Binding Ports."""
+    extra_vars = {
+        "subscription": subscription,
+        "partner_name": partner_name,
+        "dry_run": True,
+        "verb": "deploy",
+        "object": "sbp",
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - "
+        f"Deploy config for {subscription["description"]}",
+    }
+
+    return {
+        "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml",
+        "inventory": {"all": {"hosts": dict.fromkeys(edge_port_fqdn_list)}},
+        "extra_vars": extra_vars,
+    }
+
+
+@step("[FOR REAL] Deploy service binding port")
+def provision_sbp_real(
+    subscription: dict[str, Any], process_id: UUIDstr, tt_number: str, edge_port_fqdn_list: list[str], partner_name: str
+) -> LSOState:
+    """Deploy Service Binding Ports."""
+    extra_vars = {
+        "subscription": subscription,
+        "partner_name": partner_name,
+        "dry_run": False,
+        "verb": "deploy",
+        "object": "sbp",
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - "
+        f"Deploy config for {subscription["description"]}",
+    }
+
+    return {
+        "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml",
+        "inventory": {"all": {"hosts": dict.fromkeys(edge_port_fqdn_list)}},
+        "extra_vars": extra_vars,
+    }
+
+
+@step("Check service binding port functionality")
+def check_sbp_functionality(subscription: dict[str, Any], edge_port_fqdn_list: list[str]) -> LSOState:
+    """Check functionality of deployed Service Binding Ports."""
+    extra_vars = {"subscription": subscription, "verb": "check", "object": "sbp"}
+
+    return {
+        "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml",
+        "inventory": {"all": {"hosts": dict.fromkeys(edge_port_fqdn_list)}},
+        "extra_vars": extra_vars,
+    }
+
+
+@step("[DRY RUN] Deploy BGP peers")
+def deploy_bgp_peers_dry(
+    subscription: dict[str, Any], edge_port_fqdn_list: list[str], tt_number: str, process_id: UUIDstr, partner_name: str
+) -> LSOState:
+    """Perform a dry run of deploying BGP peers."""
+    extra_vars = {
+        "subscription": subscription,
+        "partner_name": partner_name,
+        "verb": "deploy",
+        "object": "bgp",
+        "dry_run": True,
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - "
+        f"Deploying BGP peers for {subscription["description"]}",
+    }
+
+    return {
+        "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml",
+        "inventory": {"all": {"hosts": dict.fromkeys(edge_port_fqdn_list)}},
+        "extra_vars": extra_vars,
+    }
+
+
+@step("[FOR REAL] Deploy BGP peers")
+def deploy_bgp_peers_real(
+    subscription: dict[str, Any], edge_port_fqdn_list: list[str], tt_number: str, process_id: UUIDstr, partner_name: str
+) -> LSOState:
+    """Deploy BGP peers."""
+    extra_vars = {
+        "subscription": subscription,
+        "partner_name": partner_name,
+        "verb": "deploy",
+        "object": "bgp",
+        "dry_run": False,
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - "
+        f"Deploying BGP peers for {subscription["description"]}",
+    }
+
+    return {
+        "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml",
+        "inventory": {"all": {"hosts": dict.fromkeys(edge_port_fqdn_list)}},
+        "extra_vars": extra_vars,
+    }
+
+
+@step("Check BGP peers")
+def check_bgp_peers(subscription: dict[str, Any], edge_port_fqdn_list: list[str]) -> LSOState:
+    """Check correct deployment of BGP peers."""
+    extra_vars = {"subscription": subscription, "verb": "check", "object": "bgp"}
+
+    return {
+        "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml",
+        "inventory": {"all": {"hosts": dict.fromkeys(edge_port_fqdn_list)}},
+        "extra_vars": extra_vars,
+    }
+
+
+@step("Update Infoblox")
+def update_dns_records(subscription: L3CoreService | IAS) -> State:
+    """Update DNS records in Infoblox."""
+    #  TODO: implement
+    return {"subscription": subscription}
+
+
+@step("Create a new SharePoint checklist item")
+def create_new_sharepoint_checklist(subscription: L3CoreService, tt_number: TTNumber, process_id: UUIDstr) -> State:
+    """Create a new checklist item in SharePoint for approving this L3 Core Service."""
+    new_ep = subscription.l3_core_service.ap_list[0].sbp.edge_port
+    new_list_item_url = SharePointClient().add_list_item(
+        list_name="l3_core_service",
+        fields={
+            "Title": f"{subscription.description}",
+            "TT_NUMBER": tt_number,
+            "ACTIVITY_TYPE": "Creation",
+            "PRODUCT_TYPE": subscription.l3_core_service_type,
+            "LOCATION": f"{new_ep.edge_port_name} {new_ep.edge_port_description} on {new_ep.node.router_fqdn}",
+            "GAP_PROCESS_URL": f"{load_oss_params().GENERAL.public_hostname}/workflows/{process_id}",
+        },
+    )
+
+    return {"checklist_url": new_list_item_url}
diff --git a/gso/workflows/l3_core_service/ias/__init__.py b/gso/workflows/l3_core_service/ias/__init__.py
new file mode 100644
index 000000000..6143fba11
--- /dev/null
+++ b/gso/workflows/l3_core_service/ias/__init__.py
@@ -0,0 +1 @@
+"""Layer 3 core service workflows."""
-- 
GitLab


From 3af964b746efa4f2ff5de6c27c26ff4f28180ba7 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Tue, 18 Mar 2025 10:47:48 +0100
Subject: [PATCH 04/87] Implement create IAS WF

---
 gso/products/product_blocks/ias.py            |  5 +-
 gso/products/product_types/ias.py             |  6 +-
 .../l3_core_service/ias/create_ias.py         | 86 +++++++++++++++++++
 3 files changed, 91 insertions(+), 6 deletions(-)
 create mode 100644 gso/workflows/l3_core_service/ias/create_ias.py

diff --git a/gso/products/product_blocks/ias.py b/gso/products/product_blocks/ias.py
index 724ae4418..1320323fa 100644
--- a/gso/products/product_blocks/ias.py
+++ b/gso/products/product_blocks/ias.py
@@ -5,8 +5,9 @@ from orchestrator.types import SubscriptionLifecycle
 from gso.products.product_blocks.l3_core_service import L3CoreServiceBlockInactive
 
 
-class IASProductBlockInactive(L3CoreServiceBlockInactive, lifecycle=[SubscriptionLifecycle.INITIAL],
-                              product_block_name="IASProductBlock"):
+class IASProductBlockInactive(
+    L3CoreServiceBlockInactive, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="IASProductBlock"
+):
     """An inactive IAS product block. See `IASProductBlock`."""
 
     ias_flavor: str | None = None
diff --git a/gso/products/product_types/ias.py b/gso/products/product_types/ias.py
index 5a130ff05..f309dccb2 100644
--- a/gso/products/product_types/ias.py
+++ b/gso/products/product_types/ias.py
@@ -34,9 +34,7 @@ class ImportedIASInactive(SubscriptionModel, is_base=True):
     ias: IASProductBlockInactive
 
 
-class ImportedIAS(
-    ImportedIASInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE]
-):
+class ImportedIAS(ImportedIASInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE]):
     """An imported IAS product that is active."""
 
-    ias: IASProductBlock
\ No newline at end of file
+    ias: IASProductBlock
diff --git a/gso/workflows/l3_core_service/ias/create_ias.py b/gso/workflows/l3_core_service/ias/create_ias.py
new file mode 100644
index 000000000..6e6a7fceb
--- /dev/null
+++ b/gso/workflows/l3_core_service/ias/create_ias.py
@@ -0,0 +1,86 @@
+"""Create a new L3 Core Service subscription including GÉANT IP and IAS."""
+
+from orchestrator.forms import FormPage
+from orchestrator.targets import Target
+from orchestrator.types import State, SubscriptionLifecycle
+from orchestrator.workflow import StepList, begin, done, 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_forms.types import FormGenerator, UUIDstr
+
+from gso.products.product_types.ias import IASInactive
+from gso.services.lso_client import lso_interaction
+from gso.utils.workflow_steps import prompt_sharepoint_checklist_url, start_moodi, stop_moodi
+from gso.workflows.l3_core_service.base_create_l3_core_service import (
+    check_bgp_peers,
+    check_sbp_functionality,
+    create_new_sharepoint_checklist,
+    deploy_bgp_peers_dry,
+    deploy_bgp_peers_real,
+    initialize_service_binding,
+    provision_sbp_dry,
+    provision_sbp_real,
+    update_dns_records,
+)
+from gso.workflows.l3_core_service.base_create_l3_core_service import (
+    initial_input_form_generator as l3_initial_input_form_generator,
+)
+
+
+def initial_input_form_generator(product_name: str) -> FormGenerator:
+    """Initial input form generator for creating a new IAS subscription."""
+    initial_generator = l3_initial_input_form_generator(product_name)
+    initial_user_input = yield from initial_generator
+
+    # Additional IAS step
+    class IASExtraForm(FormPage):
+        ias_flavor: str | None = None
+
+    ias_extra = yield IASExtraForm
+    return initial_user_input | ias_extra.model_dump()
+
+
+@step("Create subscription")
+def create_subscription(product: UUIDstr, partner: str) -> State:
+    """Create a new subscription object in the database."""
+    subscription = IASInactive.from_product_id(product, partner)
+
+    return {"subscription": subscription, "subscription_id": subscription.subscription_id}
+
+
+@step("Initialize subscription")
+def initialize_subscription(
+    subscription: IASInactive, edge_port: dict, binding_port_input: dict, product_name: str, ias_flavor: str
+) -> State:
+    """Take all user inputs and use them to populate the subscription model."""
+    subscription.ias_flavor = ias_flavor
+    return initialize_service_binding(subscription, edge_port, binding_port_input, product_name, "ias")
+
+
+@workflow(
+    "Create IAS",
+    initial_input_form=wrap_create_initial_input_form(initial_input_form_generator),
+    target=Target.CREATE,
+)
+def create_ias() -> StepList:
+    """Create a new IAS subscription."""
+    return (
+        begin
+        >> create_subscription
+        >> store_process_subscription(Target.CREATE)
+        >> initialize_subscription
+        >> start_moodi()
+        >> lso_interaction(provision_sbp_dry)
+        >> lso_interaction(provision_sbp_real)
+        >> lso_interaction(check_sbp_functionality)
+        >> lso_interaction(deploy_bgp_peers_dry)
+        >> lso_interaction(deploy_bgp_peers_real)
+        >> lso_interaction(check_bgp_peers)
+        >> update_dns_records
+        >> set_status(SubscriptionLifecycle.ACTIVE)
+        >> resync
+        >> create_new_sharepoint_checklist
+        >> prompt_sharepoint_checklist_url
+        >> stop_moodi()
+        >> done
+    )
-- 
GitLab


From 52e422242097be8e6f38acee5cc65405e43e41ea Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Tue, 18 Mar 2025 13:02:41 +0100
Subject: [PATCH 05/87] Implement terminate IAS WF

---
 .../l3_core_service/ias/terminate_ias.py      | 40 +++++++++++++++++++
 1 file changed, 40 insertions(+)
 create mode 100644 gso/workflows/l3_core_service/ias/terminate_ias.py

diff --git a/gso/workflows/l3_core_service/ias/terminate_ias.py b/gso/workflows/l3_core_service/ias/terminate_ias.py
new file mode 100644
index 000000000..3a59130fa
--- /dev/null
+++ b/gso/workflows/l3_core_service/ias/terminate_ias.py
@@ -0,0 +1,40 @@
+"""Workflow for terminating an IAS subscription."""
+
+from orchestrator import begin, workflow
+from orchestrator.forms import SubmitFormPage
+from orchestrator.targets import Target
+from orchestrator.types import SubscriptionLifecycle
+from orchestrator.workflow import StepList, done
+from orchestrator.workflows.steps import resync, set_status, store_process_subscription, unsync
+from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from pydantic_forms.types import FormGenerator, UUIDstr
+
+from gso.products.product_types.ias import IAS
+from gso.utils.types.tt_number import TTNumber
+
+
+def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+    subscription = IAS.from_subscription(subscription_id)
+
+    class TerminateForm(SubmitFormPage):
+        tt_number: TTNumber
+
+    yield TerminateForm
+    return {"subscription": subscription}
+
+
+@workflow(
+    "Terminate IAS",
+    initial_input_form=wrap_modify_initial_input_form(_input_form_generator),
+    target=Target.TERMINATE,
+)
+def terminate_ias() -> StepList:
+    """Terminate an IAS subscription."""
+    return (
+        begin
+        >> store_process_subscription(Target.TERMINATE)
+        >> unsync
+        >> set_status(SubscriptionLifecycle.TERMINATED)
+        >> resync
+        >> done
+    )
-- 
GitLab


From 3698bee52c68d708584c70ea34b736a5e1913b50 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Tue, 18 Mar 2025 14:02:17 +0100
Subject: [PATCH 06/87] Fixed mypy errors

---
 .../l3_core_service/base_create_l3_core_service.py          | 3 +--
 gso/workflows/l3_core_service/ias/create_ias.py             | 6 +++---
 2 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/gso/workflows/l3_core_service/base_create_l3_core_service.py b/gso/workflows/l3_core_service/base_create_l3_core_service.py
index ae76ab718..a1612654f 100644
--- a/gso/workflows/l3_core_service/base_create_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_l3_core_service.py
@@ -5,10 +5,9 @@ from uuid import uuid4
 
 from orchestrator.forms import FormPage, SubmitFormPage
 from orchestrator.forms.validators import Label
-from orchestrator.types import State
 from orchestrator.workflow import step
 from pydantic import BaseModel, ConfigDict, Field, NonNegativeInt, computed_field, model_validator
-from pydantic_forms.types import FormGenerator, UUIDstr
+from pydantic_forms.types import FormGenerator, UUIDstr, State
 from pydantic_forms.validators import Divider
 
 from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPTypes
diff --git a/gso/workflows/l3_core_service/ias/create_ias.py b/gso/workflows/l3_core_service/ias/create_ias.py
index 6e6a7fceb..1de57cbb1 100644
--- a/gso/workflows/l3_core_service/ias/create_ias.py
+++ b/gso/workflows/l3_core_service/ias/create_ias.py
@@ -2,11 +2,11 @@
 
 from orchestrator.forms import FormPage
 from orchestrator.targets import Target
-from orchestrator.types import State, SubscriptionLifecycle
+from orchestrator.types import SubscriptionLifecycle
 from orchestrator.workflow import StepList, begin, done, 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_forms.types import FormGenerator, UUIDstr
+from pydantic_forms.types import FormGenerator, UUIDstr, State
 
 from gso.products.product_types.ias import IASInactive
 from gso.services.lso_client import lso_interaction
@@ -53,7 +53,7 @@ def initialize_subscription(
     subscription: IASInactive, edge_port: dict, binding_port_input: dict, product_name: str, ias_flavor: str
 ) -> State:
     """Take all user inputs and use them to populate the subscription model."""
-    subscription.ias_flavor = ias_flavor
+    subscription.ias.ias_flavor = ias_flavor
     return initialize_service_binding(subscription, edge_port, binding_port_input, product_name, "ias")
 
 
-- 
GitLab


From 75aec9c97e7cc360ff28e32f477805f2edadf654 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Tue, 18 Mar 2025 14:02:40 +0100
Subject: [PATCH 07/87] Removed IAS from import WFs

---
 .../l3_core_service/create_imported_l3_core_service.py          | 2 --
 gso/workflows/l3_core_service/import_l3_core_service.py         | 2 --
 2 files changed, 4 deletions(-)

diff --git a/gso/workflows/l3_core_service/create_imported_l3_core_service.py b/gso/workflows/l3_core_service/create_imported_l3_core_service.py
index 70753dd08..20ed8bb87 100644
--- a/gso/workflows/l3_core_service/create_imported_l3_core_service.py
+++ b/gso/workflows/l3_core_service/create_imported_l3_core_service.py
@@ -84,8 +84,6 @@ def create_subscription(partner: str, service_type: L3CoreServiceType) -> dict:
     match service_type:
         case L3CoreServiceType.GEANT_IP:
             product_id = get_product_id_by_name(ProductName.IMPORTED_GEANT_IP)
-        case L3CoreServiceType.IAS:
-            product_id = get_product_id_by_name(ProductName.IMPORTED_IAS)
         case L3CoreServiceType.GWS:
             product_id = get_product_id_by_name(ProductName.IMPORTED_GWS)
         case L3CoreServiceType.LHCONE:
diff --git a/gso/workflows/l3_core_service/import_l3_core_service.py b/gso/workflows/l3_core_service/import_l3_core_service.py
index 1c9d85def..bd68546af 100644
--- a/gso/workflows/l3_core_service/import_l3_core_service.py
+++ b/gso/workflows/l3_core_service/import_l3_core_service.py
@@ -24,8 +24,6 @@ def import_l3_core_service_subscription(subscription_id: UUIDstr) -> State:
     match old_l3_core_service.l3_core_service_type:
         case L3CoreServiceType.IMPORTED_GEANT_IP:
             new_subscription_id = get_product_id_by_name(ProductName.GEANT_IP)
-        case L3CoreServiceType.IMPORTED_IAS:
-            new_subscription_id = get_product_id_by_name(ProductName.IAS)
         case L3CoreServiceType.IMPORTED_GWS:
             new_subscription_id = get_product_id_by_name(ProductName.GWS)
         case L3CoreServiceType.IMPORTED_LHCONE:
-- 
GitLab


From 6871a1cbf8bd4e203b9ebc24f7eea14d132f4463 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Tue, 18 Mar 2025 16:00:20 +0100
Subject: [PATCH 08/87] Implement modification workflow for IAS subscriptions
 and generalized the L3CoreServices modification WFs

---
 .../base_modify_l3_core_service.py            | 386 ++++++++++++++++++
 .../l3_core_service/ias/modify_ias.py         |  34 ++
 2 files changed, 420 insertions(+)
 create mode 100644 gso/workflows/l3_core_service/base_modify_l3_core_service.py
 create mode 100644 gso/workflows/l3_core_service/ias/modify_ias.py

diff --git a/gso/workflows/l3_core_service/base_modify_l3_core_service.py b/gso/workflows/l3_core_service/base_modify_l3_core_service.py
new file mode 100644
index 000000000..b57cdd7d0
--- /dev/null
+++ b/gso/workflows/l3_core_service/base_modify_l3_core_service.py
@@ -0,0 +1,386 @@
+"""Base functionality for modifying an L3 Core Service subscription."""
+
+from typing import Any, TypeAlias, cast
+from uuid import UUID, uuid4
+
+from orchestrator import step
+from orchestrator.domain import SubscriptionModel
+from orchestrator.forms import FormPage
+from pydantic import BaseModel, ConfigDict, Field, NonNegativeInt, computed_field, field_validator, model_validator
+from pydantic_forms.types import FormGenerator, State, UUIDstr, strEnum
+from pydantic_forms.validators import Choice, Divider, Label, ReadOnlyField
+
+from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPTypes
+from gso.products.product_blocks.l3_core_service import AccessPort
+from gso.products.product_blocks.service_binding_port import BFDSettings, ServiceBindingPort
+from gso.products.product_types.edge_port import EdgePort
+from gso.products.product_types.ias import IAS
+from gso.products.product_types.l3_core_service import L3CoreService
+from gso.services.subscriptions import generate_unique_id, get_active_edge_port_subscriptions
+from gso.utils.shared_enums import APType, SBPType
+from gso.utils.types.geant_ids import IMPORTED_GS_ID
+from gso.utils.types.ip_address import IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask
+from gso.utils.types.tt_number import TTNumber
+from gso.utils.types.virtual_identifiers import VLAN_ID
+
+
+class Operation(strEnum):
+    """The three operations that can be performed to modify an L3 Core Service."""
+
+    ADD = "Add an Access Port"
+    REMOVE = "Remove an existing Access Port"
+    EDIT = "Edit an existing Access Port"
+
+
+def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+    """Get input about added, removed, and modified Access Ports."""
+    subscription = SubscriptionModel.from_subscription(subscription_id)
+    product_name = subscription.product.name
+
+    class OperationSelectionForm(FormPage):
+        model_config = ConfigDict(title="Modify Edge Port")
+
+        tt_number: TTNumber
+        operation: Operation
+
+    def access_port_selector() -> TypeAlias:
+        """Generate a dropdown selector for choosing an Access Port in an input form."""
+        access_ports = subscription.l3_core.ap_list
+        options = {
+            str(access_port.subscription_instance_id): (
+                f"{access_port.sbp.gs_id} on "
+                f"{EdgePort.from_subscription(access_port.sbp.edge_port.owner_subscription_id).description} "
+                f"({access_port.ap_type})"
+            )
+            for access_port in access_ports
+        }
+
+        return cast(
+            type[Choice],
+            Choice.__call__(
+                "Select an Access Port",
+                zip(options.keys(), options.items(), strict=True),
+            ),
+        )
+
+    class BFDInputModel(BaseModel):
+        bfd_enabled: bool = False
+        bfd_interval_rx: int | None = None
+        bfd_interval_tx: int | None = None
+        bfd_multiplier: int | None = None
+
+    class IPv4BGPPeer(BaseModel):
+        peer_address: IPv4AddressType
+        authentication_key: str | None = None
+        has_custom_policies: bool = False
+        bfd_enabled: bool = False
+        multipath_enabled: bool = False
+        prefix_limit: NonNegativeInt | None = None
+        is_passive: bool = False
+        add_v4_multicast: bool = Field(default=False, exclude=True)
+        send_default_route: bool = False
+
+        @computed_field  # type: ignore[prop-decorator]
+        @property
+        def families(self) -> list[IPFamily]:
+            return [IPFamily.V4UNICAST, IPFamily.V4MULTICAST] if self.add_v4_multicast else [IPFamily.V4UNICAST]
+
+        @computed_field  # type: ignore[prop-decorator]
+        @property
+        def ip_type(self) -> IPTypes:
+            return IPTypes.IPV4
+
+    class IPv6BGPPeer(BaseModel):
+        peer_address: IPv6AddressType
+        authentication_key: str | None = None
+        has_custom_policies: bool = False
+        bfd_enabled: bool = False
+        multipath_enabled: bool = False
+        prefix_limit: NonNegativeInt | None = None
+        is_passive: bool = False
+        add_v6_multicast: bool = Field(default=False, exclude=True)
+        send_default_route: bool = False
+
+        @computed_field  # type: ignore[prop-decorator]
+        @property
+        def families(self) -> list[IPFamily]:
+            return [IPFamily.V6UNICAST, IPFamily.V6MULTICAST] if self.add_v6_multicast else [IPFamily.V6UNICAST]
+
+        @computed_field  # type: ignore[prop-decorator]
+        @property
+        def ip_type(self) -> IPTypes:
+            return IPTypes.IPV6
+
+    initial_input = yield OperationSelectionForm
+    match initial_input.operation:
+        case Operation.ADD:
+
+            class AccessPortListItem(BaseModel):
+                edge_port: str
+                ap_type: str
+                custom_service_name: str
+
+            def available_new_edge_port_selector() -> TypeAlias:
+                """Generate a dropdown selector for choosing an active Edge Port in an input form."""
+                edge_ports = get_active_edge_port_subscriptions(partner_id=subscription.customer_id)
+
+                options = {
+                    str(edge_port.subscription_id): edge_port.description
+                    for edge_port in edge_ports
+                    if edge_port.subscription_id
+                       not in [ap.sbp.edge_port.owner_subscription_id for ap in subscription.l3_core.ap_list]
+                }
+
+                return cast(
+                    type[Choice],
+                    Choice.__call__(
+                        "Select an Edge Port",
+                        zip(options.keys(), options.items(), strict=True),
+                    ),
+                )
+
+            def existing_ap_list() -> type[list]:
+                return cast(
+                    type[list],
+                    ReadOnlyField(
+                        [
+                            AccessPortListItem(
+                                edge_port=EdgePort.from_subscription(
+                                    access_port.sbp.edge_port.owner_subscription_id
+                                ).description,
+                                ap_type=access_port.ap_type.value,
+                                custom_service_name=access_port.custom_service_name or "",
+                            )
+                            for access_port in subscription.l3_core.ap_list
+                        ],
+                        default_type=list[AccessPortListItem],
+                    ),
+                )
+
+            class AddAccessPortForm(FormPage):
+                model_config = ConfigDict(title=f"Add an Edge Port to a {product_name}")
+                existing_access_ports: existing_ap_list()  # type: ignore[valid-type]
+
+                divider_a: Divider = Field(exclude=True)
+                label_a: Label = Field(
+                    "Please use the fields below to configure a new Access Port, in addition to the existing ones "
+                    "listed above.",
+                    exclude=True,
+                )
+                edge_port: available_new_edge_port_selector()  # type: ignore[valid-type]
+                ap_type: APType
+                generate_gs_id: bool = True
+                gs_id: IMPORTED_GS_ID | None = None
+                custom_service_name: str | None = None
+                is_tagged: bool = False
+                vlan_id: VLAN_ID
+                ipv4_address: IPv4AddressType
+                ipv4_mask: IPv4Netmask
+                ipv6_address: IPv6AddressType
+                ipv6_mask: IPv6Netmask
+                custom_firewall_filters: bool = False
+
+                divider_b: Divider = Field(None, exclude=True)
+                label_b: Label = Field("IPv4 settings for BFD and BGP", exclude=True)
+                v4_bfd_settings: BFDInputModel
+                v4_bgp_peer: IPv4BGPPeer
+
+                divider_c: Divider = Field(None, exclude=True)
+                label_c: Label = Field("IPv6 settings for BFD and BGP", exclude=True)
+                v6_bfd_settings: BFDInputModel
+                v6_bgp_peer: IPv6BGPPeer
+
+                @model_validator(mode="before")
+                def validate_gs_id(cls, input_data: dict[str, Any]) -> dict[str, Any]:
+                    gs_id = input_data.get("gs_id")
+                    generate_gs_id = input_data.get("generate_gs_id", True)
+
+                    if generate_gs_id and gs_id:
+                        error_message = (
+                            "You cannot provide a GS ID manually while the 'Auto-generate GS ID' option is enabled."
+                            "Please either uncheck 'Auto-generate GS ID' or remove the manual GS ID."
+                        )
+                        raise ValueError(error_message)
+                    return input_data
+
+                @field_validator("edge_port")
+                def selected_edge_port_is_new(cls, value: UUIDstr) -> UUIDstr:
+                    if value in [
+                        str(ap.sbp.edge_port.owner_subscription_id) for ap in subscription.l3_core.ap_list
+                    ]:
+                        error_message = (
+                            f"This {product_name} service is already deployed on "
+                            f"{EdgePort.from_subscription(value).description}."
+                        )
+                        raise ValueError(error_message)
+                    return value
+
+            user_input = yield AddAccessPortForm
+            return {"operation": initial_input.operation, "added_access_port": user_input}
+
+        case Operation.REMOVE:
+
+            class RemoveAccessPortForm(FormPage):
+                model_config = ConfigDict(title=f"Remove an Edge Port from a {product_name}")
+                label: Label = Field(
+                    f"Please select one of the Access Ports associated with this {product_name} that should get "
+                    f"removed.",
+                    exclude=True,
+                )
+                access_port: access_port_selector()  # type: ignore[valid-type]
+
+            user_input = yield RemoveAccessPortForm
+            return {"operation": initial_input.operation, "removed_access_port": user_input.access_port}
+
+        case Operation.EDIT:
+
+            class SelectModifyAccessPortForm(FormPage):
+                model_config = ConfigDict(title=f"Modify {product_name}")
+                label: Label = Field(
+                    f"Please select one of the Access Ports associated with this {product_name} to be modified.",
+                    exclude=True,
+                )
+                access_port: access_port_selector()  # type: ignore[valid-type]
+
+            user_input = yield SelectModifyAccessPortForm
+            current_ap = AccessPort.from_db(user_input.access_port)
+            v4_peer = next(peer for peer in current_ap.sbp.bgp_session_list if IPFamily.V4UNICAST in peer.families)
+            v6_peer = next(peer for peer in current_ap.sbp.bgp_session_list if IPFamily.V6UNICAST in peer.families)
+
+            class BindingPortModificationForm(FormPage):
+                model_config = ConfigDict(title=f"{product_name} - Modify Edge Port configuration")
+                current_ep_label: Label = Field(
+                    f'Currently configuring on Edge Port "{current_ap.sbp.edge_port.edge_port_description}"',
+                    exclude=True,
+                )
+
+                gs_id: str = current_ap.sbp.gs_id
+                custom_service_name: str | None = current_ap.custom_service_name
+                is_tagged: bool = current_ap.sbp.is_tagged
+                ap_type: APType | str = current_ap.ap_type
+                # The SBP model does not require these five fields, but in the case of L3 Core Services this will never
+                # occur since it's a layer 3 service. The ignore statements are there to put our type checker at ease.
+                vlan_id: VLAN_ID = current_ap.sbp.vlan_id  # type: ignore[assignment]
+                ipv4_address: IPv4AddressType = current_ap.sbp.ipv4_address  # type: ignore[assignment]
+                ipv4_mask: IPv4Netmask = current_ap.sbp.ipv4_mask  # type: ignore[assignment]
+                ipv6_address: IPv6AddressType = current_ap.sbp.ipv6_address  # type: ignore[assignment]
+                ipv6_mask: IPv6Netmask = current_ap.sbp.ipv6_mask  # type: ignore[assignment]
+                custom_firewall_filters: bool = current_ap.sbp.custom_firewall_filters
+
+                divider_a: Divider = Field(None, exclude=True)
+                label_a: Label = Field("IPv4 settings for BFD and BGP", exclude=True)
+                v4_bfd_settings: BFDInputModel = BFDInputModel(
+                    bfd_enabled=current_ap.sbp.v4_bfd_settings.bfd_enabled,
+                    bfd_multiplier=current_ap.sbp.v4_bfd_settings.bfd_multiplier,
+                    bfd_interval_rx=current_ap.sbp.v4_bfd_settings.bfd_interval_rx,
+                    bfd_interval_tx=current_ap.sbp.v4_bfd_settings.bfd_interval_tx,
+                )
+                v4_bgp_peer: IPv4BGPPeer = IPv4BGPPeer(
+                    **v4_peer.model_dump(exclude=set("families")),
+                    add_v4_multicast=bool(IPFamily.V4MULTICAST in v4_peer.families),
+                )
+
+                divider_b: Divider = Field(None, exclude=True)
+                label_b: Label = Field("IPv6 settings for BFD and BGP", exclude=True)
+                v6_bfd_settings: BFDInputModel = BFDInputModel(
+                    bfd_enabled=current_ap.sbp.v6_bfd_settings.bfd_enabled,
+                    bfd_multiplier=current_ap.sbp.v6_bfd_settings.bfd_multiplier,
+                    bfd_interval_rx=current_ap.sbp.v6_bfd_settings.bfd_interval_rx,
+                    bfd_interval_tx=current_ap.sbp.v6_bfd_settings.bfd_interval_tx,
+                )
+                v6_bgp_peer: IPv6BGPPeer = IPv6BGPPeer(
+                    **v6_peer.model_dump(exclude=set("families")),
+                    add_v6_multicast=bool(IPFamily.V6MULTICAST in v6_peer.families),
+                )
+
+            binding_port_input_form = yield BindingPortModificationForm
+            return {
+                "operation": initial_input.operation,
+                "modified_access_port": user_input.access_port,
+                "modified_sbp": binding_port_input_form,
+            }
+
+        case _:
+            msg = f"Invalid operation selected: {initial_input.operation}"
+            raise ValueError(msg)
+
+
+@step("Instantiate new Service Binding Ports")
+def create_new_sbp(subscription: L3CoreService | IAS, added_access_port: dict[str, Any]) -> State:
+    """Add new SBP to the L3 Core Service subscription."""
+    edge_port = EdgePort.from_subscription(added_access_port.pop("edge_port"))
+    bgp_session_list = [
+        BGPSession.new(subscription_id=uuid4(), **session, rtbh_enabled=True, is_multi_hop=True)
+        for session in [added_access_port["v4_bgp_peer"], added_access_port["v6_bgp_peer"]]
+    ]
+    v4_bfd_settings = BFDSettings.new(subscription_id=uuid4(), **added_access_port.pop("v4_bfd_settings"))
+    v6_bfd_settings = BFDSettings.new(subscription_id=uuid4(), **added_access_port.pop("v6_bfd_settings"))
+    sbp_gs_id = (
+        generate_unique_id(prefix="GS")
+        if added_access_port.pop("generate_gs_id", False)
+        else added_access_port.pop("gs_id")
+    )
+    added_access_port.pop("gs_id", None)
+    service_binding_port = ServiceBindingPort.new(
+        subscription_id=uuid4(),
+        **added_access_port,
+        v4_bfd_settings=v4_bfd_settings,
+        v6_bfd_settings=v6_bfd_settings,
+        bgp_session_list=bgp_session_list,
+        sbp_type=SBPType.L3,
+        edge_port=edge_port.edge_port,
+        gs_id=sbp_gs_id,
+    )
+    subscription.l3_core.ap_list.append(
+        AccessPort.new(
+            subscription_id=uuid4(),
+            ap_type=added_access_port["ap_type"],
+            sbp=service_binding_port,
+            custom_service_name=added_access_port.get("custom_service_name"),
+        )
+    )
+
+    return {"subscription": subscription}
+
+
+@step("Clean up removed Edge Ports")
+def remove_old_sbp(subscription: L3CoreService | IAS, removed_access_port: UUIDstr) -> State:
+    """Remove old SBP product blocks from the GÉANT IP subscription."""
+    subscription.l3_core.ap_list.remove(AccessPort.from_db(UUID(removed_access_port)))
+
+    return {"subscription": subscription}
+
+
+@step("Modify existing Service Binding Ports")
+def modify_existing_sbp(
+        subscription: L3CoreService | IAS, modified_access_port: UUIDstr, modified_sbp: dict[str, Any]
+) -> State:
+    """Update the subscription model."""
+    current_ap = next(
+        ap for ap in subscription.l3_core.ap_list if str(ap.subscription_instance_id) == modified_access_port
+    )
+    v4_peer = next(peer for peer in current_ap.sbp.bgp_session_list if IPFamily.V4UNICAST in peer.families)
+    for attribute in modified_sbp["v4_bgp_peer"]:
+        setattr(v4_peer, attribute, modified_sbp["v4_bgp_peer"][attribute])
+    for attribute in modified_sbp["v4_bfd_settings"]:
+        setattr(current_ap.sbp.v4_bfd_settings, attribute, modified_sbp["v4_bfd_settings"][attribute])
+
+    v6_peer = next(peer for peer in current_ap.sbp.bgp_session_list if IPFamily.V6UNICAST in peer.families)
+    for attribute in modified_sbp["v6_bgp_peer"]:
+        setattr(v6_peer, attribute, modified_sbp["v6_bgp_peer"][attribute])
+    for attribute in modified_sbp["v6_bfd_settings"]:
+        setattr(current_ap.sbp.v6_bfd_settings, attribute, modified_sbp["v6_bfd_settings"][attribute])
+
+    current_ap.sbp.bgp_session_list = [v4_peer, v6_peer]
+    current_ap.sbp.vlan_id = modified_sbp["vlan_id"]
+    current_ap.sbp.gs_id = modified_sbp["gs_id"]
+    current_ap.sbp.is_tagged = modified_sbp["is_tagged"]
+    current_ap.sbp.ipv4_address = modified_sbp["ipv4_address"]
+    current_ap.sbp.ipv4_mask = modified_sbp["ipv4_mask"]
+    current_ap.sbp.ipv6_address = modified_sbp["ipv6_address"]
+    current_ap.sbp.ipv6_mask = modified_sbp["ipv6_mask"]
+    current_ap.sbp.custom_firewall_filters = modified_sbp["custom_firewall_filters"]
+    current_ap.ap_type = modified_sbp["ap_type"]
+    current_ap.custom_service_name = modified_sbp["custom_service_name"]
+
+    return {"subscription": subscription}
diff --git a/gso/workflows/l3_core_service/ias/modify_ias.py b/gso/workflows/l3_core_service/ias/modify_ias.py
new file mode 100644
index 000000000..59f376e9d
--- /dev/null
+++ b/gso/workflows/l3_core_service/ias/modify_ias.py
@@ -0,0 +1,34 @@
+"""Modification workflow for a IAS subscription."""
+
+from orchestrator import begin, conditional, done, workflow
+from orchestrator.targets import Target
+from orchestrator.workflow import StepList
+from orchestrator.workflows.steps import resync, store_process_subscription, unsync
+from orchestrator.workflows.utils import wrap_modify_initial_input_form
+
+from gso.workflows.l3_core_service.base_modify_l3_core_service import Operation, \
+    initial_input_form_generator as base_initial_input_form_generator, create_new_sbp, remove_old_sbp, \
+    modify_existing_sbp
+
+
+@workflow(
+    "Modify IAS",
+    initial_input_form=wrap_modify_initial_input_form(base_initial_input_form_generator),
+    target=Target.MODIFY,
+)
+def modify_ias() -> StepList:
+    """Modify IAS subscription."""
+    access_port_is_added = conditional(lambda state: state["operation"] == Operation.ADD)
+    access_port_is_removed = conditional(lambda state: state["operation"] == Operation.REMOVE)
+    access_port_is_modified = conditional(lambda state: state["operation"] == Operation.EDIT)
+
+    return (
+        begin
+        >> store_process_subscription(Target.MODIFY)
+        >> unsync
+        >> access_port_is_added(create_new_sbp)
+        >> access_port_is_removed(remove_old_sbp)
+        >> access_port_is_modified(modify_existing_sbp)
+        >> resync
+        >> done
+    )
-- 
GitLab


From 7f00bd7364a3adc96984fe7f5a93bed335bf6906 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Tue, 18 Mar 2025 16:07:16 +0100
Subject: [PATCH 09/87] Fix linter errors

---
 .../l3_core_service/base_create_l3_core_service.py   |  2 +-
 .../l3_core_service/base_modify_l3_core_service.py   |  8 +++-----
 gso/workflows/l3_core_service/ias/create_ias.py      |  2 +-
 gso/workflows/l3_core_service/ias/modify_ias.py      | 12 +++++++++---
 4 files changed, 14 insertions(+), 10 deletions(-)

diff --git a/gso/workflows/l3_core_service/base_create_l3_core_service.py b/gso/workflows/l3_core_service/base_create_l3_core_service.py
index a1612654f..fe004a595 100644
--- a/gso/workflows/l3_core_service/base_create_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_l3_core_service.py
@@ -7,7 +7,7 @@ from orchestrator.forms import FormPage, SubmitFormPage
 from orchestrator.forms.validators import Label
 from orchestrator.workflow import step
 from pydantic import BaseModel, ConfigDict, Field, NonNegativeInt, computed_field, model_validator
-from pydantic_forms.types import FormGenerator, UUIDstr, State
+from pydantic_forms.types import FormGenerator, State, UUIDstr
 from pydantic_forms.validators import Divider
 
 from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPTypes
diff --git a/gso/workflows/l3_core_service/base_modify_l3_core_service.py b/gso/workflows/l3_core_service/base_modify_l3_core_service.py
index b57cdd7d0..54e1f7129 100644
--- a/gso/workflows/l3_core_service/base_modify_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_modify_l3_core_service.py
@@ -128,7 +128,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
                     str(edge_port.subscription_id): edge_port.description
                     for edge_port in edge_ports
                     if edge_port.subscription_id
-                       not in [ap.sbp.edge_port.owner_subscription_id for ap in subscription.l3_core.ap_list]
+                    not in [ap.sbp.edge_port.owner_subscription_id for ap in subscription.l3_core.ap_list]
                 }
 
                 return cast(
@@ -205,9 +205,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
                 @field_validator("edge_port")
                 def selected_edge_port_is_new(cls, value: UUIDstr) -> UUIDstr:
-                    if value in [
-                        str(ap.sbp.edge_port.owner_subscription_id) for ap in subscription.l3_core.ap_list
-                    ]:
+                    if value in [str(ap.sbp.edge_port.owner_subscription_id) for ap in subscription.l3_core.ap_list]:
                         error_message = (
                             f"This {product_name} service is already deployed on "
                             f"{EdgePort.from_subscription(value).description}."
@@ -353,7 +351,7 @@ def remove_old_sbp(subscription: L3CoreService | IAS, removed_access_port: UUIDs
 
 @step("Modify existing Service Binding Ports")
 def modify_existing_sbp(
-        subscription: L3CoreService | IAS, modified_access_port: UUIDstr, modified_sbp: dict[str, Any]
+    subscription: L3CoreService | IAS, modified_access_port: UUIDstr, modified_sbp: dict[str, Any]
 ) -> State:
     """Update the subscription model."""
     current_ap = next(
diff --git a/gso/workflows/l3_core_service/ias/create_ias.py b/gso/workflows/l3_core_service/ias/create_ias.py
index 1de57cbb1..184afd223 100644
--- a/gso/workflows/l3_core_service/ias/create_ias.py
+++ b/gso/workflows/l3_core_service/ias/create_ias.py
@@ -6,7 +6,7 @@ from orchestrator.types import SubscriptionLifecycle
 from orchestrator.workflow import StepList, begin, done, 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_forms.types import FormGenerator, UUIDstr, State
+from pydantic_forms.types import FormGenerator, State, UUIDstr
 
 from gso.products.product_types.ias import IASInactive
 from gso.services.lso_client import lso_interaction
diff --git a/gso/workflows/l3_core_service/ias/modify_ias.py b/gso/workflows/l3_core_service/ias/modify_ias.py
index 59f376e9d..5c4738e36 100644
--- a/gso/workflows/l3_core_service/ias/modify_ias.py
+++ b/gso/workflows/l3_core_service/ias/modify_ias.py
@@ -6,9 +6,15 @@ from orchestrator.workflow import StepList
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
 
-from gso.workflows.l3_core_service.base_modify_l3_core_service import Operation, \
-    initial_input_form_generator as base_initial_input_form_generator, create_new_sbp, remove_old_sbp, \
-    modify_existing_sbp
+from gso.workflows.l3_core_service.base_modify_l3_core_service import (
+    Operation,
+    create_new_sbp,
+    modify_existing_sbp,
+    remove_old_sbp,
+)
+from gso.workflows.l3_core_service.base_modify_l3_core_service import (
+    initial_input_form_generator as base_initial_input_form_generator,
+)
 
 
 @workflow(
-- 
GitLab


From 3ea990d67eb8173e0055ee6ec7f103d7f8f13095 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Tue, 18 Mar 2025 16:13:36 +0100
Subject: [PATCH 10/87] Update create IAS WF

---
 gso/workflows/l3_core_service/ias/create_ias.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gso/workflows/l3_core_service/ias/create_ias.py b/gso/workflows/l3_core_service/ias/create_ias.py
index 184afd223..c864d100f 100644
--- a/gso/workflows/l3_core_service/ias/create_ias.py
+++ b/gso/workflows/l3_core_service/ias/create_ias.py
@@ -54,7 +54,7 @@ def initialize_subscription(
 ) -> State:
     """Take all user inputs and use them to populate the subscription model."""
     subscription.ias.ias_flavor = ias_flavor
-    return initialize_service_binding(subscription, edge_port, binding_port_input, product_name, "ias")
+    return initialize_service_binding(subscription, edge_port, binding_port_input, product_name)
 
 
 @workflow(
-- 
GitLab


From 7902261d821815ec6199aa4c19e141a90d7e1376 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Tue, 18 Mar 2025 16:14:02 +0100
Subject: [PATCH 11/87] Update Base create L3 Core Services module

---
 .../base_create_l3_core_service.py              | 17 +++++++----------
 1 file changed, 7 insertions(+), 10 deletions(-)

diff --git a/gso/workflows/l3_core_service/base_create_l3_core_service.py b/gso/workflows/l3_core_service/base_create_l3_core_service.py
index fe004a595..7ab0ef568 100644
--- a/gso/workflows/l3_core_service/base_create_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_l3_core_service.py
@@ -166,7 +166,6 @@ def initialize_service_binding(
     edge_port: dict,
     binding_port_input: dict,
     product_name: str,
-    service_type_attr: str,
 ) -> dict:
     """Initialize a service binding port for a given service type in the subscription model."""
     edge_port_fqdn_list = []
@@ -196,16 +195,14 @@ def initialize_service_binding(
         gs_id=sbp_gs_id,
     )
 
-    service_attr = getattr(subscription, service_type_attr, None)
-    if service_attr is not None:
-        service_attr.ap_list.append(
-            AccessPortInactive.new(
-                subscription_id=uuid4(),
-                ap_type=edge_port["ap_type"],
-                sbp=service_binding_port,
-                custom_service_name=edge_port.get("custom_service_name"),
-            )
+    subscription.l3_core.ap_list.append(
+        AccessPortInactive.new(
+            subscription_id=uuid4(),
+            ap_type=edge_port["ap_type"],
+            sbp=service_binding_port,
+            custom_service_name=edge_port.get("custom_service_name"),
         )
+    )
 
     edge_port_fqdn_list.append(edge_port_subscription.edge_port.node.router_fqdn)
 
-- 
GitLab


From 8d5850a18946c3bb90a8a6682540d6bebdcb8e16 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Wed, 19 Mar 2025 08:51:30 +0100
Subject: [PATCH 12/87] Create migration scripts to port old L3 Core data to
 new models Create new L3 models based on new design

---
 .../2025-03-18_c9c9fdf624b5_re_model_ias.py   | 198 ++++++++++++++++++
 gso/products/__init__.py                      |  38 ++--
 gso/products/product_blocks/copernicus.py     |  32 +++
 gso/products/product_blocks/geant_ip.py       |  30 +++
 gso/products/product_blocks/gws.py            |  30 +++
 gso/products/product_blocks/ias.py            |  18 +-
 gso/products/product_blocks/lhcone.py         |  30 +++
 gso/products/product_types/copernicus.py      |  42 ++++
 gso/products/product_types/geant_ip.py        |  42 ++++
 gso/products/product_types/gws.py             |  36 ++++
 gso/products/product_types/l3_core_service.py |   2 +-
 gso/products/product_types/lhcone.py          |  42 ++++
 12 files changed, 515 insertions(+), 25 deletions(-)
 create mode 100644 gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py
 create mode 100644 gso/products/product_blocks/copernicus.py
 create mode 100644 gso/products/product_blocks/geant_ip.py
 create mode 100644 gso/products/product_blocks/gws.py
 create mode 100644 gso/products/product_blocks/lhcone.py
 create mode 100644 gso/products/product_types/copernicus.py
 create mode 100644 gso/products/product_types/geant_ip.py
 create mode 100644 gso/products/product_types/gws.py
 create mode 100644 gso/products/product_types/lhcone.py

diff --git a/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py b/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py
new file mode 100644
index 000000000..5ff448768
--- /dev/null
+++ b/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py
@@ -0,0 +1,198 @@
+"""Re-model L3CoreServices
+
+    GEANT_IP = "GÉANT IP"
+    IMPORTED_GEANT_IP = "IMPORTED GÉANT IP"
+    GWS = "GWS"
+    IMPORTED_GWS = "IMPORTED GWS"
+    LHCONE = "LHCONE"
+    IMPORTED_LHCONE = "IMPORTED LHCONE"
+    COPERNICUS = "COPERNICUS"
+    IMPORTED_COPERNICUS = "IMPORTED COPERNICUS"
+
+Revision ID: e1afa3790f32
+Revises: b96b0ecf6906
+Create Date: 2025-03-17 10:23:45.917222
+
+"""
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = 'e1afa3790f32'
+down_revision = 'b96b0ecf6906'
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    conn = op.get_bind()
+    conn.execute(
+        sa.text(
+            """
+DELETE FROM fixed_inputs
+WHERE fixed_inputs.product_id IN (
+    SELECT products.product_id FROM products WHERE products.name IN ('IAS', 'Imported IAS')
+)
+  AND fixed_inputs.name = 'l3_core_service_type'
+            """
+        )
+    )
+    conn.execute(
+        sa.text(
+            """
+INSERT INTO product_blocks (name, description, tag, status)
+VALUES ('IASProductBlock', 'An Internet Access Service for general internet access', 'IAS', 'active')
+RETURNING product_blocks.product_block_id
+            """
+        )
+    )
+    conn.execute(
+        sa.text(
+            """
+INSERT INTO resource_types (resource_type, description)
+VALUES ('ias_flavor', 'The flavor of the IAS service')
+RETURNING resource_types.resource_type_id
+            """
+        )
+    )
+    conn.execute(
+        sa.text(
+            """ 
+    INSERT INTO product_block_relations
+VALUES (
+    (SELECT product_blocks.product_block_id FROM product_blocks WHERE name = 'IASProductBlock'),
+    (SELECT product_blocks.product_block_id FROM product_blocks WHERE name = 'L3CoreServiceBlock'),
+    NULL,
+    NULL
+);
+            """
+        )
+    )
+
+    conn.execute(
+        sa.text(
+            """
+UPDATE product_product_blocks
+SET product_block_id = (
+    SELECT product_block_id FROM product_blocks WHERE name = 'IASProductBlock'
+)
+WHERE product_block_id = (
+    SELECT product_block_id FROM product_blocks WHERE name = 'L3CoreServiceBlock'
+)
+  AND product_id IN (
+    SELECT product_id FROM products WHERE name IN ('IAS', 'Imported IAS')
+);
+            """
+        )
+    )
+
+    conn.execute(
+        sa.text(
+            """
+UPDATE products
+SET product_type = 'IAS'
+WHERE product_type = 'L3CoreService'
+  AND name = 'IAS';
+            """
+        )
+    )
+
+    conn.execute(
+        sa.text(
+            """
+UPDATE products
+SET product_type = 'ImportedIAS'
+WHERE product_type = 'ImportedL3CoreService'
+  AND name = 'Imported IAS';
+            """
+        )
+    )
+
+    conn.execute(
+        sa.text(
+            """
+-- Step 1: Insert new subscription_instances for 'IASProductBlock' and return their IDs
+WITH inserted AS (
+    INSERT INTO subscription_instances (subscription_id, product_block_id)
+    SELECT
+        s.subscription_id,
+        (
+            SELECT product_block_id
+            FROM product_blocks
+            WHERE name = 'IASProductBlock'
+        ) AS product_block_id
+    FROM subscriptions s
+    WHERE s.product_id = (
+        SELECT product_id
+        FROM products
+        WHERE products.name = 'IAS'
+    )
+    RETURNING subscription_instance_id, subscription_id
+)
+
+-- Step 2: Link newly inserted "IASProductBlock" instances to the existing "L3CoreServiceBlock" instance
+INSERT INTO subscription_instance_relations (
+    in_use_by_id,
+    depends_on_id,
+    order_id,
+    domain_model_attr
+)
+SELECT
+    i.subscription_instance_id AS in_use_by_id,
+    e.subscription_instance_id AS depends_on_id,
+    0 AS order_id,
+    'l3_core' AS domain_model_attr
+FROM inserted i
+JOIN subscription_instances e
+  ON e.subscription_id = i.subscription_id
+  AND e.product_block_id = (
+      SELECT product_block_id
+      FROM product_blocks
+      WHERE name = 'L3CoreServiceBlock'
+  );
+            """
+        )
+    )
+    
+    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 ('IASProductBlock')),
+    (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ias_flavor'))
+)
+            """
+        )
+    )
+
+    conn.execute(
+        sa.text(
+            """
+
+                WITH subscription_instance_ids AS (
+                    SELECT subscription_instances.subscription_instance_id
+                    FROM subscription_instances
+                    WHERE subscription_instances.product_block_id IN (
+                        SELECT product_blocks.product_block_id
+                        FROM product_blocks
+                        WHERE product_blocks.name = 'IASProductBlock'
+                    )
+                )
+                INSERT INTO subscription_instance_values (subscription_instance_id, resource_type_id, value)
+                SELECT
+                    subscription_instance_ids.subscription_instance_id,
+                    resource_types.resource_type_id,
+                    'None'
+                    -- TODO we need to check this with Simone to see what the default value should be, in case it is empty string, this wont be show up in the GUI
+                FROM resource_types
+                CROSS JOIN subscription_instance_ids
+                WHERE resource_types.resource_type = 'ias_flavor'
+
+            """
+        )
+    )
+
+
+def downgrade() -> None:
+    conn = op.get_bind()
diff --git a/gso/products/__init__.py b/gso/products/__init__.py
index ad6714523..cf2126343 100644
--- a/gso/products/__init__.py
+++ b/gso/products/__init__.py
@@ -8,12 +8,16 @@
 from orchestrator.domain import SUBSCRIPTION_MODEL_REGISTRY
 from pydantic_forms.types import strEnum
 
+from gso.products.product_types.copernicus import Copernicus, ImportedCopernicus
 from gso.products.product_types.edge_port import EdgePort, ImportedEdgePort
+from gso.products.product_types.geant_ip import GeantIp, ImportedGeantIp
+from gso.products.product_types.gws import GWS, ImportedGWS
 from gso.products.product_types.ias import IAS, ImportedIAS
 from gso.products.product_types.iptrunk import ImportedIptrunk, Iptrunk
-from gso.products.product_types.l3_core_service import ImportedL3CoreService, L3CoreService
+from gso.products.product_types.l3_core_service import L3CoreService
 from gso.products.product_types.lan_switch_interconnect import ImportedLanSwitchInterconnect, LanSwitchInterconnect
 from gso.products.product_types.layer_2_circuit import ImportedLayer2Circuit, Layer2Circuit, Layer2CircuitServiceType
+from gso.products.product_types.lhcone import ImportedLHCOne, LHCOne
 from gso.products.product_types.office_router import ImportedOfficeRouter, OfficeRouter
 from gso.products.product_types.opengear import ImportedOpengear, Opengear
 from gso.products.product_types.pop_vlan import PopVlan
@@ -107,14 +111,6 @@ class ProductType(strEnum):
     IMPORTED_OPENGEAR = ImportedOpengear.__name__
     EDGE_PORT = EdgePort.__name__
     IMPORTED_EDGE_PORT = ImportedEdgePort.__name__
-    GEANT_IP = L3_CORE_SERVICE_PRODUCT_TYPE
-    IMPORTED_GEANT_IP = ImportedL3CoreService.__name__
-    GWS = L3_CORE_SERVICE_PRODUCT_TYPE
-    IMPORTED_GWS = ImportedL3CoreService.__name__
-    LHCONE = L3_CORE_SERVICE_PRODUCT_TYPE
-    IMPORTED_LHCONE = ImportedL3CoreService.__name__
-    COPERNICUS = L3_CORE_SERVICE_PRODUCT_TYPE
-    IMPORTED_COPERNICUS = ImportedL3CoreService.__name__
     GEANT_PLUS = L2_CIRCUIT_PRODUCT_TYPE
     IMPORTED_GEANT_PLUS = ImportedLayer2Circuit.__name__
     EXPRESSROUTE = L2_CIRCUIT_PRODUCT_TYPE
@@ -122,6 +118,14 @@ class ProductType(strEnum):
     VRF = VRF.__name__
     IAS = IAS.__name__
     IMPORTED_IAS = ImportedIAS.__name__
+    GEANT_IP = GeantIp.__name__
+    IMPORTED_GEANT_IP = ImportedGeantIp.__name__
+    GWS = GWS.__name__
+    IMPORTED_GWS = ImportedGWS.__name__
+    LHCONE = LHCOne.__name__
+    IMPORTED_LHCONE = ImportedLHCOne.__name__
+    COPERNICUS = Copernicus.__name__
+    IMPORTED_COPERNICUS = ImportedCopernicus.__name__
 
 
 SUBSCRIPTION_MODEL_REGISTRY.update(
@@ -145,16 +149,16 @@ SUBSCRIPTION_MODEL_REGISTRY.update(
         ProductName.IMPORTED_OPENGEAR.value: ImportedOpengear,
         ProductName.EDGE_PORT.value: EdgePort,
         ProductName.IMPORTED_EDGE_PORT.value: ImportedEdgePort,
-        ProductName.GEANT_IP.value: L3CoreService,
-        ProductName.IMPORTED_GEANT_IP.value: ImportedL3CoreService,
+        ProductName.GEANT_IP.value: GeantIp,
+        ProductName.IMPORTED_GEANT_IP.value: ImportedGeantIp,
         ProductName.IAS.value: IAS,
         ProductName.IMPORTED_IAS.value: ImportedIAS,
-        ProductName.GWS.value: L3CoreService,
-        ProductName.IMPORTED_GWS.value: ImportedL3CoreService,
-        ProductName.LHCONE.value: L3CoreService,
-        ProductName.IMPORTED_LHCONE.value: ImportedL3CoreService,
-        ProductName.COPERNICUS.value: L3CoreService,
-        ProductName.IMPORTED_COPERNICUS.value: ImportedL3CoreService,
+        ProductName.GWS.value: GWS,
+        ProductName.IMPORTED_GWS.value: ImportedGWS,
+        ProductName.LHCONE.value: LHCOne,
+        ProductName.IMPORTED_LHCONE.value: ImportedLHCOne,
+        ProductName.COPERNICUS.value: Copernicus,
+        ProductName.IMPORTED_COPERNICUS.value: ImportedCopernicus,
         ProductName.GEANT_PLUS.value: Layer2Circuit,
         ProductName.IMPORTED_GEANT_PLUS.value: ImportedLayer2Circuit,
         ProductName.EXPRESSROUTE.value: Layer2Circuit,
diff --git a/gso/products/product_blocks/copernicus.py b/gso/products/product_blocks/copernicus.py
new file mode 100644
index 000000000..3d9926cae
--- /dev/null
+++ b/gso/products/product_blocks/copernicus.py
@@ -0,0 +1,32 @@
+"""Product blocks for Copernicus AS products."""
+
+from orchestrator.domain.base import ProductBlockModel
+from orchestrator.types import SubscriptionLifecycle
+
+from gso.products.product_blocks.l3_core_service import (
+    L3CoreServiceBlock,
+    L3CoreServiceBlockInactive,
+    L3CoreServiceBlockProvisioning,
+)
+
+
+class CopernicusProductBlockInactive(
+    ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="CopernicusProductBlock"
+):
+    """An inactive Copernicus product block. See `CopernicusProductBlock`."""
+
+    l3_core: L3CoreServiceBlockInactive | None = None
+
+
+class CopernicusProductBlockProvisioning(
+    CopernicusProductBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]
+):
+    """A provisioning Copernicus product block. See `CopernicusProductBlock`."""
+
+    l3_core: L3CoreServiceBlockProvisioning
+
+
+class CopernicusProductBlock(CopernicusProductBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
+    """An active Copernicus product block."""
+
+    l3_core: L3CoreServiceBlock
diff --git a/gso/products/product_blocks/geant_ip.py b/gso/products/product_blocks/geant_ip.py
new file mode 100644
index 000000000..63323d959
--- /dev/null
+++ b/gso/products/product_blocks/geant_ip.py
@@ -0,0 +1,30 @@
+"""Product blocks for GEANT IP products."""
+
+from orchestrator.domain.base import ProductBlockModel
+from orchestrator.types import SubscriptionLifecycle
+
+from gso.products.product_blocks.l3_core_service import (
+    L3CoreServiceBlock,
+    L3CoreServiceBlockInactive,
+    L3CoreServiceBlockProvisioning,
+)
+
+
+class GeantIpProductBlockInactive(
+    ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="GeantIpProductBlock"
+):
+    """An inactive GeantIp product block. See `GeantIpProductBlock`."""
+
+    l3_core: L3CoreServiceBlockInactive | None = None
+
+
+class GeantIpProductBlockProvisioning(GeantIpProductBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
+    """A provisioning GeantIp product block. See `GeantIpProductBlock`."""
+
+    l3_core: L3CoreServiceBlockProvisioning
+
+
+class GeantIpProductBlock(GeantIpProductBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
+    """An active GeantIp product block."""
+
+    l3_core: L3CoreServiceBlock
diff --git a/gso/products/product_blocks/gws.py b/gso/products/product_blocks/gws.py
new file mode 100644
index 000000000..fd2a00b6d
--- /dev/null
+++ b/gso/products/product_blocks/gws.py
@@ -0,0 +1,30 @@
+"""Product blocks for GWS products."""
+
+from orchestrator.domain.base import ProductBlockModel
+from orchestrator.types import SubscriptionLifecycle
+
+from gso.products.product_blocks.l3_core_service import (
+    L3CoreServiceBlock,
+    L3CoreServiceBlockInactive,
+    L3CoreServiceBlockProvisioning,
+)
+
+
+class GWSProductBlockInactive(
+    ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="GWSProductBlock"
+):
+    """A GWS product block. See `GWSProductBlock`."""
+
+    l3_core: L3CoreServiceBlockInactive | None = None
+
+
+class GWSProductBlockProvisioning(GWSProductBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
+    """A provisioning GWS product block. See `GWSProductBlock`."""
+
+    l3_core: L3CoreServiceBlockProvisioning
+
+
+class GWSProductBlock(GWSProductBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
+    """An active GWS product block."""
+
+    l3_core: L3CoreServiceBlock
diff --git a/gso/products/product_blocks/ias.py b/gso/products/product_blocks/ias.py
index 1320323fa..4b8800271 100644
--- a/gso/products/product_blocks/ias.py
+++ b/gso/products/product_blocks/ias.py
@@ -1,29 +1,33 @@
 """Product blocks for IAS products."""
 
+from orchestrator.domain.base import ProductBlockModel
 from orchestrator.types import SubscriptionLifecycle
 
-from gso.products.product_blocks.l3_core_service import L3CoreServiceBlockInactive
+from gso.products.product_blocks.l3_core_service import (
+    L3CoreServiceBlock,
+    L3CoreServiceBlockInactive,
+    L3CoreServiceBlockProvisioning,
+)
 
 
 class IASProductBlockInactive(
-    L3CoreServiceBlockInactive, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="IASProductBlock"
+    ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="IASProductBlock"
 ):
     """An inactive IAS product block. See `IASProductBlock`."""
 
+    l3_core: L3CoreServiceBlockInactive | None = None
     ias_flavor: str | None = None
 
 
 class IASProductBlockProvisioning(IASProductBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
     """A provisioning IAS product block. See `IASProductBlock`."""
 
+    l3_core: L3CoreServiceBlockProvisioning
     ias_flavor: str | None = None
 
 
 class IASProductBlock(IASProductBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
-    """An active IAS product block.
-
-    Attributes:
-        ap_list: The list of Access Points where this service is present.
-    """
+    """An active IAS product block."""
 
+    l3_core: L3CoreServiceBlock
     ias_flavor: str | None = None
diff --git a/gso/products/product_blocks/lhcone.py b/gso/products/product_blocks/lhcone.py
new file mode 100644
index 000000000..daa8dde47
--- /dev/null
+++ b/gso/products/product_blocks/lhcone.py
@@ -0,0 +1,30 @@
+"""Product blocks for LHCONE AS products."""
+
+from orchestrator.domain.base import ProductBlockModel
+from orchestrator.types import SubscriptionLifecycle
+
+from gso.products.product_blocks.l3_core_service import (
+    L3CoreServiceBlock,
+    L3CoreServiceBlockInactive,
+    L3CoreServiceBlockProvisioning,
+)
+
+
+class LHCOneProductBlockInactive(
+    ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="LHCOneProductBlock"
+):
+    """An inactive LHCOne product block. See `LHCOneProductBlock`."""
+
+    l3_core: L3CoreServiceBlockInactive | None = None
+
+
+class LHCOneProductBlockProvisioning(LHCOneProductBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
+    """A provisioning LHCOne product block. See `LHCOneProductBlock`."""
+
+    l3_core: L3CoreServiceBlockProvisioning
+
+
+class LHCOneProductBlock(LHCOneProductBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
+    """An active LHCOne product block."""
+
+    l3_core: L3CoreServiceBlock
diff --git a/gso/products/product_types/copernicus.py b/gso/products/product_types/copernicus.py
new file mode 100644
index 000000000..6fc047852
--- /dev/null
+++ b/gso/products/product_types/copernicus.py
@@ -0,0 +1,42 @@
+"""Product type for LHCONE."""
+
+from orchestrator.domain.base import SubscriptionModel
+from orchestrator.types import SubscriptionLifecycle
+
+from gso.products.product_blocks.copernicus import (
+    CopernicusProductBlock,
+    CopernicusProductBlockInactive,
+    CopernicusProductBlockProvisioning,
+)
+
+
+class CopernicusInactive(SubscriptionModel, is_base=True):
+    """A Copernicus product that is inactive."""
+
+    copernicus: CopernicusProductBlockInactive
+
+
+class CopernicusProvisioning(CopernicusInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
+    """A Copernicus product that is being provisioned."""
+
+    copernicus: CopernicusProductBlockProvisioning
+
+
+class Copernicus(CopernicusProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
+    """A Copernicus product that is active."""
+
+    copernicus: CopernicusProductBlock
+
+
+class ImportedCopernicusInactive(SubscriptionModel, is_base=True):
+    """An imported Copernicus product that is inactive."""
+
+    copernicus: CopernicusInactive
+
+
+class ImportedCopernicus(
+    ImportedCopernicusInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE]
+):
+    """An imported Copernicus product that is active."""
+
+    copernicus: CopernicusProductBlock
diff --git a/gso/products/product_types/geant_ip.py b/gso/products/product_types/geant_ip.py
new file mode 100644
index 000000000..48943b4af
--- /dev/null
+++ b/gso/products/product_types/geant_ip.py
@@ -0,0 +1,42 @@
+"""Product type for IAS."""
+
+from orchestrator.domain.base import SubscriptionModel
+from orchestrator.types import SubscriptionLifecycle
+
+from gso.products.product_blocks.geant_ip import (
+    GeantIpProductBlock,
+    GeantIpProductBlockInactive,
+    GeantIpProductBlockProvisioning,
+)
+
+
+class GeantIpInactive(SubscriptionModel, is_base=True):
+    """A GeantIp product that is inactive."""
+
+    geant_ip: GeantIpProductBlockInactive
+
+
+class GeantIpProvisioning(GeantIpInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
+    """A GeantIp product that is being provisioned."""
+
+    geant_ip: GeantIpProductBlockProvisioning
+
+
+class GeantIp(GeantIpProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
+    """A GeantIp product that is active."""
+
+    geant_ip: GeantIpProductBlock
+
+
+class ImportedGeantIpInactive(SubscriptionModel, is_base=True):
+    """An imported GeantIp product that is inactive."""
+
+    geant_ip: GeantIpInactive
+
+
+class ImportedGeantIp(
+    ImportedGeantIpInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE]
+):
+    """An imported GeantIp product that is active."""
+
+    geant_ip: GeantIpProductBlock
diff --git a/gso/products/product_types/gws.py b/gso/products/product_types/gws.py
new file mode 100644
index 000000000..b79337ee5
--- /dev/null
+++ b/gso/products/product_types/gws.py
@@ -0,0 +1,36 @@
+"""Product type for IAS."""
+
+from orchestrator.domain.base import SubscriptionModel
+from orchestrator.types import SubscriptionLifecycle
+
+from gso.products.product_blocks.gws import GWSProductBlock, GWSProductBlockInactive, GWSProductBlockProvisioning
+
+
+class GWSInactive(SubscriptionModel, is_base=True):
+    """A GWS product that is inactive."""
+
+    gws: GWSProductBlockInactive
+
+
+class GWSProvisioning(GWSInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
+    """An GWS product that is being provisioned."""
+
+    gws: GWSProductBlockProvisioning
+
+
+class GWS(GWSProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
+    """An GWS product that is active."""
+
+    gws: GWSProductBlock
+
+
+class ImportedGWSInactive(SubscriptionModel, is_base=True):
+    """An imported GWS product that is inactive."""
+
+    gws: GWSProductBlockInactive
+
+
+class ImportedGWS(ImportedGWSInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE]):
+    """An imported GWS product that is active."""
+
+    gws: GWSProductBlock
diff --git a/gso/products/product_types/l3_core_service.py b/gso/products/product_types/l3_core_service.py
index b07d6bb03..d36bf2597 100644
--- a/gso/products/product_types/l3_core_service.py
+++ b/gso/products/product_types/l3_core_service.py
@@ -17,7 +17,7 @@ class L3CoreServiceType(strEnum):
     The core services offered include GÉANT IP for R&E access, and the Internet Access Service.
     """
 
-    GEANT_IP = "GÉANT IP"
+    GEANT_IP = "GÉANT IP"  # TODO: PLEASE REMOVE ACCENT
     """GÉANT IP."""
     IMPORTED_GEANT_IP = "IMPORTED GÉANT IP"
     GWS = "GWS"
diff --git a/gso/products/product_types/lhcone.py b/gso/products/product_types/lhcone.py
new file mode 100644
index 000000000..997b8cd15
--- /dev/null
+++ b/gso/products/product_types/lhcone.py
@@ -0,0 +1,42 @@
+"""Product type for LHCONE."""
+
+from orchestrator.domain.base import SubscriptionModel
+from orchestrator.types import SubscriptionLifecycle
+
+from gso.products.product_blocks.lhcone import (
+    LHCOneProductBlock,
+    LHCOneProductBlockInactive,
+    LHCOneProductBlockProvisioning,
+)
+
+
+class LHCOneInactive(SubscriptionModel, is_base=True):
+    """A LHCOne product that is inactive."""
+
+    lhcone: LHCOneProductBlockInactive
+
+
+class LHCOneProvisioning(LHCOneInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
+    """A LHCOne product that is being provisioned."""
+
+    lhcone: LHCOneProductBlockProvisioning
+
+
+class LHCOne(LHCOneProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
+    """A LHCOne product that is active."""
+
+    lhcone: LHCOneProductBlock
+
+
+class ImportedLHCOneInactive(SubscriptionModel, is_base=True):
+    """An imported LHCOne product that is inactive."""
+
+    lhcone: LHCOneInactive
+
+
+class ImportedLHCOne(
+    ImportedLHCOneInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE]
+):
+    """An imported LHCOne product that is active."""
+
+    lhcone: LHCOneProductBlock
-- 
GitLab


From 2b7c06897de98c0b00572b6a72de7b82cb225359 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Wed, 19 Mar 2025 09:23:38 +0100
Subject: [PATCH 13/87] Remove GWS and Imported GWS references from product
 types and migration scripts

---
 .../2025-03-18_c9c9fdf624b5_re_model_ias.py   |  2 --
 gso/products/__init__.py                      |  8 -----
 gso/products/product_blocks/gws.py            | 30 ----------------
 gso/products/product_types/gws.py             | 36 -------------------
 4 files changed, 76 deletions(-)
 delete mode 100644 gso/products/product_blocks/gws.py
 delete mode 100644 gso/products/product_types/gws.py

diff --git a/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py b/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py
index 5ff448768..7094eb041 100644
--- a/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py
+++ b/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py
@@ -2,8 +2,6 @@
 
     GEANT_IP = "GÉANT IP"
     IMPORTED_GEANT_IP = "IMPORTED GÉANT IP"
-    GWS = "GWS"
-    IMPORTED_GWS = "IMPORTED GWS"
     LHCONE = "LHCONE"
     IMPORTED_LHCONE = "IMPORTED LHCONE"
     COPERNICUS = "COPERNICUS"
diff --git a/gso/products/__init__.py b/gso/products/__init__.py
index cf2126343..b1ab4acba 100644
--- a/gso/products/__init__.py
+++ b/gso/products/__init__.py
@@ -11,7 +11,6 @@ from pydantic_forms.types import strEnum
 from gso.products.product_types.copernicus import Copernicus, ImportedCopernicus
 from gso.products.product_types.edge_port import EdgePort, ImportedEdgePort
 from gso.products.product_types.geant_ip import GeantIp, ImportedGeantIp
-from gso.products.product_types.gws import GWS, ImportedGWS
 from gso.products.product_types.ias import IAS, ImportedIAS
 from gso.products.product_types.iptrunk import ImportedIptrunk, Iptrunk
 from gso.products.product_types.l3_core_service import L3CoreService
@@ -66,9 +65,6 @@ class ProductName(strEnum):
     IAS = "IAS"
     """Internet Access Services."""
     IMPORTED_IAS = "Imported IAS"
-    GWS = "GWS"
-    """GÉANT Web Services."""
-    IMPORTED_GWS = "Imported GWS"
     LHCONE = "LHCOne"
     """LHCOne."""
     IMPORTED_LHCONE = "Imported LHCOne"
@@ -120,8 +116,6 @@ class ProductType(strEnum):
     IMPORTED_IAS = ImportedIAS.__name__
     GEANT_IP = GeantIp.__name__
     IMPORTED_GEANT_IP = ImportedGeantIp.__name__
-    GWS = GWS.__name__
-    IMPORTED_GWS = ImportedGWS.__name__
     LHCONE = LHCOne.__name__
     IMPORTED_LHCONE = ImportedLHCOne.__name__
     COPERNICUS = Copernicus.__name__
@@ -153,8 +147,6 @@ SUBSCRIPTION_MODEL_REGISTRY.update(
         ProductName.IMPORTED_GEANT_IP.value: ImportedGeantIp,
         ProductName.IAS.value: IAS,
         ProductName.IMPORTED_IAS.value: ImportedIAS,
-        ProductName.GWS.value: GWS,
-        ProductName.IMPORTED_GWS.value: ImportedGWS,
         ProductName.LHCONE.value: LHCOne,
         ProductName.IMPORTED_LHCONE.value: ImportedLHCOne,
         ProductName.COPERNICUS.value: Copernicus,
diff --git a/gso/products/product_blocks/gws.py b/gso/products/product_blocks/gws.py
deleted file mode 100644
index fd2a00b6d..000000000
--- a/gso/products/product_blocks/gws.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""Product blocks for GWS products."""
-
-from orchestrator.domain.base import ProductBlockModel
-from orchestrator.types import SubscriptionLifecycle
-
-from gso.products.product_blocks.l3_core_service import (
-    L3CoreServiceBlock,
-    L3CoreServiceBlockInactive,
-    L3CoreServiceBlockProvisioning,
-)
-
-
-class GWSProductBlockInactive(
-    ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="GWSProductBlock"
-):
-    """A GWS product block. See `GWSProductBlock`."""
-
-    l3_core: L3CoreServiceBlockInactive | None = None
-
-
-class GWSProductBlockProvisioning(GWSProductBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
-    """A provisioning GWS product block. See `GWSProductBlock`."""
-
-    l3_core: L3CoreServiceBlockProvisioning
-
-
-class GWSProductBlock(GWSProductBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
-    """An active GWS product block."""
-
-    l3_core: L3CoreServiceBlock
diff --git a/gso/products/product_types/gws.py b/gso/products/product_types/gws.py
deleted file mode 100644
index b79337ee5..000000000
--- a/gso/products/product_types/gws.py
+++ /dev/null
@@ -1,36 +0,0 @@
-"""Product type for IAS."""
-
-from orchestrator.domain.base import SubscriptionModel
-from orchestrator.types import SubscriptionLifecycle
-
-from gso.products.product_blocks.gws import GWSProductBlock, GWSProductBlockInactive, GWSProductBlockProvisioning
-
-
-class GWSInactive(SubscriptionModel, is_base=True):
-    """A GWS product that is inactive."""
-
-    gws: GWSProductBlockInactive
-
-
-class GWSProvisioning(GWSInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
-    """An GWS product that is being provisioned."""
-
-    gws: GWSProductBlockProvisioning
-
-
-class GWS(GWSProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
-    """An GWS product that is active."""
-
-    gws: GWSProductBlock
-
-
-class ImportedGWSInactive(SubscriptionModel, is_base=True):
-    """An imported GWS product that is inactive."""
-
-    gws: GWSProductBlockInactive
-
-
-class ImportedGWS(ImportedGWSInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE]):
-    """An imported GWS product that is active."""
-
-    gws: GWSProductBlock
-- 
GitLab


From 6988e9ddfca465dca3e2b36b79213763ec53ed71 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 20 Mar 2025 13:59:43 +0100
Subject: [PATCH 14/87] Add migration scripts for other L3 core services

---
 .../2025-03-18_c9c9fdf624b5_re_model_ias.py   | 449 +++++++++++++-----
 gso/products/__init__.py                      |  12 +-
 gso/products/product_blocks/copernicus.py     |  14 +-
 gso/products/product_blocks/geant_ip.py       |  14 +-
 gso/products/product_blocks/ias.py            |  12 +-
 gso/products/product_blocks/lhcone.py         |  12 +-
 gso/products/product_types/copernicus.py      |  14 +-
 gso/products/product_types/geant_ip.py        |  38 +-
 gso/products/product_types/ias.py             |  16 +-
 gso/products/product_types/lhcone.py          |  14 +-
 10 files changed, 389 insertions(+), 206 deletions(-)

diff --git a/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py b/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py
index 7094eb041..c220e3887 100644
--- a/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py
+++ b/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py
@@ -1,4 +1,4 @@
-"""Re-model L3CoreServices
+"""Re-model L3CoreServices.
 
     GEANT_IP = "GÉANT IP"
     IMPORTED_GEANT_IP = "IMPORTED GÉANT IP"
@@ -10,87 +10,312 @@
 Revision ID: e1afa3790f32
 Revises: b96b0ecf6906
 Create Date: 2025-03-17 10:23:45.917222
-
 """
+
 import sqlalchemy as sa
 from alembic import op
 
-# revision identifiers, used by Alembic.
-revision = 'e1afa3790f32'
-down_revision = 'b96b0ecf6906'
+# revision identifiers, used by Alembic
+revision = "e1afa3790f32"
+down_revision = "b96b0ecf6906"
 branch_labels = None
 depends_on = None
 
+_L3_CORE_SERVICE_DICT = [
+    {
+        "product": {
+            "name": "IAS",
+            "type": "IAS",
+        },
+        "imported_product": {
+            "name": "Imported IAS",
+            "type": "ImportedIAS"
+        },
+        "product_block": {
+            "name": "IASBlock",
+            "description": "An Internet Access Service for general internet access",
+            "tag": "IAS",
+        },
+    },
+    {
+        "product": {
+            "name": "GÉANT IP",
+            "type": "GeantIP"
+        },
+        "imported_product": {
+            "name": "Imported GÉANT IP",
+            "type": "ImportedGeantIP"
+        },
+        "product_block": {
+            "name": "GeantIPBlock",
+            "description": "A GÉANT IP product block",
+            "tag": "G_IP",
+        },
+    },
+    {
+        "product": {
+            "name": "LHCOne",
+            "type": "LHCOne"
+        },
+        "imported_product": {
+            "name": "Imported LHCOne",
+            "type": "ImportedLHCOne"
+        },
+        "product_block": {
+            "name": "LHCOneBlock",
+            "description": "A LHCOne product block",
+            "tag": "LHC",
+        },
+    },
+    {
+        "product": {
+            "name": "Copernicus",
+            "type": "Copernicus"
+        },
+        "imported_product": {
+            "name": "Imported Copernicus",
+            "type": "ImportedCopernicus"
+        },
+        "product_block": {
+            "name": "CopernicusBlock",
+            "description": "A Copernicus product block",
+            "tag": "COP",
+        },
+    },
+]
+
 
 def upgrade() -> None:
+    """Perform the upgrade for L3CoreServices remodeling."""
     conn = op.get_bind()
-    conn.execute(
-        sa.text(
-            """
-DELETE FROM fixed_inputs
-WHERE fixed_inputs.product_id IN (
-    SELECT products.product_id FROM products WHERE products.name IN ('IAS', 'Imported IAS')
-)
-  AND fixed_inputs.name = 'l3_core_service_type'
-            """
+
+    for l3 in _L3_CORE_SERVICE_DICT:
+
+        conn.execute(
+            sa.text(
+                f"""
+                DELETE FROM fixed_inputs
+                WHERE fixed_inputs.product_id IN (
+                    SELECT products.product_id
+                    FROM products
+                    WHERE products.name IN (
+                        '{l3["product"]["name"]}',
+                        '{l3["imported_product"]["name"]}'
+                    )
+                )
+                  AND fixed_inputs.name = 'l3_core_service_type'
+                """
+            )
         )
-    )
-    conn.execute(
-        sa.text(
-            """
-INSERT INTO product_blocks (name, description, tag, status)
-VALUES ('IASProductBlock', 'An Internet Access Service for general internet access', 'IAS', 'active')
-RETURNING product_blocks.product_block_id
-            """
+
+        conn.execute(
+            sa.text(
+                f"""
+                INSERT INTO product_blocks (name, description, tag, status)
+                VALUES (
+                    '{l3["product_block"]["name"]}',
+                    '{l3["product_block"]["description"]}',
+                    '{l3["product_block"]["tag"]}',
+                    'active'
+                )
+                RETURNING product_blocks.product_block_id
+                """
+            )
         )
-    )
-    conn.execute(
-        sa.text(
-            """
-INSERT INTO resource_types (resource_type, description)
-VALUES ('ias_flavor', 'The flavor of the IAS service')
-RETURNING resource_types.resource_type_id
-            """
+
+        if l3["product"]["name"] == "IAS":
+            conn.execute(
+                sa.text(
+                    """
+                    INSERT INTO resource_types (resource_type, description)
+                    VALUES ('ias_flavor', 'The flavor of the IAS service')
+                    RETURNING resource_types.resource_type_id
+                    """
+                )
+            )
+
+        conn.execute(
+            sa.text(
+                f"""
+                INSERT INTO product_block_relations
+                VALUES (
+                    (
+                        SELECT product_blocks.product_block_id
+                        FROM product_blocks
+                        WHERE name = '{l3["product_block"]["name"]}'
+                    ),
+                    (
+                        SELECT product_blocks.product_block_id
+                        FROM product_blocks
+                        WHERE name = 'L3CoreServiceBlock'
+                    ),
+                    NULL,
+                    NULL
+                );
+                """
+            )
         )
-    )
-    conn.execute(
-        sa.text(
-            """ 
-    INSERT INTO product_block_relations
-VALUES (
-    (SELECT product_blocks.product_block_id FROM product_blocks WHERE name = 'IASProductBlock'),
-    (SELECT product_blocks.product_block_id FROM product_blocks WHERE name = 'L3CoreServiceBlock'),
-    NULL,
-    NULL
-);
-            """
+
+        conn.execute(
+            sa.text(
+                f"""
+                UPDATE product_product_blocks
+                SET product_block_id = (
+                    SELECT product_block_id
+                    FROM product_blocks
+                    WHERE name = '{l3["product_block"]["name"]}'
+                )
+                WHERE product_block_id = (
+                    SELECT product_block_id
+                    FROM product_blocks
+                    WHERE name = 'L3CoreServiceBlock'
+                )
+                  AND product_id IN (
+                    SELECT product_id
+                    FROM products
+                    WHERE name IN (
+                        '{l3["product"]["name"]}',
+                        '{l3["imported_product"]["name"]}'
+                    )
+                );
+                """
+            )
         )
-    )
 
-    conn.execute(
-        sa.text(
-            """
-UPDATE product_product_blocks
-SET product_block_id = (
-    SELECT product_block_id FROM product_blocks WHERE name = 'IASProductBlock'
-)
-WHERE product_block_id = (
-    SELECT product_block_id FROM product_blocks WHERE name = 'L3CoreServiceBlock'
-)
-  AND product_id IN (
-    SELECT product_id FROM products WHERE name IN ('IAS', 'Imported IAS')
-);
-            """
+        conn.execute(
+            sa.text(
+                f"""
+                UPDATE products
+                SET product_type = '{l3["product"]["type"]}'
+                WHERE product_type = 'L3CoreService'
+                  AND name = '{l3["product"]["name"]}';
+                """
+            )
         )
-    )
+
+        conn.execute(
+            sa.text(
+                f"""
+                UPDATE products
+                SET product_type = '{l3["imported_product"]["type"]}'
+                WHERE product_type = 'ImportedL3CoreService'
+                  AND name = '{l3["imported_product"]["name"]}';
+                """
+            )
+        )
+
+        conn.execute(
+            sa.text(
+                f"""
+                -- Step 1: Insert new subscription_instances for '{l3["product_block"]["name"]}' and return their IDs
+                WITH inserted AS (
+                    INSERT INTO subscription_instances (subscription_id, product_block_id)
+                    SELECT
+                        s.subscription_id,
+                        (
+                            SELECT product_block_id
+                            FROM product_blocks
+                            WHERE name = '{l3["product_block"]["name"]}'
+                        ) AS product_block_id
+                    FROM subscriptions s
+                    WHERE s.product_id = (
+                        SELECT product_id
+                        FROM products
+                        WHERE products.name = '{l3["product"]["name"]}'
+                    )
+                    RETURNING subscription_instance_id, subscription_id
+                )
+
+                -- Step 2: Link newly inserted instances to the existing L3CoreServiceBlock instance
+                INSERT INTO subscription_instance_relations (
+                    in_use_by_id,
+                    depends_on_id,
+                    order_id,
+                    domain_model_attr
+                )
+                SELECT
+                    i.subscription_instance_id AS in_use_by_id,
+                    e.subscription_instance_id AS depends_on_id,
+                    0 AS order_id,
+                    'l3_core' AS domain_model_attr
+                FROM inserted i
+                JOIN subscription_instances e
+                  ON e.subscription_id = i.subscription_id
+                  AND e.product_block_id = (
+                      SELECT product_block_id
+                      FROM product_blocks
+                      WHERE name = 'L3CoreServiceBlock'
+                  );
+                """
+            )
+        )
+
+        if l3["product"]["name"] == "IAS":
+            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 ('IASBlock')
+                        ),
+                        (
+                            SELECT resource_types.resource_type_id
+                            FROM resource_types
+                            WHERE resource_types.resource_type IN ('ias_flavor')
+                        )
+                    )
+                    """
+                )
+            )
+
+            conn.execute(
+                sa.text(
+                    """
+                    WITH subscription_instance_ids AS (
+                        SELECT subscription_instances.subscription_instance_id
+                        FROM subscription_instances
+                        WHERE subscription_instances.product_block_id IN (
+                            SELECT product_blocks.product_block_id
+                            FROM product_blocks
+                            WHERE product_blocks.name = 'IASBlock'
+                        )
+                    )
+                    INSERT INTO subscription_instance_values (
+                        subscription_instance_id,
+                        resource_type_id,
+                        value
+                    )
+                    SELECT
+                        subscription_instance_ids.subscription_instance_id,
+                        resource_types.resource_type_id,
+                        'IASPS Opt-OUT'
+                    FROM resource_types
+                    CROSS JOIN subscription_instance_ids
+                    WHERE resource_types.resource_type = 'ias_flavor'
+                    """
+                )
+            )
 
     conn.execute(
         sa.text(
             """
-UPDATE products
-SET product_type = 'IAS'
-WHERE product_type = 'L3CoreService'
-  AND name = 'IAS';
+            DELETE FROM processes
+            WHERE processes.pid IN (
+                SELECT processes_subscriptions.pid
+                FROM processes_subscriptions
+                WHERE processes_subscriptions.subscription_id IN (
+                    SELECT subscriptions.subscription_id
+                    FROM subscriptions
+                    WHERE subscriptions.product_id IN (
+                        SELECT products.product_id
+                        FROM products
+                        WHERE products.name IN ('Imported GWS', 'GWS')
+                    )
+                )
+            )
             """
         )
     )
@@ -98,10 +323,16 @@ WHERE product_type = 'L3CoreService'
     conn.execute(
         sa.text(
             """
-UPDATE products
-SET product_type = 'ImportedIAS'
-WHERE product_type = 'ImportedL3CoreService'
-  AND name = 'Imported IAS';
+            DELETE FROM processes_subscriptions
+            WHERE processes_subscriptions.subscription_id IN (
+                SELECT subscriptions.subscription_id
+                FROM subscriptions
+                WHERE subscriptions.product_id IN (
+                    SELECT products.product_id
+                    FROM products
+                    WHERE products.name IN ('Imported GWS', 'GWS')
+                )
+            )
             """
         )
     )
@@ -109,57 +340,29 @@ WHERE product_type = 'ImportedL3CoreService'
     conn.execute(
         sa.text(
             """
--- Step 1: Insert new subscription_instances for 'IASProductBlock' and return their IDs
-WITH inserted AS (
-    INSERT INTO subscription_instances (subscription_id, product_block_id)
-    SELECT
-        s.subscription_id,
-        (
-            SELECT product_block_id
-            FROM product_blocks
-            WHERE name = 'IASProductBlock'
-        ) AS product_block_id
-    FROM subscriptions s
-    WHERE s.product_id = (
-        SELECT product_id
-        FROM products
-        WHERE products.name = 'IAS'
-    )
-    RETURNING subscription_instance_id, subscription_id
-)
-
--- Step 2: Link newly inserted "IASProductBlock" instances to the existing "L3CoreServiceBlock" instance
-INSERT INTO subscription_instance_relations (
-    in_use_by_id,
-    depends_on_id,
-    order_id,
-    domain_model_attr
-)
-SELECT
-    i.subscription_instance_id AS in_use_by_id,
-    e.subscription_instance_id AS depends_on_id,
-    0 AS order_id,
-    'l3_core' AS domain_model_attr
-FROM inserted i
-JOIN subscription_instances e
-  ON e.subscription_id = i.subscription_id
-  AND e.product_block_id = (
-      SELECT product_block_id
-      FROM product_blocks
-      WHERE name = 'L3CoreServiceBlock'
-  );
+            DELETE FROM subscription_instances
+            WHERE subscription_instances.subscription_id IN (
+                SELECT subscriptions.subscription_id
+                FROM subscriptions
+                WHERE subscriptions.product_id IN (
+                    SELECT products.product_id
+                    FROM products
+                    WHERE products.name IN ('Imported GWS', 'GWS')
+                )
+            )
             """
         )
     )
-    
+
     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 ('IASProductBlock')),
-    (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ias_flavor'))
-)
+            DELETE FROM subscriptions
+            WHERE subscriptions.product_id IN (
+                SELECT products.product_id
+                FROM products
+                WHERE products.name IN ('Imported GWS', 'GWS')
+            )
             """
         )
     )
@@ -167,30 +370,14 @@ VALUES (
     conn.execute(
         sa.text(
             """
-
-                WITH subscription_instance_ids AS (
-                    SELECT subscription_instances.subscription_instance_id
-                    FROM subscription_instances
-                    WHERE subscription_instances.product_block_id IN (
-                        SELECT product_blocks.product_block_id
-                        FROM product_blocks
-                        WHERE product_blocks.name = 'IASProductBlock'
-                    )
-                )
-                INSERT INTO subscription_instance_values (subscription_instance_id, resource_type_id, value)
-                SELECT
-                    subscription_instance_ids.subscription_instance_id,
-                    resource_types.resource_type_id,
-                    'None'
-                    -- TODO we need to check this with Simone to see what the default value should be, in case it is empty string, this wont be show up in the GUI
-                FROM resource_types
-                CROSS JOIN subscription_instance_ids
-                WHERE resource_types.resource_type = 'ias_flavor'
-
+            DELETE FROM products
+            WHERE products.name IN ('Imported GWS', 'GWS')
             """
         )
     )
 
 
 def downgrade() -> None:
+    """Perform the downgrade (no actions defined)."""
     conn = op.get_bind()
+    # No downgrade logic provided.
diff --git a/gso/products/__init__.py b/gso/products/__init__.py
index b1ab4acba..df3bb48dd 100644
--- a/gso/products/__init__.py
+++ b/gso/products/__init__.py
@@ -10,7 +10,7 @@ from pydantic_forms.types import strEnum
 
 from gso.products.product_types.copernicus import Copernicus, ImportedCopernicus
 from gso.products.product_types.edge_port import EdgePort, ImportedEdgePort
-from gso.products.product_types.geant_ip import GeantIp, ImportedGeantIp
+from gso.products.product_types.geant_ip import GeantIP, ImportedGeantIP
 from gso.products.product_types.ias import IAS, ImportedIAS
 from gso.products.product_types.iptrunk import ImportedIptrunk, Iptrunk
 from gso.products.product_types.l3_core_service import L3CoreService
@@ -81,7 +81,7 @@ class ProductName(strEnum):
     """VRFs."""
 
 
-L3_CORE_SERVICE_PRODUCT_TYPE = L3CoreService.__name__
+L3_CORE_SERVICE_PRODUCT_TYPE = L3CoreService.__name__  # TODO: Remove this line after removing L3CoreService
 L2_CIRCUIT_PRODUCT_TYPE = Layer2Circuit.__name__
 
 
@@ -114,8 +114,8 @@ class ProductType(strEnum):
     VRF = VRF.__name__
     IAS = IAS.__name__
     IMPORTED_IAS = ImportedIAS.__name__
-    GEANT_IP = GeantIp.__name__
-    IMPORTED_GEANT_IP = ImportedGeantIp.__name__
+    GEANT_IP = GeantIP.__name__
+    IMPORTED_GEANT_IP = ImportedGeantIP.__name__
     LHCONE = LHCOne.__name__
     IMPORTED_LHCONE = ImportedLHCOne.__name__
     COPERNICUS = Copernicus.__name__
@@ -143,8 +143,8 @@ SUBSCRIPTION_MODEL_REGISTRY.update(
         ProductName.IMPORTED_OPENGEAR.value: ImportedOpengear,
         ProductName.EDGE_PORT.value: EdgePort,
         ProductName.IMPORTED_EDGE_PORT.value: ImportedEdgePort,
-        ProductName.GEANT_IP.value: GeantIp,
-        ProductName.IMPORTED_GEANT_IP.value: ImportedGeantIp,
+        ProductName.GEANT_IP.value: GeantIP,
+        ProductName.IMPORTED_GEANT_IP.value: ImportedGeantIP,
         ProductName.IAS.value: IAS,
         ProductName.IMPORTED_IAS.value: ImportedIAS,
         ProductName.LHCONE.value: LHCOne,
diff --git a/gso/products/product_blocks/copernicus.py b/gso/products/product_blocks/copernicus.py
index 3d9926cae..3b6a77cb2 100644
--- a/gso/products/product_blocks/copernicus.py
+++ b/gso/products/product_blocks/copernicus.py
@@ -10,23 +10,21 @@ from gso.products.product_blocks.l3_core_service import (
 )
 
 
-class CopernicusProductBlockInactive(
-    ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="CopernicusProductBlock"
+class CopernicusBlockInactive(
+    ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="CopernicusBlock"
 ):
-    """An inactive Copernicus product block. See `CopernicusProductBlock`."""
+    """An inactive Copernicus product block. See `CopernicusBlock`."""
 
     l3_core: L3CoreServiceBlockInactive | None = None
 
 
-class CopernicusProductBlockProvisioning(
-    CopernicusProductBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]
-):
-    """A provisioning Copernicus product block. See `CopernicusProductBlock`."""
+class CopernicusBlockProvisioning(CopernicusBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
+    """A provisioning Copernicus product block. See `CopernicusBlock`."""
 
     l3_core: L3CoreServiceBlockProvisioning
 
 
-class CopernicusProductBlock(CopernicusProductBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
+class CopernicusBlock(CopernicusBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
     """An active Copernicus product block."""
 
     l3_core: L3CoreServiceBlock
diff --git a/gso/products/product_blocks/geant_ip.py b/gso/products/product_blocks/geant_ip.py
index 63323d959..15865f8bc 100644
--- a/gso/products/product_blocks/geant_ip.py
+++ b/gso/products/product_blocks/geant_ip.py
@@ -10,21 +10,21 @@ from gso.products.product_blocks.l3_core_service import (
 )
 
 
-class GeantIpProductBlockInactive(
-    ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="GeantIpProductBlock"
+class GeantIPBlockInactive(
+    ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="GeantIPBlock"
 ):
-    """An inactive GeantIp product block. See `GeantIpProductBlock`."""
+    """An inactive GeantIP product block. See `GeantIPBlock`."""
 
     l3_core: L3CoreServiceBlockInactive | None = None
 
 
-class GeantIpProductBlockProvisioning(GeantIpProductBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
-    """A provisioning GeantIp product block. See `GeantIpProductBlock`."""
+class GeantIPBlockProvisioning(GeantIPBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
+    """A provisioning GeantIP product block. See `GeantIPBlock`."""
 
     l3_core: L3CoreServiceBlockProvisioning
 
 
-class GeantIpProductBlock(GeantIpProductBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
-    """An active GeantIp product block."""
+class GeantIPBlock(GeantIPBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
+    """An active GeantIP product block."""
 
     l3_core: L3CoreServiceBlock
diff --git a/gso/products/product_blocks/ias.py b/gso/products/product_blocks/ias.py
index 4b8800271..91811e1a7 100644
--- a/gso/products/product_blocks/ias.py
+++ b/gso/products/product_blocks/ias.py
@@ -10,23 +10,21 @@ from gso.products.product_blocks.l3_core_service import (
 )
 
 
-class IASProductBlockInactive(
-    ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="IASProductBlock"
-):
-    """An inactive IAS product block. See `IASProductBlock`."""
+class IASBlockInactive(ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="IASBlock"):
+    """An inactive IAS product block. See `IASBlock`."""
 
     l3_core: L3CoreServiceBlockInactive | None = None
     ias_flavor: str | None = None
 
 
-class IASProductBlockProvisioning(IASProductBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
-    """A provisioning IAS product block. See `IASProductBlock`."""
+class IASBlockProvisioning(IASBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
+    """A provisioning IAS product block. See `IASBlock`."""
 
     l3_core: L3CoreServiceBlockProvisioning
     ias_flavor: str | None = None
 
 
-class IASProductBlock(IASProductBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
+class IASBlock(IASBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
     """An active IAS product block."""
 
     l3_core: L3CoreServiceBlock
diff --git a/gso/products/product_blocks/lhcone.py b/gso/products/product_blocks/lhcone.py
index daa8dde47..48292a5aa 100644
--- a/gso/products/product_blocks/lhcone.py
+++ b/gso/products/product_blocks/lhcone.py
@@ -10,21 +10,21 @@ from gso.products.product_blocks.l3_core_service import (
 )
 
 
-class LHCOneProductBlockInactive(
-    ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="LHCOneProductBlock"
+class LHCOneBlockInactive(
+    ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="LHCOneBlock"
 ):
-    """An inactive LHCOne product block. See `LHCOneProductBlock`."""
+    """An inactive LHCOne product block. See `LHCOneBlock`."""
 
     l3_core: L3CoreServiceBlockInactive | None = None
 
 
-class LHCOneProductBlockProvisioning(LHCOneProductBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
-    """A provisioning LHCOne product block. See `LHCOneProductBlock`."""
+class LHCOneBlockProvisioning(LHCOneBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
+    """A provisioning LHCOne product block. See `LHCOneBlock`."""
 
     l3_core: L3CoreServiceBlockProvisioning
 
 
-class LHCOneProductBlock(LHCOneProductBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
+class LHCOneBlock(LHCOneBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
     """An active LHCOne product block."""
 
     l3_core: L3CoreServiceBlock
diff --git a/gso/products/product_types/copernicus.py b/gso/products/product_types/copernicus.py
index 6fc047852..348f9baa3 100644
--- a/gso/products/product_types/copernicus.py
+++ b/gso/products/product_types/copernicus.py
@@ -4,28 +4,28 @@ from orchestrator.domain.base import SubscriptionModel
 from orchestrator.types import SubscriptionLifecycle
 
 from gso.products.product_blocks.copernicus import (
-    CopernicusProductBlock,
-    CopernicusProductBlockInactive,
-    CopernicusProductBlockProvisioning,
+    CopernicusBlock,
+    CopernicusBlockInactive,
+    CopernicusBlockProvisioning,
 )
 
 
 class CopernicusInactive(SubscriptionModel, is_base=True):
     """A Copernicus product that is inactive."""
 
-    copernicus: CopernicusProductBlockInactive
+    copernicus: CopernicusBlockInactive
 
 
 class CopernicusProvisioning(CopernicusInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
     """A Copernicus product that is being provisioned."""
 
-    copernicus: CopernicusProductBlockProvisioning
+    copernicus: CopernicusBlockProvisioning
 
 
 class Copernicus(CopernicusProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
     """A Copernicus product that is active."""
 
-    copernicus: CopernicusProductBlock
+    copernicus: CopernicusBlock
 
 
 class ImportedCopernicusInactive(SubscriptionModel, is_base=True):
@@ -39,4 +39,4 @@ class ImportedCopernicus(
 ):
     """An imported Copernicus product that is active."""
 
-    copernicus: CopernicusProductBlock
+    copernicus: CopernicusBlock
diff --git a/gso/products/product_types/geant_ip.py b/gso/products/product_types/geant_ip.py
index 48943b4af..0d7f4858b 100644
--- a/gso/products/product_types/geant_ip.py
+++ b/gso/products/product_types/geant_ip.py
@@ -4,39 +4,39 @@ from orchestrator.domain.base import SubscriptionModel
 from orchestrator.types import SubscriptionLifecycle
 
 from gso.products.product_blocks.geant_ip import (
-    GeantIpProductBlock,
-    GeantIpProductBlockInactive,
-    GeantIpProductBlockProvisioning,
+    GeantIPBlock,
+    GeantIPBlockInactive,
+    GeantIPBlockProvisioning,
 )
 
 
-class GeantIpInactive(SubscriptionModel, is_base=True):
-    """A GeantIp product that is inactive."""
+class GeantIPInactive(SubscriptionModel, is_base=True):
+    """A GeantIP product that is inactive."""
 
-    geant_ip: GeantIpProductBlockInactive
+    geant_ip: GeantIPBlockInactive
 
 
-class GeantIpProvisioning(GeantIpInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
-    """A GeantIp product that is being provisioned."""
+class GeantIPProvisioning(GeantIPInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
+    """A GeantIP product that is being provisioned."""
 
-    geant_ip: GeantIpProductBlockProvisioning
+    geant_ip: GeantIPBlockProvisioning
 
 
-class GeantIp(GeantIpProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
-    """A GeantIp product that is active."""
+class GeantIP(GeantIPProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
+    """A GeantIP product that is active."""
 
-    geant_ip: GeantIpProductBlock
+    geant_ip: GeantIPBlock
 
 
-class ImportedGeantIpInactive(SubscriptionModel, is_base=True):
-    """An imported GeantIp product that is inactive."""
+class ImportedGeantIPInactive(SubscriptionModel, is_base=True):
+    """An imported GeantIP product that is inactive."""
 
-    geant_ip: GeantIpInactive
+    geant_ip: GeantIPInactive
 
 
-class ImportedGeantIp(
-    ImportedGeantIpInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE]
+class ImportedGeantIP(
+    ImportedGeantIPInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE]
 ):
-    """An imported GeantIp product that is active."""
+    """An imported GeantIP product that is active."""
 
-    geant_ip: GeantIpProductBlock
+    geant_ip: GeantIPBlock
diff --git a/gso/products/product_types/ias.py b/gso/products/product_types/ias.py
index f309dccb2..f67a74c5b 100644
--- a/gso/products/product_types/ias.py
+++ b/gso/products/product_types/ias.py
@@ -4,37 +4,37 @@ from orchestrator.domain.base import SubscriptionModel
 from orchestrator.types import SubscriptionLifecycle
 
 from gso.products.product_blocks.ias import (
-    IASProductBlock,
-    IASProductBlockInactive,
-    IASProductBlockProvisioning,
+    IASBlock,
+    IASBlockInactive,
+    IASBlockProvisioning,
 )
 
 
 class IASInactive(SubscriptionModel, is_base=True):
     """An IAS product that is inactive."""
 
-    ias: IASProductBlockInactive
+    ias: IASBlockInactive
 
 
 class IASProvisioning(IASInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
     """An IAS product that is being provisioned."""
 
-    ias: IASProductBlockProvisioning
+    ias: IASBlockProvisioning
 
 
 class IAS(IASProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
     """An IAS product that is active."""
 
-    ias: IASProductBlock
+    ias: IASBlock
 
 
 class ImportedIASInactive(SubscriptionModel, is_base=True):
     """An imported IAS product that is inactive."""
 
-    ias: IASProductBlockInactive
+    ias: IASBlockInactive
 
 
 class ImportedIAS(ImportedIASInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE]):
     """An imported IAS product that is active."""
 
-    ias: IASProductBlock
+    ias: IASBlock
diff --git a/gso/products/product_types/lhcone.py b/gso/products/product_types/lhcone.py
index 997b8cd15..ab4f8ea8e 100644
--- a/gso/products/product_types/lhcone.py
+++ b/gso/products/product_types/lhcone.py
@@ -4,28 +4,28 @@ from orchestrator.domain.base import SubscriptionModel
 from orchestrator.types import SubscriptionLifecycle
 
 from gso.products.product_blocks.lhcone import (
-    LHCOneProductBlock,
-    LHCOneProductBlockInactive,
-    LHCOneProductBlockProvisioning,
+    LHCOneBlock,
+    LHCOneBlockInactive,
+    LHCOneBlockProvisioning,
 )
 
 
 class LHCOneInactive(SubscriptionModel, is_base=True):
     """A LHCOne product that is inactive."""
 
-    lhcone: LHCOneProductBlockInactive
+    lhcone: LHCOneBlockInactive
 
 
 class LHCOneProvisioning(LHCOneInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
     """A LHCOne product that is being provisioned."""
 
-    lhcone: LHCOneProductBlockProvisioning
+    lhcone: LHCOneBlockProvisioning
 
 
 class LHCOne(LHCOneProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
     """A LHCOne product that is active."""
 
-    lhcone: LHCOneProductBlock
+    lhcone: LHCOneBlock
 
 
 class ImportedLHCOneInactive(SubscriptionModel, is_base=True):
@@ -39,4 +39,4 @@ class ImportedLHCOne(
 ):
     """An imported LHCOne product that is active."""
 
-    lhcone: LHCOneProductBlock
+    lhcone: LHCOneBlock
-- 
GitLab


From de25d748f0f7413c9d62fd2f6a083df57cf02de0 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Thu, 20 Mar 2025 14:42:01 +0100
Subject: [PATCH 15/87] Update the based l3 core services functionality

---
 .../base_create_l3_core_service.py            | 27 ++++++++++++-------
 .../l3_core_service/ias/create_ias.py         |  2 +-
 2 files changed, 19 insertions(+), 10 deletions(-)

diff --git a/gso/workflows/l3_core_service/base_create_l3_core_service.py b/gso/workflows/l3_core_service/base_create_l3_core_service.py
index 7ab0ef568..ba543a669 100644
--- a/gso/workflows/l3_core_service/base_create_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_l3_core_service.py
@@ -166,6 +166,7 @@ def initialize_service_binding(
     edge_port: dict,
     binding_port_input: dict,
     product_name: str,
+    service_name: str,
 ) -> dict:
     """Initialize a service binding port for a given service type in the subscription model."""
     edge_port_fqdn_list = []
@@ -195,14 +196,15 @@ def initialize_service_binding(
         gs_id=sbp_gs_id,
     )
 
-    subscription.l3_core.ap_list.append(
-        AccessPortInactive.new(
-            subscription_id=uuid4(),
-            ap_type=edge_port["ap_type"],
-            sbp=service_binding_port,
-            custom_service_name=edge_port.get("custom_service_name"),
+    if service_name := getattr(subscription, service_name):
+        service_name.l3_core.ap_list.append(
+            AccessPortInactive.new(
+                subscription_id=uuid4(),
+                ap_type=edge_port["ap_type"],
+                sbp=service_binding_port,
+                custom_service_name=edge_port.get("custom_service_name"),
+            )
         )
-    )
 
     edge_port_fqdn_list.append(edge_port_subscription.edge_port.node.router_fqdn)
 
@@ -332,9 +334,16 @@ def update_dns_records(subscription: L3CoreService | IAS) -> State:
 
 
 @step("Create a new SharePoint checklist item")
-def create_new_sharepoint_checklist(subscription: L3CoreService, tt_number: TTNumber, process_id: UUIDstr) -> State:
+def create_new_sharepoint_checklist(
+        subscription: L3CoreService,
+        tt_number: TTNumber,
+        process_id: UUIDstr,
+        service_name: str,
+) -> State:
     """Create a new checklist item in SharePoint for approving this L3 Core Service."""
-    new_ep = subscription.l3_core_service.ap_list[0].sbp.edge_port
+    service_name = getattr(subscription, service_name)
+    if service_name := getattr(subscription, service_name):
+        new_ep = subscription.l3_core_service.ap_list[0].sbp.edge_port
     new_list_item_url = SharePointClient().add_list_item(
         list_name="l3_core_service",
         fields={
diff --git a/gso/workflows/l3_core_service/ias/create_ias.py b/gso/workflows/l3_core_service/ias/create_ias.py
index c864d100f..e61b7dc41 100644
--- a/gso/workflows/l3_core_service/ias/create_ias.py
+++ b/gso/workflows/l3_core_service/ias/create_ias.py
@@ -45,7 +45,7 @@ def create_subscription(product: UUIDstr, partner: str) -> State:
     """Create a new subscription object in the database."""
     subscription = IASInactive.from_product_id(product, partner)
 
-    return {"subscription": subscription, "subscription_id": subscription.subscription_id}
+    return {"subscription": subscription, "subscription_id": subscription.subscription_id, "service_name": "ias"}
 
 
 @step("Initialize subscription")
-- 
GitLab


From 210da50026ab3edc39acb10311d813d54ebeee73 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Fri, 21 Mar 2025 10:16:31 +0100
Subject: [PATCH 16/87] Upgrade base create core service modul

---
 .../l3_core_service/base_create_l3_core_service.py  | 13 ++++++-------
 gso/workflows/l3_core_service/ias/create_ias.py     |  7 ++++---
 2 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/gso/workflows/l3_core_service/base_create_l3_core_service.py b/gso/workflows/l3_core_service/base_create_l3_core_service.py
index ba543a669..529b07ac9 100644
--- a/gso/workflows/l3_core_service/base_create_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_l3_core_service.py
@@ -14,6 +14,7 @@ from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPType
 from gso.products.product_blocks.l3_core_service import AccessPortInactive
 from gso.products.product_blocks.service_binding_port import BFDSettings, ServiceBindingPortInactive
 from gso.products.product_types.edge_port import EdgePort
+from gso.products.product_types.geant_ip import GeantIPInactive
 from gso.products.product_types.ias import IAS, IASInactive
 from gso.products.product_types.l3_core_service import L3CoreService, L3CoreServiceInactive
 from gso.services.lso_client import LSOState
@@ -162,7 +163,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
 
 
 def initialize_service_binding(
-    subscription: IASInactive | L3CoreServiceInactive,
+    subscription: IASInactive | L3CoreServiceInactive | GeantIPInactive,
     edge_port: dict,
     binding_port_input: dict,
     product_name: str,
@@ -196,8 +197,8 @@ def initialize_service_binding(
         gs_id=sbp_gs_id,
     )
 
-    if service_name := getattr(subscription, service_name):
-        service_name.l3_core.ap_list.append(
+    if service := getattr(subscription, service_name):
+        service.l3_core.ap_list.append(
             AccessPortInactive.new(
                 subscription_id=uuid4(),
                 ap_type=edge_port["ap_type"],
@@ -341,16 +342,14 @@ def create_new_sharepoint_checklist(
         service_name: str,
 ) -> State:
     """Create a new checklist item in SharePoint for approving this L3 Core Service."""
-    service_name = getattr(subscription, service_name)
-    if service_name := getattr(subscription, service_name):
-        new_ep = subscription.l3_core_service.ap_list[0].sbp.edge_port
+    service = getattr(subscription, service_name)
+    new_ep = service.l3_core.ap_list[0].sbp.edge_port
     new_list_item_url = SharePointClient().add_list_item(
         list_name="l3_core_service",
         fields={
             "Title": f"{subscription.description}",
             "TT_NUMBER": tt_number,
             "ACTIVITY_TYPE": "Creation",
-            "PRODUCT_TYPE": subscription.l3_core_service_type,
             "LOCATION": f"{new_ep.edge_port_name} {new_ep.edge_port_description} on {new_ep.node.router_fqdn}",
             "GAP_PROCESS_URL": f"{load_oss_params().GENERAL.public_hostname}/workflows/{process_id}",
         },
diff --git a/gso/workflows/l3_core_service/ias/create_ias.py b/gso/workflows/l3_core_service/ias/create_ias.py
index e61b7dc41..aa993288b 100644
--- a/gso/workflows/l3_core_service/ias/create_ias.py
+++ b/gso/workflows/l3_core_service/ias/create_ias.py
@@ -1,4 +1,4 @@
-"""Create a new L3 Core Service subscription including GÉANT IP and IAS."""
+"""Create a new IAS workflow."""
 
 from orchestrator.forms import FormPage
 from orchestrator.targets import Target
@@ -50,11 +50,12 @@ def create_subscription(product: UUIDstr, partner: str) -> State:
 
 @step("Initialize subscription")
 def initialize_subscription(
-    subscription: IASInactive, edge_port: dict, binding_port_input: dict, product_name: str, ias_flavor: str
+        subscription: IASInactive, edge_port: dict, binding_port_input: dict, product_name: str, ias_flavor: str,
+        service_name: str
 ) -> State:
     """Take all user inputs and use them to populate the subscription model."""
     subscription.ias.ias_flavor = ias_flavor
-    return initialize_service_binding(subscription, edge_port, binding_port_input, product_name)
+    return initialize_service_binding(subscription, edge_port, binding_port_input, product_name, service_name)
 
 
 @workflow(
-- 
GitLab


From 1abc52dbe8c6a8209ca9aae71439c2c172244bcc Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Fri, 21 Mar 2025 10:17:08 +0100
Subject: [PATCH 17/87] Add GEANT_IP WFs

---
 .../l3_core_service/geant_ip/__init__.py      |  1 +
 .../geant_ip/create_geant_ip.py               | 68 +++++++++++++++++++
 .../geant_ip/modify_geant_ip.py               | 40 +++++++++++
 .../geant_ip/terminate_geant_ip.py            | 40 +++++++++++
 gso/workflows/l3_core_service/ias/__init__.py |  2 +-
 5 files changed, 150 insertions(+), 1 deletion(-)
 create mode 100644 gso/workflows/l3_core_service/geant_ip/__init__.py
 create mode 100644 gso/workflows/l3_core_service/geant_ip/create_geant_ip.py
 create mode 100644 gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py
 create mode 100644 gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py

diff --git a/gso/workflows/l3_core_service/geant_ip/__init__.py b/gso/workflows/l3_core_service/geant_ip/__init__.py
new file mode 100644
index 000000000..5db92fcc1
--- /dev/null
+++ b/gso/workflows/l3_core_service/geant_ip/__init__.py
@@ -0,0 +1 @@
+"""GÉANT IP service workflows."""
diff --git a/gso/workflows/l3_core_service/geant_ip/create_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/create_geant_ip.py
new file mode 100644
index 000000000..3530558ff
--- /dev/null
+++ b/gso/workflows/l3_core_service/geant_ip/create_geant_ip.py
@@ -0,0 +1,68 @@
+"""Create GÉANT IP subscription workflow."""
+
+from orchestrator.targets import Target
+from orchestrator.types import SubscriptionLifecycle
+from orchestrator.workflow import StepList, begin, done, 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_forms.types import State, UUIDstr
+
+from gso.products.product_types.geant_ip import GeantIPInactive
+from gso.services.lso_client import lso_interaction
+from gso.utils.workflow_steps import prompt_sharepoint_checklist_url, start_moodi, stop_moodi
+from gso.workflows.l3_core_service.base_create_l3_core_service import (
+    check_bgp_peers,
+    check_sbp_functionality,
+    create_new_sharepoint_checklist,
+    deploy_bgp_peers_dry,
+    deploy_bgp_peers_real,
+    initialize_service_binding,
+    provision_sbp_dry,
+    provision_sbp_real,
+    update_dns_records, initial_input_form_generator,
+)
+
+
+@step("Create subscription")
+def create_subscription(product: UUIDstr, partner: str) -> State:
+    """Create a new subscription object in the database."""
+    subscription = GeantIPInactive.from_product_id(product, partner)
+
+    return {"subscription": subscription, "subscription_id": subscription.subscription_id, "service_name": "geant_ip"}
+
+
+@step("Initialize subscription")
+def initialize_subscription(
+        subscription: GeantIPInactive, edge_port: dict, binding_port_input: dict, product_name: str, service_name: str
+) -> State:
+    """Take all user inputs and use them to populate the subscription model."""
+    return initialize_service_binding(subscription, edge_port, binding_port_input, product_name, service_name)
+
+
+@workflow(
+    "Create GÉANT IP",
+    initial_input_form=wrap_create_initial_input_form(initial_input_form_generator),
+    target=Target.CREATE,
+)
+def create_geant_ip() -> StepList:
+    """Create a new GÉANT IP subscription."""
+    return (
+            begin
+            >> create_subscription
+            >> store_process_subscription(Target.CREATE)
+            >> initialize_subscription
+            >> start_moodi()
+            >> lso_interaction(provision_sbp_dry)
+            >> lso_interaction(provision_sbp_real)
+            >> lso_interaction(check_sbp_functionality)
+            >> lso_interaction(deploy_bgp_peers_dry)
+            >> lso_interaction(deploy_bgp_peers_real)
+            >> lso_interaction(check_bgp_peers)
+            >> update_dns_records
+            >> set_status(SubscriptionLifecycle.ACTIVE)
+            >> resync
+            >> create_new_sharepoint_checklist
+            >> prompt_sharepoint_checklist_url
+            >> stop_moodi()
+            >> done
+    )
diff --git a/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py
new file mode 100644
index 000000000..3ec646cba
--- /dev/null
+++ b/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py
@@ -0,0 +1,40 @@
+"""Modification workflow for a GÉANT IP  subscription."""
+
+from orchestrator import begin, conditional, done, workflow
+from orchestrator.targets import Target
+from orchestrator.workflow import StepList
+from orchestrator.workflows.steps import resync, store_process_subscription, unsync
+from orchestrator.workflows.utils import wrap_modify_initial_input_form
+
+from gso.workflows.l3_core_service.base_modify_l3_core_service import (
+    Operation,
+    create_new_sbp,
+    modify_existing_sbp,
+    remove_old_sbp,
+)
+from gso.workflows.l3_core_service.base_modify_l3_core_service import (
+    initial_input_form_generator as base_initial_input_form_generator,
+)
+
+
+@workflow(
+    "Modify GÉANT IP ",
+    initial_input_form=wrap_modify_initial_input_form(base_initial_input_form_generator),
+    target=Target.MODIFY,
+)
+def modify_geant_ip() -> StepList:
+    """Modify GÉANT IP  subscription."""
+    access_port_is_added = conditional(lambda state: state["operation"] == Operation.ADD)
+    access_port_is_removed = conditional(lambda state: state["operation"] == Operation.REMOVE)
+    access_port_is_modified = conditional(lambda state: state["operation"] == Operation.EDIT)
+
+    return (
+        begin
+        >> store_process_subscription(Target.MODIFY)
+        >> unsync
+        >> access_port_is_added(create_new_sbp)
+        >> access_port_is_removed(remove_old_sbp)
+        >> access_port_is_modified(modify_existing_sbp)
+        >> resync
+        >> done
+    )
diff --git a/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py
new file mode 100644
index 000000000..7d38a0089
--- /dev/null
+++ b/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py
@@ -0,0 +1,40 @@
+"""Workflow for terminating an GÉANT IP subscription."""
+
+from orchestrator import begin, workflow
+from orchestrator.forms import SubmitFormPage
+from orchestrator.targets import Target
+from orchestrator.types import SubscriptionLifecycle
+from orchestrator.workflow import StepList, done
+from orchestrator.workflows.steps import resync, set_status, store_process_subscription, unsync
+from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from pydantic_forms.types import FormGenerator, UUIDstr
+
+from gso.products.product_types.geant_ip import GeantIP
+from gso.utils.types.tt_number import TTNumber
+
+
+def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+    subscription = GeantIP.from_subscription(subscription_id)
+
+    class TerminateForm(SubmitFormPage):
+        tt_number: TTNumber
+
+    yield TerminateForm
+    return {"subscription": subscription}
+
+
+@workflow(
+    "Terminate GÉANT IP",
+    initial_input_form=wrap_modify_initial_input_form(_input_form_generator),
+    target=Target.TERMINATE,
+)
+def terminate_geeant_ip() -> StepList:
+    """Terminate an GÉANT IP subscription."""
+    return (
+            begin
+            >> store_process_subscription(Target.TERMINATE)
+            >> unsync
+            >> set_status(SubscriptionLifecycle.TERMINATED)
+            >> resync
+            >> done
+    )
diff --git a/gso/workflows/l3_core_service/ias/__init__.py b/gso/workflows/l3_core_service/ias/__init__.py
index 6143fba11..a39a157a3 100644
--- a/gso/workflows/l3_core_service/ias/__init__.py
+++ b/gso/workflows/l3_core_service/ias/__init__.py
@@ -1 +1 @@
-"""Layer 3 core service workflows."""
+"""IAS Service Workflow."""
-- 
GitLab


From 696bca442d6dfeb5fe24ec3e18393515725f48f1 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Fri, 21 Mar 2025 10:23:34 +0100
Subject: [PATCH 18/87] Add LHCOne WFs

---
 .../base_create_l3_core_service.py            |  3 +-
 .../l3_core_service/lhcone/__init__.py        |  1 +
 .../l3_core_service/lhcone/create_lhcone.py   | 68 +++++++++++++++++++
 .../l3_core_service/lhcone/modify_lhcone.py   | 40 +++++++++++
 .../lhcone/terminate_lhcone.py                | 40 +++++++++++
 5 files changed, 151 insertions(+), 1 deletion(-)
 create mode 100644 gso/workflows/l3_core_service/lhcone/__init__.py
 create mode 100644 gso/workflows/l3_core_service/lhcone/create_lhcone.py
 create mode 100644 gso/workflows/l3_core_service/lhcone/modify_lhcone.py
 create mode 100644 gso/workflows/l3_core_service/lhcone/terminate_lhcone.py

diff --git a/gso/workflows/l3_core_service/base_create_l3_core_service.py b/gso/workflows/l3_core_service/base_create_l3_core_service.py
index 529b07ac9..dc678f8c3 100644
--- a/gso/workflows/l3_core_service/base_create_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_l3_core_service.py
@@ -17,6 +17,7 @@ from gso.products.product_types.edge_port import EdgePort
 from gso.products.product_types.geant_ip import GeantIPInactive
 from gso.products.product_types.ias import IAS, IASInactive
 from gso.products.product_types.l3_core_service import L3CoreService, L3CoreServiceInactive
+from gso.products.product_types.lhcone import LHCOneInactive
 from gso.services.lso_client import LSOState
 from gso.services.partners import get_partner_by_id
 from gso.services.sharepoint import SharePointClient
@@ -163,7 +164,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
 
 
 def initialize_service_binding(
-    subscription: IASInactive | L3CoreServiceInactive | GeantIPInactive,
+    subscription: IASInactive | L3CoreServiceInactive | GeantIPInactive | LHCOneInactive,
     edge_port: dict,
     binding_port_input: dict,
     product_name: str,
diff --git a/gso/workflows/l3_core_service/lhcone/__init__.py b/gso/workflows/l3_core_service/lhcone/__init__.py
new file mode 100644
index 000000000..848a9f861
--- /dev/null
+++ b/gso/workflows/l3_core_service/lhcone/__init__.py
@@ -0,0 +1 @@
+"""LHCONE service workflows."""
diff --git a/gso/workflows/l3_core_service/lhcone/create_lhcone.py b/gso/workflows/l3_core_service/lhcone/create_lhcone.py
new file mode 100644
index 000000000..485acb0be
--- /dev/null
+++ b/gso/workflows/l3_core_service/lhcone/create_lhcone.py
@@ -0,0 +1,68 @@
+"""Create LHCOne subscription workflow."""
+
+from orchestrator.targets import Target
+from orchestrator.types import SubscriptionLifecycle
+from orchestrator.workflow import StepList, begin, done, 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_forms.types import State, UUIDstr
+
+from gso.products.product_types.lhcone import LHCOneInactive
+from gso.services.lso_client import lso_interaction
+from gso.utils.workflow_steps import prompt_sharepoint_checklist_url, start_moodi, stop_moodi
+from gso.workflows.l3_core_service.base_create_l3_core_service import (
+    check_bgp_peers,
+    check_sbp_functionality,
+    create_new_sharepoint_checklist,
+    deploy_bgp_peers_dry,
+    deploy_bgp_peers_real,
+    initialize_service_binding,
+    provision_sbp_dry,
+    provision_sbp_real,
+    update_dns_records, initial_input_form_generator,
+)
+
+
+@step("Create subscription")
+def create_subscription(product: UUIDstr, partner: str) -> State:
+    """Create a new subscription object in the database."""
+    subscription = LHCOneInactive.from_product_id(product, partner)
+
+    return {"subscription": subscription, "subscription_id": subscription.subscription_id, "service_name": "lhcone"}
+
+
+@step("Initialize subscription")
+def initialize_subscription(
+        subscription: LHCOneInactive, edge_port: dict, binding_port_input: dict, product_name: str, service_name: str
+) -> State:
+    """Take all user inputs and use them to populate the subscription model."""
+    return initialize_service_binding(subscription, edge_port, binding_port_input, product_name, service_name)
+
+
+@workflow(
+    "Create LHCONE",
+    initial_input_form=wrap_create_initial_input_form(initial_input_form_generator),
+    target=Target.CREATE,
+)
+def create_lhcone() -> StepList:
+    """Create a new LHCONE subscription."""
+    return (
+            begin
+            >> create_subscription
+            >> store_process_subscription(Target.CREATE)
+            >> initialize_subscription
+            >> start_moodi()
+            >> lso_interaction(provision_sbp_dry)
+            >> lso_interaction(provision_sbp_real)
+            >> lso_interaction(check_sbp_functionality)
+            >> lso_interaction(deploy_bgp_peers_dry)
+            >> lso_interaction(deploy_bgp_peers_real)
+            >> lso_interaction(check_bgp_peers)
+            >> update_dns_records
+            >> set_status(SubscriptionLifecycle.ACTIVE)
+            >> resync
+            >> create_new_sharepoint_checklist
+            >> prompt_sharepoint_checklist_url
+            >> stop_moodi()
+            >> done
+    )
diff --git a/gso/workflows/l3_core_service/lhcone/modify_lhcone.py b/gso/workflows/l3_core_service/lhcone/modify_lhcone.py
new file mode 100644
index 000000000..14d9e9a7b
--- /dev/null
+++ b/gso/workflows/l3_core_service/lhcone/modify_lhcone.py
@@ -0,0 +1,40 @@
+"""Modification workflow for a LHCOne  subscription."""
+
+from orchestrator import begin, conditional, done, workflow
+from orchestrator.targets import Target
+from orchestrator.workflow import StepList
+from orchestrator.workflows.steps import resync, store_process_subscription, unsync
+from orchestrator.workflows.utils import wrap_modify_initial_input_form
+
+from gso.workflows.l3_core_service.base_modify_l3_core_service import (
+    Operation,
+    create_new_sbp,
+    modify_existing_sbp,
+    remove_old_sbp,
+)
+from gso.workflows.l3_core_service.base_modify_l3_core_service import (
+    initial_input_form_generator as base_initial_input_form_generator,
+)
+
+
+@workflow(
+    "Modify LHCOne ",
+    initial_input_form=wrap_modify_initial_input_form(base_initial_input_form_generator),
+    target=Target.MODIFY,
+)
+def modify_lhcone() -> StepList:
+    """Modify LHCOne  subscription."""
+    access_port_is_added = conditional(lambda state: state["operation"] == Operation.ADD)
+    access_port_is_removed = conditional(lambda state: state["operation"] == Operation.REMOVE)
+    access_port_is_modified = conditional(lambda state: state["operation"] == Operation.EDIT)
+
+    return (
+        begin
+        >> store_process_subscription(Target.MODIFY)
+        >> unsync
+        >> access_port_is_added(create_new_sbp)
+        >> access_port_is_removed(remove_old_sbp)
+        >> access_port_is_modified(modify_existing_sbp)
+        >> resync
+        >> done
+    )
diff --git a/gso/workflows/l3_core_service/lhcone/terminate_lhcone.py b/gso/workflows/l3_core_service/lhcone/terminate_lhcone.py
new file mode 100644
index 000000000..0289abb26
--- /dev/null
+++ b/gso/workflows/l3_core_service/lhcone/terminate_lhcone.py
@@ -0,0 +1,40 @@
+"""Workflow for terminating an LHCOne subscription."""
+
+from orchestrator import begin, workflow
+from orchestrator.forms import SubmitFormPage
+from orchestrator.targets import Target
+from orchestrator.types import SubscriptionLifecycle
+from orchestrator.workflow import StepList, done
+from orchestrator.workflows.steps import resync, set_status, store_process_subscription, unsync
+from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from pydantic_forms.types import FormGenerator, UUIDstr
+
+from gso.products.product_types.lhcone import LHCOne
+from gso.utils.types.tt_number import TTNumber
+
+
+def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+    subscription = LHCOne.from_subscription(subscription_id)
+
+    class TerminateForm(SubmitFormPage):
+        tt_number: TTNumber
+
+    yield TerminateForm
+    return {"subscription": subscription}
+
+
+@workflow(
+    "Terminate LHCOne",
+    initial_input_form=wrap_modify_initial_input_form(_input_form_generator),
+    target=Target.TERMINATE,
+)
+def terminate_lhcone() -> StepList:
+    """Terminate an LHCOne subscription."""
+    return (
+            begin
+            >> store_process_subscription(Target.TERMINATE)
+            >> unsync
+            >> set_status(SubscriptionLifecycle.TERMINATED)
+            >> resync
+            >> done
+    )
-- 
GitLab


From 1d45e1c33887dffaf3a16525076e3a06196de0dd Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Mon, 24 Mar 2025 12:59:32 +0100
Subject: [PATCH 19/87] rename L3 product blocks

---
 .../2025-03-18_c9c9fdf624b5_re_model_ias.py   |   4 +-
 gso/products/product_blocks/ias.py            |  15 +-
 .../base_create_l3_core_service.py            |   2 +-
 .../geant_ip/migrate_ias_service.py           | 419 ++++++++++++++++++
 .../l3_core_service/ias/create_ias.py         |   5 +-
 5 files changed, 437 insertions(+), 8 deletions(-)
 create mode 100644 gso/workflows/l3_core_service/geant_ip/migrate_ias_service.py

diff --git a/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py b/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py
index c220e3887..a28404426 100644
--- a/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py
+++ b/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py
@@ -21,7 +21,7 @@ down_revision = "b96b0ecf6906"
 branch_labels = None
 depends_on = None
 
-_L3_CORE_SERVICE_DICT = [
+_L3_CORE_SERVICES = [
     {
         "product": {
             "name": "IAS",
@@ -89,7 +89,7 @@ def upgrade() -> None:
     """Perform the upgrade for L3CoreServices remodeling."""
     conn = op.get_bind()
 
-    for l3 in _L3_CORE_SERVICE_DICT:
+    for l3 in _L3_CORE_SERVICES:
 
         conn.execute(
             sa.text(
diff --git a/gso/products/product_blocks/ias.py b/gso/products/product_blocks/ias.py
index 91811e1a7..615fc58dd 100644
--- a/gso/products/product_blocks/ias.py
+++ b/gso/products/product_blocks/ias.py
@@ -2,6 +2,7 @@
 
 from orchestrator.domain.base import ProductBlockModel
 from orchestrator.types import SubscriptionLifecycle
+from pydantic_forms.types import strEnum
 
 from gso.products.product_blocks.l3_core_service import (
     L3CoreServiceBlock,
@@ -10,22 +11,30 @@ from gso.products.product_blocks.l3_core_service import (
 )
 
 
+class IASFlavor(strEnum):
+    """IAS flavors."""
+
+    IAS_PS_OPT_OUT = "IASPS Opt-OUT"
+    IAS_PS_OPT_IN = "IASPS  Opt-IN"
+    IASGWS = "IASGWS"
+
+
 class IASBlockInactive(ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="IASBlock"):
     """An inactive IAS product block. See `IASBlock`."""
 
     l3_core: L3CoreServiceBlockInactive | None = None
-    ias_flavor: str | None = None
+    ias_flavor: IASFlavor | None = IASFlavor.IAS_PS_OPT_OUT
 
 
 class IASBlockProvisioning(IASBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
     """A provisioning IAS product block. See `IASBlock`."""
 
     l3_core: L3CoreServiceBlockProvisioning
-    ias_flavor: str | None = None
+    ias_flavor: IASFlavor
 
 
 class IASBlock(IASBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
     """An active IAS product block."""
 
     l3_core: L3CoreServiceBlock
-    ias_flavor: str | None = None
+    ias_flavor: IASFlavor
diff --git a/gso/workflows/l3_core_service/base_create_l3_core_service.py b/gso/workflows/l3_core_service/base_create_l3_core_service.py
index dc678f8c3..fb631058f 100644
--- a/gso/workflows/l3_core_service/base_create_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_l3_core_service.py
@@ -198,7 +198,7 @@ def initialize_service_binding(
         gs_id=sbp_gs_id,
     )
 
-    if service := getattr(subscription, service_name):
+    if service := getattr(subscription, service_name):  # TODO: I think we should remove this if statement
         service.l3_core.ap_list.append(
             AccessPortInactive.new(
                 subscription_id=uuid4(),
diff --git a/gso/workflows/l3_core_service/geant_ip/migrate_ias_service.py b/gso/workflows/l3_core_service/geant_ip/migrate_ias_service.py
new file mode 100644
index 000000000..3f97983c5
--- /dev/null
+++ b/gso/workflows/l3_core_service/geant_ip/migrate_ias_service.py
@@ -0,0 +1,419 @@
+"""A modification workflow that migrates a L3 Core Service to a new Edge Port.
+
+In one run of a migration workflow, only a single Access Port can be replaced. This is due to the nature of these
+services. When a service is dual homed for example, only one of the Access Ports should be migrated. The other one will
+remain the way it is.
+
+At the start of the workflow, the operator will select one Access Port that is to be migrated, and will then select a
+destination Edge Port that this service should be placed on. All other Access Ports will be left as-is.
+"""
+
+import json
+from typing import Any
+
+from orchestrator import workflow
+from orchestrator.config.assignee import Assignee
+from orchestrator.forms import FormPage, SubmitFormPage
+from orchestrator.targets import Target
+from orchestrator.utils.errors import ProcessFailureError
+from orchestrator.utils.json import json_dumps
+from orchestrator.workflow import StepList, begin, conditional, done, inputstep, step
+from orchestrator.workflows.steps import resync, store_process_subscription, unsync
+from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from pydantic import ConfigDict, Field
+from pydantic_forms.types import FormGenerator, State, UUIDstr
+from pydantic_forms.validators import Choice, Divider, Label
+
+from gso.products.product_types.edge_port import EdgePort
+from gso.products.product_types.l3_core_service import L3CoreService
+from gso.services.lso_client import LSOState, lso_interaction
+from gso.services.partners import get_partner_by_id
+from gso.services.subscriptions import get_active_edge_port_subscriptions
+from gso.utils.types.tt_number import TTNumber
+from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, SKIP_MOODI_KEY, start_moodi, stop_moodi
+from gso.workflows.shared import create_summary_form
+
+
+def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+    """Gather input from the operator on what destination Edge Ports this L3 Core Service should be migrated to."""
+    subscription = L3CoreService.from_subscription(subscription_id)
+    partner_id = subscription.customer_id
+    current_ep_list = {
+        str(
+            ap.sbp.edge_port.owner_subscription_id
+        ): f"{EdgePort.from_subscription(ap.sbp.edge_port.owner_subscription_id).description} ({ap.ap_type})"
+        for ap in subscription.l3_core_service.ap_list
+    }
+    source_edge_port_selector = Choice(
+        "Select an Edge Port",
+        zip(current_ep_list.keys(), current_ep_list.items(), strict=True),  # type: ignore[arg-type]
+    )
+
+    class L3CoreServiceSourceEdgePortSelectionForm(FormPage):
+        model_config = ConfigDict(title=f"Migrating a(n) {subscription.l3_core_service_type} AP to a new Edge Port")
+
+        tt_number: TTNumber
+        divider: Divider = Field(None, exclude=True)
+        skip_moodi: bool = False
+        is_human_initiated_wf: bool = True
+        source_edge_port: source_edge_port_selector | str  # type: ignore[valid-type]
+
+    source_ep_user_input = yield L3CoreServiceSourceEdgePortSelectionForm
+
+    def _destination_edge_port_selector(pid: UUIDstr) -> Choice:
+        existing_ep_list = [ap.sbp.edge_port.owner_subscription_id for ap in subscription.l3_core_service.ap_list]
+        edge_port_subscriptions = list(
+            filter(
+                lambda ep: bool(ep.customer_id == pid) and ep.subscription_id not in existing_ep_list,
+                get_active_edge_port_subscriptions(),
+            )
+        )
+        edge_ports = {str(port.subscription_id): port.description for port in edge_port_subscriptions}
+
+        return Choice(
+            "Select an Edge Port",
+            zip(edge_ports.keys(), edge_ports.items(), strict=True),  # type: ignore[arg-type]
+        )
+
+    class L3CoreServiceEdgePortSelectionForm(FormPage):
+        destination_edge_port: _destination_edge_port_selector(partner_id) | str  # type: ignore[valid-type]
+
+    destination_ep_user_input = yield L3CoreServiceEdgePortSelectionForm
+
+    if source_ep_user_input.is_human_initiated_wf:
+        summary_input = {
+            "source_edge_port": EdgePort.from_subscription(source_ep_user_input.source_edge_port).description,
+            "destination_edge_port": EdgePort.from_subscription(
+                destination_ep_user_input.destination_edge_port
+            ).description,
+        }
+        yield from create_summary_form(
+            summary_input, subscription.l3_core_service_type.value, list(summary_input.keys())
+        )
+
+    return (
+        {"subscription_id": subscription_id, "subscription": subscription}
+        | source_ep_user_input.model_dump()
+        | destination_ep_user_input.model_dump()
+        | {
+            IS_HUMAN_INITIATED_WF_KEY: source_ep_user_input.is_human_initiated_wf,
+            SKIP_MOODI_KEY: source_ep_user_input.skip_moodi,
+        }
+    )
+
+
+@step("Inject Partner Name")
+def inject_partner_name(subscription: L3CoreService) -> LSOState:
+    """Resolve and inject partner name into the state."""
+    partner_name = get_partner_by_id(subscription.customer_id).name
+
+    return {"partner_name": partner_name}
+
+
+@step("Show BGP neighbors")
+def show_bgp_neighbors(
+    subscription: L3CoreService, process_id: UUIDstr, tt_number: TTNumber, source_edge_port: EdgePort, partner_name: str
+) -> LSOState:
+    """List all BGP neighbors on the source router, to present an expected base-line for the new one."""
+    source_access_port_fqdn = source_edge_port.edge_port.node.router_fqdn
+
+    return {
+        "playbook_name": "gap_ansible/playbooks/manage_bgp_peers.yaml",
+        "inventory": {"all": {"hosts": {source_access_port_fqdn: None}}},
+        "extra_vars": {
+            "dry_run": True,
+            "verb": "check",
+            "object": "bgp",
+            "subscription": subscription,
+            "partner_name": partner_name,
+            "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Show BGP neighbors.",
+        },
+        "source_access_port_fqdn": source_access_port_fqdn,
+    }
+
+
+@step("[DRY RUN] Deactivate BGP session on the source router")
+def deactivate_bgp_dry(
+    subscription: L3CoreService,
+    process_id: UUIDstr,
+    tt_number: TTNumber,
+    source_access_port_fqdn: str,
+    partner_name: str,
+) -> LSOState:
+    """Perform a dry run of deactivating the BGP session on the source router."""
+    return {
+        "playbook_name": "gap_ansible/playbooks/manage_bgp_peers.yaml",
+        "inventory": {"all": {"hosts": {source_access_port_fqdn: None}}},
+        "extra_vars": {
+            "dry_run": True,
+            "verb": "deactivate",
+            "object": "bgp",
+            "subscription": subscription,
+            "partner_name": partner_name,
+            "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Deactivate BGP session.",
+        },
+    }
+
+
+@step("[FOR REAL] Deactivate BGP session on the source router")
+def deactivate_bgp_real(
+    subscription: L3CoreService,
+    process_id: UUIDstr,
+    tt_number: TTNumber,
+    source_access_port_fqdn: str,
+    partner_name: str,
+) -> LSOState:
+    """Deactivate the BGP session on the source router."""
+    return {
+        "playbook_name": "gap_ansible/playbooks/manage_bgp_peers.yaml",
+        "inventory": {"all": {"hosts": {source_access_port_fqdn: None}}},
+        "extra_vars": {
+            "dry_run": False,
+            "verb": "deactivate",
+            "object": "bgp",
+            "subscription": subscription,
+            "partner_name": partner_name,
+            "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Deactivate BGP session.",
+        },
+    }
+
+
+@inputstep("Verify pre-check results", assignee=Assignee.SYSTEM)
+def inform_operator_traffic_check() -> FormGenerator:
+    """Wait for confirmation from an operator that the results from the pre-checks look OK.
+
+    In case the results look OK, the workflow can continue. If the results don't look OK, the workflow can still be
+    aborted at this time, without the subscription going out of sync. Moodi will also not start, and the subscription
+    model has not been updated yet. Effectively, this prevents any changes inside the orchestrator from occurring. The
+    one thing that must be rolled back manually, is the deactivated configuration that sits on the source device.
+    """
+
+    class PreCheckPage(SubmitFormPage):
+        model_config = ConfigDict(title="Please confirm before continuing")
+
+        info_label_1: Label = "Please verify that traffic has moved as expected."
+        info_label_3: Label = "If traffic is misbehaving, this is your last chance to abort this workflow cleanly."
+
+    yield PreCheckPage
+    return {}
+
+
+@step("[DRY RUN] Deactivate SBP config on the source router")
+def deactivate_sbp_dry(
+    subscription: L3CoreService,
+    process_id: UUIDstr,
+    tt_number: TTNumber,
+    source_access_port_fqdn: str,
+    partner_name: str,
+) -> LSOState:
+    """Perform a dry run of deactivating SBP config on the source router."""
+    return {
+        "playbook_name": "gap_ansible/playbooks/manage_sbp.yaml",
+        "inventory": {"all": {"hosts": {source_access_port_fqdn: None}}},
+        "extra_vars": {
+            "dry_run": True,
+            "verb": "deactivate",
+            "subscription": subscription,
+            "partner_name": partner_name,
+            "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Deactivate BGP session.",
+        },
+    }
+
+
+@step("[FOR REAL] Deactivate SBP config on the source router")
+def deactivate_sbp_real(
+    subscription: L3CoreService,
+    process_id: UUIDstr,
+    tt_number: TTNumber,
+    source_access_port_fqdn: str,
+    partner_name: str,
+) -> LSOState:
+    """Deactivate the BGP session on the source router."""
+    return {
+        "playbook_name": "gap_ansible/playbooks/manage_sbp.yaml",
+        "inventory": {"all": {"hosts": {source_access_port_fqdn: None}}},
+        "extra_vars": {
+            "dry_run": False,
+            "verb": "deactivate",
+            "subscription": subscription,
+            "partner_name": partner_name,
+            "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Deactivate BGP session.",
+        },
+        "__remove_keys": ["source_access_port_fqdn"],
+    }
+
+
+@step("Generate updated subscription model")
+def generate_scoped_subscription_model(
+    subscription: L3CoreService, source_edge_port: EdgePort, destination_edge_port: EdgePort
+) -> State:
+    """Calculate what the updated subscription model will look like, but don't update the actual subscription yet.
+
+    The new subscription is used for running Ansible playbooks remotely, but the updated subscription model is not
+    stored yet, to avoid issues recovering when the workflow is aborted.
+    """
+    updated_subscription = json.loads(json_dumps(subscription))
+    for index, ap in enumerate(updated_subscription["l3_core_service"]["ap_list"]):
+        if ap["sbp"]["edge_port"]["owner_subscription_id"] == str(source_edge_port.subscription_id):
+            #  We have found the AP that is to be replaced, we can return all the necessary information to the state.
+            #  First, remove all unneeded unchanged APs that should not be included when executing a playbook.
+            updated_subscription["l3_core_service"]["ap_list"] = [
+                updated_subscription["l3_core_service"]["ap_list"][index]
+            ]
+            #  Then replace the AP that is migrated such that it includes the destination EP instead of the source one.
+            updated_subscription["l3_core_service"]["ap_list"][0]["sbp"]["edge_port"] = json.loads(
+                json_dumps(destination_edge_port.edge_port)
+            )
+            return {"scoped_subscription": updated_subscription, "replaced_ap_index": index}
+
+    msg = "Failed to find selected EP in current subscription."
+    raise ProcessFailureError(msg, details=source_edge_port)
+
+
+@step("[DRY RUN] Configure service on destination Edge Port")
+def deploy_destination_ep_dry(
+    scoped_subscription: dict[str, Any],
+    process_id: UUIDstr,
+    tt_number: TTNumber,
+    destination_edge_port: EdgePort,
+    partner_name: str,
+) -> LSOState:
+    """Deploy Access Port on the destination Edge Port, as a dry run.
+
+    Only the updated Access Port is sent as part of the subscription model, to reduce the scope of the playbook.
+    """
+    return {
+        "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml",
+        "inventory": {"all": {"hosts": {destination_edge_port.edge_port.node.router_fqdn: None}}},
+        "extra_vars": {
+            "dry_run": True,
+            "verb": "deploy",
+            "object": "sbp",
+            "subscription": scoped_subscription,
+            "partner_name": partner_name,
+            "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - "
+            "Deploying SBP and standard IDs.",
+        },
+    }
+
+
+@step("[FOR REAL] Configure service on destination Edge Port")
+def deploy_destination_ep_real(
+    scoped_subscription: dict[str, Any],
+    destination_edge_port: EdgePort,
+    process_id: UUIDstr,
+    tt_number: TTNumber,
+    partner_name: str,
+) -> LSOState:
+    """Deploy Access Port on the destination Edge Port."""
+    return {
+        "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml",
+        "inventory": {"all": {"hosts": {destination_edge_port.edge_port.node.router_fqdn: None}}},
+        "extra_vars": {
+            "dry_run": False,
+            "verb": "deploy",
+            "object": "sbp",
+            "subscription": scoped_subscription,
+            "partner_name": partner_name,
+            "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - "
+            "Deploying SBP and standard IDs.",
+        },
+    }
+
+
+@step("[DRY RUN] Deploy BGP session")
+def deploy_bgp_session_dry(
+    scoped_subscription: dict[str, Any],
+    destination_edge_port: EdgePort,
+    process_id: UUIDstr,
+    tt_number: TTNumber,
+    partner_name: str,
+) -> LSOState:
+    """Perform a dry run of deploying the destination BGP session."""
+    return {
+        "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml",
+        "inventory": {"all": {"hosts": {destination_edge_port.edge_port.node.router_fqdn: None}}},
+        "extra_vars": {
+            "dry_run": True,
+            "verb": "deploy",
+            "object": "bgp",
+            "subscription": scoped_subscription,
+            "partner_name": partner_name,
+            "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Deploying BGP session for SBP.",
+        },
+    }
+
+
+@step("[FOR REAL] Deploy BGP session")
+def deploy_bgp_session_real(
+    scoped_subscription: dict[str, Any],
+    destination_edge_port: EdgePort,
+    process_id: UUIDstr,
+    tt_number: TTNumber,
+    partner_name: str,
+) -> LSOState:
+    """Deploy the destination BGP session."""
+    return {
+        "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml",
+        "inventory": {"all": {"hosts": {destination_edge_port.edge_port.node.router_fqdn: None}}},
+        "extra_vars": {
+            "dry_run": False,
+            "verb": "deploy",
+            "object": "bgp",
+            "subscription": scoped_subscription,
+            "partner_name": partner_name,
+            "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Deploying BGP session for SBP.",
+        },
+        "__remove_keys": ["scoped_subscription"],
+    }
+
+
+@step("Update Infoblox")
+def update_dns_records(subscription: L3CoreService) -> State:
+    """Update DNS records in Infoblox."""
+    #  TODO: implement
+    return {"subscription": subscription}
+
+
+@step("Update subscription model")
+def update_subscription_model(
+    subscription: L3CoreService, destination_edge_port: EdgePort, replaced_ap_index: int
+) -> State:
+    """Update the subscription model with the destination Edge Port attached to the Access Port that is migrated."""
+    subscription.l3_core_service.ap_list[replaced_ap_index].sbp.edge_port = destination_edge_port.edge_port
+
+    return {"subscription": subscription, "__remove_keys": ["replaced_ap_index"]}
+
+
+@workflow(
+    "Migrate L3 Core Service",
+    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
+    target=Target.MODIFY,
+)
+def migrate_l3_core_service() -> StepList:
+    """Migrate a L3 Core Service to a destination Edge Port."""
+    is_human_initiated_wf = conditional(lambda state: bool(state.get(IS_HUMAN_INITIATED_WF_KEY)))
+
+    return (
+        begin
+        >> store_process_subscription(Target.MODIFY)
+        >> inject_partner_name
+        >> is_human_initiated_wf(lso_interaction(show_bgp_neighbors))  # TODO: send OTRS email with pre-check results
+        >> is_human_initiated_wf(lso_interaction(deactivate_bgp_dry))
+        >> is_human_initiated_wf(lso_interaction(deactivate_bgp_real))
+        >> is_human_initiated_wf(lso_interaction(deactivate_sbp_dry))
+        >> is_human_initiated_wf(lso_interaction(deactivate_sbp_real))
+        >> is_human_initiated_wf(inform_operator_traffic_check)
+        >> unsync
+        >> generate_scoped_subscription_model
+        >> start_moodi()  # TODO: include results from first LSO run
+        >> lso_interaction(deploy_destination_ep_dry)
+        >> lso_interaction(deploy_destination_ep_real)
+        >> lso_interaction(deploy_bgp_session_dry)
+        >> lso_interaction(deploy_bgp_session_real)
+        >> update_dns_records
+        >> update_subscription_model
+        >> resync
+        >> stop_moodi()
+        >> done
+    )
diff --git a/gso/workflows/l3_core_service/ias/create_ias.py b/gso/workflows/l3_core_service/ias/create_ias.py
index aa993288b..c661de933 100644
--- a/gso/workflows/l3_core_service/ias/create_ias.py
+++ b/gso/workflows/l3_core_service/ias/create_ias.py
@@ -8,6 +8,7 @@ from orchestrator.workflows.steps import resync, set_status, store_process_subsc
 from orchestrator.workflows.utils import wrap_create_initial_input_form
 from pydantic_forms.types import FormGenerator, State, UUIDstr
 
+from gso.products.product_blocks.ias import IASFlavor
 from gso.products.product_types.ias import IASInactive
 from gso.services.lso_client import lso_interaction
 from gso.utils.workflow_steps import prompt_sharepoint_checklist_url, start_moodi, stop_moodi
@@ -34,7 +35,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
 
     # Additional IAS step
     class IASExtraForm(FormPage):
-        ias_flavor: str | None = None
+        ias_flavor: IASFlavor | None = IASFlavor.IAS_PS_OPT_OUT
 
     ias_extra = yield IASExtraForm
     return initial_user_input | ias_extra.model_dump()
@@ -84,4 +85,4 @@ def create_ias() -> StepList:
         >> prompt_sharepoint_checklist_url
         >> stop_moodi()
         >> done
-    )
+    )  # TODO think about making these steps as step list in base files
-- 
GitLab


From 51c58a9df62ee5209b86910c981eabd3227a9da7 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Tue, 25 Mar 2025 13:52:59 +0100
Subject: [PATCH 20/87] refactor modify migratin for all L3 Core Services

---
 gso/workflows/__init__.py                     |   3 +
 .../base_create_l3_core_service.py            |   3 +-
 ...ice.py => base_migrate_l3_core_service.py} | 190 ++++++++----------
 .../geant_ip/migrate_geant_ip.py              |  66 ++++++
 .../l3_core_service/ias/migrate_ias.py        |  66 ++++++
 .../l3_core_service/lhcone/migrate_lhcone.py  |  66 ++++++
 6 files changed, 282 insertions(+), 112 deletions(-)
 rename gso/workflows/l3_core_service/{geant_ip/migrate_ias_service.py => base_migrate_l3_core_service.py} (72%)
 create mode 100644 gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py
 create mode 100644 gso/workflows/l3_core_service/ias/migrate_ias.py
 create mode 100644 gso/workflows/l3_core_service/lhcone/migrate_lhcone.py

diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py
index e977c62c2..2ecf5b5fa 100644
--- a/gso/workflows/__init__.py
+++ b/gso/workflows/__init__.py
@@ -124,6 +124,9 @@ LazyWorkflowInstance("gso.workflows.l3_core_service.modify_l3_core_service", "mo
 LazyWorkflowInstance("gso.workflows.l3_core_service.create_imported_l3_core_service", "create_imported_l3_core_service")
 LazyWorkflowInstance("gso.workflows.l3_core_service.import_l3_core_service", "import_l3_core_service")
 LazyWorkflowInstance("gso.workflows.l3_core_service.migrate_l3_core_service", "migrate_l3_core_service")
+LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.migrate_geant_ip", "migrate_geant_ip")
+LazyWorkflowInstance("gso.workflows.l3_core_service.ias.migrate_ias", "migrate_ias")
+LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.migrate_lhcone", "migrate_lhcone")
 LazyWorkflowInstance("gso.workflows.l3_core_service.validate_l3_core_service", "validate_l3_core_service")
 LazyWorkflowInstance("gso.workflows.l3_core_service.terminate_l3_core_service", "terminate_l3_core_service")
 LazyWorkflowInstance("gso.workflows.l3_core_service.validate_prefix_list", "validate_prefix_list")
diff --git a/gso/workflows/l3_core_service/base_create_l3_core_service.py b/gso/workflows/l3_core_service/base_create_l3_core_service.py
index fb631058f..1a7a151ee 100644
--- a/gso/workflows/l3_core_service/base_create_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_l3_core_service.py
@@ -13,6 +13,7 @@ from pydantic_forms.validators import Divider
 from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPTypes
 from gso.products.product_blocks.l3_core_service import AccessPortInactive
 from gso.products.product_blocks.service_binding_port import BFDSettings, ServiceBindingPortInactive
+from gso.products.product_types.copernicus import CopernicusInactive
 from gso.products.product_types.edge_port import EdgePort
 from gso.products.product_types.geant_ip import GeantIPInactive
 from gso.products.product_types.ias import IAS, IASInactive
@@ -164,7 +165,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
 
 
 def initialize_service_binding(
-    subscription: IASInactive | L3CoreServiceInactive | GeantIPInactive | LHCOneInactive,
+    subscription: IASInactive | GeantIPInactive | LHCOneInactive | CopernicusInactive,
     edge_port: dict,
     binding_port_input: dict,
     product_name: str,
diff --git a/gso/workflows/l3_core_service/geant_ip/migrate_ias_service.py b/gso/workflows/l3_core_service/base_migrate_l3_core_service.py
similarity index 72%
rename from gso/workflows/l3_core_service/geant_ip/migrate_ias_service.py
rename to gso/workflows/l3_core_service/base_migrate_l3_core_service.py
index 3f97983c5..fc0638677 100644
--- a/gso/workflows/l3_core_service/geant_ip/migrate_ias_service.py
+++ b/gso/workflows/l3_core_service/base_migrate_l3_core_service.py
@@ -9,40 +9,41 @@ destination Edge Port that this service should be placed on. All other Access Po
 """
 
 import json
-from typing import Any
+from typing import Any, Union
 
-from orchestrator import workflow
 from orchestrator.config.assignee import Assignee
 from orchestrator.forms import FormPage, SubmitFormPage
-from orchestrator.targets import Target
 from orchestrator.utils.errors import ProcessFailureError
 from orchestrator.utils.json import json_dumps
-from orchestrator.workflow import StepList, begin, conditional, done, inputstep, step
-from orchestrator.workflows.steps import resync, store_process_subscription, unsync
-from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from orchestrator.workflow import inputstep, step
 from pydantic import ConfigDict, Field
 from pydantic_forms.types import FormGenerator, State, UUIDstr
 from pydantic_forms.validators import Choice, Divider, Label
 
+from gso.products.product_types.geant_ip import GeantIP
+from gso.products.product_types.lhcone import LHCOne
+from gso.products.product_types.ias import IAS
+from gso.products.product_types.copernicus import Copernicus
 from gso.products.product_types.edge_port import EdgePort
-from gso.products.product_types.l3_core_service import L3CoreService
-from gso.services.lso_client import LSOState, lso_interaction
+from gso.services.lso_client import LSOState
 from gso.services.partners import get_partner_by_id
 from gso.services.subscriptions import get_active_edge_port_subscriptions
 from gso.utils.types.tt_number import TTNumber
-from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, SKIP_MOODI_KEY, start_moodi, stop_moodi
+from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, SKIP_MOODI_KEY
 from gso.workflows.shared import create_summary_form
 
+L3CoreServiceType = Union[GeantIP, IAS, LHCOne, Copernicus]
 
-def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
-    """Gather input from the operator on what destination Edge Ports this L3 Core Service should be migrated to."""
-    subscription = L3CoreService.from_subscription(subscription_id)
+
+def initial_input_form(subscription: L3CoreServiceType, service_name: str) -> FormGenerator:
     partner_id = subscription.customer_id
+    ap_list = getattr(subscription, service_name).l3_core.ap_list
+
     current_ep_list = {
         str(
             ap.sbp.edge_port.owner_subscription_id
         ): f"{EdgePort.from_subscription(ap.sbp.edge_port.owner_subscription_id).description} ({ap.ap_type})"
-        for ap in subscription.l3_core_service.ap_list
+        for ap in ap_list
     }
     source_edge_port_selector = Choice(
         "Select an Edge Port",
@@ -50,7 +51,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     )
 
     class L3CoreServiceSourceEdgePortSelectionForm(FormPage):
-        model_config = ConfigDict(title=f"Migrating a(n) {subscription.l3_core_service_type} AP to a new Edge Port")
+        model_config = ConfigDict(title=f"Migrating a(n) {subscription.product.name} AP to a new Edge Port")
 
         tt_number: TTNumber
         divider: Divider = Field(None, exclude=True)
@@ -61,7 +62,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     source_ep_user_input = yield L3CoreServiceSourceEdgePortSelectionForm
 
     def _destination_edge_port_selector(pid: UUIDstr) -> Choice:
-        existing_ep_list = [ap.sbp.edge_port.owner_subscription_id for ap in subscription.l3_core_service.ap_list]
+        existing_ep_list = [ap.sbp.edge_port.owner_subscription_id for ap in ap_list]
         edge_port_subscriptions = list(
             filter(
                 lambda ep: bool(ep.customer_id == pid) and ep.subscription_id not in existing_ep_list,
@@ -79,7 +80,6 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
         destination_edge_port: _destination_edge_port_selector(partner_id) | str  # type: ignore[valid-type]
 
     destination_ep_user_input = yield L3CoreServiceEdgePortSelectionForm
-
     if source_ep_user_input.is_human_initiated_wf:
         summary_input = {
             "source_edge_port": EdgePort.from_subscription(source_ep_user_input.source_edge_port).description,
@@ -88,22 +88,22 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
             ).description,
         }
         yield from create_summary_form(
-            summary_input, subscription.l3_core_service_type.value, list(summary_input.keys())
+            summary_input, subscription.product.name, list(summary_input.keys())
         )
-
     return (
-        {"subscription_id": subscription_id, "subscription": subscription}
-        | source_ep_user_input.model_dump()
-        | destination_ep_user_input.model_dump()
-        | {
-            IS_HUMAN_INITIATED_WF_KEY: source_ep_user_input.is_human_initiated_wf,
-            SKIP_MOODI_KEY: source_ep_user_input.skip_moodi,
-        }
+            {"subscription_id": subscription.subscription_id, "subscription": subscription}
+            | source_ep_user_input.model_dump()
+            | destination_ep_user_input.model_dump()
+            | {
+                IS_HUMAN_INITIATED_WF_KEY: source_ep_user_input.is_human_initiated_wf,
+                SKIP_MOODI_KEY: source_ep_user_input.skip_moodi,
+                "service_name": service_name,
+            }
     )
 
 
 @step("Inject Partner Name")
-def inject_partner_name(subscription: L3CoreService) -> LSOState:
+def inject_partner_name(subscription: L3CoreServiceType) -> LSOState:
     """Resolve and inject partner name into the state."""
     partner_name = get_partner_by_id(subscription.customer_id).name
 
@@ -112,7 +112,8 @@ def inject_partner_name(subscription: L3CoreService) -> LSOState:
 
 @step("Show BGP neighbors")
 def show_bgp_neighbors(
-    subscription: L3CoreService, process_id: UUIDstr, tt_number: TTNumber, source_edge_port: EdgePort, partner_name: str
+        subscription: L3CoreServiceType, process_id: UUIDstr, tt_number: TTNumber, source_edge_port: EdgePort,
+        partner_name: str
 ) -> LSOState:
     """List all BGP neighbors on the source router, to present an expected base-line for the new one."""
     source_access_port_fqdn = source_edge_port.edge_port.node.router_fqdn
@@ -134,11 +135,11 @@ def show_bgp_neighbors(
 
 @step("[DRY RUN] Deactivate BGP session on the source router")
 def deactivate_bgp_dry(
-    subscription: L3CoreService,
-    process_id: UUIDstr,
-    tt_number: TTNumber,
-    source_access_port_fqdn: str,
-    partner_name: str,
+        subscription: L3CoreServiceType,
+        process_id: UUIDstr,
+        tt_number: TTNumber,
+        source_access_port_fqdn: str,
+        partner_name: str,
 ) -> LSOState:
     """Perform a dry run of deactivating the BGP session on the source router."""
     return {
@@ -157,11 +158,11 @@ def deactivate_bgp_dry(
 
 @step("[FOR REAL] Deactivate BGP session on the source router")
 def deactivate_bgp_real(
-    subscription: L3CoreService,
-    process_id: UUIDstr,
-    tt_number: TTNumber,
-    source_access_port_fqdn: str,
-    partner_name: str,
+        subscription: L3CoreServiceType,
+        process_id: UUIDstr,
+        tt_number: TTNumber,
+        source_access_port_fqdn: str,
+        partner_name: str,
 ) -> LSOState:
     """Deactivate the BGP session on the source router."""
     return {
@@ -200,11 +201,11 @@ def inform_operator_traffic_check() -> FormGenerator:
 
 @step("[DRY RUN] Deactivate SBP config on the source router")
 def deactivate_sbp_dry(
-    subscription: L3CoreService,
-    process_id: UUIDstr,
-    tt_number: TTNumber,
-    source_access_port_fqdn: str,
-    partner_name: str,
+        subscription: L3CoreServiceType,
+        process_id: UUIDstr,
+        tt_number: TTNumber,
+        source_access_port_fqdn: str,
+        partner_name: str,
 ) -> LSOState:
     """Perform a dry run of deactivating SBP config on the source router."""
     return {
@@ -222,11 +223,11 @@ def deactivate_sbp_dry(
 
 @step("[FOR REAL] Deactivate SBP config on the source router")
 def deactivate_sbp_real(
-    subscription: L3CoreService,
-    process_id: UUIDstr,
-    tt_number: TTNumber,
-    source_access_port_fqdn: str,
-    partner_name: str,
+        subscription: L3CoreServiceType,
+        process_id: UUIDstr,
+        tt_number: TTNumber,
+        source_access_port_fqdn: str,
+        partner_name: str,
 ) -> LSOState:
     """Deactivate the BGP session on the source router."""
     return {
@@ -245,7 +246,7 @@ def deactivate_sbp_real(
 
 @step("Generate updated subscription model")
 def generate_scoped_subscription_model(
-    subscription: L3CoreService, source_edge_port: EdgePort, destination_edge_port: EdgePort
+        subscription: L3CoreServiceType, source_edge_port: EdgePort, destination_edge_port: EdgePort, service_name: str
 ) -> State:
     """Calculate what the updated subscription model will look like, but don't update the actual subscription yet.
 
@@ -253,15 +254,15 @@ def generate_scoped_subscription_model(
     stored yet, to avoid issues recovering when the workflow is aborted.
     """
     updated_subscription = json.loads(json_dumps(subscription))
-    for index, ap in enumerate(updated_subscription["l3_core_service"]["ap_list"]):
+    for index, ap in enumerate(updated_subscription[service_name]["l3_core"]["ap_list"]):
         if ap["sbp"]["edge_port"]["owner_subscription_id"] == str(source_edge_port.subscription_id):
             #  We have found the AP that is to be replaced, we can return all the necessary information to the state.
             #  First, remove all unneeded unchanged APs that should not be included when executing a playbook.
-            updated_subscription["l3_core_service"]["ap_list"] = [
-                updated_subscription["l3_core_service"]["ap_list"][index]
+            updated_subscription[service_name]["l3_core"]["ap_list"] = [
+                updated_subscription[service_name]["l3_core"]["ap_list"][index]
             ]
             #  Then replace the AP that is migrated such that it includes the destination EP instead of the source one.
-            updated_subscription["l3_core_service"]["ap_list"][0]["sbp"]["edge_port"] = json.loads(
+            updated_subscription[service_name]["l3_core"]["ap_list"][0]["sbp"]["edge_port"] = json.loads(
                 json_dumps(destination_edge_port.edge_port)
             )
             return {"scoped_subscription": updated_subscription, "replaced_ap_index": index}
@@ -272,11 +273,11 @@ def generate_scoped_subscription_model(
 
 @step("[DRY RUN] Configure service on destination Edge Port")
 def deploy_destination_ep_dry(
-    scoped_subscription: dict[str, Any],
-    process_id: UUIDstr,
-    tt_number: TTNumber,
-    destination_edge_port: EdgePort,
-    partner_name: str,
+        scoped_subscription: dict[str, Any],
+        process_id: UUIDstr,
+        tt_number: TTNumber,
+        destination_edge_port: EdgePort,
+        partner_name: str,
 ) -> LSOState:
     """Deploy Access Port on the destination Edge Port, as a dry run.
 
@@ -292,18 +293,18 @@ def deploy_destination_ep_dry(
             "subscription": scoped_subscription,
             "partner_name": partner_name,
             "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - "
-            "Deploying SBP and standard IDs.",
+                              "Deploying SBP and standard IDs.",
         },
     }
 
 
 @step("[FOR REAL] Configure service on destination Edge Port")
 def deploy_destination_ep_real(
-    scoped_subscription: dict[str, Any],
-    destination_edge_port: EdgePort,
-    process_id: UUIDstr,
-    tt_number: TTNumber,
-    partner_name: str,
+        scoped_subscription: dict[str, Any],
+        destination_edge_port: EdgePort,
+        process_id: UUIDstr,
+        tt_number: TTNumber,
+        partner_name: str,
 ) -> LSOState:
     """Deploy Access Port on the destination Edge Port."""
     return {
@@ -316,18 +317,18 @@ def deploy_destination_ep_real(
             "subscription": scoped_subscription,
             "partner_name": partner_name,
             "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - "
-            "Deploying SBP and standard IDs.",
+                              "Deploying SBP and standard IDs.",
         },
     }
 
 
 @step("[DRY RUN] Deploy BGP session")
 def deploy_bgp_session_dry(
-    scoped_subscription: dict[str, Any],
-    destination_edge_port: EdgePort,
-    process_id: UUIDstr,
-    tt_number: TTNumber,
-    partner_name: str,
+        scoped_subscription: dict[str, Any],
+        destination_edge_port: EdgePort,
+        process_id: UUIDstr,
+        tt_number: TTNumber,
+        partner_name: str,
 ) -> LSOState:
     """Perform a dry run of deploying the destination BGP session."""
     return {
@@ -346,11 +347,11 @@ def deploy_bgp_session_dry(
 
 @step("[FOR REAL] Deploy BGP session")
 def deploy_bgp_session_real(
-    scoped_subscription: dict[str, Any],
-    destination_edge_port: EdgePort,
-    process_id: UUIDstr,
-    tt_number: TTNumber,
-    partner_name: str,
+        scoped_subscription: dict[str, Any],
+        destination_edge_port: EdgePort,
+        process_id: UUIDstr,
+        tt_number: TTNumber,
+        partner_name: str,
 ) -> LSOState:
     """Deploy the destination BGP session."""
     return {
@@ -369,7 +370,7 @@ def deploy_bgp_session_real(
 
 
 @step("Update Infoblox")
-def update_dns_records(subscription: L3CoreService) -> State:
+def update_dns_records(subscription: L3CoreServiceType) -> State:
     """Update DNS records in Infoblox."""
     #  TODO: implement
     return {"subscription": subscription}
@@ -377,43 +378,10 @@ def update_dns_records(subscription: L3CoreService) -> State:
 
 @step("Update subscription model")
 def update_subscription_model(
-    subscription: L3CoreService, destination_edge_port: EdgePort, replaced_ap_index: int
+        subscription: L3CoreServiceType, destination_edge_port: EdgePort, replaced_ap_index: int, service_name: str
 ) -> State:
     """Update the subscription model with the destination Edge Port attached to the Access Port that is migrated."""
-    subscription.l3_core_service.ap_list[replaced_ap_index].sbp.edge_port = destination_edge_port.edge_port
+    ap_list = getattr(subscription, service_name).l3_core.ap_list
+    ap_list[replaced_ap_index].sbp.edge_port = destination_edge_port.edge_port
 
     return {"subscription": subscription, "__remove_keys": ["replaced_ap_index"]}
-
-
-@workflow(
-    "Migrate L3 Core Service",
-    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
-    target=Target.MODIFY,
-)
-def migrate_l3_core_service() -> StepList:
-    """Migrate a L3 Core Service to a destination Edge Port."""
-    is_human_initiated_wf = conditional(lambda state: bool(state.get(IS_HUMAN_INITIATED_WF_KEY)))
-
-    return (
-        begin
-        >> store_process_subscription(Target.MODIFY)
-        >> inject_partner_name
-        >> is_human_initiated_wf(lso_interaction(show_bgp_neighbors))  # TODO: send OTRS email with pre-check results
-        >> is_human_initiated_wf(lso_interaction(deactivate_bgp_dry))
-        >> is_human_initiated_wf(lso_interaction(deactivate_bgp_real))
-        >> is_human_initiated_wf(lso_interaction(deactivate_sbp_dry))
-        >> is_human_initiated_wf(lso_interaction(deactivate_sbp_real))
-        >> is_human_initiated_wf(inform_operator_traffic_check)
-        >> unsync
-        >> generate_scoped_subscription_model
-        >> start_moodi()  # TODO: include results from first LSO run
-        >> lso_interaction(deploy_destination_ep_dry)
-        >> lso_interaction(deploy_destination_ep_real)
-        >> lso_interaction(deploy_bgp_session_dry)
-        >> lso_interaction(deploy_bgp_session_real)
-        >> update_dns_records
-        >> update_subscription_model
-        >> resync
-        >> stop_moodi()
-        >> done
-    )
diff --git a/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py
new file mode 100644
index 000000000..2a16499e5
--- /dev/null
+++ b/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py
@@ -0,0 +1,66 @@
+"""A modification workflow that migrates a GÉANT IP Service to a new Edge Port.
+
+In one run of a migration workflow, only a single Access Port can be replaced. This is due to the nature of these
+services. When a service is dually homed, for example, only one of the Access Ports should be migrated. The other one will
+remain the way it is.
+
+At the start of the workflow, the operator will select one Access Port that is to be migrated, and will then select a
+destination Edge Port that this service should be placed on. All other Access Ports will be left as-is.
+"""
+
+from orchestrator import workflow
+from orchestrator.targets import Target
+from orchestrator.workflow import StepList, begin, conditional, done
+from orchestrator.workflows.steps import resync, store_process_subscription, unsync
+from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from pydantic_forms.types import FormGenerator, UUIDstr
+
+from gso.products import GeantIP
+from gso.services.lso_client import lso_interaction
+from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, start_moodi, stop_moodi
+from gso.workflows.l3_core_service.base_migrate_l3_core_service import show_bgp_neighbors, deactivate_bgp_dry, \
+    deactivate_bgp_real, inform_operator_traffic_check, inject_partner_name, deactivate_sbp_dry, deactivate_sbp_real, \
+    deploy_destination_ep_dry, deploy_destination_ep_real, deploy_bgp_session_dry, deploy_bgp_session_real, \
+    update_dns_records, update_subscription_model, initial_input_form, generate_scoped_subscription_model
+
+
+def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+    """Gather input from the operator on what destination Edge Ports this GÉANT IP Service should be migrated to."""
+    subscription = GeantIP.from_subscription(subscription_id)
+
+    return initial_input_form(subscription, service_name='geant_ip')
+
+
+@workflow(
+    "Migrate GÉANT IP Service",
+    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
+    target=Target.MODIFY,
+)
+def migrate_geant_ip() -> StepList:
+    """Migrate a GÉANT IP Service to a destination Edge Port."""
+    is_human_initiated_wf = conditional(lambda state: bool(state.get(IS_HUMAN_INITIATED_WF_KEY)))
+
+    return (
+            begin
+            >> store_process_subscription(Target.MODIFY)
+            >> inject_partner_name
+            >> is_human_initiated_wf(
+        lso_interaction(show_bgp_neighbors))  # TODO: send OTRS email with pre-check results
+            >> is_human_initiated_wf(lso_interaction(deactivate_bgp_dry))
+            >> is_human_initiated_wf(lso_interaction(deactivate_bgp_real))
+            >> is_human_initiated_wf(lso_interaction(deactivate_sbp_dry))
+            >> is_human_initiated_wf(lso_interaction(deactivate_sbp_real))
+            >> is_human_initiated_wf(inform_operator_traffic_check)
+            >> unsync
+            >> generate_scoped_subscription_model
+            >> start_moodi()  # TODO: include results from first LSO run
+            >> lso_interaction(deploy_destination_ep_dry)
+            >> lso_interaction(deploy_destination_ep_real)
+            >> lso_interaction(deploy_bgp_session_dry)
+            >> lso_interaction(deploy_bgp_session_real)
+            >> update_dns_records
+            >> update_subscription_model
+            >> resync
+            >> stop_moodi()
+            >> done
+    )
diff --git a/gso/workflows/l3_core_service/ias/migrate_ias.py b/gso/workflows/l3_core_service/ias/migrate_ias.py
new file mode 100644
index 000000000..bf86d0e51
--- /dev/null
+++ b/gso/workflows/l3_core_service/ias/migrate_ias.py
@@ -0,0 +1,66 @@
+"""A modification workflow that migrates an IAS Service to a new Edge Port.
+
+In one run of a migration workflow, only a single Access Port can be replaced. This is due to the nature of these
+services. When a service is dually homed, for example, only one of the Access Ports should be migrated. The other one will
+remain the way it is.
+
+At the start of the workflow, the operator will select one Access Port that is to be migrated, and will then select a
+destination Edge Port that this service should be placed on. All other Access Ports will be left as-is.
+"""
+
+from orchestrator import workflow
+from orchestrator.targets import Target
+from orchestrator.workflow import StepList, begin, conditional, done
+from orchestrator.workflows.steps import resync, store_process_subscription, unsync
+from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from pydantic_forms.types import FormGenerator, UUIDstr
+
+from gso.products import GeantIP, IAS
+from gso.services.lso_client import lso_interaction
+from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, start_moodi, stop_moodi
+from gso.workflows.l3_core_service.base_migrate_l3_core_service import show_bgp_neighbors, deactivate_bgp_dry, \
+    deactivate_bgp_real, inform_operator_traffic_check, inject_partner_name, deactivate_sbp_dry, deactivate_sbp_real, \
+    deploy_destination_ep_dry, deploy_destination_ep_real, deploy_bgp_session_dry, deploy_bgp_session_real, \
+    update_dns_records, update_subscription_model, initial_input_form, generate_scoped_subscription_model
+
+
+def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+    """Gather input from the operator on what destination Edge Ports this IAS Service should be migrated to."""
+    subscription = IAS.from_subscription(subscription_id)
+
+    return initial_input_form(subscription, service_name='ias')
+
+
+@workflow(
+    "Migrate IAS Service",
+    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
+    target=Target.MODIFY,
+)
+def migrate_ias() -> StepList:
+    """Migrate an IAS Service to a destination Edge Port."""
+    is_human_initiated_wf = conditional(lambda state: bool(state.get(IS_HUMAN_INITIATED_WF_KEY)))
+
+    return (
+            begin
+            >> store_process_subscription(Target.MODIFY)
+            >> inject_partner_name
+            >> is_human_initiated_wf(
+        lso_interaction(show_bgp_neighbors))  # TODO: send OTRS email with pre-check results
+            >> is_human_initiated_wf(lso_interaction(deactivate_bgp_dry))
+            >> is_human_initiated_wf(lso_interaction(deactivate_bgp_real))
+            >> is_human_initiated_wf(lso_interaction(deactivate_sbp_dry))
+            >> is_human_initiated_wf(lso_interaction(deactivate_sbp_real))
+            >> is_human_initiated_wf(inform_operator_traffic_check)
+            >> unsync
+            >> generate_scoped_subscription_model
+            >> start_moodi()  # TODO: include results from first LSO run
+            >> lso_interaction(deploy_destination_ep_dry)
+            >> lso_interaction(deploy_destination_ep_real)
+            >> lso_interaction(deploy_bgp_session_dry)
+            >> lso_interaction(deploy_bgp_session_real)
+            >> update_dns_records
+            >> update_subscription_model
+            >> resync
+            >> stop_moodi()
+            >> done
+    )
diff --git a/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py b/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py
new file mode 100644
index 000000000..06027e9d7
--- /dev/null
+++ b/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py
@@ -0,0 +1,66 @@
+"""A modification workflow that migrates an LHCOne Service to a new Edge Port.
+
+In one run of a migration workflow, only a single Access Port can be replaced. This is due to the nature of these
+services. When a service is dually homed, for example, only one of the Access Ports should be migrated. The other one will
+remain the way it is.
+
+At the start of the workflow, the operator will select one Access Port that is to be migrated, and will then select a
+destination Edge Port that this service should be placed on. All other Access Ports will be left as-is.
+"""
+
+from orchestrator import workflow
+from orchestrator.targets import Target
+from orchestrator.workflow import StepList, begin, conditional, done
+from orchestrator.workflows.steps import resync, store_process_subscription, unsync
+from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from pydantic_forms.types import FormGenerator, UUIDstr
+
+from gso.products import LHCOne
+from gso.services.lso_client import lso_interaction
+from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, start_moodi, stop_moodi
+from gso.workflows.l3_core_service.base_migrate_l3_core_service import show_bgp_neighbors, deactivate_bgp_dry, \
+    deactivate_bgp_real, inform_operator_traffic_check, inject_partner_name, deactivate_sbp_dry, deactivate_sbp_real, \
+    deploy_destination_ep_dry, deploy_destination_ep_real, deploy_bgp_session_dry, deploy_bgp_session_real, \
+    update_dns_records, update_subscription_model, initial_input_form, generate_scoped_subscription_model
+
+
+def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+    """Gather input from the operator on what destination Edge Ports this LHCOne Service should be migrated to."""
+    subscription = LHCOne.from_subscription(subscription_id)
+
+    return initial_input_form(subscription, service_name='lhcone')
+
+
+@workflow(
+    "Migrate LHCOne Service",
+    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
+    target=Target.MODIFY,
+)
+def migrate_lhcone() -> StepList:
+    """Migrate a LHCOne Service to a destination Edge Port."""
+    is_human_initiated_wf = conditional(lambda state: bool(state.get(IS_HUMAN_INITIATED_WF_KEY)))
+
+    return (
+            begin
+            >> store_process_subscription(Target.MODIFY)
+            >> inject_partner_name
+            >> is_human_initiated_wf(
+        lso_interaction(show_bgp_neighbors))  # TODO: send OTRS email with pre-check results
+            >> is_human_initiated_wf(lso_interaction(deactivate_bgp_dry))
+            >> is_human_initiated_wf(lso_interaction(deactivate_bgp_real))
+            >> is_human_initiated_wf(lso_interaction(deactivate_sbp_dry))
+            >> is_human_initiated_wf(lso_interaction(deactivate_sbp_real))
+            >> is_human_initiated_wf(inform_operator_traffic_check)
+            >> unsync
+            >> generate_scoped_subscription_model
+            >> start_moodi()  # TODO: include results from first LSO run
+            >> lso_interaction(deploy_destination_ep_dry)
+            >> lso_interaction(deploy_destination_ep_real)
+            >> lso_interaction(deploy_bgp_session_dry)
+            >> lso_interaction(deploy_bgp_session_real)
+            >> update_dns_records
+            >> update_subscription_model
+            >> resync
+            >> stop_moodi()
+            >> done
+    )
-- 
GitLab


From c7b9b6f3a04e5b678dbc933a861ea2ce89b94d3e Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Tue, 25 Mar 2025 14:50:58 +0100
Subject: [PATCH 21/87] reformat files with ruff

---
 .../base_create_l3_core_service.py            |  10 +-
 .../base_migrate_l3_core_service.py           | 121 +++++++++---------
 .../geant_ip/create_geant_ip.py               |  41 +++---
 .../geant_ip/migrate_geant_ip.py              |  66 ++++++----
 .../geant_ip/terminate_geant_ip.py            |  12 +-
 .../l3_core_service/ias/create_ias.py         |   8 +-
 .../l3_core_service/ias/migrate_ias.py        |  68 ++++++----
 .../l3_core_service/lhcone/create_lhcone.py   |  41 +++---
 .../l3_core_service/lhcone/migrate_lhcone.py  |  66 ++++++----
 .../lhcone/terminate_lhcone.py                |  12 +-
 10 files changed, 244 insertions(+), 201 deletions(-)

diff --git a/gso/workflows/l3_core_service/base_create_l3_core_service.py b/gso/workflows/l3_core_service/base_create_l3_core_service.py
index 1a7a151ee..8ddf3101d 100644
--- a/gso/workflows/l3_core_service/base_create_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_l3_core_service.py
@@ -17,7 +17,7 @@ from gso.products.product_types.copernicus import CopernicusInactive
 from gso.products.product_types.edge_port import EdgePort
 from gso.products.product_types.geant_ip import GeantIPInactive
 from gso.products.product_types.ias import IAS, IASInactive
-from gso.products.product_types.l3_core_service import L3CoreService, L3CoreServiceInactive
+from gso.products.product_types.l3_core_service import L3CoreService
 from gso.products.product_types.lhcone import LHCOneInactive
 from gso.services.lso_client import LSOState
 from gso.services.partners import get_partner_by_id
@@ -338,10 +338,10 @@ def update_dns_records(subscription: L3CoreService | IAS) -> State:
 
 @step("Create a new SharePoint checklist item")
 def create_new_sharepoint_checklist(
-        subscription: L3CoreService,
-        tt_number: TTNumber,
-        process_id: UUIDstr,
-        service_name: str,
+    subscription: L3CoreService,
+    tt_number: TTNumber,
+    process_id: UUIDstr,
+    service_name: str,
 ) -> State:
     """Create a new checklist item in SharePoint for approving this L3 Core Service."""
     service = getattr(subscription, service_name)
diff --git a/gso/workflows/l3_core_service/base_migrate_l3_core_service.py b/gso/workflows/l3_core_service/base_migrate_l3_core_service.py
index fc0638677..47422785e 100644
--- a/gso/workflows/l3_core_service/base_migrate_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_migrate_l3_core_service.py
@@ -20,11 +20,11 @@ from pydantic import ConfigDict, Field
 from pydantic_forms.types import FormGenerator, State, UUIDstr
 from pydantic_forms.validators import Choice, Divider, Label
 
-from gso.products.product_types.geant_ip import GeantIP
-from gso.products.product_types.lhcone import LHCOne
-from gso.products.product_types.ias import IAS
 from gso.products.product_types.copernicus import Copernicus
 from gso.products.product_types.edge_port import EdgePort
+from gso.products.product_types.geant_ip import GeantIP
+from gso.products.product_types.ias import IAS
+from gso.products.product_types.lhcone import LHCOne
 from gso.services.lso_client import LSOState
 from gso.services.partners import get_partner_by_id
 from gso.services.subscriptions import get_active_edge_port_subscriptions
@@ -87,18 +87,16 @@ def initial_input_form(subscription: L3CoreServiceType, service_name: str) -> Fo
                 destination_ep_user_input.destination_edge_port
             ).description,
         }
-        yield from create_summary_form(
-            summary_input, subscription.product.name, list(summary_input.keys())
-        )
+        yield from create_summary_form(summary_input, subscription.product.name, list(summary_input.keys()))
     return (
-            {"subscription_id": subscription.subscription_id, "subscription": subscription}
-            | source_ep_user_input.model_dump()
-            | destination_ep_user_input.model_dump()
-            | {
-                IS_HUMAN_INITIATED_WF_KEY: source_ep_user_input.is_human_initiated_wf,
-                SKIP_MOODI_KEY: source_ep_user_input.skip_moodi,
-                "service_name": service_name,
-            }
+        {"subscription_id": subscription.subscription_id, "subscription": subscription}
+        | source_ep_user_input.model_dump()
+        | destination_ep_user_input.model_dump()
+        | {
+            IS_HUMAN_INITIATED_WF_KEY: source_ep_user_input.is_human_initiated_wf,
+            SKIP_MOODI_KEY: source_ep_user_input.skip_moodi,
+            "service_name": service_name,
+        }
     )
 
 
@@ -112,8 +110,11 @@ def inject_partner_name(subscription: L3CoreServiceType) -> LSOState:
 
 @step("Show BGP neighbors")
 def show_bgp_neighbors(
-        subscription: L3CoreServiceType, process_id: UUIDstr, tt_number: TTNumber, source_edge_port: EdgePort,
-        partner_name: str
+    subscription: L3CoreServiceType,
+    process_id: UUIDstr,
+    tt_number: TTNumber,
+    source_edge_port: EdgePort,
+    partner_name: str,
 ) -> LSOState:
     """List all BGP neighbors on the source router, to present an expected base-line for the new one."""
     source_access_port_fqdn = source_edge_port.edge_port.node.router_fqdn
@@ -135,11 +136,11 @@ def show_bgp_neighbors(
 
 @step("[DRY RUN] Deactivate BGP session on the source router")
 def deactivate_bgp_dry(
-        subscription: L3CoreServiceType,
-        process_id: UUIDstr,
-        tt_number: TTNumber,
-        source_access_port_fqdn: str,
-        partner_name: str,
+    subscription: L3CoreServiceType,
+    process_id: UUIDstr,
+    tt_number: TTNumber,
+    source_access_port_fqdn: str,
+    partner_name: str,
 ) -> LSOState:
     """Perform a dry run of deactivating the BGP session on the source router."""
     return {
@@ -158,11 +159,11 @@ def deactivate_bgp_dry(
 
 @step("[FOR REAL] Deactivate BGP session on the source router")
 def deactivate_bgp_real(
-        subscription: L3CoreServiceType,
-        process_id: UUIDstr,
-        tt_number: TTNumber,
-        source_access_port_fqdn: str,
-        partner_name: str,
+    subscription: L3CoreServiceType,
+    process_id: UUIDstr,
+    tt_number: TTNumber,
+    source_access_port_fqdn: str,
+    partner_name: str,
 ) -> LSOState:
     """Deactivate the BGP session on the source router."""
     return {
@@ -201,11 +202,11 @@ def inform_operator_traffic_check() -> FormGenerator:
 
 @step("[DRY RUN] Deactivate SBP config on the source router")
 def deactivate_sbp_dry(
-        subscription: L3CoreServiceType,
-        process_id: UUIDstr,
-        tt_number: TTNumber,
-        source_access_port_fqdn: str,
-        partner_name: str,
+    subscription: L3CoreServiceType,
+    process_id: UUIDstr,
+    tt_number: TTNumber,
+    source_access_port_fqdn: str,
+    partner_name: str,
 ) -> LSOState:
     """Perform a dry run of deactivating SBP config on the source router."""
     return {
@@ -223,11 +224,11 @@ def deactivate_sbp_dry(
 
 @step("[FOR REAL] Deactivate SBP config on the source router")
 def deactivate_sbp_real(
-        subscription: L3CoreServiceType,
-        process_id: UUIDstr,
-        tt_number: TTNumber,
-        source_access_port_fqdn: str,
-        partner_name: str,
+    subscription: L3CoreServiceType,
+    process_id: UUIDstr,
+    tt_number: TTNumber,
+    source_access_port_fqdn: str,
+    partner_name: str,
 ) -> LSOState:
     """Deactivate the BGP session on the source router."""
     return {
@@ -246,7 +247,7 @@ def deactivate_sbp_real(
 
 @step("Generate updated subscription model")
 def generate_scoped_subscription_model(
-        subscription: L3CoreServiceType, source_edge_port: EdgePort, destination_edge_port: EdgePort, service_name: str
+    subscription: L3CoreServiceType, source_edge_port: EdgePort, destination_edge_port: EdgePort, service_name: str
 ) -> State:
     """Calculate what the updated subscription model will look like, but don't update the actual subscription yet.
 
@@ -273,11 +274,11 @@ def generate_scoped_subscription_model(
 
 @step("[DRY RUN] Configure service on destination Edge Port")
 def deploy_destination_ep_dry(
-        scoped_subscription: dict[str, Any],
-        process_id: UUIDstr,
-        tt_number: TTNumber,
-        destination_edge_port: EdgePort,
-        partner_name: str,
+    scoped_subscription: dict[str, Any],
+    process_id: UUIDstr,
+    tt_number: TTNumber,
+    destination_edge_port: EdgePort,
+    partner_name: str,
 ) -> LSOState:
     """Deploy Access Port on the destination Edge Port, as a dry run.
 
@@ -293,18 +294,18 @@ def deploy_destination_ep_dry(
             "subscription": scoped_subscription,
             "partner_name": partner_name,
             "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - "
-                              "Deploying SBP and standard IDs.",
+            "Deploying SBP and standard IDs.",
         },
     }
 
 
 @step("[FOR REAL] Configure service on destination Edge Port")
 def deploy_destination_ep_real(
-        scoped_subscription: dict[str, Any],
-        destination_edge_port: EdgePort,
-        process_id: UUIDstr,
-        tt_number: TTNumber,
-        partner_name: str,
+    scoped_subscription: dict[str, Any],
+    destination_edge_port: EdgePort,
+    process_id: UUIDstr,
+    tt_number: TTNumber,
+    partner_name: str,
 ) -> LSOState:
     """Deploy Access Port on the destination Edge Port."""
     return {
@@ -317,18 +318,18 @@ def deploy_destination_ep_real(
             "subscription": scoped_subscription,
             "partner_name": partner_name,
             "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - "
-                              "Deploying SBP and standard IDs.",
+            "Deploying SBP and standard IDs.",
         },
     }
 
 
 @step("[DRY RUN] Deploy BGP session")
 def deploy_bgp_session_dry(
-        scoped_subscription: dict[str, Any],
-        destination_edge_port: EdgePort,
-        process_id: UUIDstr,
-        tt_number: TTNumber,
-        partner_name: str,
+    scoped_subscription: dict[str, Any],
+    destination_edge_port: EdgePort,
+    process_id: UUIDstr,
+    tt_number: TTNumber,
+    partner_name: str,
 ) -> LSOState:
     """Perform a dry run of deploying the destination BGP session."""
     return {
@@ -347,11 +348,11 @@ def deploy_bgp_session_dry(
 
 @step("[FOR REAL] Deploy BGP session")
 def deploy_bgp_session_real(
-        scoped_subscription: dict[str, Any],
-        destination_edge_port: EdgePort,
-        process_id: UUIDstr,
-        tt_number: TTNumber,
-        partner_name: str,
+    scoped_subscription: dict[str, Any],
+    destination_edge_port: EdgePort,
+    process_id: UUIDstr,
+    tt_number: TTNumber,
+    partner_name: str,
 ) -> LSOState:
     """Deploy the destination BGP session."""
     return {
@@ -378,7 +379,7 @@ def update_dns_records(subscription: L3CoreServiceType) -> State:
 
 @step("Update subscription model")
 def update_subscription_model(
-        subscription: L3CoreServiceType, destination_edge_port: EdgePort, replaced_ap_index: int, service_name: str
+    subscription: L3CoreServiceType, destination_edge_port: EdgePort, replaced_ap_index: int, service_name: str
 ) -> State:
     """Update the subscription model with the destination Edge Port attached to the Access Port that is migrated."""
     ap_list = getattr(subscription, service_name).l3_core.ap_list
diff --git a/gso/workflows/l3_core_service/geant_ip/create_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/create_geant_ip.py
index 3530558ff..bee6b5d7b 100644
--- a/gso/workflows/l3_core_service/geant_ip/create_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/create_geant_ip.py
@@ -16,10 +16,11 @@ from gso.workflows.l3_core_service.base_create_l3_core_service import (
     create_new_sharepoint_checklist,
     deploy_bgp_peers_dry,
     deploy_bgp_peers_real,
+    initial_input_form_generator,
     initialize_service_binding,
     provision_sbp_dry,
     provision_sbp_real,
-    update_dns_records, initial_input_form_generator,
+    update_dns_records,
 )
 
 
@@ -33,7 +34,7 @@ def create_subscription(product: UUIDstr, partner: str) -> State:
 
 @step("Initialize subscription")
 def initialize_subscription(
-        subscription: GeantIPInactive, edge_port: dict, binding_port_input: dict, product_name: str, service_name: str
+    subscription: GeantIPInactive, edge_port: dict, binding_port_input: dict, product_name: str, service_name: str
 ) -> State:
     """Take all user inputs and use them to populate the subscription model."""
     return initialize_service_binding(subscription, edge_port, binding_port_input, product_name, service_name)
@@ -47,22 +48,22 @@ def initialize_subscription(
 def create_geant_ip() -> StepList:
     """Create a new GÉANT IP subscription."""
     return (
-            begin
-            >> create_subscription
-            >> store_process_subscription(Target.CREATE)
-            >> initialize_subscription
-            >> start_moodi()
-            >> lso_interaction(provision_sbp_dry)
-            >> lso_interaction(provision_sbp_real)
-            >> lso_interaction(check_sbp_functionality)
-            >> lso_interaction(deploy_bgp_peers_dry)
-            >> lso_interaction(deploy_bgp_peers_real)
-            >> lso_interaction(check_bgp_peers)
-            >> update_dns_records
-            >> set_status(SubscriptionLifecycle.ACTIVE)
-            >> resync
-            >> create_new_sharepoint_checklist
-            >> prompt_sharepoint_checklist_url
-            >> stop_moodi()
-            >> done
+        begin
+        >> create_subscription
+        >> store_process_subscription(Target.CREATE)
+        >> initialize_subscription
+        >> start_moodi()
+        >> lso_interaction(provision_sbp_dry)
+        >> lso_interaction(provision_sbp_real)
+        >> lso_interaction(check_sbp_functionality)
+        >> lso_interaction(deploy_bgp_peers_dry)
+        >> lso_interaction(deploy_bgp_peers_real)
+        >> lso_interaction(check_bgp_peers)
+        >> update_dns_records
+        >> set_status(SubscriptionLifecycle.ACTIVE)
+        >> resync
+        >> create_new_sharepoint_checklist
+        >> prompt_sharepoint_checklist_url
+        >> stop_moodi()
+        >> done
     )
diff --git a/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py
index 2a16499e5..170bc37a8 100644
--- a/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py
@@ -18,17 +18,30 @@ from pydantic_forms.types import FormGenerator, UUIDstr
 from gso.products import GeantIP
 from gso.services.lso_client import lso_interaction
 from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, start_moodi, stop_moodi
-from gso.workflows.l3_core_service.base_migrate_l3_core_service import show_bgp_neighbors, deactivate_bgp_dry, \
-    deactivate_bgp_real, inform_operator_traffic_check, inject_partner_name, deactivate_sbp_dry, deactivate_sbp_real, \
-    deploy_destination_ep_dry, deploy_destination_ep_real, deploy_bgp_session_dry, deploy_bgp_session_real, \
-    update_dns_records, update_subscription_model, initial_input_form, generate_scoped_subscription_model
+from gso.workflows.l3_core_service.base_migrate_l3_core_service import (
+    deactivate_bgp_dry,
+    deactivate_bgp_real,
+    deactivate_sbp_dry,
+    deactivate_sbp_real,
+    deploy_bgp_session_dry,
+    deploy_bgp_session_real,
+    deploy_destination_ep_dry,
+    deploy_destination_ep_real,
+    generate_scoped_subscription_model,
+    inform_operator_traffic_check,
+    initial_input_form,
+    inject_partner_name,
+    show_bgp_neighbors,
+    update_dns_records,
+    update_subscription_model,
+)
 
 
 def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     """Gather input from the operator on what destination Edge Ports this GÉANT IP Service should be migrated to."""
     subscription = GeantIP.from_subscription(subscription_id)
 
-    return initial_input_form(subscription, service_name='geant_ip')
+    return initial_input_form(subscription, service_name="geant_ip")
 
 
 @workflow(
@@ -41,26 +54,25 @@ def migrate_geant_ip() -> StepList:
     is_human_initiated_wf = conditional(lambda state: bool(state.get(IS_HUMAN_INITIATED_WF_KEY)))
 
     return (
-            begin
-            >> store_process_subscription(Target.MODIFY)
-            >> inject_partner_name
-            >> is_human_initiated_wf(
-        lso_interaction(show_bgp_neighbors))  # TODO: send OTRS email with pre-check results
-            >> is_human_initiated_wf(lso_interaction(deactivate_bgp_dry))
-            >> is_human_initiated_wf(lso_interaction(deactivate_bgp_real))
-            >> is_human_initiated_wf(lso_interaction(deactivate_sbp_dry))
-            >> is_human_initiated_wf(lso_interaction(deactivate_sbp_real))
-            >> is_human_initiated_wf(inform_operator_traffic_check)
-            >> unsync
-            >> generate_scoped_subscription_model
-            >> start_moodi()  # TODO: include results from first LSO run
-            >> lso_interaction(deploy_destination_ep_dry)
-            >> lso_interaction(deploy_destination_ep_real)
-            >> lso_interaction(deploy_bgp_session_dry)
-            >> lso_interaction(deploy_bgp_session_real)
-            >> update_dns_records
-            >> update_subscription_model
-            >> resync
-            >> stop_moodi()
-            >> done
+        begin
+        >> store_process_subscription(Target.MODIFY)
+        >> inject_partner_name
+        >> is_human_initiated_wf(lso_interaction(show_bgp_neighbors))  # TODO: send OTRS email with pre-check results
+        >> is_human_initiated_wf(lso_interaction(deactivate_bgp_dry))
+        >> is_human_initiated_wf(lso_interaction(deactivate_bgp_real))
+        >> is_human_initiated_wf(lso_interaction(deactivate_sbp_dry))
+        >> is_human_initiated_wf(lso_interaction(deactivate_sbp_real))
+        >> is_human_initiated_wf(inform_operator_traffic_check)
+        >> unsync
+        >> generate_scoped_subscription_model
+        >> start_moodi()  # TODO: include results from first LSO run
+        >> lso_interaction(deploy_destination_ep_dry)
+        >> lso_interaction(deploy_destination_ep_real)
+        >> lso_interaction(deploy_bgp_session_dry)
+        >> lso_interaction(deploy_bgp_session_real)
+        >> update_dns_records
+        >> update_subscription_model
+        >> resync
+        >> stop_moodi()
+        >> done
     )
diff --git a/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py
index 7d38a0089..a67758336 100644
--- a/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py
@@ -31,10 +31,10 @@ def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 def terminate_geeant_ip() -> StepList:
     """Terminate an GÉANT IP subscription."""
     return (
-            begin
-            >> store_process_subscription(Target.TERMINATE)
-            >> unsync
-            >> set_status(SubscriptionLifecycle.TERMINATED)
-            >> resync
-            >> done
+        begin
+        >> store_process_subscription(Target.TERMINATE)
+        >> unsync
+        >> set_status(SubscriptionLifecycle.TERMINATED)
+        >> resync
+        >> done
     )
diff --git a/gso/workflows/l3_core_service/ias/create_ias.py b/gso/workflows/l3_core_service/ias/create_ias.py
index c661de933..9ca5f34f3 100644
--- a/gso/workflows/l3_core_service/ias/create_ias.py
+++ b/gso/workflows/l3_core_service/ias/create_ias.py
@@ -51,8 +51,12 @@ def create_subscription(product: UUIDstr, partner: str) -> State:
 
 @step("Initialize subscription")
 def initialize_subscription(
-        subscription: IASInactive, edge_port: dict, binding_port_input: dict, product_name: str, ias_flavor: str,
-        service_name: str
+    subscription: IASInactive,
+    edge_port: dict,
+    binding_port_input: dict,
+    product_name: str,
+    ias_flavor: str,
+    service_name: str,
 ) -> State:
     """Take all user inputs and use them to populate the subscription model."""
     subscription.ias.ias_flavor = ias_flavor
diff --git a/gso/workflows/l3_core_service/ias/migrate_ias.py b/gso/workflows/l3_core_service/ias/migrate_ias.py
index bf86d0e51..70cae3281 100644
--- a/gso/workflows/l3_core_service/ias/migrate_ias.py
+++ b/gso/workflows/l3_core_service/ias/migrate_ias.py
@@ -15,20 +15,33 @@ from orchestrator.workflows.steps import resync, store_process_subscription, uns
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
 from pydantic_forms.types import FormGenerator, UUIDstr
 
-from gso.products import GeantIP, IAS
+from gso.products import IAS
 from gso.services.lso_client import lso_interaction
 from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, start_moodi, stop_moodi
-from gso.workflows.l3_core_service.base_migrate_l3_core_service import show_bgp_neighbors, deactivate_bgp_dry, \
-    deactivate_bgp_real, inform_operator_traffic_check, inject_partner_name, deactivate_sbp_dry, deactivate_sbp_real, \
-    deploy_destination_ep_dry, deploy_destination_ep_real, deploy_bgp_session_dry, deploy_bgp_session_real, \
-    update_dns_records, update_subscription_model, initial_input_form, generate_scoped_subscription_model
+from gso.workflows.l3_core_service.base_migrate_l3_core_service import (
+    deactivate_bgp_dry,
+    deactivate_bgp_real,
+    deactivate_sbp_dry,
+    deactivate_sbp_real,
+    deploy_bgp_session_dry,
+    deploy_bgp_session_real,
+    deploy_destination_ep_dry,
+    deploy_destination_ep_real,
+    generate_scoped_subscription_model,
+    inform_operator_traffic_check,
+    initial_input_form,
+    inject_partner_name,
+    show_bgp_neighbors,
+    update_dns_records,
+    update_subscription_model,
+)
 
 
 def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     """Gather input from the operator on what destination Edge Ports this IAS Service should be migrated to."""
     subscription = IAS.from_subscription(subscription_id)
 
-    return initial_input_form(subscription, service_name='ias')
+    return initial_input_form(subscription, service_name="ias")
 
 
 @workflow(
@@ -41,26 +54,25 @@ def migrate_ias() -> StepList:
     is_human_initiated_wf = conditional(lambda state: bool(state.get(IS_HUMAN_INITIATED_WF_KEY)))
 
     return (
-            begin
-            >> store_process_subscription(Target.MODIFY)
-            >> inject_partner_name
-            >> is_human_initiated_wf(
-        lso_interaction(show_bgp_neighbors))  # TODO: send OTRS email with pre-check results
-            >> is_human_initiated_wf(lso_interaction(deactivate_bgp_dry))
-            >> is_human_initiated_wf(lso_interaction(deactivate_bgp_real))
-            >> is_human_initiated_wf(lso_interaction(deactivate_sbp_dry))
-            >> is_human_initiated_wf(lso_interaction(deactivate_sbp_real))
-            >> is_human_initiated_wf(inform_operator_traffic_check)
-            >> unsync
-            >> generate_scoped_subscription_model
-            >> start_moodi()  # TODO: include results from first LSO run
-            >> lso_interaction(deploy_destination_ep_dry)
-            >> lso_interaction(deploy_destination_ep_real)
-            >> lso_interaction(deploy_bgp_session_dry)
-            >> lso_interaction(deploy_bgp_session_real)
-            >> update_dns_records
-            >> update_subscription_model
-            >> resync
-            >> stop_moodi()
-            >> done
+        begin
+        >> store_process_subscription(Target.MODIFY)
+        >> inject_partner_name
+        >> is_human_initiated_wf(lso_interaction(show_bgp_neighbors))  # TODO: send OTRS email with pre-check results
+        >> is_human_initiated_wf(lso_interaction(deactivate_bgp_dry))
+        >> is_human_initiated_wf(lso_interaction(deactivate_bgp_real))
+        >> is_human_initiated_wf(lso_interaction(deactivate_sbp_dry))
+        >> is_human_initiated_wf(lso_interaction(deactivate_sbp_real))
+        >> is_human_initiated_wf(inform_operator_traffic_check)
+        >> unsync
+        >> generate_scoped_subscription_model
+        >> start_moodi()  # TODO: include results from first LSO run
+        >> lso_interaction(deploy_destination_ep_dry)
+        >> lso_interaction(deploy_destination_ep_real)
+        >> lso_interaction(deploy_bgp_session_dry)
+        >> lso_interaction(deploy_bgp_session_real)
+        >> update_dns_records
+        >> update_subscription_model
+        >> resync
+        >> stop_moodi()
+        >> done
     )
diff --git a/gso/workflows/l3_core_service/lhcone/create_lhcone.py b/gso/workflows/l3_core_service/lhcone/create_lhcone.py
index 485acb0be..4ad5016be 100644
--- a/gso/workflows/l3_core_service/lhcone/create_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/create_lhcone.py
@@ -16,10 +16,11 @@ from gso.workflows.l3_core_service.base_create_l3_core_service import (
     create_new_sharepoint_checklist,
     deploy_bgp_peers_dry,
     deploy_bgp_peers_real,
+    initial_input_form_generator,
     initialize_service_binding,
     provision_sbp_dry,
     provision_sbp_real,
-    update_dns_records, initial_input_form_generator,
+    update_dns_records,
 )
 
 
@@ -33,7 +34,7 @@ def create_subscription(product: UUIDstr, partner: str) -> State:
 
 @step("Initialize subscription")
 def initialize_subscription(
-        subscription: LHCOneInactive, edge_port: dict, binding_port_input: dict, product_name: str, service_name: str
+    subscription: LHCOneInactive, edge_port: dict, binding_port_input: dict, product_name: str, service_name: str
 ) -> State:
     """Take all user inputs and use them to populate the subscription model."""
     return initialize_service_binding(subscription, edge_port, binding_port_input, product_name, service_name)
@@ -47,22 +48,22 @@ def initialize_subscription(
 def create_lhcone() -> StepList:
     """Create a new LHCONE subscription."""
     return (
-            begin
-            >> create_subscription
-            >> store_process_subscription(Target.CREATE)
-            >> initialize_subscription
-            >> start_moodi()
-            >> lso_interaction(provision_sbp_dry)
-            >> lso_interaction(provision_sbp_real)
-            >> lso_interaction(check_sbp_functionality)
-            >> lso_interaction(deploy_bgp_peers_dry)
-            >> lso_interaction(deploy_bgp_peers_real)
-            >> lso_interaction(check_bgp_peers)
-            >> update_dns_records
-            >> set_status(SubscriptionLifecycle.ACTIVE)
-            >> resync
-            >> create_new_sharepoint_checklist
-            >> prompt_sharepoint_checklist_url
-            >> stop_moodi()
-            >> done
+        begin
+        >> create_subscription
+        >> store_process_subscription(Target.CREATE)
+        >> initialize_subscription
+        >> start_moodi()
+        >> lso_interaction(provision_sbp_dry)
+        >> lso_interaction(provision_sbp_real)
+        >> lso_interaction(check_sbp_functionality)
+        >> lso_interaction(deploy_bgp_peers_dry)
+        >> lso_interaction(deploy_bgp_peers_real)
+        >> lso_interaction(check_bgp_peers)
+        >> update_dns_records
+        >> set_status(SubscriptionLifecycle.ACTIVE)
+        >> resync
+        >> create_new_sharepoint_checklist
+        >> prompt_sharepoint_checklist_url
+        >> stop_moodi()
+        >> done
     )
diff --git a/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py b/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py
index 06027e9d7..f5923d7ad 100644
--- a/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py
@@ -18,17 +18,30 @@ from pydantic_forms.types import FormGenerator, UUIDstr
 from gso.products import LHCOne
 from gso.services.lso_client import lso_interaction
 from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, start_moodi, stop_moodi
-from gso.workflows.l3_core_service.base_migrate_l3_core_service import show_bgp_neighbors, deactivate_bgp_dry, \
-    deactivate_bgp_real, inform_operator_traffic_check, inject_partner_name, deactivate_sbp_dry, deactivate_sbp_real, \
-    deploy_destination_ep_dry, deploy_destination_ep_real, deploy_bgp_session_dry, deploy_bgp_session_real, \
-    update_dns_records, update_subscription_model, initial_input_form, generate_scoped_subscription_model
+from gso.workflows.l3_core_service.base_migrate_l3_core_service import (
+    deactivate_bgp_dry,
+    deactivate_bgp_real,
+    deactivate_sbp_dry,
+    deactivate_sbp_real,
+    deploy_bgp_session_dry,
+    deploy_bgp_session_real,
+    deploy_destination_ep_dry,
+    deploy_destination_ep_real,
+    generate_scoped_subscription_model,
+    inform_operator_traffic_check,
+    initial_input_form,
+    inject_partner_name,
+    show_bgp_neighbors,
+    update_dns_records,
+    update_subscription_model,
+)
 
 
 def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     """Gather input from the operator on what destination Edge Ports this LHCOne Service should be migrated to."""
     subscription = LHCOne.from_subscription(subscription_id)
 
-    return initial_input_form(subscription, service_name='lhcone')
+    return initial_input_form(subscription, service_name="lhcone")
 
 
 @workflow(
@@ -41,26 +54,25 @@ def migrate_lhcone() -> StepList:
     is_human_initiated_wf = conditional(lambda state: bool(state.get(IS_HUMAN_INITIATED_WF_KEY)))
 
     return (
-            begin
-            >> store_process_subscription(Target.MODIFY)
-            >> inject_partner_name
-            >> is_human_initiated_wf(
-        lso_interaction(show_bgp_neighbors))  # TODO: send OTRS email with pre-check results
-            >> is_human_initiated_wf(lso_interaction(deactivate_bgp_dry))
-            >> is_human_initiated_wf(lso_interaction(deactivate_bgp_real))
-            >> is_human_initiated_wf(lso_interaction(deactivate_sbp_dry))
-            >> is_human_initiated_wf(lso_interaction(deactivate_sbp_real))
-            >> is_human_initiated_wf(inform_operator_traffic_check)
-            >> unsync
-            >> generate_scoped_subscription_model
-            >> start_moodi()  # TODO: include results from first LSO run
-            >> lso_interaction(deploy_destination_ep_dry)
-            >> lso_interaction(deploy_destination_ep_real)
-            >> lso_interaction(deploy_bgp_session_dry)
-            >> lso_interaction(deploy_bgp_session_real)
-            >> update_dns_records
-            >> update_subscription_model
-            >> resync
-            >> stop_moodi()
-            >> done
+        begin
+        >> store_process_subscription(Target.MODIFY)
+        >> inject_partner_name
+        >> is_human_initiated_wf(lso_interaction(show_bgp_neighbors))  # TODO: send OTRS email with pre-check results
+        >> is_human_initiated_wf(lso_interaction(deactivate_bgp_dry))
+        >> is_human_initiated_wf(lso_interaction(deactivate_bgp_real))
+        >> is_human_initiated_wf(lso_interaction(deactivate_sbp_dry))
+        >> is_human_initiated_wf(lso_interaction(deactivate_sbp_real))
+        >> is_human_initiated_wf(inform_operator_traffic_check)
+        >> unsync
+        >> generate_scoped_subscription_model
+        >> start_moodi()  # TODO: include results from first LSO run
+        >> lso_interaction(deploy_destination_ep_dry)
+        >> lso_interaction(deploy_destination_ep_real)
+        >> lso_interaction(deploy_bgp_session_dry)
+        >> lso_interaction(deploy_bgp_session_real)
+        >> update_dns_records
+        >> update_subscription_model
+        >> resync
+        >> stop_moodi()
+        >> done
     )
diff --git a/gso/workflows/l3_core_service/lhcone/terminate_lhcone.py b/gso/workflows/l3_core_service/lhcone/terminate_lhcone.py
index 0289abb26..4df83d02b 100644
--- a/gso/workflows/l3_core_service/lhcone/terminate_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/terminate_lhcone.py
@@ -31,10 +31,10 @@ def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 def terminate_lhcone() -> StepList:
     """Terminate an LHCOne subscription."""
     return (
-            begin
-            >> store_process_subscription(Target.TERMINATE)
-            >> unsync
-            >> set_status(SubscriptionLifecycle.TERMINATED)
-            >> resync
-            >> done
+        begin
+        >> store_process_subscription(Target.TERMINATE)
+        >> unsync
+        >> set_status(SubscriptionLifecycle.TERMINATED)
+        >> resync
+        >> done
     )
-- 
GitLab


From 43f681fe7ede8e552f9fcfdfdbdab5af1fe07605 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Tue, 25 Mar 2025 15:03:53 +0100
Subject: [PATCH 22/87] Add Copernicus create, modify and terminate WFs

---
 .../l3_core_service/copernicus/__init__.py    |  1 +
 .../copernicus/create_copernicus.py           | 69 +++++++++++++++++++
 .../copernicus/modify_copernicus.py           | 40 +++++++++++
 .../copernicus/terminate_copernicus.py        | 40 +++++++++++
 4 files changed, 150 insertions(+)
 create mode 100644 gso/workflows/l3_core_service/copernicus/__init__.py
 create mode 100644 gso/workflows/l3_core_service/copernicus/create_copernicus.py
 create mode 100644 gso/workflows/l3_core_service/copernicus/modify_copernicus.py
 create mode 100644 gso/workflows/l3_core_service/copernicus/terminate_copernicus.py

diff --git a/gso/workflows/l3_core_service/copernicus/__init__.py b/gso/workflows/l3_core_service/copernicus/__init__.py
new file mode 100644
index 000000000..2b2a70aad
--- /dev/null
+++ b/gso/workflows/l3_core_service/copernicus/__init__.py
@@ -0,0 +1 @@
+"""Copernicus service workflows."""
diff --git a/gso/workflows/l3_core_service/copernicus/create_copernicus.py b/gso/workflows/l3_core_service/copernicus/create_copernicus.py
new file mode 100644
index 000000000..fc4a8e5a4
--- /dev/null
+++ b/gso/workflows/l3_core_service/copernicus/create_copernicus.py
@@ -0,0 +1,69 @@
+"""Create Copernicus subscription workflow."""
+
+from orchestrator.targets import Target
+from orchestrator.types import SubscriptionLifecycle
+from orchestrator.workflow import StepList, begin, done, 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_forms.types import State, UUIDstr
+
+from gso.products.product_types.copernicus import CopernicusInactive
+from gso.services.lso_client import lso_interaction
+from gso.utils.workflow_steps import prompt_sharepoint_checklist_url, start_moodi, stop_moodi
+from gso.workflows.l3_core_service.base_create_l3_core_service import (
+    check_bgp_peers,
+    check_sbp_functionality,
+    create_new_sharepoint_checklist,
+    deploy_bgp_peers_dry,
+    deploy_bgp_peers_real,
+    initial_input_form_generator,
+    initialize_service_binding,
+    provision_sbp_dry,
+    provision_sbp_real,
+    update_dns_records,
+)
+
+
+@step("Create subscription")
+def create_subscription(product: UUIDstr, partner: str) -> State:
+    """Create a new subscription object in the database."""
+    subscription = CopernicusInactive.from_product_id(product, partner)
+
+    return {"subscription": subscription, "subscription_id": subscription.subscription_id, "service_name": "copernicus"}
+
+
+@step("Initialize subscription")
+def initialize_subscription(
+    subscription: CopernicusInactive, edge_port: dict, binding_port_input: dict, product_name: str, service_name: str
+) -> State:
+    """Take all user inputs and use them to populate the subscription model."""
+    return initialize_service_binding(subscription, edge_port, binding_port_input, product_name, service_name)
+
+
+@workflow(
+    "Create Copernicus",
+    initial_input_form=wrap_create_initial_input_form(initial_input_form_generator),
+    target=Target.CREATE,
+)
+def create_copernicus() -> StepList:
+    """Create a new Copernicus subscription."""
+    return (
+        begin
+        >> create_subscription
+        >> store_process_subscription(Target.CREATE)
+        >> initialize_subscription
+        >> start_moodi()
+        >> lso_interaction(provision_sbp_dry)
+        >> lso_interaction(provision_sbp_real)
+        >> lso_interaction(check_sbp_functionality)
+        >> lso_interaction(deploy_bgp_peers_dry)
+        >> lso_interaction(deploy_bgp_peers_real)
+        >> lso_interaction(check_bgp_peers)
+        >> update_dns_records
+        >> set_status(SubscriptionLifecycle.ACTIVE)
+        >> resync
+        >> create_new_sharepoint_checklist
+        >> prompt_sharepoint_checklist_url
+        >> stop_moodi()
+        >> done
+    )
diff --git a/gso/workflows/l3_core_service/copernicus/modify_copernicus.py b/gso/workflows/l3_core_service/copernicus/modify_copernicus.py
new file mode 100644
index 000000000..8f7cbb23b
--- /dev/null
+++ b/gso/workflows/l3_core_service/copernicus/modify_copernicus.py
@@ -0,0 +1,40 @@
+"""Modification workflow for a Copernicus  subscription."""
+
+from orchestrator import begin, conditional, done, workflow
+from orchestrator.targets import Target
+from orchestrator.workflow import StepList
+from orchestrator.workflows.steps import resync, store_process_subscription, unsync
+from orchestrator.workflows.utils import wrap_modify_initial_input_form
+
+from gso.workflows.l3_core_service.base_modify_l3_core_service import (
+    Operation,
+    create_new_sbp,
+    modify_existing_sbp,
+    remove_old_sbp,
+)
+from gso.workflows.l3_core_service.base_modify_l3_core_service import (
+    initial_input_form_generator as base_initial_input_form_generator,
+)
+
+
+@workflow(
+    "Modify Copernicus ",
+    initial_input_form=wrap_modify_initial_input_form(base_initial_input_form_generator),
+    target=Target.MODIFY,
+)
+def modify_copernicus() -> StepList:
+    """Modify Copernicus  subscription."""
+    access_port_is_added = conditional(lambda state: state["operation"] == Operation.ADD)
+    access_port_is_removed = conditional(lambda state: state["operation"] == Operation.REMOVE)
+    access_port_is_modified = conditional(lambda state: state["operation"] == Operation.EDIT)
+
+    return (
+        begin
+        >> store_process_subscription(Target.MODIFY)
+        >> unsync
+        >> access_port_is_added(create_new_sbp)
+        >> access_port_is_removed(remove_old_sbp)
+        >> access_port_is_modified(modify_existing_sbp)
+        >> resync
+        >> done
+    )
diff --git a/gso/workflows/l3_core_service/copernicus/terminate_copernicus.py b/gso/workflows/l3_core_service/copernicus/terminate_copernicus.py
new file mode 100644
index 000000000..532277e76
--- /dev/null
+++ b/gso/workflows/l3_core_service/copernicus/terminate_copernicus.py
@@ -0,0 +1,40 @@
+"""Workflow for terminating an Copernicus subscription."""
+
+from orchestrator import begin, workflow
+from orchestrator.forms import SubmitFormPage
+from orchestrator.targets import Target
+from orchestrator.types import SubscriptionLifecycle
+from orchestrator.workflow import StepList, done
+from orchestrator.workflows.steps import resync, set_status, store_process_subscription, unsync
+from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from pydantic_forms.types import FormGenerator, UUIDstr
+
+from gso.products.product_types.copernicus import Copernicus
+from gso.utils.types.tt_number import TTNumber
+
+
+def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+    subscription = Copernicus.from_subscription(subscription_id)
+
+    class TerminateForm(SubmitFormPage):
+        tt_number: TTNumber
+
+    yield TerminateForm
+    return {"subscription": subscription}
+
+
+@workflow(
+    "Terminate Copernicus",
+    initial_input_form=wrap_modify_initial_input_form(_input_form_generator),
+    target=Target.TERMINATE,
+)
+def terminate_copernicus() -> StepList:
+    """Terminate a Copernicus subscription."""
+    return (
+        begin
+        >> store_process_subscription(Target.TERMINATE)
+        >> unsync
+        >> set_status(SubscriptionLifecycle.TERMINATED)
+        >> resync
+        >> done
+    )
-- 
GitLab


From 4094219b62a97e74f3f4a4f638f5d97cc9bd2c96 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Tue, 25 Mar 2025 15:04:13 +0100
Subject: [PATCH 23/87] Fix terminate_geant_ip naming

---
 gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py
index a67758336..9a73d9d9c 100644
--- a/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py
@@ -28,7 +28,7 @@ def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     initial_input_form=wrap_modify_initial_input_form(_input_form_generator),
     target=Target.TERMINATE,
 )
-def terminate_geeant_ip() -> StepList:
+def terminate_geant_ip() -> StepList:
     """Terminate an GÉANT IP subscription."""
     return (
         begin
-- 
GitLab


From 563fdad52cd5c04f078171b98ee418d5fb5a3685 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Tue, 25 Mar 2025 15:16:49 +0100
Subject: [PATCH 24/87] add seperate import wfs for L3 Core Services

---
 gso/workflows/__init__.py                     |  4 +-
 .../geant_ip/import_geant_ip.py               | 37 +++++++++++++++++++
 .../l3_core_service/ias/import_ias.py         | 30 +++++++++++++++
 .../l3_core_service/lhcone/import_lhcone.py   | 30 +++++++++++++++
 4 files changed, 100 insertions(+), 1 deletion(-)
 create mode 100644 gso/workflows/l3_core_service/geant_ip/import_geant_ip.py
 create mode 100644 gso/workflows/l3_core_service/ias/import_ias.py
 create mode 100644 gso/workflows/l3_core_service/lhcone/import_lhcone.py

diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py
index 2ecf5b5fa..eba91ad45 100644
--- a/gso/workflows/__init__.py
+++ b/gso/workflows/__init__.py
@@ -122,7 +122,9 @@ LazyWorkflowInstance("gso.workflows.edge_port.migrate_edge_port", "migrate_edge_
 LazyWorkflowInstance("gso.workflows.l3_core_service.create_l3_core_service", "create_l3_core_service")
 LazyWorkflowInstance("gso.workflows.l3_core_service.modify_l3_core_service", "modify_l3_core_service")
 LazyWorkflowInstance("gso.workflows.l3_core_service.create_imported_l3_core_service", "create_imported_l3_core_service")
-LazyWorkflowInstance("gso.workflows.l3_core_service.import_l3_core_service", "import_l3_core_service")
+LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.import_geant_ip", "import_geant_ip")
+LazyWorkflowInstance("gso.workflows.l3_core_service.ias.import_ias", "import_ias")
+LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.import_lhcone", "import_lhcone")
 LazyWorkflowInstance("gso.workflows.l3_core_service.migrate_l3_core_service", "migrate_l3_core_service")
 LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.migrate_geant_ip", "migrate_geant_ip")
 LazyWorkflowInstance("gso.workflows.l3_core_service.ias.migrate_ias", "migrate_ias")
diff --git a/gso/workflows/l3_core_service/geant_ip/import_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/import_geant_ip.py
new file mode 100644
index 000000000..886fc3065
--- /dev/null
+++ b/gso/workflows/l3_core_service/geant_ip/import_geant_ip.py
@@ -0,0 +1,37 @@
+"""A modification workflow for migrating an `ImportedGeantIP` to a `GeantIP` subscription."""
+
+from orchestrator.targets import Target
+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 pydantic_forms.types import State, UUIDstr
+
+from gso.products import ProductName
+from gso.products.product_types.geant_ip import GeantIP, ImportedGeantIP
+from gso.services.partners import get_partner_by_id
+from gso.services.subscriptions import get_product_id_by_name
+
+
+@step("Create imported subscription")
+def import_l3_core_service_subscription(subscription_id: UUIDstr) -> State:
+    """Take an imported subscription, and turn it into a GÉANT IP subscription."""
+    old_l3_core_service = ImportedGeantIP.from_subscription(subscription_id)
+    new_product_id = get_product_id_by_name(ProductName.GEANT_IP)
+    new_subscription = GeantIP.from_other_product(old_l3_core_service, new_product_id)
+    new_subscription.description = (
+        f"{new_subscription.product.name} service for {get_partner_by_id(new_subscription.customer_id).name}"
+    )
+    return {"subscription": new_subscription}
+
+
+@workflow("Import GÉANT IP Core Service", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None))
+def import_geant_ip() -> StepList:
+    """Modify an imported subscription into a GÉANT IP subscription to complete the import."""
+    return (
+        init
+        >> store_process_subscription(Target.MODIFY)
+        >> unsync
+        >> import_l3_core_service_subscription
+        >> resync
+        >> done
+    )
diff --git a/gso/workflows/l3_core_service/ias/import_ias.py b/gso/workflows/l3_core_service/ias/import_ias.py
new file mode 100644
index 000000000..5c1318f8c
--- /dev/null
+++ b/gso/workflows/l3_core_service/ias/import_ias.py
@@ -0,0 +1,30 @@
+"""A modification workflow for migrating an `ImportedIAS` to an `IAS` subscription."""
+
+from orchestrator.targets import Target
+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 pydantic_forms.types import State, UUIDstr
+
+from gso.products import ProductName
+from gso.products.product_types.ias import IAS, ImportedIAS
+from gso.services.partners import get_partner_by_id
+from gso.services.subscriptions import get_product_id_by_name
+
+
+@step("Create imported subscription")
+def import_ias_subscription(subscription_id: UUIDstr) -> State:
+    """Take an imported subscription, and turn it into an IAS subscription."""
+    old_l3_core_service = ImportedIAS.from_subscription(subscription_id)
+    new_product_id = get_product_id_by_name(ProductName.GEANT_IP)
+    new_subscription = IAS.from_other_product(old_l3_core_service, new_product_id)
+    new_subscription.description = (
+        f"{new_subscription.product.name} service for {get_partner_by_id(new_subscription.customer_id).name}"
+    )
+    return {"subscription": new_subscription}
+
+
+@workflow("Import IAS Core Service", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None))
+def import_l3_core_service() -> StepList:
+    """Modify an imported subscription into an IAS subscription to complete the import."""
+    return init >> store_process_subscription(Target.MODIFY) >> unsync >> import_ias_subscription >> resync >> done
diff --git a/gso/workflows/l3_core_service/lhcone/import_lhcone.py b/gso/workflows/l3_core_service/lhcone/import_lhcone.py
new file mode 100644
index 000000000..9ee7c5e79
--- /dev/null
+++ b/gso/workflows/l3_core_service/lhcone/import_lhcone.py
@@ -0,0 +1,30 @@
+"""A modification workflow for migrating an `ImportedLHCOne` to an `LHCOne` subscription."""
+
+from orchestrator.targets import Target
+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 pydantic_forms.types import State, UUIDstr
+
+from gso.products import ProductName
+from gso.products.product_types.lhcone import ImportedLHCOne, LHCOne
+from gso.services.partners import get_partner_by_id
+from gso.services.subscriptions import get_product_id_by_name
+
+
+@step("Create imported subscription")
+def import_lhcone_subscription(subscription_id: UUIDstr) -> State:
+    """Take an imported subscription, and turn it into a LHCOne Service subscription."""
+    old_l3_core_service = ImportedLHCOne.from_subscription(subscription_id)
+    new_product_id = get_product_id_by_name(ProductName.GEANT_IP)
+    new_subscription = LHCOne.from_other_product(old_l3_core_service, new_product_id)
+    new_subscription.description = (
+        f"{new_subscription.product.name} service for {get_partner_by_id(new_subscription.customer_id).name}"
+    )
+    return {"subscription": new_subscription}
+
+
+@workflow("Import LHCOne Service", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None))
+def import_lhcone() -> StepList:
+    """Modify an imported subscription into a LHCOne subscription to complete the import."""
+    return init >> store_process_subscription(Target.MODIFY) >> unsync >> import_lhcone_subscription >> resync >> done
-- 
GitLab


From e5a885e80f85f5c0fcfa2168bd8a9e1c26c96ba2 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Tue, 25 Mar 2025 15:26:59 +0100
Subject: [PATCH 25/87] add import wf for Copernicus service

---
 gso/workflows/__init__.py                     |  1 +
 .../copernicus/import_copernicus.py           | 32 +++++++++++++++++++
 .../l3_core_service/ias/import_ias.py         |  2 +-
 .../l3_core_service/lhcone/import_lhcone.py   |  2 +-
 4 files changed, 35 insertions(+), 2 deletions(-)
 create mode 100644 gso/workflows/l3_core_service/copernicus/import_copernicus.py

diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py
index eba91ad45..a6957f5b2 100644
--- a/gso/workflows/__init__.py
+++ b/gso/workflows/__init__.py
@@ -125,6 +125,7 @@ LazyWorkflowInstance("gso.workflows.l3_core_service.create_imported_l3_core_serv
 LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.import_geant_ip", "import_geant_ip")
 LazyWorkflowInstance("gso.workflows.l3_core_service.ias.import_ias", "import_ias")
 LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.import_lhcone", "import_lhcone")
+LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.import_copernicus", "import_copernicus")
 LazyWorkflowInstance("gso.workflows.l3_core_service.migrate_l3_core_service", "migrate_l3_core_service")
 LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.migrate_geant_ip", "migrate_geant_ip")
 LazyWorkflowInstance("gso.workflows.l3_core_service.ias.migrate_ias", "migrate_ias")
diff --git a/gso/workflows/l3_core_service/copernicus/import_copernicus.py b/gso/workflows/l3_core_service/copernicus/import_copernicus.py
new file mode 100644
index 000000000..38a876f1f
--- /dev/null
+++ b/gso/workflows/l3_core_service/copernicus/import_copernicus.py
@@ -0,0 +1,32 @@
+"""A modification workflow for migrating an `ImportedCopernicus` to an `Copernicus` subscription."""
+
+from orchestrator.targets import Target
+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 pydantic_forms.types import State, UUIDstr
+
+from gso.products import ProductName
+from gso.products.product_types.copernicus import ImportedCopernicus, Copernicus
+from gso.services.partners import get_partner_by_id
+from gso.services.subscriptions import get_product_id_by_name
+
+
+@step("Create imported subscription")
+def import_copernicus_subscription(subscription_id: UUIDstr) -> State:
+    """Take an imported subscription, and turn it into a Copernicus Service subscription."""
+    old_l3_core_service = ImportedCopernicus.from_subscription(subscription_id)
+    new_product_id = get_product_id_by_name(ProductName.COPERNICUS)
+    new_subscription = Copernicus.from_other_product(old_l3_core_service, new_product_id)
+    new_subscription.description = (
+        f"{new_subscription.product.name} service for {get_partner_by_id(new_subscription.customer_id).name}"
+    )
+    return {"subscription": new_subscription}
+
+
+@workflow("Import Copernicus Service", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None))
+def import_copernicus() -> StepList:
+    """Modify an imported subscription into a Copernicus subscription to complete the import."""
+    return (
+        init >> store_process_subscription(Target.MODIFY) >> unsync >> import_copernicus_subscription >> resync >> done
+    )
diff --git a/gso/workflows/l3_core_service/ias/import_ias.py b/gso/workflows/l3_core_service/ias/import_ias.py
index 5c1318f8c..9deef38df 100644
--- a/gso/workflows/l3_core_service/ias/import_ias.py
+++ b/gso/workflows/l3_core_service/ias/import_ias.py
@@ -16,7 +16,7 @@ from gso.services.subscriptions import get_product_id_by_name
 def import_ias_subscription(subscription_id: UUIDstr) -> State:
     """Take an imported subscription, and turn it into an IAS subscription."""
     old_l3_core_service = ImportedIAS.from_subscription(subscription_id)
-    new_product_id = get_product_id_by_name(ProductName.GEANT_IP)
+    new_product_id = get_product_id_by_name(ProductName.IAS)
     new_subscription = IAS.from_other_product(old_l3_core_service, new_product_id)
     new_subscription.description = (
         f"{new_subscription.product.name} service for {get_partner_by_id(new_subscription.customer_id).name}"
diff --git a/gso/workflows/l3_core_service/lhcone/import_lhcone.py b/gso/workflows/l3_core_service/lhcone/import_lhcone.py
index 9ee7c5e79..749bf2e7b 100644
--- a/gso/workflows/l3_core_service/lhcone/import_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/import_lhcone.py
@@ -16,7 +16,7 @@ from gso.services.subscriptions import get_product_id_by_name
 def import_lhcone_subscription(subscription_id: UUIDstr) -> State:
     """Take an imported subscription, and turn it into a LHCOne Service subscription."""
     old_l3_core_service = ImportedLHCOne.from_subscription(subscription_id)
-    new_product_id = get_product_id_by_name(ProductName.GEANT_IP)
+    new_product_id = get_product_id_by_name(ProductName.LHCONE)
     new_subscription = LHCOne.from_other_product(old_l3_core_service, new_product_id)
     new_subscription.description = (
         f"{new_subscription.product.name} service for {get_partner_by_id(new_subscription.customer_id).name}"
-- 
GitLab


From 8a4f312bfcdc65c747785c47b9eaca130567ab1b Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Tue, 25 Mar 2025 15:27:21 +0100
Subject: [PATCH 26/87] add import wf for Copernicus service

---
 gso/workflows/l3_core_service/copernicus/import_copernicus.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gso/workflows/l3_core_service/copernicus/import_copernicus.py b/gso/workflows/l3_core_service/copernicus/import_copernicus.py
index 38a876f1f..85cf9b305 100644
--- a/gso/workflows/l3_core_service/copernicus/import_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/import_copernicus.py
@@ -7,7 +7,7 @@ from orchestrator.workflows.utils import wrap_modify_initial_input_form
 from pydantic_forms.types import State, UUIDstr
 
 from gso.products import ProductName
-from gso.products.product_types.copernicus import ImportedCopernicus, Copernicus
+from gso.products.product_types.copernicus import Copernicus, ImportedCopernicus
 from gso.services.partners import get_partner_by_id
 from gso.services.subscriptions import get_product_id_by_name
 
-- 
GitLab


From 5674c95d534c80015ad238bc55c712af5900d53e Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Tue, 25 Mar 2025 15:34:08 +0100
Subject: [PATCH 27/87] add migrate wf for copernicus

---
 gso/workflows/__init__.py                     |  5 +-
 .../copernicus/migrate_copernicus.py          | 78 +++++++++++++++++++
 2 files changed, 82 insertions(+), 1 deletion(-)
 create mode 100644 gso/workflows/l3_core_service/copernicus/migrate_copernicus.py

diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py
index a6957f5b2..e00030451 100644
--- a/gso/workflows/__init__.py
+++ b/gso/workflows/__init__.py
@@ -126,10 +126,13 @@ LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.import_geant_ip", "
 LazyWorkflowInstance("gso.workflows.l3_core_service.ias.import_ias", "import_ias")
 LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.import_lhcone", "import_lhcone")
 LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.import_copernicus", "import_copernicus")
-LazyWorkflowInstance("gso.workflows.l3_core_service.migrate_l3_core_service", "migrate_l3_core_service")
+LazyWorkflowInstance(
+    "gso.workflows.l3_core_service.migrate_l3_core_service", "migrate_l3_core_service"
+)  # TODO: Remove this workflow at the end
 LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.migrate_geant_ip", "migrate_geant_ip")
 LazyWorkflowInstance("gso.workflows.l3_core_service.ias.migrate_ias", "migrate_ias")
 LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.migrate_lhcone", "migrate_lhcone")
+LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.migrate_copernicus", "migrate_copernicus")
 LazyWorkflowInstance("gso.workflows.l3_core_service.validate_l3_core_service", "validate_l3_core_service")
 LazyWorkflowInstance("gso.workflows.l3_core_service.terminate_l3_core_service", "terminate_l3_core_service")
 LazyWorkflowInstance("gso.workflows.l3_core_service.validate_prefix_list", "validate_prefix_list")
diff --git a/gso/workflows/l3_core_service/copernicus/migrate_copernicus.py b/gso/workflows/l3_core_service/copernicus/migrate_copernicus.py
new file mode 100644
index 000000000..b4e9d9346
--- /dev/null
+++ b/gso/workflows/l3_core_service/copernicus/migrate_copernicus.py
@@ -0,0 +1,78 @@
+"""A modification workflow that migrates a Copernicus Service to a new Edge Port.
+
+In one run of a migration workflow, only a single Access Port can be replaced. This is due to the nature of these
+services. When a service is dually homed, for example, only one of the Access Ports should be migrated. The other one
+will remain the way it is.
+
+At the start of the workflow, the operator will select one Access Port that is to be migrated, and will then select a
+destination Edge Port that this service should be placed on. All other Access Ports will be left as-is.
+"""
+
+from orchestrator import workflow
+from orchestrator.targets import Target
+from orchestrator.workflow import StepList, begin, conditional, done
+from orchestrator.workflows.steps import resync, store_process_subscription, unsync
+from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from pydantic_forms.types import FormGenerator, UUIDstr
+
+from gso.products import Copernicus  # TODO replace this everywhere with the correct product
+from gso.services.lso_client import lso_interaction
+from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, start_moodi, stop_moodi
+from gso.workflows.l3_core_service.base_migrate_l3_core_service import (
+    deactivate_bgp_dry,
+    deactivate_bgp_real,
+    deactivate_sbp_dry,
+    deactivate_sbp_real,
+    deploy_bgp_session_dry,
+    deploy_bgp_session_real,
+    deploy_destination_ep_dry,
+    deploy_destination_ep_real,
+    generate_scoped_subscription_model,
+    inform_operator_traffic_check,
+    initial_input_form,
+    inject_partner_name,
+    show_bgp_neighbors,
+    update_dns_records,
+    update_subscription_model,
+)
+
+
+def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+    """Gather input from the operator on what destination Edge Ports this Copernicus Service should be migrated to."""
+    subscription = Copernicus.from_subscription(subscription_id)
+
+    return initial_input_form(subscription, service_name="copernicus")
+
+
+@workflow(
+    "Migrate Copernicus Service",
+    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
+    target=Target.MODIFY,
+)
+def migrate_copernicus() -> StepList:
+    """Migrate a Copernicus Service to a destination Edge Port."""
+    is_human_initiated_wf = conditional(lambda state: bool(state.get(IS_HUMAN_INITIATED_WF_KEY)))
+
+    return (
+        begin
+        >> store_process_subscription(Target.MODIFY)
+        >> inject_partner_name
+        >> is_human_initiated_wf(lso_interaction(show_bgp_neighbors))  # TODO: send OTRS email with pre-check results
+        >> is_human_initiated_wf(lso_interaction(deactivate_bgp_dry))
+        >> is_human_initiated_wf(lso_interaction(deactivate_bgp_real))
+        >> is_human_initiated_wf(lso_interaction(deactivate_sbp_dry))
+        >> is_human_initiated_wf(lso_interaction(deactivate_sbp_real))
+        >> is_human_initiated_wf(inform_operator_traffic_check)
+        >> unsync
+        >> generate_scoped_subscription_model
+        >> start_moodi()  # TODO: include results from first LSO run
+        >> lso_interaction(deploy_destination_ep_dry)
+        >> lso_interaction(deploy_destination_ep_real)
+        >> lso_interaction(deploy_bgp_session_dry)
+        >> lso_interaction(deploy_bgp_session_real)
+        >> update_dns_records
+        >> update_subscription_model
+        >> resync
+        >> stop_moodi()
+        >> done
+    )
-- 
GitLab


From a144532d8b24a719ef571bb5de8232f256d06130 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Tue, 25 Mar 2025 15:38:44 +0100
Subject: [PATCH 28/87] Add product types to a shared module

---
 gso/workflows/l3_core_service/shared.py | 9 +++++++++
 1 file changed, 9 insertions(+)
 create mode 100644 gso/workflows/l3_core_service/shared.py

diff --git a/gso/workflows/l3_core_service/shared.py b/gso/workflows/l3_core_service/shared.py
new file mode 100644
index 000000000..6facbbd83
--- /dev/null
+++ b/gso/workflows/l3_core_service/shared.py
@@ -0,0 +1,9 @@
+"""Shared logic for L3 Core Service."""
+
+from gso.products.product_types.copernicus import Copernicus, CopernicusInactive
+from gso.products.product_types.geant_ip import GeantIP, GeantIPInactive
+from gso.products.product_types.ias import IAS, IASInactive
+from gso.products.product_types.lhcone import LHCOne, LHCOneInactive
+
+L3ProductTypes = IAS | LHCOne | Copernicus | GeantIP
+L3InactiveProductTypes = IASInactive | LHCOneInactive | CopernicusInactive | GeantIPInactive
\ No newline at end of file
-- 
GitLab


From 8fd2116a4a4ed64df0ca89a964d306ff0b108456 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Tue, 25 Mar 2025 15:39:25 +0100
Subject: [PATCH 29/87] Update base modules to adapt new shared product types

---
 .../base_create_l3_core_service.py            | 32 +++++++++----------
 .../base_migrate_l3_core_service.py           | 29 +++++++----------
 2 files changed, 27 insertions(+), 34 deletions(-)

diff --git a/gso/workflows/l3_core_service/base_create_l3_core_service.py b/gso/workflows/l3_core_service/base_create_l3_core_service.py
index 8ddf3101d..09a375139 100644
--- a/gso/workflows/l3_core_service/base_create_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_l3_core_service.py
@@ -13,12 +13,7 @@ from pydantic_forms.validators import Divider
 from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPTypes
 from gso.products.product_blocks.l3_core_service import AccessPortInactive
 from gso.products.product_blocks.service_binding_port import BFDSettings, ServiceBindingPortInactive
-from gso.products.product_types.copernicus import CopernicusInactive
 from gso.products.product_types.edge_port import EdgePort
-from gso.products.product_types.geant_ip import GeantIPInactive
-from gso.products.product_types.ias import IAS, IASInactive
-from gso.products.product_types.l3_core_service import L3CoreService
-from gso.products.product_types.lhcone import LHCOneInactive
 from gso.services.lso_client import LSOState
 from gso.services.partners import get_partner_by_id
 from gso.services.sharepoint import SharePointClient
@@ -33,6 +28,7 @@ from gso.utils.types.geant_ids import IMPORTED_GS_ID
 from gso.utils.types.ip_address import IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask
 from gso.utils.types.tt_number import TTNumber
 from gso.utils.types.virtual_identifiers import VLAN_ID
+from gso.workflows.l3_core_service.shared import L3InactiveProductTypes, L3ProductTypes
 
 
 def initial_input_form_generator(product_name: str) -> FormGenerator:
@@ -165,7 +161,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
 
 
 def initialize_service_binding(
-    subscription: IASInactive | GeantIPInactive | LHCOneInactive | CopernicusInactive,
+    subscription: L3InactiveProductTypes,
     edge_port: dict,
     binding_port_input: dict,
     product_name: str,
@@ -199,16 +195,17 @@ def initialize_service_binding(
         gs_id=sbp_gs_id,
     )
 
-    if service := getattr(subscription, service_name):  # TODO: I think we should remove this if statement
-        service.l3_core.ap_list.append(
-            AccessPortInactive.new(
-                subscription_id=uuid4(),
-                ap_type=edge_port["ap_type"],
-                sbp=service_binding_port,
-                custom_service_name=edge_port.get("custom_service_name"),
-            )
+    service = getattr(subscription, service_name)
+    assert service is not None, f"{service_name} is not set on subscription"
+
+    service.l3_core.ap_list.append(
+        AccessPortInactive.new(
+            subscription_id=uuid4(),
+            ap_type=edge_port["ap_type"],
+            sbp=service_binding_port,
+            custom_service_name=edge_port.get("custom_service_name"),
         )
-
+    )
     edge_port_fqdn_list.append(edge_port_subscription.edge_port.node.router_fqdn)
 
     partner_name = get_partner_by_id(subscription.customer_id).name
@@ -330,7 +327,7 @@ def check_bgp_peers(subscription: dict[str, Any], edge_port_fqdn_list: list[str]
 
 
 @step("Update Infoblox")
-def update_dns_records(subscription: L3CoreService | IAS) -> State:
+def update_dns_records(subscription: L3ProductTypes) -> State:
     """Update DNS records in Infoblox."""
     #  TODO: implement
     return {"subscription": subscription}
@@ -338,13 +335,14 @@ def update_dns_records(subscription: L3CoreService | IAS) -> State:
 
 @step("Create a new SharePoint checklist item")
 def create_new_sharepoint_checklist(
-    subscription: L3CoreService,
+    subscription: L3ProductTypes,
     tt_number: TTNumber,
     process_id: UUIDstr,
     service_name: str,
 ) -> State:
     """Create a new checklist item in SharePoint for approving this L3 Core Service."""
     service = getattr(subscription, service_name)
+    assert service is not None, f"{service_name} is not set on subscription"
     new_ep = service.l3_core.ap_list[0].sbp.edge_port
     new_list_item_url = SharePointClient().add_list_item(
         list_name="l3_core_service",
diff --git a/gso/workflows/l3_core_service/base_migrate_l3_core_service.py b/gso/workflows/l3_core_service/base_migrate_l3_core_service.py
index 47422785e..2fdb4207e 100644
--- a/gso/workflows/l3_core_service/base_migrate_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_migrate_l3_core_service.py
@@ -9,7 +9,7 @@ destination Edge Port that this service should be placed on. All other Access Po
 """
 
 import json
-from typing import Any, Union
+from typing import Any
 
 from orchestrator.config.assignee import Assignee
 from orchestrator.forms import FormPage, SubmitFormPage
@@ -20,22 +20,17 @@ from pydantic import ConfigDict, Field
 from pydantic_forms.types import FormGenerator, State, UUIDstr
 from pydantic_forms.validators import Choice, Divider, Label
 
-from gso.products.product_types.copernicus import Copernicus
 from gso.products.product_types.edge_port import EdgePort
-from gso.products.product_types.geant_ip import GeantIP
-from gso.products.product_types.ias import IAS
-from gso.products.product_types.lhcone import LHCOne
 from gso.services.lso_client import LSOState
 from gso.services.partners import get_partner_by_id
 from gso.services.subscriptions import get_active_edge_port_subscriptions
 from gso.utils.types.tt_number import TTNumber
 from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, SKIP_MOODI_KEY
+from gso.workflows.l3_core_service.shared import L3ProductTypes
 from gso.workflows.shared import create_summary_form
 
-L3CoreServiceType = Union[GeantIP, IAS, LHCOne, Copernicus]
 
-
-def initial_input_form(subscription: L3CoreServiceType, service_name: str) -> FormGenerator:
+def initial_input_form(subscription: L3ProductTypes, service_name: str) -> FormGenerator:
     partner_id = subscription.customer_id
     ap_list = getattr(subscription, service_name).l3_core.ap_list
 
@@ -101,7 +96,7 @@ def initial_input_form(subscription: L3CoreServiceType, service_name: str) -> Fo
 
 
 @step("Inject Partner Name")
-def inject_partner_name(subscription: L3CoreServiceType) -> LSOState:
+def inject_partner_name(subscription: L3ProductTypes) -> LSOState:
     """Resolve and inject partner name into the state."""
     partner_name = get_partner_by_id(subscription.customer_id).name
 
@@ -110,7 +105,7 @@ def inject_partner_name(subscription: L3CoreServiceType) -> LSOState:
 
 @step("Show BGP neighbors")
 def show_bgp_neighbors(
-    subscription: L3CoreServiceType,
+    subscription: L3ProductTypes,
     process_id: UUIDstr,
     tt_number: TTNumber,
     source_edge_port: EdgePort,
@@ -136,7 +131,7 @@ def show_bgp_neighbors(
 
 @step("[DRY RUN] Deactivate BGP session on the source router")
 def deactivate_bgp_dry(
-    subscription: L3CoreServiceType,
+    subscription: L3ProductTypes,
     process_id: UUIDstr,
     tt_number: TTNumber,
     source_access_port_fqdn: str,
@@ -159,7 +154,7 @@ def deactivate_bgp_dry(
 
 @step("[FOR REAL] Deactivate BGP session on the source router")
 def deactivate_bgp_real(
-    subscription: L3CoreServiceType,
+    subscription: L3ProductTypes,
     process_id: UUIDstr,
     tt_number: TTNumber,
     source_access_port_fqdn: str,
@@ -202,7 +197,7 @@ def inform_operator_traffic_check() -> FormGenerator:
 
 @step("[DRY RUN] Deactivate SBP config on the source router")
 def deactivate_sbp_dry(
-    subscription: L3CoreServiceType,
+    subscription: L3ProductTypes,
     process_id: UUIDstr,
     tt_number: TTNumber,
     source_access_port_fqdn: str,
@@ -224,7 +219,7 @@ def deactivate_sbp_dry(
 
 @step("[FOR REAL] Deactivate SBP config on the source router")
 def deactivate_sbp_real(
-    subscription: L3CoreServiceType,
+    subscription: L3ProductTypes,
     process_id: UUIDstr,
     tt_number: TTNumber,
     source_access_port_fqdn: str,
@@ -247,7 +242,7 @@ def deactivate_sbp_real(
 
 @step("Generate updated subscription model")
 def generate_scoped_subscription_model(
-    subscription: L3CoreServiceType, source_edge_port: EdgePort, destination_edge_port: EdgePort, service_name: str
+    subscription: L3ProductTypes, source_edge_port: EdgePort, destination_edge_port: EdgePort, service_name: str
 ) -> State:
     """Calculate what the updated subscription model will look like, but don't update the actual subscription yet.
 
@@ -371,7 +366,7 @@ def deploy_bgp_session_real(
 
 
 @step("Update Infoblox")
-def update_dns_records(subscription: L3CoreServiceType) -> State:
+def update_dns_records(subscription: L3ProductTypes) -> State:
     """Update DNS records in Infoblox."""
     #  TODO: implement
     return {"subscription": subscription}
@@ -379,7 +374,7 @@ def update_dns_records(subscription: L3CoreServiceType) -> State:
 
 @step("Update subscription model")
 def update_subscription_model(
-    subscription: L3CoreServiceType, destination_edge_port: EdgePort, replaced_ap_index: int, service_name: str
+    subscription: L3ProductTypes, destination_edge_port: EdgePort, replaced_ap_index: int, service_name: str
 ) -> State:
     """Update the subscription model with the destination Edge Port attached to the Access Port that is migrated."""
     ap_list = getattr(subscription, service_name).l3_core.ap_list
-- 
GitLab


From 1f7c0af0c6995593b94e4d3260be09444cacb65c Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Tue, 25 Mar 2025 15:50:49 +0100
Subject: [PATCH 30/87] Update base modules for modify after rebasing with
 develop

---
 .../base_modify_l3_core_service.py            | 100 ++++++++++++++----
 1 file changed, 79 insertions(+), 21 deletions(-)

diff --git a/gso/workflows/l3_core_service/base_modify_l3_core_service.py b/gso/workflows/l3_core_service/base_modify_l3_core_service.py
index 54e1f7129..e2c942027 100644
--- a/gso/workflows/l3_core_service/base_modify_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_modify_l3_core_service.py
@@ -255,7 +255,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
                 gs_id: str = current_ap.sbp.gs_id
                 custom_service_name: str | None = current_ap.custom_service_name
                 is_tagged: bool = current_ap.sbp.is_tagged
-                ap_type: APType | str = current_ap.ap_type
+                ap_type: APType | str = current_ap.ap_type  # FIXME: remove str workaround
                 # The SBP model does not require these five fields, but in the case of L3 Core Services this will never
                 # occur since it's a layer 3 service. The ignore statements are there to put our type checker at ease.
                 vlan_id: VLAN_ID = current_ap.sbp.vlan_id  # type: ignore[assignment]
@@ -267,29 +267,87 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
                 divider_a: Divider = Field(None, exclude=True)
                 label_a: Label = Field("IPv4 settings for BFD and BGP", exclude=True)
-                v4_bfd_settings: BFDInputModel = BFDInputModel(
-                    bfd_enabled=current_ap.sbp.v4_bfd_settings.bfd_enabled,
-                    bfd_multiplier=current_ap.sbp.v4_bfd_settings.bfd_multiplier,
-                    bfd_interval_rx=current_ap.sbp.v4_bfd_settings.bfd_interval_rx,
-                    bfd_interval_tx=current_ap.sbp.v4_bfd_settings.bfd_interval_tx,
-                )
-                v4_bgp_peer: IPv4BGPPeer = IPv4BGPPeer(
-                    **v4_peer.model_dump(exclude=set("families")),
-                    add_v4_multicast=bool(IPFamily.V4MULTICAST in v4_peer.families),
-                )
+                v4_bfd_enabled: bool = Field(current_ap.sbp.v4_bfd_settings.bfd_enabled, exclude=True)
+                v4_bfd_multiplier: int | None = Field(current_ap.sbp.v4_bfd_settings.bfd_multiplier, exclude=True)
+                v4_bfd_interval_rx: int | None = Field(current_ap.sbp.v4_bfd_settings.bfd_interval_rx, exclude=True)
+                v4_bfd_interval_tx: int | None = Field(current_ap.sbp.v4_bfd_settings.bfd_interval_tx, exclude=True)
+
+                v4_bgp_peer_address: IPv4AddressType = Field(IPv4AddressType(v4_peer.peer_address), exclude=True)
+                v4_bgp_authentication_key: str | None = Field(v4_peer.authentication_key, exclude=True)
+                v4_bgp_has_custom_policies: bool = Field(v4_peer.has_custom_policies, exclude=True)
+                v4_bgp_bfd_enabled: bool = Field(v4_peer.bfd_enabled, exclude=True)
+                v4_bgp_multipath_enabled: bool = Field(v4_peer.multipath_enabled, exclude=True)
+                v4_bgp_prefix_limit: NonNegativeInt | None = Field(v4_peer.prefix_limit, exclude=True)
+                v4_bgp_is_passive: bool = Field(v4_peer.is_passive, exclude=True)
+                v4_bgp_send_default_route: bool = Field(v4_peer.send_default_route, exclude=True)
+                v4_bgp_add_v4_multicast: bool = Field(bool(IPFamily.V4MULTICAST in v4_peer.families), exclude=True)
 
                 divider_b: Divider = Field(None, exclude=True)
                 label_b: Label = Field("IPv6 settings for BFD and BGP", exclude=True)
-                v6_bfd_settings: BFDInputModel = BFDInputModel(
-                    bfd_enabled=current_ap.sbp.v6_bfd_settings.bfd_enabled,
-                    bfd_multiplier=current_ap.sbp.v6_bfd_settings.bfd_multiplier,
-                    bfd_interval_rx=current_ap.sbp.v6_bfd_settings.bfd_interval_rx,
-                    bfd_interval_tx=current_ap.sbp.v6_bfd_settings.bfd_interval_tx,
-                )
-                v6_bgp_peer: IPv6BGPPeer = IPv6BGPPeer(
-                    **v6_peer.model_dump(exclude=set("families")),
-                    add_v6_multicast=bool(IPFamily.V6MULTICAST in v6_peer.families),
-                )
+                v6_bfd_enabled: bool = Field(current_ap.sbp.v6_bfd_settings.bfd_enabled, exclude=True)
+                v6_bfd_multiplier: int | None = Field(current_ap.sbp.v6_bfd_settings.bfd_multiplier, exclude=True)
+                v6_bfd_interval_rx: int | None = Field(current_ap.sbp.v6_bfd_settings.bfd_interval_rx, exclude=True)
+                v6_bfd_interval_tx: int | None = Field(current_ap.sbp.v6_bfd_settings.bfd_interval_tx, exclude=True)
+
+                v6_bgp_peer_address: IPv6AddressType = Field(IPv6AddressType(v6_peer.peer_address), exclude=True)
+                v6_bgp_authentication_key: str | None = Field(v6_peer.authentication_key, exclude=True)
+                v6_bgp_has_custom_policies: bool = Field(v6_peer.has_custom_policies, exclude=True)
+                v6_bgp_bfd_enabled: bool = Field(v6_peer.bfd_enabled, exclude=True)
+                v6_bgp_multipath_enabled: bool = Field(v6_peer.multipath_enabled, exclude=True)
+                v6_bgp_prefix_limit: NonNegativeInt | None = Field(v6_peer.prefix_limit, exclude=True)
+                v6_bgp_is_passive: bool = Field(v6_peer.is_passive, exclude=True)
+                v6_bgp_send_default_route: bool = Field(v6_peer.send_default_route, exclude=True)
+                v6_bgp_add_v6_multicast: bool = Field(bool(IPFamily.V6MULTICAST in v6_peer.families), exclude=True)
+
+                @computed_field  # type: ignore[prop-decorator]
+                @property
+                def v4_bfd_settings(self) -> BFDInputModel:
+                    return BFDInputModel(
+                        bfd_enabled=self.v4_bfd_enabled,
+                        bfd_multiplier=self.v4_bfd_multiplier,
+                        bfd_interval_rx=self.v4_bfd_interval_rx,
+                        bfd_interval_tx=self.v4_bfd_interval_tx,
+                    )
+
+                @computed_field  # type: ignore[prop-decorator]
+                @property
+                def v4_bgp_peer(self) -> IPv4BGPPeer:
+                    return IPv4BGPPeer(
+                        peer_address=self.v4_bgp_peer_address,
+                        authentication_key=self.v4_bgp_authentication_key,
+                        has_custom_policies=self.v4_bgp_has_custom_policies,
+                        bfd_enabled=self.v4_bgp_bfd_enabled,
+                        multipath_enabled=self.v4_bgp_multipath_enabled,
+                        prefix_limit=self.v4_bgp_prefix_limit,
+                        is_passive=self.v4_bgp_is_passive,
+                        send_default_route=self.v4_bgp_send_default_route,
+                        add_v4_multicast=self.v4_bgp_add_v4_multicast,
+                    )
+
+                @computed_field  # type: ignore[prop-decorator]
+                @property
+                def v6_bfd_settings(self) -> BFDInputModel:
+                    return BFDInputModel(
+                        bfd_enabled=self.v6_bfd_enabled,
+                        bfd_multiplier=self.v6_bfd_multiplier,
+                        bfd_interval_rx=self.v6_bfd_interval_rx,
+                        bfd_interval_tx=self.v6_bfd_interval_tx,
+                    )
+
+                @computed_field  # type: ignore[prop-decorator]
+                @property
+                def v6_bgp_peer(self) -> IPv6BGPPeer:
+                    return IPv6BGPPeer(
+                        peer_address=self.v6_bgp_peer_address,
+                        authentication_key=self.v6_bgp_authentication_key,
+                        has_custom_policies=self.v6_bgp_has_custom_policies,
+                        bfd_enabled=self.v6_bgp_bfd_enabled,
+                        multipath_enabled=self.v6_bgp_multipath_enabled,
+                        prefix_limit=self.v6_bgp_prefix_limit,
+                        is_passive=self.v6_bgp_is_passive,
+                        send_default_route=self.v6_bgp_send_default_route,
+                        add_v6_multicast=self.v6_bgp_add_v6_multicast,
+                    )
 
             binding_port_input_form = yield BindingPortModificationForm
             return {
-- 
GitLab


From 55393b589edf70c646ab1216c4e82d6ff59cfa4e Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Tue, 25 Mar 2025 16:08:50 +0100
Subject: [PATCH 31/87] add create imported wfs for all L3 services

---
 gso/workflows/__init__.py                     |  10 +-
 .../base_create_imported_l3_core_service.py   | 113 ++++++++++++++++++
 .../copernicus/create_imported_copernicus.py  |  43 +++++++
 .../geant_ip/create_imported_geant_ip.py      |  43 +++++++
 .../ias/create_imported_ias.py                |  43 +++++++
 .../lhcone/create_imported_lhcone.py          |  43 +++++++
 gso/workflows/l3_core_service/shared.py       |   2 +-
 7 files changed, 295 insertions(+), 2 deletions(-)
 create mode 100644 gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
 create mode 100644 gso/workflows/l3_core_service/copernicus/create_imported_copernicus.py
 create mode 100644 gso/workflows/l3_core_service/geant_ip/create_imported_geant_ip.py
 create mode 100644 gso/workflows/l3_core_service/ias/create_imported_ias.py
 create mode 100644 gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py

diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py
index e00030451..e23831502 100644
--- a/gso/workflows/__init__.py
+++ b/gso/workflows/__init__.py
@@ -121,7 +121,15 @@ LazyWorkflowInstance("gso.workflows.edge_port.migrate_edge_port", "migrate_edge_
 #  L3 Core Service workflows
 LazyWorkflowInstance("gso.workflows.l3_core_service.create_l3_core_service", "create_l3_core_service")
 LazyWorkflowInstance("gso.workflows.l3_core_service.modify_l3_core_service", "modify_l3_core_service")
-LazyWorkflowInstance("gso.workflows.l3_core_service.create_imported_l3_core_service", "create_imported_l3_core_service")
+LazyWorkflowInstance(
+    "gso.workflows.l3_core_service.create_imported_l3_core_service", "create_imported_l3_core_service"
+)  # TODO:Remove this workflow at the end
+LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.create_imported_geant_ip", "create_imported_geant_ip")
+LazyWorkflowInstance("gso.workflows.l3_core_service.ias.create_imported_ias", "create_imported_ias")
+LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.create_imported_lhcone", "create_imported_lhcone")
+LazyWorkflowInstance(
+    "gso.workflows.l3_core_service.copernicus.create_imported_copernicus", "create_imported_copernicus"
+)
 LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.import_geant_ip", "import_geant_ip")
 LazyWorkflowInstance("gso.workflows.l3_core_service.ias.import_ias", "import_ias")
 LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.import_lhcone", "import_lhcone")
diff --git a/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
new file mode 100644
index 000000000..008ea930a
--- /dev/null
+++ b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
@@ -0,0 +1,113 @@
+"""A creation workflow for adding an existing L3 Core Service to the service database."""
+
+from uuid import uuid4
+
+from orchestrator import workflow
+from orchestrator.forms import SubmitFormPage
+from orchestrator.targets import Target
+from orchestrator.types import SubscriptionLifecycle
+from orchestrator.utils.errors import ProcessFailureError
+from orchestrator.workflow import StepList, begin, done, step
+from orchestrator.workflows.steps import resync, set_status, store_process_subscription
+from pydantic import BaseModel, NonNegativeInt
+from pydantic_forms.types import FormGenerator, UUIDstr
+
+from gso.products import ProductName
+from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPTypes
+from gso.products.product_blocks.l3_core_service import AccessPortInactive
+from gso.products.product_blocks.service_binding_port import BFDSettings, ServiceBindingPortInactive
+from gso.products.product_types.edge_port import EdgePort
+from gso.products.product_types.l3_core_service import ImportedL3CoreServiceInactive, L3CoreServiceType
+from gso.services.partners import get_partner_by_name
+from gso.services.subscriptions import get_product_id_by_name
+from gso.utils.shared_enums import SBPType
+from gso.utils.types.geant_ids import IMPORTED_GS_ID
+from gso.utils.types.ip_address import IPAddress, IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask
+from gso.utils.types.virtual_identifiers import VLAN_ID
+
+
+def initial_input_form_generator() -> FormGenerator:
+    """Take all information passed to this workflow by the API endpoint that was called."""
+
+    class BFDSettingsModel(BaseModel):
+        bfd_enabled: bool = False
+        bfd_interval_rx: int | None = None
+        bfd_interval_tx: int | None = None
+        bfd_multiplier: int | None = None
+
+    class BaseBGPPeer(BaseModel):
+        bfd_enabled: bool = False
+        has_custom_policies: bool = False
+        authentication_key: str | None
+        multipath_enabled: bool = False
+        send_default_route: bool = False
+        is_passive: bool = False
+        peer_address: IPAddress
+        families: list[IPFamily]
+        is_multi_hop: bool
+        rtbh_enabled: bool
+        prefix_limit: NonNegativeInt | None = None
+
+    class ServiceBindingPort(BaseModel):
+        edge_port: UUIDstr
+        ap_type: str
+        custom_service_name: str | None = None
+        gs_id: IMPORTED_GS_ID
+        sbp_type: SBPType = SBPType.L3
+        is_tagged: bool = False
+        vlan_id: VLAN_ID
+        custom_firewall_filters: bool = False
+        ipv4_address: IPv4AddressType
+        ipv4_mask: IPv4Netmask
+        ipv6_address: IPv6AddressType
+        ipv6_mask: IPv6Netmask
+        rtbh_enabled: bool = True
+        is_multi_hop: bool = True
+        bgp_peers: list[BaseBGPPeer]
+        v4_bfd_settings: BFDSettingsModel
+        v6_bfd_settings: BFDSettingsModel
+
+    class ImportL3CoreServiceForm(SubmitFormPage):
+        partner: str
+        service_binding_ports: list[ServiceBindingPort]
+        service_type: L3CoreServiceType  # TODO: Think about how to remove this field
+
+    user_input = yield ImportL3CoreServiceForm
+
+    return user_input.model_dump()
+
+
+@step("Initialize subscription")
+def initialize_subscription(subscription: ImportedL3CoreServiceInactive, service_binding_ports: list) -> dict:
+    """Initialize the subscription with the user input."""
+    for service_binding_port in service_binding_ports:
+        edge_port_subscription = EdgePort.from_subscription(service_binding_port.pop("edge_port"))
+        bgp_peers = service_binding_port.pop("bgp_peers")
+        sbp_bgp_session_list = [
+            BGPSession.new(
+                subscription_id=uuid4(),
+                ip_type=IPTypes.IPV4
+                if any(family in {IPFamily.V4UNICAST, IPFamily.V4MULTICAST} for family in session["families"])
+                else IPTypes.IPV6,
+                **session,
+            )
+            for session in bgp_peers
+        ]
+        service_binding_port_subscription = ServiceBindingPortInactive.new(
+            subscription_id=uuid4(),
+            edge_port=edge_port_subscription.edge_port,
+            bgp_session_list=sbp_bgp_session_list,
+            v4_bfd_settings=BFDSettings.new(subscription_id=uuid4(), **(service_binding_port.pop("v4_bfd_settings"))),
+            v6_bfd_settings=BFDSettings.new(subscription_id=uuid4(), **(service_binding_port.pop("v6_bfd_settings"))),
+            **service_binding_port,
+        )
+        subscription.l3_core_service.ap_list.append(
+            AccessPortInactive.new(
+                subscription_id=uuid4(),
+                ap_type=service_binding_port["ap_type"],
+                sbp=service_binding_port_subscription,
+                custom_service_name=service_binding_port.get("custom_service_name"),
+            )
+        )
+
+    return {"subscription": subscription}
diff --git a/gso/workflows/l3_core_service/copernicus/create_imported_copernicus.py b/gso/workflows/l3_core_service/copernicus/create_imported_copernicus.py
new file mode 100644
index 000000000..ba74f4cfd
--- /dev/null
+++ b/gso/workflows/l3_core_service/copernicus/create_imported_copernicus.py
@@ -0,0 +1,43 @@
+"""A creation workflow for adding an existing Copernicus to the service database."""
+
+from orchestrator import workflow
+from orchestrator.targets import Target
+from orchestrator.types import SubscriptionLifecycle
+from orchestrator.workflow import StepList, begin, done, step
+from orchestrator.workflows.steps import resync, set_status, store_process_subscription
+
+from gso.products import ProductName
+from gso.products.product_types.copernicus import ImportedCopernicusInactive
+from gso.services.partners import get_partner_by_name
+from gso.services.subscriptions import get_product_id_by_name
+from gso.workflows.l3_core_service.base_create_imported_l3_core_service import (
+    initial_input_form_generator,
+    initialize_subscription,
+)
+
+
+@step("Create subscription")
+def create_subscription(partner: str) -> dict:
+    """Create a new subscription object in the database."""
+    partner_id = get_partner_by_name(partner).partner_id
+    product_id = get_product_id_by_name(ProductName.IMPORTED_COPERNICUS)
+    subscription = ImportedCopernicusInactive.from_product_id(product_id, partner_id)
+    return {"subscription": subscription, "subscription_id": subscription.subscription_id, "service_name": "copernicus"}
+
+
+@workflow(
+    "Create imported Copernicus",
+    initial_input_form=initial_input_form_generator,
+    target=Target.CREATE,
+)
+def create_imported_copernicus() -> StepList:
+    """Import a Copernicus without provisioning it."""
+    return (
+        begin
+        >> create_subscription
+        >> store_process_subscription(Target.CREATE)
+        >> initialize_subscription
+        >> set_status(SubscriptionLifecycle.ACTIVE)
+        >> resync
+        >> done
+    )
diff --git a/gso/workflows/l3_core_service/geant_ip/create_imported_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/create_imported_geant_ip.py
new file mode 100644
index 000000000..3b7443baa
--- /dev/null
+++ b/gso/workflows/l3_core_service/geant_ip/create_imported_geant_ip.py
@@ -0,0 +1,43 @@
+"""A creation workflow for adding an existing Imported GÉANT IP to the service database."""
+
+from orchestrator import workflow
+from orchestrator.targets import Target
+from orchestrator.types import SubscriptionLifecycle
+from orchestrator.workflow import StepList, begin, done, step
+from orchestrator.workflows.steps import resync, set_status, store_process_subscription
+
+from gso.products import ProductName
+from gso.products.product_types.geant_ip import ImportedGeantIPInactive
+from gso.services.partners import get_partner_by_name
+from gso.services.subscriptions import get_product_id_by_name
+from gso.workflows.l3_core_service.base_create_imported_l3_core_service import (
+    initial_input_form_generator,
+    initialize_subscription,
+)
+
+
+@step("Create subscription")
+def create_subscription(partner: str) -> dict:
+    """Create a new subscription object in the database."""
+    partner_id = get_partner_by_name(partner).partner_id
+    product_id = get_product_id_by_name(ProductName.IMPORTED_GEANT_IP)
+    subscription = ImportedGeantIPInactive.from_product_id(product_id, partner_id)
+    return {"subscription": subscription, "subscription_id": subscription.subscription_id, "service_name": "geant_ip"}
+
+
+@workflow(
+    "Create imported GÉANT IP",
+    initial_input_form=initial_input_form_generator,
+    target=Target.CREATE,
+)
+def create_imported_geant_ip() -> StepList:
+    """Import a GÉANT IP without provisioning it."""
+    return (
+        begin
+        >> create_subscription
+        >> store_process_subscription(Target.CREATE)
+        >> initialize_subscription
+        >> set_status(SubscriptionLifecycle.ACTIVE)
+        >> resync
+        >> done
+    )
diff --git a/gso/workflows/l3_core_service/ias/create_imported_ias.py b/gso/workflows/l3_core_service/ias/create_imported_ias.py
new file mode 100644
index 000000000..705606823
--- /dev/null
+++ b/gso/workflows/l3_core_service/ias/create_imported_ias.py
@@ -0,0 +1,43 @@
+"""A creation workflow for adding an existing Imported IAS to the service database."""
+
+from orchestrator import workflow
+from orchestrator.targets import Target
+from orchestrator.types import SubscriptionLifecycle
+from orchestrator.workflow import StepList, begin, done, step
+from orchestrator.workflows.steps import resync, set_status, store_process_subscription
+
+from gso.products import ProductName
+from gso.products.product_types.ias import ImportedIASInactive
+from gso.services.partners import get_partner_by_name
+from gso.services.subscriptions import get_product_id_by_name
+from gso.workflows.l3_core_service.base_create_imported_l3_core_service import (
+    initial_input_form_generator,
+    initialize_subscription,
+)
+
+
+@step("Create subscription")
+def create_subscription(partner: str) -> dict:
+    """Create a new subscription object in the database."""
+    partner_id = get_partner_by_name(partner).partner_id
+    product_id = get_product_id_by_name(ProductName.IMPORTED_GEANT_IP)
+    subscription = ImportedIASInactive.from_product_id(product_id, partner_id)
+    return {"subscription": subscription, "subscription_id": subscription.subscription_id, "service_name": "ias"}
+
+
+@workflow(
+    "Create imported IAS",
+    initial_input_form=initial_input_form_generator,
+    target=Target.CREATE,
+)
+def create_imported_ias() -> StepList:
+    """Import an IAS without provisioning it."""
+    return (
+        begin
+        >> create_subscription
+        >> store_process_subscription(Target.CREATE)
+        >> initialize_subscription
+        >> set_status(SubscriptionLifecycle.ACTIVE)
+        >> resync
+        >> done
+    )
diff --git a/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py b/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py
new file mode 100644
index 000000000..f3b8b1620
--- /dev/null
+++ b/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py
@@ -0,0 +1,43 @@
+"""A creation workflow for adding an existing Imported LHCOne to the service database."""
+
+from orchestrator import workflow
+from orchestrator.targets import Target
+from orchestrator.types import SubscriptionLifecycle
+from orchestrator.workflow import StepList, begin, done, step
+from orchestrator.workflows.steps import resync, set_status, store_process_subscription
+
+from gso.products import ProductName
+from gso.products.product_types.lhcone import ImportedLHCOneInactive
+from gso.services.partners import get_partner_by_name
+from gso.services.subscriptions import get_product_id_by_name
+from gso.workflows.l3_core_service.base_create_imported_l3_core_service import (
+    initial_input_form_generator,
+    initialize_subscription,
+)
+
+
+@step("Create subscription")
+def create_subscription(partner: str) -> dict:
+    """Create a new subscription object in the database."""
+    partner_id = get_partner_by_name(partner).partner_id
+    product_id = get_product_id_by_name(ProductName.IMPORTED_GEANT_IP)
+    subscription = ImportedLHCOneInactive.from_product_id(product_id, partner_id)
+    return {"subscription": subscription, "subscription_id": subscription.subscription_id, "service_name": "lhcone"}
+
+
+@workflow(
+    "Create imported LHCOne",
+    initial_input_form=initial_input_form_generator,
+    target=Target.CREATE,
+)
+def create_lhcone() -> StepList:
+    """Import a LHCOne without provisioning it."""
+    return (
+        begin
+        >> create_subscription
+        >> store_process_subscription(Target.CREATE)
+        >> initialize_subscription
+        >> set_status(SubscriptionLifecycle.ACTIVE)
+        >> resync
+        >> done
+    )
diff --git a/gso/workflows/l3_core_service/shared.py b/gso/workflows/l3_core_service/shared.py
index 6facbbd83..3b462ad60 100644
--- a/gso/workflows/l3_core_service/shared.py
+++ b/gso/workflows/l3_core_service/shared.py
@@ -6,4 +6,4 @@ from gso.products.product_types.ias import IAS, IASInactive
 from gso.products.product_types.lhcone import LHCOne, LHCOneInactive
 
 L3ProductTypes = IAS | LHCOne | Copernicus | GeantIP
-L3InactiveProductTypes = IASInactive | LHCOneInactive | CopernicusInactive | GeantIPInactive
\ No newline at end of file
+L3InactiveProductTypes = IASInactive | LHCOneInactive | CopernicusInactive | GeantIPInactive
-- 
GitLab


From e629a0bb28472c347f8f5f812bc09cedaab353fe Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Tue, 25 Mar 2025 16:09:34 +0100
Subject: [PATCH 32/87] run ruff again

---
 .../base_create_imported_l3_core_service.py            | 10 +---------
 1 file changed, 1 insertion(+), 9 deletions(-)

diff --git a/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
index 008ea930a..00c8394bc 100644
--- a/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
@@ -2,24 +2,16 @@
 
 from uuid import uuid4
 
-from orchestrator import workflow
 from orchestrator.forms import SubmitFormPage
-from orchestrator.targets import Target
-from orchestrator.types import SubscriptionLifecycle
-from orchestrator.utils.errors import ProcessFailureError
-from orchestrator.workflow import StepList, begin, done, step
-from orchestrator.workflows.steps import resync, set_status, store_process_subscription
+from orchestrator.workflow import step
 from pydantic import BaseModel, NonNegativeInt
 from pydantic_forms.types import FormGenerator, UUIDstr
 
-from gso.products import ProductName
 from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPTypes
 from gso.products.product_blocks.l3_core_service import AccessPortInactive
 from gso.products.product_blocks.service_binding_port import BFDSettings, ServiceBindingPortInactive
 from gso.products.product_types.edge_port import EdgePort
 from gso.products.product_types.l3_core_service import ImportedL3CoreServiceInactive, L3CoreServiceType
-from gso.services.partners import get_partner_by_name
-from gso.services.subscriptions import get_product_id_by_name
 from gso.utils.shared_enums import SBPType
 from gso.utils.types.geant_ids import IMPORTED_GS_ID
 from gso.utils.types.ip_address import IPAddress, IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask
-- 
GitLab


From 51dcddccbb0ab5249d4752def5af540c3b9a7a70 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Tue, 25 Mar 2025 16:11:16 +0100
Subject: [PATCH 33/87] Fix the types and update the access to the service in
 base_modify WF.

---
 .../base_modify_l3_core_service.py            | 42 ++++++++++++-------
 1 file changed, 26 insertions(+), 16 deletions(-)

diff --git a/gso/workflows/l3_core_service/base_modify_l3_core_service.py b/gso/workflows/l3_core_service/base_modify_l3_core_service.py
index e2c942027..1be1e0dfe 100644
--- a/gso/workflows/l3_core_service/base_modify_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_modify_l3_core_service.py
@@ -14,14 +14,13 @@ from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPType
 from gso.products.product_blocks.l3_core_service import AccessPort
 from gso.products.product_blocks.service_binding_port import BFDSettings, ServiceBindingPort
 from gso.products.product_types.edge_port import EdgePort
-from gso.products.product_types.ias import IAS
-from gso.products.product_types.l3_core_service import L3CoreService
 from gso.services.subscriptions import generate_unique_id, get_active_edge_port_subscriptions
 from gso.utils.shared_enums import APType, SBPType
 from gso.utils.types.geant_ids import IMPORTED_GS_ID
 from gso.utils.types.ip_address import IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask
 from gso.utils.types.tt_number import TTNumber
 from gso.utils.types.virtual_identifiers import VLAN_ID
+from gso.workflows.l3_core_service.shared import L3ProductTypes
 
 
 class Operation(strEnum):
@@ -32,10 +31,12 @@ class Operation(strEnum):
     EDIT = "Edit an existing Access Port"
 
 
-def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+def initial_input_form_generator(subscription_id: UUIDstr, service_name:str) -> FormGenerator:
     """Get input about added, removed, and modified Access Ports."""
     subscription = SubscriptionModel.from_subscription(subscription_id)
     product_name = subscription.product.name
+    service = getattr(subscription, service_name)
+    assert service is not None, f"{service_name} is not set on subscription"
 
     class OperationSelectionForm(FormPage):
         model_config = ConfigDict(title="Modify Edge Port")
@@ -45,7 +46,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
     def access_port_selector() -> TypeAlias:
         """Generate a dropdown selector for choosing an Access Port in an input form."""
-        access_ports = subscription.l3_core.ap_list
+        access_ports = service.l3_core.ap_list
         options = {
             str(access_port.subscription_instance_id): (
                 f"{access_port.sbp.gs_id} on "
@@ -128,7 +129,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
                     str(edge_port.subscription_id): edge_port.description
                     for edge_port in edge_ports
                     if edge_port.subscription_id
-                    not in [ap.sbp.edge_port.owner_subscription_id for ap in subscription.l3_core.ap_list]
+                    not in [ap.sbp.edge_port.owner_subscription_id for ap in service.l3_core.ap_list]
                 }
 
                 return cast(
@@ -151,7 +152,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
                                 ap_type=access_port.ap_type.value,
                                 custom_service_name=access_port.custom_service_name or "",
                             )
-                            for access_port in subscription.l3_core.ap_list
+                            for access_port in service.l3_core.ap_list
                         ],
                         default_type=list[AccessPortListItem],
                     ),
@@ -205,7 +206,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
                 @field_validator("edge_port")
                 def selected_edge_port_is_new(cls, value: UUIDstr) -> UUIDstr:
-                    if value in [str(ap.sbp.edge_port.owner_subscription_id) for ap in subscription.l3_core.ap_list]:
+                    if value in [str(ap.sbp.edge_port.owner_subscription_id) for ap in service.l3_core.ap_list]:
                         error_message = (
                             f"This {product_name} service is already deployed on "
                             f"{EdgePort.from_subscription(value).description}."
@@ -214,7 +215,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
                     return value
 
             user_input = yield AddAccessPortForm
-            return {"operation": initial_input.operation, "added_access_port": user_input}
+            return {"operation": initial_input.operation, "added_access_port": user_input, "service_name": service_name}
 
         case Operation.REMOVE:
 
@@ -228,7 +229,11 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
                 access_port: access_port_selector()  # type: ignore[valid-type]
 
             user_input = yield RemoveAccessPortForm
-            return {"operation": initial_input.operation, "removed_access_port": user_input.access_port}
+            return {
+                "operation": initial_input.operation,
+                "removed_access_port": user_input.access_port,
+                "service_name": service_name
+            }
 
         case Operation.EDIT:
 
@@ -354,6 +359,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
                 "operation": initial_input.operation,
                 "modified_access_port": user_input.access_port,
                 "modified_sbp": binding_port_input_form,
+                "service_name": service_name,
             }
 
         case _:
@@ -362,7 +368,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
 
 @step("Instantiate new Service Binding Ports")
-def create_new_sbp(subscription: L3CoreService | IAS, added_access_port: dict[str, Any]) -> State:
+def create_new_sbp(subscription: L3ProductTypes, added_access_port: dict[str, Any], service_name: str) -> State:
     """Add new SBP to the L3 Core Service subscription."""
     edge_port = EdgePort.from_subscription(added_access_port.pop("edge_port"))
     bgp_session_list = [
@@ -387,7 +393,9 @@ def create_new_sbp(subscription: L3CoreService | IAS, added_access_port: dict[st
         edge_port=edge_port.edge_port,
         gs_id=sbp_gs_id,
     )
-    subscription.l3_core.ap_list.append(
+    service = getattr(subscription, service_name)
+    assert service is not None, f"{service_name} is not set on subscription"
+    service.l3_core.ap_list.append(
         AccessPort.new(
             subscription_id=uuid4(),
             ap_type=added_access_port["ap_type"],
@@ -400,20 +408,22 @@ def create_new_sbp(subscription: L3CoreService | IAS, added_access_port: dict[st
 
 
 @step("Clean up removed Edge Ports")
-def remove_old_sbp(subscription: L3CoreService | IAS, removed_access_port: UUIDstr) -> State:
-    """Remove old SBP product blocks from the GÉANT IP subscription."""
-    subscription.l3_core.ap_list.remove(AccessPort.from_db(UUID(removed_access_port)))
+def remove_old_sbp(subscription: L3ProductTypes, removed_access_port: UUIDstr, service_name:str) -> State:
+    """Remove old SBP product blocks from the specific L3 core service subscription."""
+    service = getattr(subscription, service_name)
+    service.l3_core.ap_list.remove(AccessPort.from_db(UUID(removed_access_port)))
 
     return {"subscription": subscription}
 
 
 @step("Modify existing Service Binding Ports")
 def modify_existing_sbp(
-    subscription: L3CoreService | IAS, modified_access_port: UUIDstr, modified_sbp: dict[str, Any]
+    subscription: L3ProductTypes, modified_access_port: UUIDstr, modified_sbp: dict[str, Any], service_name: str
 ) -> State:
     """Update the subscription model."""
+    service = getattr(subscription, service_name)
     current_ap = next(
-        ap for ap in subscription.l3_core.ap_list if str(ap.subscription_instance_id) == modified_access_port
+        ap for ap in service.l3_core.ap_list if str(ap.subscription_instance_id) == modified_access_port
     )
     v4_peer = next(peer for peer in current_ap.sbp.bgp_session_list if IPFamily.V4UNICAST in peer.families)
     for attribute in modified_sbp["v4_bgp_peer"]:
-- 
GitLab


From 92b06ae4635e698a88be6025934fe7a7067a6e3b Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Tue, 25 Mar 2025 16:11:56 +0100
Subject: [PATCH 34/87] Update IAS modification WF based on the change on base
 WF

---
 gso/workflows/l3_core_service/ias/modify_ias.py | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/gso/workflows/l3_core_service/ias/modify_ias.py b/gso/workflows/l3_core_service/ias/modify_ias.py
index 5c4738e36..8b22842a8 100644
--- a/gso/workflows/l3_core_service/ias/modify_ias.py
+++ b/gso/workflows/l3_core_service/ias/modify_ias.py
@@ -5,6 +5,7 @@ from orchestrator.targets import Target
 from orchestrator.workflow import StepList
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from pydantic_forms.types import FormGenerator
 
 from gso.workflows.l3_core_service.base_modify_l3_core_service import (
     Operation,
@@ -17,9 +18,13 @@ from gso.workflows.l3_core_service.base_modify_l3_core_service import (
 )
 
 
+def initial_input_form_generator(subscription_id: str) -> FormGenerator:
+    """Gather input from the operator on what operation to perform on the IAS subscription."""
+    return base_initial_input_form_generator(subscription_id, service_name="ias")
+
 @workflow(
     "Modify IAS",
-    initial_input_form=wrap_modify_initial_input_form(base_initial_input_form_generator),
+    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
     target=Target.MODIFY,
 )
 def modify_ias() -> StepList:
-- 
GitLab


From 9d689eae3228158212c26c70a028434390219911 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Tue, 25 Mar 2025 16:14:12 +0100
Subject: [PATCH 35/87] Update geant_ip modification WF based on the change on
 base WF

---
 gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py
index 3ec646cba..ece7c7f7a 100644
--- a/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py
@@ -5,6 +5,7 @@ from orchestrator.targets import Target
 from orchestrator.workflow import StepList
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from pydantic_forms.types import FormGenerator
 
 from gso.workflows.l3_core_service.base_modify_l3_core_service import (
     Operation,
@@ -17,13 +18,17 @@ from gso.workflows.l3_core_service.base_modify_l3_core_service import (
 )
 
 
+def initial_input_form_generator(subscription_id: str) -> FormGenerator:
+    """Gather input from the operator on what operation to perform on the GÉANT IP subscription."""
+    return base_initial_input_form_generator(subscription_id, service_name="geant_ip")
+
 @workflow(
     "Modify GÉANT IP ",
     initial_input_form=wrap_modify_initial_input_form(base_initial_input_form_generator),
     target=Target.MODIFY,
 )
 def modify_geant_ip() -> StepList:
-    """Modify GÉANT IP  subscription."""
+    """Modify GÉANT IP subscription."""
     access_port_is_added = conditional(lambda state: state["operation"] == Operation.ADD)
     access_port_is_removed = conditional(lambda state: state["operation"] == Operation.REMOVE)
     access_port_is_modified = conditional(lambda state: state["operation"] == Operation.EDIT)
-- 
GitLab


From 92fc0174f557a22f9289f8b28e91ae2ffca14020 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Tue, 25 Mar 2025 17:19:58 +0100
Subject: [PATCH 36/87] adjust import commands for L3 core services

---
 gso/cli/imports.py                            | 20 ++++++++++++++-----
 gso/products/product_types/l3_core_service.py |  2 +-
 .../base_create_imported_l3_core_service.py   |  7 ++++---
 .../copernicus/create_imported_copernicus.py  |  4 ++--
 .../geant_ip/create_imported_geant_ip.py      |  4 ++--
 .../ias/create_imported_ias.py                | 18 ++++++++++++++++-
 .../lhcone/create_imported_lhcone.py          |  4 ++--
 gso/workflows/l3_core_service/shared.py       |  4 ++++
 test/cli/test_imports.py                      | 10 ++++++++--
 9 files changed, 55 insertions(+), 18 deletions(-)

diff --git a/gso/cli/imports.py b/gso/cli/imports.py
index 85db5b756..c0dca0a91 100644
--- a/gso/cli/imports.py
+++ b/gso/cli/imports.py
@@ -18,7 +18,7 @@ from pydantic_forms.types import UUIDstr
 from sqlalchemy.exc import SQLAlchemyError
 
 from gso.db.models import PartnerTable
-from gso.products import ProductType
+from gso.products import ProductName, ProductType
 from gso.products.product_blocks.bgp_session import IPFamily
 from gso.products.product_blocks.edge_port import EdgePortType, EncapsulationType
 from gso.products.product_blocks.iptrunk import IptrunkType
@@ -665,15 +665,25 @@ def import_l3_core_service(filepath: str = common_filepath_option) -> None:
 
     for l3_core_service in l3_core_service_list:
         partner = l3_core_service["partner"]
-        service_type = l3_core_service["service_type"]
-        typer.echo(f"Creating imported {service_type} for {partner}")
+        product_name = l3_core_service["product_name"]
+        typer.echo(f"Creating imported {product_name} for {partner}")
 
         try:
             initial_data = L3CoreServiceImportModel(**l3_core_service)
-            start_process("create_imported_l3_core_service", [initial_data.model_dump()])
+            if product_name == ProductName.IAS.value:
+                start_process("create_imported_ias", [initial_data.model_dump()])
+            elif product_name == ProductName.LHCONE.value:
+                start_process("create_imported_lhcone", [initial_data.model_dump()])
+            elif product_name == ProductName.COPERNICUS.value:
+                start_process("create_imported_copernicus", [initial_data.model_dump()])
+            elif product_name == ProductName.GEANT_IP.value:
+                start_process("create_imported_geant_ip", [initial_data.model_dump()])
+            else:
+                raise ValidationError(f"Invalid service name: {product_name}")
+
             edge_ports = [sbp["edge_port"] for sbp in l3_core_service["service_binding_ports"]]
             successfully_imported_data.append(edge_ports)
-            typer.echo(f"Successfully created imported {service_type} for {partner}")
+            typer.echo(f"Successfully created imported {product_name} for {partner}")
         except ValidationError as e:
             typer.echo(f"Validation error: {e}")
 
diff --git a/gso/products/product_types/l3_core_service.py b/gso/products/product_types/l3_core_service.py
index d36bf2597..d0de84760 100644
--- a/gso/products/product_types/l3_core_service.py
+++ b/gso/products/product_types/l3_core_service.py
@@ -17,7 +17,7 @@ class L3CoreServiceType(strEnum):
     The core services offered include GÉANT IP for R&E access, and the Internet Access Service.
     """
 
-    GEANT_IP = "GÉANT IP"  # TODO: PLEASE REMOVE ACCENT
+    GEANT_IP = "GÉANT IP"  # TODO: Think about removing accents
     """GÉANT IP."""
     IMPORTED_GEANT_IP = "IMPORTED GÉANT IP"
     GWS = "GWS"
diff --git a/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
index 00c8394bc..c8e3a36aa 100644
--- a/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
@@ -11,14 +11,15 @@ from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPType
 from gso.products.product_blocks.l3_core_service import AccessPortInactive
 from gso.products.product_blocks.service_binding_port import BFDSettings, ServiceBindingPortInactive
 from gso.products.product_types.edge_port import EdgePort
-from gso.products.product_types.l3_core_service import ImportedL3CoreServiceInactive, L3CoreServiceType
+from gso.products.product_types.l3_core_service import ImportedL3CoreServiceInactive
 from gso.utils.shared_enums import SBPType
 from gso.utils.types.geant_ids import IMPORTED_GS_ID
 from gso.utils.types.ip_address import IPAddress, IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask
 from gso.utils.types.virtual_identifiers import VLAN_ID
+from gso.workflows.l3_core_service.shared import L3ProductNameTypes
 
 
-def initial_input_form_generator() -> FormGenerator:
+def base_initial_input_form_generator() -> FormGenerator:
     """Take all information passed to this workflow by the API endpoint that was called."""
 
     class BFDSettingsModel(BaseModel):
@@ -62,7 +63,7 @@ def initial_input_form_generator() -> FormGenerator:
     class ImportL3CoreServiceForm(SubmitFormPage):
         partner: str
         service_binding_ports: list[ServiceBindingPort]
-        service_type: L3CoreServiceType  # TODO: Think about how to remove this field
+        product_name: L3ProductNameTypes
 
     user_input = yield ImportL3CoreServiceForm
 
diff --git a/gso/workflows/l3_core_service/copernicus/create_imported_copernicus.py b/gso/workflows/l3_core_service/copernicus/create_imported_copernicus.py
index ba74f4cfd..f2970ecaa 100644
--- a/gso/workflows/l3_core_service/copernicus/create_imported_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/create_imported_copernicus.py
@@ -11,7 +11,7 @@ from gso.products.product_types.copernicus import ImportedCopernicusInactive
 from gso.services.partners import get_partner_by_name
 from gso.services.subscriptions import get_product_id_by_name
 from gso.workflows.l3_core_service.base_create_imported_l3_core_service import (
-    initial_input_form_generator,
+    base_initial_input_form_generator,
     initialize_subscription,
 )
 
@@ -27,7 +27,7 @@ def create_subscription(partner: str) -> dict:
 
 @workflow(
     "Create imported Copernicus",
-    initial_input_form=initial_input_form_generator,
+    initial_input_form=base_initial_input_form_generator,
     target=Target.CREATE,
 )
 def create_imported_copernicus() -> StepList:
diff --git a/gso/workflows/l3_core_service/geant_ip/create_imported_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/create_imported_geant_ip.py
index 3b7443baa..3d1bef513 100644
--- a/gso/workflows/l3_core_service/geant_ip/create_imported_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/create_imported_geant_ip.py
@@ -11,7 +11,7 @@ from gso.products.product_types.geant_ip import ImportedGeantIPInactive
 from gso.services.partners import get_partner_by_name
 from gso.services.subscriptions import get_product_id_by_name
 from gso.workflows.l3_core_service.base_create_imported_l3_core_service import (
-    initial_input_form_generator,
+    base_initial_input_form_generator,
     initialize_subscription,
 )
 
@@ -27,7 +27,7 @@ def create_subscription(partner: str) -> dict:
 
 @workflow(
     "Create imported GÉANT IP",
-    initial_input_form=initial_input_form_generator,
+    initial_input_form=base_initial_input_form_generator,
     target=Target.CREATE,
 )
 def create_imported_geant_ip() -> StepList:
diff --git a/gso/workflows/l3_core_service/ias/create_imported_ias.py b/gso/workflows/l3_core_service/ias/create_imported_ias.py
index 705606823..c1a493e39 100644
--- a/gso/workflows/l3_core_service/ias/create_imported_ias.py
+++ b/gso/workflows/l3_core_service/ias/create_imported_ias.py
@@ -1,21 +1,37 @@
 """A creation workflow for adding an existing Imported IAS to the service database."""
 
 from orchestrator import workflow
+from orchestrator.forms import FormPage
 from orchestrator.targets import Target
 from orchestrator.types import SubscriptionLifecycle
 from orchestrator.workflow import StepList, begin, done, step
 from orchestrator.workflows.steps import resync, set_status, store_process_subscription
+from pydantic_forms.types import FormGenerator
 
 from gso.products import ProductName
+from gso.products.product_blocks.ias import IASFlavor
 from gso.products.product_types.ias import ImportedIASInactive
 from gso.services.partners import get_partner_by_name
 from gso.services.subscriptions import get_product_id_by_name
 from gso.workflows.l3_core_service.base_create_imported_l3_core_service import (
-    initial_input_form_generator,
+    base_initial_input_form_generator,
     initialize_subscription,
 )
 
 
+def initial_input_form_generator() -> FormGenerator:
+    """Initial input form generator for creating a new imported IAS subscription."""
+    initial_generator = base_initial_input_form_generator()
+    initial_user_input = yield from initial_generator
+
+    # Additional IAS step
+    class IASExtraForm(FormPage):
+        ias_flavor: IASFlavor | None = IASFlavor.IAS_PS_OPT_OUT
+
+    ias_extra = yield IASExtraForm
+    return initial_user_input | ias_extra.model_dump()
+
+
 @step("Create subscription")
 def create_subscription(partner: str) -> dict:
     """Create a new subscription object in the database."""
diff --git a/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py b/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py
index f3b8b1620..0e53a2250 100644
--- a/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py
@@ -11,7 +11,7 @@ from gso.products.product_types.lhcone import ImportedLHCOneInactive
 from gso.services.partners import get_partner_by_name
 from gso.services.subscriptions import get_product_id_by_name
 from gso.workflows.l3_core_service.base_create_imported_l3_core_service import (
-    initial_input_form_generator,
+    base_initial_input_form_generator,
     initialize_subscription,
 )
 
@@ -27,7 +27,7 @@ def create_subscription(partner: str) -> dict:
 
 @workflow(
     "Create imported LHCOne",
-    initial_input_form=initial_input_form_generator,
+    initial_input_form=base_initial_input_form_generator,
     target=Target.CREATE,
 )
 def create_lhcone() -> StepList:
diff --git a/gso/workflows/l3_core_service/shared.py b/gso/workflows/l3_core_service/shared.py
index 3b462ad60..8a7fac7a7 100644
--- a/gso/workflows/l3_core_service/shared.py
+++ b/gso/workflows/l3_core_service/shared.py
@@ -1,5 +1,6 @@
 """Shared logic for L3 Core Service."""
 
+from gso.products import ProductName
 from gso.products.product_types.copernicus import Copernicus, CopernicusInactive
 from gso.products.product_types.geant_ip import GeantIP, GeantIPInactive
 from gso.products.product_types.ias import IAS, IASInactive
@@ -7,3 +8,6 @@ from gso.products.product_types.lhcone import LHCOne, LHCOneInactive
 
 L3ProductTypes = IAS | LHCOne | Copernicus | GeantIP
 L3InactiveProductTypes = IASInactive | LHCOneInactive | CopernicusInactive | GeantIPInactive
+L3ProductNameTypes = (
+    ProductName.IAS.value | ProductName.LHCONE.value | ProductName.COPERNICUS.value | ProductName.GEANT_IP.value
+)
diff --git a/test/cli/test_imports.py b/test/cli/test_imports.py
index 26ab3bdd9..c5110d955 100644
--- a/test/cli/test_imports.py
+++ b/test/cli/test_imports.py
@@ -17,6 +17,7 @@ from gso.cli.imports import (
     import_super_pop_switches,
     import_switches,
 )
+from gso.products import ProductName
 from gso.products.product_blocks.bgp_session import IPFamily
 from gso.products.product_blocks.edge_port import EdgePortType, EncapsulationType
 from gso.products.product_blocks.iptrunk import IptrunkType
@@ -294,7 +295,7 @@ def l3_core_service_data(temp_file, faker, partner_factory, edge_port_subscripti
     def _l3_core_service_data(**kwargs):
         l3_core_service_data = {
             "partner": partner_factory()["name"],
-            "service_type": "IMPORTED IAS",
+            "product_name": ProductName.IAS.value,
             "service_binding_ports": [
                 {
                     "edge_port": str(edge_port_subscription_factory().subscription_id),
@@ -396,8 +397,13 @@ def l3_core_service_data(temp_file, faker, partner_factory, edge_port_subscripti
         }
         l3_core_service_data.update(**kwargs)
 
-        temp_file.write_text(json.dumps([l3_core_service_data]))
+        temp_file.write_text(
+            json.dumps([
+                l3_core_service_data,
+            ])
+        )
         return {"path": str(temp_file), "data": l3_core_service_data}
+        # TODO: Think about IAS extra form
 
     return _l3_core_service_data
 
-- 
GitLab


From 4a876a61241a4ad72c4a7683f93abe46092fa203 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Wed, 26 Mar 2025 09:50:37 +0100
Subject: [PATCH 37/87] Fix IAS flavor type in create WF

---
 gso/workflows/l3_core_service/ias/create_ias.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gso/workflows/l3_core_service/ias/create_ias.py b/gso/workflows/l3_core_service/ias/create_ias.py
index 9ca5f34f3..687b00f8d 100644
--- a/gso/workflows/l3_core_service/ias/create_ias.py
+++ b/gso/workflows/l3_core_service/ias/create_ias.py
@@ -55,7 +55,7 @@ def initialize_subscription(
     edge_port: dict,
     binding_port_input: dict,
     product_name: str,
-    ias_flavor: str,
+    ias_flavor: IASFlavor,
     service_name: str,
 ) -> State:
     """Take all user inputs and use them to populate the subscription model."""
-- 
GitLab


From 071b5c4d4f9f3e94d2074a5b96674ca2552254b5 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Wed, 26 Mar 2025 15:34:44 +0100
Subject: [PATCH 38/87] Fix migration order

---
 gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py b/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py
index a28404426..1cec2203d 100644
--- a/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py
+++ b/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py
@@ -17,7 +17,7 @@ from alembic import op
 
 # revision identifiers, used by Alembic
 revision = "e1afa3790f32"
-down_revision = "b96b0ecf6906"
+down_revision = "b14f71db2b58"
 branch_labels = None
 depends_on = None
 
-- 
GitLab


From c87e7f7b2fe13af51f758d516baa1f3b914e5d14 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Wed, 26 Mar 2025 15:35:00 +0100
Subject: [PATCH 39/87] Add new WFs to the DB

---
 ...c4411ea_remove_old_l3_core_services_wfs.py | 169 ++++++++++++++++++
 gso/workflows/__init__.py                     |  49 ++---
 2 files changed, 196 insertions(+), 22 deletions(-)
 create mode 100644 gso/migrations/versions/2025-03-26_9fbb3c4411ea_remove_old_l3_core_services_wfs.py

diff --git a/gso/migrations/versions/2025-03-26_9fbb3c4411ea_remove_old_l3_core_services_wfs.py b/gso/migrations/versions/2025-03-26_9fbb3c4411ea_remove_old_l3_core_services_wfs.py
new file mode 100644
index 000000000..e4f8c68cd
--- /dev/null
+++ b/gso/migrations/versions/2025-03-26_9fbb3c4411ea_remove_old_l3_core_services_wfs.py
@@ -0,0 +1,169 @@
+"""Remove old l3 core services wfs.
+
+Revision ID: 9fbb3c4411ea
+Revises: e1afa3790f32
+Create Date: 2025-03-26 13:39:41.657079
+
+"""
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = '9fbb3c4411ea'
+down_revision = 'e1afa3790f32'
+branch_labels = None
+depends_on = None
+
+from orchestrator.migrations.helpers import create_workflow, delete_workflow
+
+new_workflows = [
+    {
+        "name": "create_ias",
+        "target": "CREATE",
+        "description": "Create IAS",
+        "product_type": "IAS"
+    },
+    {
+        "name": "modify_ias",
+        "target": "MODIFY",
+        "description": "Modify IAS",
+        "product_type": "IAS"
+    },
+    {
+        "name": "terminate_ias",
+        "target": "TERMINATE",
+        "description": "Terminate IAS",
+        "product_type": "IAS"
+    },
+    {
+        "name": "migrate_ias",
+        "target": "MODIFY",
+        "description": "Migrate IAS",
+        "product_type": "IAS"
+    },
+    {
+        "name": "create_imported_ias",
+        "target": "CREATE",
+        "description": "Create Imported IAS",
+        "product_type": "ImportedIAS"
+    },
+{
+        "name": "create_geant_ip",
+        "target": "CREATE",
+        "description": "Create GÉANT IP",
+        "product_type": "GEANT_IP"
+    },
+    {
+        "name": "modify_geant_ip",
+        "target": "MODIFY",
+        "description": "Modify GÉANT IP",
+        "product_type": "GEANT_IP"
+    },
+    {
+        "name": "terminate_geant_ip",
+        "target": "TERMINATE",
+        "description": "Terminate GÉANT IP",
+        "product_type": "GEANT_IP"
+    },
+    {
+        "name": "migrate_geant_ip",
+        "target": "MODIFY",
+        "description": "Migrate GÉANT IP",
+        "product_type": "GEANT_IP"
+    },
+    {
+        "name": "create_imported_geant_ip",
+        "target": "CREATE",
+        "description": "Create Imported GÉANT IP",
+        "product_type": "IMPORTED_GEANT_IP"
+    },
+    {
+        "name": "import_geant_ip",
+        "target": "MODIFY",
+        "description": "Import GÉANT IP",
+        "product_type": "IMPORTED_GEANT_IP"
+    },
+    {
+        "name": "create_lhcone",
+        "target": "CREATE",
+        "description": "Create LHCONE",
+        "product_type": "LHCONE"
+    },
+    {
+        "name": "modify_lhcone",
+        "target": "MODIFY",
+        "description": "Modify LHCONE",
+        "product_type": "LHCONE"
+    },
+    {
+        "name": "terminate_lhcone",
+        "target": "TERMINATE",
+        "description": "Terminate LHCONE",
+        "product_type": "LHCONE"
+    },
+    {
+        "name": "migrate_lhcone",
+        "target": "MODIFY",
+        "description": "Migrate LHCONE",
+        "product_type": "LHCONE"
+    },
+    {
+        "name": "create_imported_lhcone",
+        "target": "CREATE",
+        "description": "Create Imported LHCONE",
+        "product_type": "IMPORTED_LHCONE"
+    },
+    {
+        "name": "import_lhcone",
+        "target": "MODIFY",
+        "description": "Import LHCONE",
+        "product_type": "IMPORTED_LHCONE"
+    },
+    {
+        "name": "create_copernicus",
+        "target": "CREATE",
+        "description": "Create COPERNICUS",
+        "product_type": "COPERNICUS"
+    },
+    {
+        "name": "modify_copernicus",
+        "target": "MODIFY",
+        "description": "Modify COPERNICUS",
+        "product_type": "COPERNICUS"
+    },
+    {
+        "name": "terminate_copernicus",
+        "target": "TERMINATE",
+        "description": "Terminate COPERNICUS",
+        "product_type": "COPERNICUS"
+    },
+    {
+        "name": "migrate_copernicus",
+        "target": "MODIFY",
+        "description": "Migrate COPERNICUS",
+        "product_type": "COPERNICUS"
+    },
+    {
+        "name": "create_imported_copernicus",
+        "target": "CREATE",
+        "description": "Create Imported COPERNICUS",
+        "product_type": "IMPORTED_COPERNICUS"
+    },
+    {
+        "name": "import_copernicus",
+        "target": "MODIFY",
+        "description": "Import COPERNICUS",
+        "product_type": "IMPORTED_COPERNICUS"
+    }
+]
+
+
+def upgrade() -> None:
+    conn = op.get_bind()
+    for workflow in new_workflows:
+        create_workflow(conn, workflow)
+
+
+def downgrade() -> None:
+    conn = op.get_bind()
+    for workflow in new_workflows:
+        delete_workflow(conn, workflow["name"])
diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py
index e23831502..d9a4be3c4 100644
--- a/gso/workflows/__init__.py
+++ b/gso/workflows/__init__.py
@@ -118,32 +118,37 @@ LazyWorkflowInstance("gso.workflows.edge_port.create_imported_edge_port", "creat
 LazyWorkflowInstance("gso.workflows.edge_port.import_edge_port", "import_edge_port")
 LazyWorkflowInstance("gso.workflows.edge_port.migrate_edge_port", "migrate_edge_port")
 
-#  L3 Core Service workflows
-LazyWorkflowInstance("gso.workflows.l3_core_service.create_l3_core_service", "create_l3_core_service")
-LazyWorkflowInstance("gso.workflows.l3_core_service.modify_l3_core_service", "modify_l3_core_service")
-LazyWorkflowInstance(
-    "gso.workflows.l3_core_service.create_imported_l3_core_service", "create_imported_l3_core_service"
-)  # TODO:Remove this workflow at the end
-LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.create_imported_geant_ip", "create_imported_geant_ip")
+#  IAS workflows
+LazyWorkflowInstance("gso.workflows.l3_core_service.ias.create_ias", "create_ias")
+LazyWorkflowInstance("gso.workflows.l3_core_service.ias.modify_ias", "modify_ias")
+LazyWorkflowInstance("gso.workflows.l3_core_service.ias.terminate_ias", "terminate_ias")
+LazyWorkflowInstance("gso.workflows.l3_core_service.ias.migrate_ias", "migrate_ias")
 LazyWorkflowInstance("gso.workflows.l3_core_service.ias.create_imported_ias", "create_imported_ias")
-LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.create_imported_lhcone", "create_imported_lhcone")
-LazyWorkflowInstance(
-    "gso.workflows.l3_core_service.copernicus.create_imported_copernicus", "create_imported_copernicus"
-)
-LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.import_geant_ip", "import_geant_ip")
 LazyWorkflowInstance("gso.workflows.l3_core_service.ias.import_ias", "import_ias")
-LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.import_lhcone", "import_lhcone")
+
+#  Copernicus workflows
+LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.create_copernicus", "create_copernicus")
+LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.modify_copernicus", "modify_copernicus")
+LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.terminate_copernicus", "terminate_copernicus")
+LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.migrate_copernicus", "migrate_copernicus")
+LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.create_imported_copernicus", "create_imported_copernicus")
 LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.import_copernicus", "import_copernicus")
-LazyWorkflowInstance(
-    "gso.workflows.l3_core_service.migrate_l3_core_service", "migrate_l3_core_service"
-)  # TODO: Remove this workflow at the end
-LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.migrate_geant_ip", "migrate_geant_ip")
-LazyWorkflowInstance("gso.workflows.l3_core_service.ias.migrate_ias", "migrate_ias")
+
+#  LHCoNE workflows
+LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.create_lhcone", "create_lhcone")
+LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.modify_lhcone", "modify_lhcone")
+LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.terminate_lhcone", "terminate_lhcone")
 LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.migrate_lhcone", "migrate_lhcone")
-LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.migrate_copernicus", "migrate_copernicus")
-LazyWorkflowInstance("gso.workflows.l3_core_service.validate_l3_core_service", "validate_l3_core_service")
-LazyWorkflowInstance("gso.workflows.l3_core_service.terminate_l3_core_service", "terminate_l3_core_service")
-LazyWorkflowInstance("gso.workflows.l3_core_service.validate_prefix_list", "validate_prefix_list")
+LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.create_imported_lhcone", "create_imported_lhcone")
+LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.import_lhcone", "import_lhcone")
+
+#  GÉANT IP workflows
+LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.create_geant_ip", "create_geant_ip")
+LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.modify_geant_ip", "modify_geant_ip")
+LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.terminate_geant_ip", "terminate_geant_ip")
+LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.migrate_geant_ip", "migrate_geant_ip")
+LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.create_imported_geant_ip", "create_imported_geant_ip")
+LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.import_geant_ip", "import_geant_ip")
 
 # Layer 2 Circuit workflows
 LazyWorkflowInstance("gso.workflows.l2_circuit.create_layer_2_circuit", "create_layer_2_circuit")
-- 
GitLab


From ccdc6e12d6f8fb7bc1233d3c472b15a96f09c530 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Wed, 26 Mar 2025 16:10:41 +0100
Subject: [PATCH 40/87] fix import_ias migration name

---
 ..._re_model_ias.py => 2025-03-25_c9c9fdf624b5_re_model_ias.py} | 0
 gso/workflows/l3_core_service/ias/import_ias.py                 | 2 +-
 2 files changed, 1 insertion(+), 1 deletion(-)
 rename gso/migrations/versions/{2025-03-18_c9c9fdf624b5_re_model_ias.py => 2025-03-25_c9c9fdf624b5_re_model_ias.py} (100%)

diff --git a/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py b/gso/migrations/versions/2025-03-25_c9c9fdf624b5_re_model_ias.py
similarity index 100%
rename from gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py
rename to gso/migrations/versions/2025-03-25_c9c9fdf624b5_re_model_ias.py
diff --git a/gso/workflows/l3_core_service/ias/import_ias.py b/gso/workflows/l3_core_service/ias/import_ias.py
index 9deef38df..7b0018c70 100644
--- a/gso/workflows/l3_core_service/ias/import_ias.py
+++ b/gso/workflows/l3_core_service/ias/import_ias.py
@@ -25,6 +25,6 @@ def import_ias_subscription(subscription_id: UUIDstr) -> State:
 
 
 @workflow("Import IAS Core Service", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None))
-def import_l3_core_service() -> StepList:
+def import_ias() -> StepList:
     """Modify an imported subscription into an IAS subscription to complete the import."""
     return init >> store_process_subscription(Target.MODIFY) >> unsync >> import_ias_subscription >> resync >> done
-- 
GitLab


From 90038dde3d959b96ff3a8a4f8e9838b71f132c62 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Wed, 26 Mar 2025 16:18:45 +0100
Subject: [PATCH 41/87] add import_ias wf to the migration

---
 ...03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py} | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)
 rename gso/migrations/versions/{2025-03-26_9fbb3c4411ea_remove_old_l3_core_services_wfs.py => 2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py} (95%)

diff --git a/gso/migrations/versions/2025-03-26_9fbb3c4411ea_remove_old_l3_core_services_wfs.py b/gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py
similarity index 95%
rename from gso/migrations/versions/2025-03-26_9fbb3c4411ea_remove_old_l3_core_services_wfs.py
rename to gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py
index e4f8c68cd..89b576893 100644
--- a/gso/migrations/versions/2025-03-26_9fbb3c4411ea_remove_old_l3_core_services_wfs.py
+++ b/gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py
@@ -1,4 +1,4 @@
-"""Remove old l3 core services wfs.
+"""Add new L3 core services wfs.
 
 Revision ID: 9fbb3c4411ea
 Revises: e1afa3790f32
@@ -46,7 +46,13 @@ new_workflows = [
         "description": "Create Imported IAS",
         "product_type": "ImportedIAS"
     },
-{
+    {
+        "name": "import_ias",
+        "target": "MODIFY",
+        "description": "Import IAS",
+        "product_type": "IMPORTED_IAS"
+    },
+    {
         "name": "create_geant_ip",
         "target": "CREATE",
         "description": "Create GÉANT IP",
-- 
GitLab


From 1cd480e27486230ad496b9852ad506143d454c30 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Wed, 26 Mar 2025 17:00:05 +0100
Subject: [PATCH 42/87] add validation wfs adjust import tests refactore code

---
 gso/cli/imports.py                            |  34 ++--
 ...> 2025-03-25_e1afa3790f32_re_model_ias.py} |   0
 ...bb3c4411ea_add_new_l3_core_services_wfs.py |  64 +++++--
 gso/products/__init__.py                      |   2 -
 gso/products/product_types/l3_core_service.py |  82 --------
 gso/services/subscriptions.py                 |   5 +-
 gso/workflows/__init__.py                     |  10 +-
 .../base_create_l3_core_service.py            |   4 +-
 .../base_migrate_l3_core_service.py           |  14 +-
 .../base_modify_l3_core_service.py            |  21 ++-
 .../base_validate_l3_core_service.py          |  83 ++++++++
 .../copernicus/create_copernicus.py           |   7 +-
 .../copernicus/validate_copernicus.py         |  38 ++++
 .../geant_ip/create_geant_ip.py               |   7 +-
 .../geant_ip/modify_geant_ip.py               |   1 +
 .../geant_ip/validate_geant_ip.py             |  38 ++++
 .../l3_core_service/ias/create_ias.py         |   5 +-
 .../ias/create_imported_ias.py                |   2 +-
 .../l3_core_service/ias/modify_ias.py         |   1 +
 .../l3_core_service/ias/validate_ias.py       |  38 ++++
 .../l3_core_service/lhcone/create_lhcone.py   |   7 +-
 .../l3_core_service/lhcone/validate_lhcone.py |  38 ++++
 gso/workflows/l3_core_service/shared.py       |  14 +-
 test/cli/test_imports.py                      |  12 +-
 test/fixtures/__init__.py                     |  10 +-
 test/fixtures/l3_core_service_fixtures.py     | 178 +++++++++---------
 .../test_create_imported_l3_core_service.py   |  25 ++-
 .../test_create_l3_core_service.py            |   9 +-
 .../test_validate_prefix_list.py              |   3 +-
 29 files changed, 504 insertions(+), 248 deletions(-)
 rename gso/migrations/versions/{2025-03-25_c9c9fdf624b5_re_model_ias.py => 2025-03-25_e1afa3790f32_re_model_ias.py} (100%)
 delete mode 100644 gso/products/product_types/l3_core_service.py
 create mode 100644 gso/workflows/l3_core_service/base_validate_l3_core_service.py
 create mode 100644 gso/workflows/l3_core_service/copernicus/validate_copernicus.py
 create mode 100644 gso/workflows/l3_core_service/geant_ip/validate_geant_ip.py
 create mode 100644 gso/workflows/l3_core_service/ias/validate_ias.py
 create mode 100644 gso/workflows/l3_core_service/lhcone/validate_lhcone.py

diff --git a/gso/cli/imports.py b/gso/cli/imports.py
index c0dca0a91..1a7740d7c 100644
--- a/gso/cli/imports.py
+++ b/gso/cli/imports.py
@@ -21,12 +21,12 @@ from gso.db.models import PartnerTable
 from gso.products import ProductName, ProductType
 from gso.products.product_blocks.bgp_session import IPFamily
 from gso.products.product_blocks.edge_port import EdgePortType, EncapsulationType
+from gso.products.product_blocks.ias import IASFlavor
 from gso.products.product_blocks.iptrunk import IptrunkType
 from gso.products.product_blocks.layer_2_circuit import Layer2CircuitType
 from gso.products.product_blocks.router import RouterRole
 from gso.products.product_blocks.switch import SwitchModel
 from gso.products.product_types.edge_port import EdgePort
-from gso.products.product_types.l3_core_service import L3CoreServiceType
 from gso.products.product_types.layer_2_circuit import Layer2CircuitServiceType
 from gso.services.partners import (
     PartnerEmail,
@@ -53,10 +53,18 @@ from gso.utils.types.ip_address import (
     PortNumber,
 )
 from gso.utils.types.virtual_identifiers import VC_ID, VLAN_ID
+from gso.workflows.l3_core_service.shared import L3ProductNameTypes
 
 app: typer.Typer = typer.Typer()
 IMPORT_WAIT_MESSAGE = "Waiting for the dust to settle before importing new products..."
 
+_L3_MIGRATION_MAP = {
+    ProductName.COPERNICUS: "create_imported_copernicus",
+    ProductName.GEANT_IP: "create_imported_geant_ip",
+    ProductName.IAS: "create_imported_ias",
+    ProductName.LHCONE: "create_imported_lhcone",
+}
+
 
 class CreatePartner(BaseModel):
     """Required inputs for creating a partner."""
@@ -289,7 +297,7 @@ class L3CoreServiceImportModel(BaseModel):
 
     partner: str
     service_binding_ports: list[ServiceBindingPort]
-    service_type: L3CoreServiceType
+    product_name: L3ProductNameTypes
 
     @field_validator("partner")
     def check_if_partner_exists(cls, value: str) -> str:
@@ -314,6 +322,10 @@ class L3CoreServiceImportModel(BaseModel):
         return value
 
 
+class IASImportModel(L3CoreServiceImportModel):
+    ias_flavor: IASFlavor | None = IASFlavor.IAS_PS_OPT_OUT
+
+
 class LanSwitchInterconnectRouterSideImportModel(BaseModel):
     """Import LAN Switch Interconnect Router side model."""
 
@@ -664,24 +676,18 @@ def import_l3_core_service(filepath: str = common_filepath_option) -> None:
     l3_core_service_list = _read_data(Path(filepath))
 
     for l3_core_service in l3_core_service_list:
-        partner = l3_core_service["partner"]
-        product_name = l3_core_service["product_name"]
+        partner = l3_core_service[0]["partner"]
+        product_name = l3_core_service[0]["product_name"]
         typer.echo(f"Creating imported {product_name} for {partner}")
 
         try:
-            initial_data = L3CoreServiceImportModel(**l3_core_service)
             if product_name == ProductName.IAS.value:
-                start_process("create_imported_ias", [initial_data.model_dump()])
-            elif product_name == ProductName.LHCONE.value:
-                start_process("create_imported_lhcone", [initial_data.model_dump()])
-            elif product_name == ProductName.COPERNICUS.value:
-                start_process("create_imported_copernicus", [initial_data.model_dump()])
-            elif product_name == ProductName.GEANT_IP.value:
-                start_process("create_imported_geant_ip", [initial_data.model_dump()])
+                initial_data = IASImportModel(**l3_core_service[0], **l3_core_service[1])
             else:
-                raise ValidationError(f"Invalid service name: {product_name}")
+                initial_data = L3CoreServiceImportModel(**l3_core_service[0], **l3_core_service[1])
 
-            edge_ports = [sbp["edge_port"] for sbp in l3_core_service["service_binding_ports"]]
+            start_process(_L3_MIGRATION_MAP[product_name], [initial_data.model_dump()])
+            edge_ports = [sbp["edge_port"] for sbp in l3_core_service[0]["service_binding_ports"]]
             successfully_imported_data.append(edge_ports)
             typer.echo(f"Successfully created imported {product_name} for {partner}")
         except ValidationError as e:
diff --git a/gso/migrations/versions/2025-03-25_c9c9fdf624b5_re_model_ias.py b/gso/migrations/versions/2025-03-25_e1afa3790f32_re_model_ias.py
similarity index 100%
rename from gso/migrations/versions/2025-03-25_c9c9fdf624b5_re_model_ias.py
rename to gso/migrations/versions/2025-03-25_e1afa3790f32_re_model_ias.py
diff --git a/gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py b/gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py
index 89b576893..c03f891f2 100644
--- a/gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py
+++ b/gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py
@@ -50,116 +50,140 @@ new_workflows = [
         "name": "import_ias",
         "target": "MODIFY",
         "description": "Import IAS",
-        "product_type": "IMPORTED_IAS"
+        "product_type": "ImportedIAS"
+    },
+    {
+        "name": "validate_ias",
+        "target": "SYSTEM",
+        "description": "Validate IAS Configuration",
+        "product_type": "IAS"
     },
     {
         "name": "create_geant_ip",
         "target": "CREATE",
         "description": "Create GÉANT IP",
-        "product_type": "GEANT_IP"
+        "product_type": "GeantIP"
     },
     {
         "name": "modify_geant_ip",
         "target": "MODIFY",
         "description": "Modify GÉANT IP",
-        "product_type": "GEANT_IP"
+        "product_type": "GeantIP"
     },
     {
         "name": "terminate_geant_ip",
         "target": "TERMINATE",
         "description": "Terminate GÉANT IP",
-        "product_type": "GEANT_IP"
+        "product_type": "GeantIP"
     },
     {
         "name": "migrate_geant_ip",
         "target": "MODIFY",
         "description": "Migrate GÉANT IP",
-        "product_type": "GEANT_IP"
+        "product_type": "GeantIP"
     },
     {
         "name": "create_imported_geant_ip",
         "target": "CREATE",
         "description": "Create Imported GÉANT IP",
-        "product_type": "IMPORTED_GEANT_IP"
+        "product_type": "ImportedGeantIP"
     },
     {
         "name": "import_geant_ip",
         "target": "MODIFY",
         "description": "Import GÉANT IP",
-        "product_type": "IMPORTED_GEANT_IP"
+        "product_type": "ImportedGeantIP"
+    },
+    {
+        "name": "validate_geant_ip",
+        "target": "SYSTEM",
+        "description": "Validate GÉANT IP Configuration",
+        "product_type": "GeantIP"
     },
     {
         "name": "create_lhcone",
         "target": "CREATE",
         "description": "Create LHCONE",
-        "product_type": "LHCONE"
+        "product_type": "LHCOne"
     },
     {
         "name": "modify_lhcone",
         "target": "MODIFY",
         "description": "Modify LHCONE",
-        "product_type": "LHCONE"
+        "product_type": "LHCOne"
     },
     {
         "name": "terminate_lhcone",
         "target": "TERMINATE",
         "description": "Terminate LHCONE",
-        "product_type": "LHCONE"
+        "product_type": "LHCOne"
     },
     {
         "name": "migrate_lhcone",
         "target": "MODIFY",
         "description": "Migrate LHCONE",
-        "product_type": "LHCONE"
+        "product_type": "LHCOne"
     },
     {
         "name": "create_imported_lhcone",
         "target": "CREATE",
         "description": "Create Imported LHCONE",
-        "product_type": "IMPORTED_LHCONE"
+        "product_type": "ImportedLHCOne"
     },
     {
         "name": "import_lhcone",
         "target": "MODIFY",
         "description": "Import LHCONE",
-        "product_type": "IMPORTED_LHCONE"
+        "product_type": "ImportedLHCOne"
+    },
+    {
+        "name": "validate_lhcone",
+        "target": "SYSTEM",
+        "description": "Validate LHCONE Configuration",
+        "product_type": "LHCOne"
     },
     {
         "name": "create_copernicus",
         "target": "CREATE",
         "description": "Create COPERNICUS",
-        "product_type": "COPERNICUS"
+        "product_type": "Copernicus"
     },
     {
         "name": "modify_copernicus",
         "target": "MODIFY",
         "description": "Modify COPERNICUS",
-        "product_type": "COPERNICUS"
+        "product_type": "Copernicus"
     },
     {
         "name": "terminate_copernicus",
         "target": "TERMINATE",
         "description": "Terminate COPERNICUS",
-        "product_type": "COPERNICUS"
+        "product_type": "Copernicus"
     },
     {
         "name": "migrate_copernicus",
         "target": "MODIFY",
         "description": "Migrate COPERNICUS",
-        "product_type": "COPERNICUS"
+        "product_type": "Copernicus"
     },
     {
         "name": "create_imported_copernicus",
         "target": "CREATE",
         "description": "Create Imported COPERNICUS",
-        "product_type": "IMPORTED_COPERNICUS"
+        "product_type": "ImportedCopernicus"
     },
     {
         "name": "import_copernicus",
         "target": "MODIFY",
         "description": "Import COPERNICUS",
-        "product_type": "IMPORTED_COPERNICUS"
-    }
+        "product_type": "ImportedCopernicus"
+    },
+    {
+        "name": "validate_copernicus",
+        "target": "SYSTEM",
+        "description": "Validate Copernicus Configuration",
+        "product_type": "Copernicus"
+    },
 ]
 
 
diff --git a/gso/products/__init__.py b/gso/products/__init__.py
index df3bb48dd..22fa171bb 100644
--- a/gso/products/__init__.py
+++ b/gso/products/__init__.py
@@ -13,7 +13,6 @@ from gso.products.product_types.edge_port import EdgePort, ImportedEdgePort
 from gso.products.product_types.geant_ip import GeantIP, ImportedGeantIP
 from gso.products.product_types.ias import IAS, ImportedIAS
 from gso.products.product_types.iptrunk import ImportedIptrunk, Iptrunk
-from gso.products.product_types.l3_core_service import L3CoreService
 from gso.products.product_types.lan_switch_interconnect import ImportedLanSwitchInterconnect, LanSwitchInterconnect
 from gso.products.product_types.layer_2_circuit import ImportedLayer2Circuit, Layer2Circuit, Layer2CircuitServiceType
 from gso.products.product_types.lhcone import ImportedLHCOne, LHCOne
@@ -81,7 +80,6 @@ class ProductName(strEnum):
     """VRFs."""
 
 
-L3_CORE_SERVICE_PRODUCT_TYPE = L3CoreService.__name__  # TODO: Remove this line after removing L3CoreService
 L2_CIRCUIT_PRODUCT_TYPE = Layer2Circuit.__name__
 
 
diff --git a/gso/products/product_types/l3_core_service.py b/gso/products/product_types/l3_core_service.py
deleted file mode 100644
index d0de84760..000000000
--- a/gso/products/product_types/l3_core_service.py
+++ /dev/null
@@ -1,82 +0,0 @@
-"""L3 Core Service product type."""
-
-from orchestrator.domain import SubscriptionModel
-from orchestrator.types import SubscriptionLifecycle
-from pydantic_forms.types import strEnum
-
-from gso.products.product_blocks.l3_core_service import (
-    L3CoreServiceBlock,
-    L3CoreServiceBlockInactive,
-    L3CoreServiceBlockProvisioning,
-)
-
-
-class L3CoreServiceType(strEnum):
-    """Available types of Layer 3 Core Services.
-
-    The core services offered include GÉANT IP for R&E access, and the Internet Access Service.
-    """
-
-    GEANT_IP = "GÉANT IP"  # TODO: Think about removing accents
-    """GÉANT IP."""
-    IMPORTED_GEANT_IP = "IMPORTED GÉANT IP"
-    GWS = "GWS"
-    """GÉANT Web Services."""
-    IMPORTED_GWS = "IMPORTED GWS"
-    LHCONE = "LHCONE"
-    """LHCOne."""
-    IMPORTED_LHCONE = "IMPORTED LHCONE"
-    COPERNICUS = "COPERNICUS"
-    """Copernicus."""
-    IMPORTED_COPERNICUS = "IMPORTED COPERNICUS"
-
-
-L3_CORE_SERVICE_TYPES = [
-    L3CoreServiceType.GEANT_IP,
-    L3CoreServiceType.GWS,
-    L3CoreServiceType.LHCONE,
-    L3CoreServiceType.COPERNICUS,
-]
-IMPORTED_L3_CORE_SERVICE_TYPES = [
-    L3CoreServiceType.IMPORTED_GEANT_IP,
-    L3CoreServiceType.IMPORTED_GWS,
-    L3CoreServiceType.IMPORTED_LHCONE,
-    L3CoreServiceType.IMPORTED_COPERNICUS,
-]
-
-
-class L3CoreServiceInactive(SubscriptionModel, is_base=True):
-    """An inactive L3 Core Service subscription."""
-
-    l3_core_service_type: L3CoreServiceType
-    l3_core_service: L3CoreServiceBlockInactive
-
-
-class L3CoreServiceProvisioning(L3CoreServiceInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
-    """A L3 Core Service subscription that's being provisioned."""
-
-    l3_core_service_type: L3CoreServiceType
-    l3_core_service: L3CoreServiceBlockProvisioning
-
-
-class L3CoreService(L3CoreServiceProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
-    """An active L3 Core Service subscription."""
-
-    l3_core_service_type: L3CoreServiceType
-    l3_core_service: L3CoreServiceBlock
-
-
-class ImportedL3CoreServiceInactive(SubscriptionModel, is_base=True):
-    """An imported, inactive L3 Core Service subscription."""
-
-    l3_core_service_type: L3CoreServiceType
-    l3_core_service: L3CoreServiceBlockInactive
-
-
-class ImportedL3CoreService(
-    ImportedL3CoreServiceInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE]
-):
-    """An imported L3 Core Service subscription."""
-
-    l3_core_service_type: L3CoreServiceType
-    l3_core_service: L3CoreServiceBlock
diff --git a/gso/services/subscriptions.py b/gso/services/subscriptions.py
index 182e54a97..7cd1775a3 100644
--- a/gso/services/subscriptions.py
+++ b/gso/services/subscriptions.py
@@ -23,8 +23,9 @@ from pydantic_forms.types import UUIDstr
 from sqlalchemy import and_, text
 from sqlalchemy.exc import SQLAlchemyError
 
-from gso.products import L2_CIRCUIT_PRODUCT_TYPE, L3_CORE_SERVICE_PRODUCT_TYPE, ProductName, ProductType
+from gso.products import L2_CIRCUIT_PRODUCT_TYPE, ProductName, ProductType
 from gso.products.product_types.site import Site
+from gso.workflows.l3_core_service.shared import L3_CORE_SERVICE_PRODUCT_TYPES
 
 SubscriptionType = dict[str, Any]
 
@@ -197,7 +198,7 @@ def get_active_l3_services_linked_to_edge_port(edge_port_id: UUIDstr) -> list[Su
         .join(ProductTable)
         .filter(
             and_(
-                ProductTable.product_type.in_([L3_CORE_SERVICE_PRODUCT_TYPE]),
+                ProductTable.product_type.in_(L3_CORE_SERVICE_PRODUCT_TYPES),
                 SubscriptionTable.status == SubscriptionLifecycle.ACTIVE,
             )
         )
diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py
index d9a4be3c4..fb4e97116 100644
--- a/gso/workflows/__init__.py
+++ b/gso/workflows/__init__.py
@@ -125,22 +125,27 @@ LazyWorkflowInstance("gso.workflows.l3_core_service.ias.terminate_ias", "termina
 LazyWorkflowInstance("gso.workflows.l3_core_service.ias.migrate_ias", "migrate_ias")
 LazyWorkflowInstance("gso.workflows.l3_core_service.ias.create_imported_ias", "create_imported_ias")
 LazyWorkflowInstance("gso.workflows.l3_core_service.ias.import_ias", "import_ias")
+LazyWorkflowInstance("gso.workflows.l3_core_service.ias.validate_ias", "validate_ias")
 
 #  Copernicus workflows
 LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.create_copernicus", "create_copernicus")
 LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.modify_copernicus", "modify_copernicus")
 LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.terminate_copernicus", "terminate_copernicus")
 LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.migrate_copernicus", "migrate_copernicus")
-LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.create_imported_copernicus", "create_imported_copernicus")
+LazyWorkflowInstance(
+    "gso.workflows.l3_core_service.copernicus.create_imported_copernicus", "create_imported_copernicus"
+)
 LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.import_copernicus", "import_copernicus")
+LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.validate_copernicus", "validate_copernicus")
 
-#  LHCoNE workflows
+#  LHCOne workflows
 LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.create_lhcone", "create_lhcone")
 LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.modify_lhcone", "modify_lhcone")
 LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.terminate_lhcone", "terminate_lhcone")
 LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.migrate_lhcone", "migrate_lhcone")
 LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.create_imported_lhcone", "create_imported_lhcone")
 LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.import_lhcone", "import_lhcone")
+LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.validate_lhcone", "validate_lhcone")
 
 #  GÉANT IP workflows
 LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.create_geant_ip", "create_geant_ip")
@@ -149,6 +154,7 @@ LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.terminate_geant_ip"
 LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.migrate_geant_ip", "migrate_geant_ip")
 LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.create_imported_geant_ip", "create_imported_geant_ip")
 LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.import_geant_ip", "import_geant_ip")
+LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.validate_geant_ip", "validate_geant_ip")
 
 # Layer 2 Circuit workflows
 LazyWorkflowInstance("gso.workflows.l2_circuit.create_layer_2_circuit", "create_layer_2_circuit")
diff --git a/gso/workflows/l3_core_service/base_create_l3_core_service.py b/gso/workflows/l3_core_service/base_create_l3_core_service.py
index 09a375139..576be6ce2 100644
--- a/gso/workflows/l3_core_service/base_create_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_l3_core_service.py
@@ -28,7 +28,7 @@ from gso.utils.types.geant_ids import IMPORTED_GS_ID
 from gso.utils.types.ip_address import IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask
 from gso.utils.types.tt_number import TTNumber
 from gso.utils.types.virtual_identifiers import VLAN_ID
-from gso.workflows.l3_core_service.shared import L3InactiveProductTypes, L3ProductTypes
+from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes, L3InactiveProductTypes, L3ProductTypes
 
 
 def initial_input_form_generator(product_name: str) -> FormGenerator:
@@ -165,7 +165,7 @@ def initialize_service_binding(
     edge_port: dict,
     binding_port_input: dict,
     product_name: str,
-    service_name: str,
+    service_name: L3CoreServiceNameTypes,
 ) -> dict:
     """Initialize a service binding port for a given service type in the subscription model."""
     edge_port_fqdn_list = []
diff --git a/gso/workflows/l3_core_service/base_migrate_l3_core_service.py b/gso/workflows/l3_core_service/base_migrate_l3_core_service.py
index 2fdb4207e..a1bcedb8f 100644
--- a/gso/workflows/l3_core_service/base_migrate_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_migrate_l3_core_service.py
@@ -26,11 +26,11 @@ from gso.services.partners import get_partner_by_id
 from gso.services.subscriptions import get_active_edge_port_subscriptions
 from gso.utils.types.tt_number import TTNumber
 from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, SKIP_MOODI_KEY
-from gso.workflows.l3_core_service.shared import L3ProductTypes
+from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes, L3ProductTypes
 from gso.workflows.shared import create_summary_form
 
 
-def initial_input_form(subscription: L3ProductTypes, service_name: str) -> FormGenerator:
+def initial_input_form(subscription: L3ProductTypes, service_name: L3CoreServiceNameTypes) -> FormGenerator:
     partner_id = subscription.customer_id
     ap_list = getattr(subscription, service_name).l3_core.ap_list
 
@@ -242,7 +242,10 @@ def deactivate_sbp_real(
 
 @step("Generate updated subscription model")
 def generate_scoped_subscription_model(
-    subscription: L3ProductTypes, source_edge_port: EdgePort, destination_edge_port: EdgePort, service_name: str
+    subscription: L3ProductTypes,
+    source_edge_port: EdgePort,
+    destination_edge_port: EdgePort,
+    service_name: L3CoreServiceNameTypes,
 ) -> State:
     """Calculate what the updated subscription model will look like, but don't update the actual subscription yet.
 
@@ -374,7 +377,10 @@ def update_dns_records(subscription: L3ProductTypes) -> State:
 
 @step("Update subscription model")
 def update_subscription_model(
-    subscription: L3ProductTypes, destination_edge_port: EdgePort, replaced_ap_index: int, service_name: str
+    subscription: L3ProductTypes,
+    destination_edge_port: EdgePort,
+    replaced_ap_index: int,
+    service_name: L3CoreServiceNameTypes,
 ) -> State:
     """Update the subscription model with the destination Edge Port attached to the Access Port that is migrated."""
     ap_list = getattr(subscription, service_name).l3_core.ap_list
diff --git a/gso/workflows/l3_core_service/base_modify_l3_core_service.py b/gso/workflows/l3_core_service/base_modify_l3_core_service.py
index 1be1e0dfe..c051f495f 100644
--- a/gso/workflows/l3_core_service/base_modify_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_modify_l3_core_service.py
@@ -20,7 +20,7 @@ from gso.utils.types.geant_ids import IMPORTED_GS_ID
 from gso.utils.types.ip_address import IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask
 from gso.utils.types.tt_number import TTNumber
 from gso.utils.types.virtual_identifiers import VLAN_ID
-from gso.workflows.l3_core_service.shared import L3ProductTypes
+from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes, L3ProductTypes
 
 
 class Operation(strEnum):
@@ -31,7 +31,7 @@ class Operation(strEnum):
     EDIT = "Edit an existing Access Port"
 
 
-def initial_input_form_generator(subscription_id: UUIDstr, service_name:str) -> FormGenerator:
+def initial_input_form_generator(subscription_id: UUIDstr, service_name: L3CoreServiceNameTypes) -> FormGenerator:
     """Get input about added, removed, and modified Access Ports."""
     subscription = SubscriptionModel.from_subscription(subscription_id)
     product_name = subscription.product.name
@@ -232,7 +232,7 @@ def initial_input_form_generator(subscription_id: UUIDstr, service_name:str) ->
             return {
                 "operation": initial_input.operation,
                 "removed_access_port": user_input.access_port,
-                "service_name": service_name
+                "service_name": service_name,
             }
 
         case Operation.EDIT:
@@ -368,7 +368,9 @@ def initial_input_form_generator(subscription_id: UUIDstr, service_name:str) ->
 
 
 @step("Instantiate new Service Binding Ports")
-def create_new_sbp(subscription: L3ProductTypes, added_access_port: dict[str, Any], service_name: str) -> State:
+def create_new_sbp(
+    subscription: L3ProductTypes, added_access_port: dict[str, Any], service_name: L3CoreServiceNameTypes
+) -> State:
     """Add new SBP to the L3 Core Service subscription."""
     edge_port = EdgePort.from_subscription(added_access_port.pop("edge_port"))
     bgp_session_list = [
@@ -408,7 +410,7 @@ def create_new_sbp(subscription: L3ProductTypes, added_access_port: dict[str, An
 
 
 @step("Clean up removed Edge Ports")
-def remove_old_sbp(subscription: L3ProductTypes, removed_access_port: UUIDstr, service_name:str) -> State:
+def remove_old_sbp(subscription: L3ProductTypes, removed_access_port: UUIDstr, service_name: str) -> State:
     """Remove old SBP product blocks from the specific L3 core service subscription."""
     service = getattr(subscription, service_name)
     service.l3_core.ap_list.remove(AccessPort.from_db(UUID(removed_access_port)))
@@ -418,13 +420,14 @@ def remove_old_sbp(subscription: L3ProductTypes, removed_access_port: UUIDstr, s
 
 @step("Modify existing Service Binding Ports")
 def modify_existing_sbp(
-    subscription: L3ProductTypes, modified_access_port: UUIDstr, modified_sbp: dict[str, Any], service_name: str
+    subscription: L3ProductTypes,
+    modified_access_port: UUIDstr,
+    modified_sbp: dict[str, Any],
+    service_name: L3CoreServiceNameTypes,
 ) -> State:
     """Update the subscription model."""
     service = getattr(subscription, service_name)
-    current_ap = next(
-        ap for ap in service.l3_core.ap_list if str(ap.subscription_instance_id) == modified_access_port
-    )
+    current_ap = next(ap for ap in service.l3_core.ap_list if str(ap.subscription_instance_id) == modified_access_port)
     v4_peer = next(peer for peer in current_ap.sbp.bgp_session_list if IPFamily.V4UNICAST in peer.families)
     for attribute in modified_sbp["v4_bgp_peer"]:
         setattr(v4_peer, attribute, modified_sbp["v4_bgp_peer"][attribute])
diff --git a/gso/workflows/l3_core_service/base_validate_l3_core_service.py b/gso/workflows/l3_core_service/base_validate_l3_core_service.py
new file mode 100644
index 000000000..9c0cef044
--- /dev/null
+++ b/gso/workflows/l3_core_service/base_validate_l3_core_service.py
@@ -0,0 +1,83 @@
+"""Validation workflow for L3 Core Service subscription objects."""
+
+from typing import Any
+
+from orchestrator.targets import Target
+from orchestrator.workflow import StepList, begin, done, step, workflow
+from orchestrator.workflows.steps import resync, store_process_subscription, unsync
+from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from pydantic_forms.types import UUIDstr
+
+from gso.services.lso_client import LSOState, anonymous_lso_interaction
+from gso.services.partners import get_partner_by_id
+from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes, L3ProductTypes
+
+
+@step("Prepare list of all Access Ports")
+def build_fqdn_list(subscription: L3ProductTypes, service_name: L3CoreServiceNameTypes) -> dict[str, list[str]]:
+    """Build the list of all FQDNs that are in the list of access ports of a L3 Core Service subscription."""
+    ap_list = getattr(subscription, service_name).l3_core.ap_list
+    ap_fqdn_list = [ap.sbp.edge_port.node.router_fqdn for ap in ap_list]
+    return {"ap_fqdn_list": ap_fqdn_list}
+
+
+@step("Check SBP config for drift")
+def validate_sbp_config(subscription: dict[str, Any], process_id: UUIDstr, ap_fqdn_list: list[str]) -> LSOState:
+    """Workflow step for running a playbook that checks whether SBP config has drifted."""
+    extra_vars = {
+        "subscription": subscription,
+        "partner_name": get_partner_by_id(subscription["customer_id"]).name,
+        "dry_run": True,
+        "verb": "deploy",
+        "object": "sbp",
+        "is_verification_workflow": "true",
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - Validate config for {subscription["description"]}",
+    }
+
+    return {
+        "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml",
+        "inventory": {"all": {"hosts": dict.fromkeys(ap_fqdn_list)}},
+        "extra_vars": extra_vars,
+    }
+
+
+@step("Check BGP peer config for drift")
+def validate_bgp_peers(subscription: dict[str, Any], process_id: UUIDstr, ap_fqdn_list: list[str]) -> LSOState:
+    """Workflow step for running a playbook that checks whether BGP peer config has drifted."""
+    extra_vars = {
+        "subscription": subscription,
+        "partner_name": get_partner_by_id(subscription["customer_id"]).name,
+        "verb": "deploy",
+        "object": "bgp",
+        "dry_run": True,
+        "is_verification_workflow": "true",
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - Validate BGP peer config for {subscription["description"]}",
+    }
+
+    return {
+        "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml",
+        "inventory": {"all": {"hosts": dict.fromkeys(ap_fqdn_list)}},
+        "extra_vars": extra_vars,
+    }
+
+
+@step("Validate DNS records")
+def validate_dns_records() -> None:
+    """Validate DNS records in Infoblox."""
+    # TODO: implement
+
+
+@workflow("Validate L3 Core Service", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None)))
+def validate_l3_core_service() -> StepList:
+    """Validate an existing L3 Core Service subscription."""
+    return (
+        begin
+        >> store_process_subscription(Target.SYSTEM)
+        >> unsync
+        >> build_fqdn_list
+        >> anonymous_lso_interaction(validate_sbp_config)
+        >> anonymous_lso_interaction(validate_bgp_peers)
+        >> validate_dns_records
+        >> resync
+        >> done
+    )
diff --git a/gso/workflows/l3_core_service/copernicus/create_copernicus.py b/gso/workflows/l3_core_service/copernicus/create_copernicus.py
index fc4a8e5a4..2504fe0b6 100644
--- a/gso/workflows/l3_core_service/copernicus/create_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/create_copernicus.py
@@ -22,6 +22,7 @@ from gso.workflows.l3_core_service.base_create_l3_core_service import (
     provision_sbp_real,
     update_dns_records,
 )
+from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes
 
 
 @step("Create subscription")
@@ -34,7 +35,11 @@ def create_subscription(product: UUIDstr, partner: str) -> State:
 
 @step("Initialize subscription")
 def initialize_subscription(
-    subscription: CopernicusInactive, edge_port: dict, binding_port_input: dict, product_name: str, service_name: str
+    subscription: CopernicusInactive,
+    edge_port: dict,
+    binding_port_input: dict,
+    product_name: str,
+    service_name: L3CoreServiceNameTypes,
 ) -> State:
     """Take all user inputs and use them to populate the subscription model."""
     return initialize_service_binding(subscription, edge_port, binding_port_input, product_name, service_name)
diff --git a/gso/workflows/l3_core_service/copernicus/validate_copernicus.py b/gso/workflows/l3_core_service/copernicus/validate_copernicus.py
new file mode 100644
index 000000000..873524f55
--- /dev/null
+++ b/gso/workflows/l3_core_service/copernicus/validate_copernicus.py
@@ -0,0 +1,38 @@
+"""Validation workflow for Copernicus subscription objects."""
+
+from orchestrator.targets import Target
+from orchestrator.workflow import StepList, begin, done, step, workflow
+from orchestrator.workflows.steps import resync, store_process_subscription, unsync
+from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from pydantic_forms.types import State
+
+from gso.services.lso_client import anonymous_lso_interaction
+from gso.workflows.l3_core_service.base_validate_l3_core_service import (
+    build_fqdn_list,
+    validate_bgp_peers,
+    validate_dns_records,
+    validate_sbp_config,
+)
+
+
+@step("Inject service name")
+def inject_srvice_name() -> State:
+    """Inject the service name into the subscription."""
+    return {"service_name": "copernicus"}
+
+
+@workflow("Validate Copernicus", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None)))
+def validate_copernicus() -> StepList:
+    """Validate an existing Copernicus subscription."""
+    return (
+        begin
+        >> inject_srvice_name
+        >> store_process_subscription(Target.SYSTEM)
+        >> unsync
+        >> build_fqdn_list
+        >> anonymous_lso_interaction(validate_sbp_config)
+        >> anonymous_lso_interaction(validate_bgp_peers)
+        >> validate_dns_records
+        >> resync
+        >> done
+    )
diff --git a/gso/workflows/l3_core_service/geant_ip/create_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/create_geant_ip.py
index bee6b5d7b..346480000 100644
--- a/gso/workflows/l3_core_service/geant_ip/create_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/create_geant_ip.py
@@ -22,6 +22,7 @@ from gso.workflows.l3_core_service.base_create_l3_core_service import (
     provision_sbp_real,
     update_dns_records,
 )
+from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes
 
 
 @step("Create subscription")
@@ -34,7 +35,11 @@ def create_subscription(product: UUIDstr, partner: str) -> State:
 
 @step("Initialize subscription")
 def initialize_subscription(
-    subscription: GeantIPInactive, edge_port: dict, binding_port_input: dict, product_name: str, service_name: str
+    subscription: GeantIPInactive,
+    edge_port: dict,
+    binding_port_input: dict,
+    product_name: str,
+    service_name: L3CoreServiceNameTypes,
 ) -> State:
     """Take all user inputs and use them to populate the subscription model."""
     return initialize_service_binding(subscription, edge_port, binding_port_input, product_name, service_name)
diff --git a/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py
index ece7c7f7a..0fe8b0b89 100644
--- a/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py
@@ -22,6 +22,7 @@ def initial_input_form_generator(subscription_id: str) -> FormGenerator:
     """Gather input from the operator on what operation to perform on the GÉANT IP subscription."""
     return base_initial_input_form_generator(subscription_id, service_name="geant_ip")
 
+
 @workflow(
     "Modify GÉANT IP ",
     initial_input_form=wrap_modify_initial_input_form(base_initial_input_form_generator),
diff --git a/gso/workflows/l3_core_service/geant_ip/validate_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/validate_geant_ip.py
new file mode 100644
index 000000000..514956162
--- /dev/null
+++ b/gso/workflows/l3_core_service/geant_ip/validate_geant_ip.py
@@ -0,0 +1,38 @@
+"""Validation workflow for GÉANT IP subscription objects."""
+
+from orchestrator.targets import Target
+from orchestrator.workflow import StepList, begin, done, step, workflow
+from orchestrator.workflows.steps import resync, store_process_subscription, unsync
+from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from pydantic_forms.types import State
+
+from gso.services.lso_client import anonymous_lso_interaction
+from gso.workflows.l3_core_service.base_validate_l3_core_service import (
+    build_fqdn_list,
+    validate_bgp_peers,
+    validate_dns_records,
+    validate_sbp_config,
+)
+
+
+@step("Inject service name")
+def inject_srvice_name() -> State:
+    """Inject the service name into the subscription."""
+    return {"service_name": "geant_ip"}
+
+
+@workflow("Validate GÉANT IP", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None)))
+def validate_geant_ip() -> StepList:
+    """Validate an existing Copernicus subscription."""
+    return (
+        begin
+        >> inject_srvice_name
+        >> store_process_subscription(Target.SYSTEM)
+        >> unsync
+        >> build_fqdn_list
+        >> anonymous_lso_interaction(validate_sbp_config)
+        >> anonymous_lso_interaction(validate_bgp_peers)
+        >> validate_dns_records
+        >> resync
+        >> done
+    )
diff --git a/gso/workflows/l3_core_service/ias/create_ias.py b/gso/workflows/l3_core_service/ias/create_ias.py
index 687b00f8d..53b54081f 100644
--- a/gso/workflows/l3_core_service/ias/create_ias.py
+++ b/gso/workflows/l3_core_service/ias/create_ias.py
@@ -26,6 +26,7 @@ from gso.workflows.l3_core_service.base_create_l3_core_service import (
 from gso.workflows.l3_core_service.base_create_l3_core_service import (
     initial_input_form_generator as l3_initial_input_form_generator,
 )
+from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes
 
 
 def initial_input_form_generator(product_name: str) -> FormGenerator:
@@ -34,7 +35,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
     initial_user_input = yield from initial_generator
 
     # Additional IAS step
-    class IASExtraForm(FormPage):
+    class IASExtraForm(FormPage):  # TODO: Think about the order of this form when user is filling it
         ias_flavor: IASFlavor | None = IASFlavor.IAS_PS_OPT_OUT
 
     ias_extra = yield IASExtraForm
@@ -56,7 +57,7 @@ def initialize_subscription(
     binding_port_input: dict,
     product_name: str,
     ias_flavor: IASFlavor,
-    service_name: str,
+    service_name: L3CoreServiceNameTypes,
 ) -> State:
     """Take all user inputs and use them to populate the subscription model."""
     subscription.ias.ias_flavor = ias_flavor
diff --git a/gso/workflows/l3_core_service/ias/create_imported_ias.py b/gso/workflows/l3_core_service/ias/create_imported_ias.py
index c1a493e39..463371076 100644
--- a/gso/workflows/l3_core_service/ias/create_imported_ias.py
+++ b/gso/workflows/l3_core_service/ias/create_imported_ias.py
@@ -25,7 +25,7 @@ def initial_input_form_generator() -> FormGenerator:
     initial_user_input = yield from initial_generator
 
     # Additional IAS step
-    class IASExtraForm(FormPage):
+    class IASExtraForm(FormPage):  # TODO: Think about the order of this form when user is filling it
         ias_flavor: IASFlavor | None = IASFlavor.IAS_PS_OPT_OUT
 
     ias_extra = yield IASExtraForm
diff --git a/gso/workflows/l3_core_service/ias/modify_ias.py b/gso/workflows/l3_core_service/ias/modify_ias.py
index 8b22842a8..d4f062290 100644
--- a/gso/workflows/l3_core_service/ias/modify_ias.py
+++ b/gso/workflows/l3_core_service/ias/modify_ias.py
@@ -22,6 +22,7 @@ def initial_input_form_generator(subscription_id: str) -> FormGenerator:
     """Gather input from the operator on what operation to perform on the IAS subscription."""
     return base_initial_input_form_generator(subscription_id, service_name="ias")
 
+
 @workflow(
     "Modify IAS",
     initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
diff --git a/gso/workflows/l3_core_service/ias/validate_ias.py b/gso/workflows/l3_core_service/ias/validate_ias.py
new file mode 100644
index 000000000..c9df01381
--- /dev/null
+++ b/gso/workflows/l3_core_service/ias/validate_ias.py
@@ -0,0 +1,38 @@
+"""Validation workflow for IAS subscription objects."""
+
+from orchestrator.targets import Target
+from orchestrator.workflow import StepList, begin, done, step, workflow
+from orchestrator.workflows.steps import resync, store_process_subscription, unsync
+from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from pydantic_forms.types import State
+
+from gso.services.lso_client import anonymous_lso_interaction
+from gso.workflows.l3_core_service.base_validate_l3_core_service import (
+    build_fqdn_list,
+    validate_bgp_peers,
+    validate_dns_records,
+    validate_sbp_config,
+)
+
+
+@step("Inject service name")
+def inject_srvice_name() -> State:
+    """Inject the service name into the subscription."""
+    return {"service_name": "ias"}
+
+
+@workflow("Validate IAS", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None)))
+def validate_ias() -> StepList:
+    """Validate an existing IAS subscription."""
+    return (
+        begin
+        >> inject_srvice_name
+        >> store_process_subscription(Target.SYSTEM)
+        >> unsync
+        >> build_fqdn_list
+        >> anonymous_lso_interaction(validate_sbp_config)
+        >> anonymous_lso_interaction(validate_bgp_peers)
+        >> validate_dns_records
+        >> resync
+        >> done
+    )
diff --git a/gso/workflows/l3_core_service/lhcone/create_lhcone.py b/gso/workflows/l3_core_service/lhcone/create_lhcone.py
index 4ad5016be..c39252268 100644
--- a/gso/workflows/l3_core_service/lhcone/create_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/create_lhcone.py
@@ -22,6 +22,7 @@ from gso.workflows.l3_core_service.base_create_l3_core_service import (
     provision_sbp_real,
     update_dns_records,
 )
+from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes
 
 
 @step("Create subscription")
@@ -34,7 +35,11 @@ def create_subscription(product: UUIDstr, partner: str) -> State:
 
 @step("Initialize subscription")
 def initialize_subscription(
-    subscription: LHCOneInactive, edge_port: dict, binding_port_input: dict, product_name: str, service_name: str
+    subscription: LHCOneInactive,
+    edge_port: dict,
+    binding_port_input: dict,
+    product_name: str,
+    service_name: L3CoreServiceNameTypes,
 ) -> State:
     """Take all user inputs and use them to populate the subscription model."""
     return initialize_service_binding(subscription, edge_port, binding_port_input, product_name, service_name)
diff --git a/gso/workflows/l3_core_service/lhcone/validate_lhcone.py b/gso/workflows/l3_core_service/lhcone/validate_lhcone.py
new file mode 100644
index 000000000..172f6caea
--- /dev/null
+++ b/gso/workflows/l3_core_service/lhcone/validate_lhcone.py
@@ -0,0 +1,38 @@
+"""Validation workflow for LHCONE subscription objects."""
+
+from orchestrator.targets import Target
+from orchestrator.workflow import StepList, begin, done, step, workflow
+from orchestrator.workflows.steps import resync, store_process_subscription, unsync
+from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from pydantic_forms.types import State
+
+from gso.services.lso_client import anonymous_lso_interaction
+from gso.workflows.l3_core_service.base_validate_l3_core_service import (
+    build_fqdn_list,
+    validate_bgp_peers,
+    validate_dns_records,
+    validate_sbp_config,
+)
+
+
+@step("Inject service name")
+def inject_srvice_name() -> State:
+    """Inject the service name into the subscription."""
+    return {"service_name": "lhcone"}
+
+
+@workflow("Validate LHCOne", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None)))
+def validate_lhcone() -> StepList:
+    """Validate an existing LHCone subscription."""
+    return (
+        begin
+        >> inject_srvice_name
+        >> store_process_subscription(Target.SYSTEM)
+        >> unsync
+        >> build_fqdn_list
+        >> anonymous_lso_interaction(validate_sbp_config)
+        >> anonymous_lso_interaction(validate_bgp_peers)
+        >> validate_dns_records
+        >> resync
+        >> done
+    )
diff --git a/gso/workflows/l3_core_service/shared.py b/gso/workflows/l3_core_service/shared.py
index 8a7fac7a7..3e44a3685 100644
--- a/gso/workflows/l3_core_service/shared.py
+++ b/gso/workflows/l3_core_service/shared.py
@@ -1,5 +1,7 @@
 """Shared logic for L3 Core Service."""
 
+from typing import Literal
+
 from gso.products import ProductName
 from gso.products.product_types.copernicus import Copernicus, CopernicusInactive
 from gso.products.product_types.geant_ip import GeantIP, GeantIPInactive
@@ -8,6 +10,12 @@ from gso.products.product_types.lhcone import LHCOne, LHCOneInactive
 
 L3ProductTypes = IAS | LHCOne | Copernicus | GeantIP
 L3InactiveProductTypes = IASInactive | LHCOneInactive | CopernicusInactive | GeantIPInactive
-L3ProductNameTypes = (
-    ProductName.IAS.value | ProductName.LHCONE.value | ProductName.COPERNICUS.value | ProductName.GEANT_IP.value
-)
+L3ProductNameTypes = Literal[ProductName.IAS, ProductName.LHCONE, ProductName.COPERNICUS, ProductName.GEANT_IP]
+L3_PRODUCT_NAMES = [
+    ProductName.GEANT_IP,
+    ProductName.IAS,
+    ProductName.LHCONE,
+    ProductName.COPERNICUS,
+]
+L3_CORE_SERVICE_PRODUCT_TYPES = [GeantIP.__name__, IAS.__name__, LHCOne.__name__, Copernicus.__name__]
+L3CoreServiceNameTypes: Literal["geant_ip", "ias", "lhcone", "copernicus"]
diff --git a/test/cli/test_imports.py b/test/cli/test_imports.py
index c5110d955..ff5126f34 100644
--- a/test/cli/test_imports.py
+++ b/test/cli/test_imports.py
@@ -20,6 +20,7 @@ from gso.cli.imports import (
 from gso.products import ProductName
 from gso.products.product_blocks.bgp_session import IPFamily
 from gso.products.product_blocks.edge_port import EdgePortType, EncapsulationType
+from gso.products.product_blocks.ias import IASFlavor
 from gso.products.product_blocks.iptrunk import IptrunkType
 from gso.products.product_blocks.layer_2_circuit import Layer2CircuitType
 from gso.products.product_blocks.router import RouterRole
@@ -293,6 +294,9 @@ def edge_port_data(temp_file, faker, router_subscription_factory, partner_factor
 @pytest.fixture()
 def l3_core_service_data(temp_file, faker, partner_factory, edge_port_subscription_factory):
     def _l3_core_service_data(**kwargs):
+        extra_ias_data = {
+            "ias_flavor": IASFlavor.IAS_PS_OPT_OUT,
+        }
         l3_core_service_data = {
             "partner": partner_factory()["name"],
             "product_name": ProductName.IAS.value,
@@ -399,11 +403,13 @@ def l3_core_service_data(temp_file, faker, partner_factory, edge_port_subscripti
 
         temp_file.write_text(
             json.dumps([
-                l3_core_service_data,
+                [
+                    l3_core_service_data,
+                    extra_ias_data,
+                ]
             ])
         )
-        return {"path": str(temp_file), "data": l3_core_service_data}
-        # TODO: Think about IAS extra form
+        return {"path": str(temp_file)}
 
     return _l3_core_service_data
 
diff --git a/test/fixtures/__init__.py b/test/fixtures/__init__.py
index 8118b5833..bba69807a 100644
--- a/test/fixtures/__init__.py
+++ b/test/fixtures/__init__.py
@@ -4,7 +4,10 @@ from test.fixtures.l3_core_service_fixtures import (
     access_port_factory,
     bfd_settings_factory,
     bgp_session_subscription_factory,
-    l3_core_service_subscription_factory,
+    lhcone_subscription_factory,
+    geant_ip_subscription_factory,
+    ias_subscription_factory,
+    copernicus_subscription_factory,
     service_binding_port_factory,
 )
 from test.fixtures.lan_switch_interconnect_fixtures import lan_switch_interconnect_subscription_factory
@@ -24,7 +27,10 @@ __all__ = [
     "edge_port_subscription_factory",
     "iptrunk_side_subscription_factory",
     "iptrunk_subscription_factory",
-    "l3_core_service_subscription_factory",
+    "lhcone_subscription_factory",
+    "geant_ip_subscription_factory",
+    "ias_subscription_factory",
+    "copernicus_subscription_factory",
     "lan_switch_interconnect_subscription_factory",
     "layer_2_circuit_subscription_factory",
     "office_router_subscription_factory",
diff --git a/test/fixtures/l3_core_service_fixtures.py b/test/fixtures/l3_core_service_fixtures.py
index fc72ca399..e45ce4cbb 100644
--- a/test/fixtures/l3_core_service_fixtures.py
+++ b/test/fixtures/l3_core_service_fixtures.py
@@ -7,19 +7,16 @@ from orchestrator.domain import SubscriptionModel
 from orchestrator.types import SubscriptionLifecycle
 from pydantic import NonNegativeInt
 
-from gso.products import ProductName
+from gso.products import ProductName, ImportedIAS, IAS, GeantIP, ImportedGeantIP
 from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPTypes
+from gso.products.product_blocks.ias import IASFlavor
 from gso.products.product_blocks.l3_core_service import AccessPort
 from gso.products.product_blocks.service_binding_port import BFDSettings, ServiceBindingPort
 from gso.products.product_types.edge_port import EdgePort
-from gso.products.product_types.l3_core_service import (
-    ImportedL3CoreService,
-    L3CoreServiceInactive,
-    L3CoreServiceType,
-)
 from gso.services import subscriptions
 from gso.utils.shared_enums import APType, SBPType
 from gso.utils.types.ip_address import IPAddress
+from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes
 
 
 @pytest.fixture()
@@ -140,94 +137,107 @@ def access_port_factory(faker, service_binding_port_factory):
     return create_access_port
 
 
-@pytest.fixture()
-def l3_core_service_subscription_factory(
-    faker,
-    partner_factory,
-    access_port_factory,
+def make_subscription_factory(
+    product_name: ProductName, imported_class, native_class, service_name: L3CoreServiceNameTypes
 ):
-    def create_l3_core_service_subscription(
-        l3_core_service_type: L3CoreServiceType,
+    def factory(faker, partner_factory, access_port_factory, save_l3_core_subscription):
+        def create_subscription(
+            description=None,
+            partner: dict | None = None,
+            ap_list: list[AccessPort] | None = None,
+            start_date="2023-05-24T00:00:00+00:00",
+            status: SubscriptionLifecycle | None = None,
+            is_imported: bool | None = False,
+        ) -> SubscriptionModel:
+            partner = partner or partner_factory()
+            product_id = subscriptions.get_product_id_by_name(product_name)
+            subscription_class = imported_class if is_imported else native_class
+
+            subscription = subscription_class.from_product_id(
+                product_id, customer_id=partner["partner_id"], insync=True
+            )
+
+            return save_l3_core_subscription(
+                ap_list, description, subscription, start_date, status, service_name=service_name
+            )
+
+        return create_subscription
+
+    return factory
+
+
+@pytest.fixture()
+def save_l3_core_subscription(access_port_factory, faker):
+    def _save_subscription(
+        ap_list, description, subscription, start_date, status, service_name: L3CoreServiceNameTypes
+    ):
+        # Default ap_list creation with primary and backup access ports
+        getattr(subscription, service_name).ap_list = ap_list or [
+            access_port_factory(ap_type=APType.PRIMARY),
+            access_port_factory(ap_type=APType.BACKUP),
+        ]
+        # Update subscription with description, start date, and status
+        subscription = SubscriptionModel.from_other_lifecycle(
+            subscription,
+            SubscriptionLifecycle.ACTIVE,
+        )
+        subscription.description = description or faker.sentence()
+        subscription.start_date = start_date
+        subscription.status = status or SubscriptionLifecycle.ACTIVE
+        subscription.save()
+        db.session.commit()
+        return subscription
+
+    return _save_subscription
+
+
+@pytest.fixture()
+def ias_subscription_factory(faker, partner_factory, access_port_factory, save_l3_core_subscription):
+    def create_ias_subscription(
         description=None,
         partner: dict | None = None,
         ap_list: list[AccessPort] | None = None,
         start_date="2023-05-24T00:00:00+00:00",
         status: SubscriptionLifecycle | None = None,
+        ias_flavor: str | None = IASFlavor.IAS_PS_OPT_OUT,
+        *,
+        is_imported: bool | None = False,
     ) -> SubscriptionModel:
         partner = partner or partner_factory()
-        match l3_core_service_type:
-            case L3CoreServiceType.GEANT_IP:
-                product_id = subscriptions.get_product_id_by_name(ProductName.GEANT_IP)
-                l3_core_service_subscription = L3CoreServiceInactive.from_product_id(
-                    product_id, customer_id=partner["partner_id"], insync=True
-                )
-            case L3CoreServiceType.IMPORTED_GEANT_IP:
-                product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_GEANT_IP)
-                l3_core_service_subscription = ImportedL3CoreService.from_product_id(
-                    product_id, customer_id=partner["partner_id"], insync=True
-                )
-            case L3CoreServiceType.IAS:
-                product_id = subscriptions.get_product_id_by_name(ProductName.IAS)
-                l3_core_service_subscription = L3CoreServiceInactive.from_product_id(
-                    product_id, customer_id=partner["partner_id"], insync=True
-                )
-            case L3CoreServiceType.IMPORTED_IAS:
-                product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_IAS)
-                l3_core_service_subscription = ImportedL3CoreService.from_product_id(
-                    product_id, customer_id=partner["partner_id"], insync=True
-                )
-            case L3CoreServiceType.GWS:
-                product_id = subscriptions.get_product_id_by_name(ProductName.GWS)
-                l3_core_service_subscription = L3CoreServiceInactive.from_product_id(
-                    product_id, customer_id=partner["partner_id"], insync=True
-                )
-            case L3CoreServiceType.IMPORTED_GWS:
-                product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_GWS)
-                l3_core_service_subscription = ImportedL3CoreService.from_product_id(
-                    product_id, customer_id=partner["partner_id"], insync=True
-                )
-            case L3CoreServiceType.LHCONE:
-                product_id = subscriptions.get_product_id_by_name(ProductName.LHCONE)
-                l3_core_service_subscription = L3CoreServiceInactive.from_product_id(
-                    product_id, customer_id=partner["partner_id"], insync=True
-                )
-            case L3CoreServiceType.IMPORTED_LHCONE:
-                product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_LHCONE)
-                l3_core_service_subscription = ImportedL3CoreService.from_product_id(
-                    product_id, customer_id=partner["partner_id"], insync=True
-                )
-            case L3CoreServiceType.COPERNICUS:
-                product_id = subscriptions.get_product_id_by_name(ProductName.COPERNICUS)
-                l3_core_service_subscription = L3CoreServiceInactive.from_product_id(
-                    product_id, customer_id=partner["partner_id"], insync=True
-                )
-            case L3CoreServiceType.IMPORTED_COPERNICUS:
-                product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_COPERNICUS)
-                l3_core_service_subscription = ImportedL3CoreService.from_product_id(
-                    product_id, customer_id=partner["partner_id"], insync=True
-                )
-            case _:
-                msg = f"L3 Core Service type not found: {l3_core_service_type}"
-                raise ValueError(msg)
 
-        # Default ap_list creation with primary and backup access ports
-        l3_core_service_subscription.l3_core_service.ap_list = ap_list or [
-            access_port_factory(ap_type=APType.PRIMARY),
-            access_port_factory(ap_type=APType.BACKUP),
-        ]
+        if is_imported:
+            product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_IAS)
+            subscription = ImportedIAS.from_product_id(product_id, customer_id=partner["partner_id"], insync=True)
+        else:
+            product_id = subscriptions.get_product_id_by_name(ProductName.IAS)
+            subscription = IAS.from_product_id(product_id, customer_id=partner["partner_id"], insync=True)
 
-        # Update subscription with description, start date, and status
-        l3_core_service_subscription = SubscriptionModel.from_other_lifecycle(
-            l3_core_service_subscription,
-            SubscriptionLifecycle.ACTIVE,
-        )
-        l3_core_service_subscription.description = description or faker.sentence()
-        l3_core_service_subscription.start_date = start_date
-        l3_core_service_subscription.status = status or SubscriptionLifecycle.ACTIVE
-        l3_core_service_subscription.save()
+        subscription.ias.ias_flavor = ias_flavor
 
-        db.session.commit()
+        return save_l3_core_subscription(ap_list, description, subscription, start_date, status, service_name="ias")
+
+    return create_ias_subscription
+
+
+@pytest.fixture()
+def geant_ip_subscription_factory(faker, partner_factory, access_port_factory, save_l3_core_subscription):
+    return make_subscription_factory(
+        product_name=ProductName.GEANT_IP, imported_class=ImportedGeantIP, native_class=GeantIP, service_name="geant_ip"
+    )(faker, partner_factory, access_port_factory, save_l3_core_subscription)
 
-        return l3_core_service_subscription
 
-    return create_l3_core_service_subscription
+@pytest.fixture()
+def copernicus_subscription_factory(faker, partner_factory, access_port_factory, save_l3_core_subscription):
+    return make_subscription_factory(
+        product_name=ProductName.COPERNICUS,
+        imported_class=ImportedGeantIP,
+        native_class=GeantIP,
+        service_name="copernicus",
+    )(faker, partner_factory, access_port_factory, save_l3_core_subscription)
+
+
+@pytest.fixture()
+def lhcone_subscription_factory(faker, partner_factory, access_port_factory, save_l3_core_subscription):
+    return make_subscription_factory(
+        product_name=ProductName.LHCONE, imported_class=ImportedGeantIP, native_class=GeantIP, service_name="lhcone"
+    )(faker, partner_factory, access_port_factory, save_l3_core_subscription)
diff --git a/test/workflows/l3_core_service/test_create_imported_l3_core_service.py b/test/workflows/l3_core_service/test_create_imported_l3_core_service.py
index 5350814fa..b44227741 100644
--- a/test/workflows/l3_core_service/test_create_imported_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_create_imported_l3_core_service.py
@@ -1,19 +1,30 @@
 import pytest
 from orchestrator.types import SubscriptionLifecycle
 
+from gso.products import ProductName
 from gso.products.product_blocks.bgp_session import IPFamily
+from gso.products.product_blocks.ias import IASFlavor
 from gso.products.product_types.l3_core_service import L3_CORE_SERVICE_TYPES, ImportedL3CoreService
 from gso.utils.shared_enums import SBPType
+from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES
 from test.workflows import assert_complete, extract_state, run_workflow
 
+_L3_CORE_MIGRATION_MAP = {
+    ProductName.COPERNICUS: "create_imported_copernicus",
+    ProductName.GEANT_IP: "create_imported_geant_ip",
+    ProductName.IAS: "create_imported_ias",
+    ProductName.LHCONE: "create_imported_lhcone",
+}
 
-@pytest.mark.parametrize("l3_core_service_type", L3_CORE_SERVICE_TYPES)
-def test_create_imported_l3_core_service_success(
-    faker, partner_factory, edge_port_subscription_factory, l3_core_service_type
-):
+
+@pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES)
+def test_create_imported_l3_core_service_success(faker, partner_factory, edge_port_subscription_factory, product_name):
+    extra_ias_data = {
+        "ias_flavor": IASFlavor.IAS_PS_OPT_OUT,
+    }
     creation_form_input_data = {
         "partner": partner_factory()["name"],
-        "service_type": l3_core_service_type,
+        "product_name": product_name,
         "service_binding_ports": [
             {
                 "edge_port": edge_port_subscription_factory().subscription_id,
@@ -69,7 +80,9 @@ def test_create_imported_l3_core_service_success(
         ],
     }
 
-    result, _, _ = run_workflow("create_imported_l3_core_service", [creation_form_input_data])
+    result, _, _ = run_workflow(
+        f"create_imported_{_L3_CORE_MIGRATION_MAP[product_name]}", [extra_ias_data, creation_form_input_data]
+    )
     state = extract_state(result)
     subscription = ImportedL3CoreService.from_subscription(state["subscription_id"])
     assert_complete(result)
diff --git a/test/workflows/l3_core_service/test_create_l3_core_service.py b/test/workflows/l3_core_service/test_create_l3_core_service.py
index b3c43e597..2a5b3af07 100644
--- a/test/workflows/l3_core_service/test_create_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_create_l3_core_service.py
@@ -7,6 +7,7 @@ from gso.products import ProductName
 from gso.products.product_types.l3_core_service import L3CoreService
 from gso.services.subscriptions import get_product_id_by_name
 from gso.utils.shared_enums import APType
+from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES
 from test import USER_CONFIRM_EMPTY_FORM
 from test.services.conftest import MockedSharePointClient
 from test.workflows import (
@@ -35,23 +36,21 @@ def base_bgp_peer_input(faker):
     return _base_bgp_peer_input
 
 
-@pytest.mark.parametrize(
-    "l3_core_type", [ProductName.GEANT_IP, ProductName.IAS, ProductName.GWS, ProductName.LHCONE, ProductName.COPERNICUS]
-)
+@pytest.mark.parametrize("l3_product_name", L3_PRODUCT_NAMES)
 @pytest.mark.workflow()
 @patch("gso.services.lso_client._send_request")
 @patch("gso.workflows.l3_core_service.create_l3_core_service.SharePointClient")
 def test_create_l3_core_service_success(
     mock_sharepoint_client,
     mock_lso_client,
-    l3_core_type,
+    l3_product_name,
     faker,
     partner_factory,
     edge_port_subscription_factory,
     base_bgp_peer_input,
 ):
     partner = partner_factory()
-    product_id = get_product_id_by_name(l3_core_type)
+    product_id = get_product_id_by_name(l3_product_name)
     edge_port_a = str(edge_port_subscription_factory(partner=partner).subscription_id)
     mock_sharepoint_client.return_value = MockedSharePointClient
 
diff --git a/test/workflows/l3_core_service/test_validate_prefix_list.py b/test/workflows/l3_core_service/test_validate_prefix_list.py
index 5ec52d8e5..4ef8872bd 100644
--- a/test/workflows/l3_core_service/test_validate_prefix_list.py
+++ b/test/workflows/l3_core_service/test_validate_prefix_list.py
@@ -2,9 +2,8 @@ from unittest.mock import patch
 
 import pytest
 
-from gso.products.product_types.l3_core_service import L3_CORE_SERVICE_TYPES, L3CoreService, L3CoreServiceType
-from gso.utils.shared_enums import APType, Vendor
 from test import USER_CONFIRM_EMPTY_FORM
+from gso.utils.shared_enums import Vendor
 from test.workflows import (
     assert_complete,
     assert_lso_failure,
-- 
GitLab


From 900cbf4437b410a6f9d378137340bebc11e0ddf2 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Wed, 26 Mar 2025 17:09:54 +0100
Subject: [PATCH 43/87] rename descriptions

---
 ...bb3c4411ea_add_new_l3_core_services_wfs.py | 26 +++++++++----------
 1 file changed, 13 insertions(+), 13 deletions(-)

diff --git a/gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py b/gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py
index c03f891f2..a39be4d5c 100644
--- a/gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py
+++ b/gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py
@@ -103,79 +103,79 @@ new_workflows = [
     {
         "name": "create_lhcone",
         "target": "CREATE",
-        "description": "Create LHCONE",
+        "description": "Create LHCOne",
         "product_type": "LHCOne"
     },
     {
         "name": "modify_lhcone",
         "target": "MODIFY",
-        "description": "Modify LHCONE",
+        "description": "Modify LHCOne",
         "product_type": "LHCOne"
     },
     {
         "name": "terminate_lhcone",
         "target": "TERMINATE",
-        "description": "Terminate LHCONE",
+        "description": "Terminate LHCOne",
         "product_type": "LHCOne"
     },
     {
         "name": "migrate_lhcone",
         "target": "MODIFY",
-        "description": "Migrate LHCONE",
+        "description": "Migrate LHCOne",
         "product_type": "LHCOne"
     },
     {
         "name": "create_imported_lhcone",
         "target": "CREATE",
-        "description": "Create Imported LHCONE",
+        "description": "Create Imported LHCOne",
         "product_type": "ImportedLHCOne"
     },
     {
         "name": "import_lhcone",
         "target": "MODIFY",
-        "description": "Import LHCONE",
+        "description": "Import LHCOne",
         "product_type": "ImportedLHCOne"
     },
     {
         "name": "validate_lhcone",
         "target": "SYSTEM",
-        "description": "Validate LHCONE Configuration",
+        "description": "Validate LHCOne Configuration",
         "product_type": "LHCOne"
     },
     {
         "name": "create_copernicus",
         "target": "CREATE",
-        "description": "Create COPERNICUS",
+        "description": "Create Copernicus",
         "product_type": "Copernicus"
     },
     {
         "name": "modify_copernicus",
         "target": "MODIFY",
-        "description": "Modify COPERNICUS",
+        "description": "Modify Copernicus",
         "product_type": "Copernicus"
     },
     {
         "name": "terminate_copernicus",
         "target": "TERMINATE",
-        "description": "Terminate COPERNICUS",
+        "description": "Terminate Copernicus",
         "product_type": "Copernicus"
     },
     {
         "name": "migrate_copernicus",
         "target": "MODIFY",
-        "description": "Migrate COPERNICUS",
+        "description": "Migrate Copernicus",
         "product_type": "Copernicus"
     },
     {
         "name": "create_imported_copernicus",
         "target": "CREATE",
-        "description": "Create Imported COPERNICUS",
+        "description": "Create Imported Copernicus",
         "product_type": "ImportedCopernicus"
     },
     {
         "name": "import_copernicus",
         "target": "MODIFY",
-        "description": "Import COPERNICUS",
+        "description": "Import Copernicus",
         "product_type": "ImportedCopernicus"
     },
     {
-- 
GitLab


From b0f634ab1426a0b44c121e859ba660dd13433d52 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Wed, 26 Mar 2025 23:00:40 +0100
Subject: [PATCH 44/87] update old l3 wf refrences with the new one and fixes
 imports

---
 ...c38adde1a18e_update_wf_in_process_table.py | 114 ++++++++++++++++++
 .../base_create_imported_l3_core_service.py   |   5 +-
 .../geant_ip/import_geant_ip.py               |  11 +-
 gso/workflows/l3_core_service/shared.py       |  13 +-
 4 files changed, 126 insertions(+), 17 deletions(-)
 create mode 100644 gso/migrations/versions/2025-03-26_c38adde1a18e_update_wf_in_process_table.py

diff --git a/gso/migrations/versions/2025-03-26_c38adde1a18e_update_wf_in_process_table.py b/gso/migrations/versions/2025-03-26_c38adde1a18e_update_wf_in_process_table.py
new file mode 100644
index 000000000..a1878359d
--- /dev/null
+++ b/gso/migrations/versions/2025-03-26_c38adde1a18e_update_wf_in_process_table.py
@@ -0,0 +1,114 @@
+"""Update wf in process table and delete old workflows
+
+Revision ID: c38adde1a18e
+Revises: 9fbb3c4411ea
+Create Date: 2025-03-26 21:38:13.445657
+
+"""
+
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = 'c38adde1a18e'
+down_revision = '9fbb3c4411ea'
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    """Update workflow IDs in the processes table based on product_type and current workflow."""
+    conn = op.get_bind()
+
+    # Mapping for all workflow updates:
+    update_mappings = [
+        # IAS updates
+        {"new_workflow": "create_ias", "old_workflow": "create_l3_core_service", "product_type": "IAS"},
+        {"new_workflow": "modify_ias", "old_workflow": "modify_l3_core_service", "product_type": "IAS"},
+        {"new_workflow": "terminate_ias", "old_workflow": "terminate_l3_core_service", "product_type": "IAS"},
+        {"new_workflow": "migrate_ias", "old_workflow": "migrate_l3_core_service", "product_type": "IAS"},
+        {"new_workflow": "validate_ias", "old_workflow": "validate_l3_core_service", "product_type": "IAS"},
+        {"new_workflow": "create_imported_ias", "old_workflow": "create_imported_l3_core_service",
+         "product_type": "IAS"},
+        {"new_workflow": "import_ias", "old_workflow": "import_l3_core_service", "product_type": "IAS"},
+
+        # GeantIP updates
+        {"new_workflow": "create_geant_ip", "old_workflow": "create_l3_core_service", "product_type": "GeantIP"},
+        {"new_workflow": "modify_geant_ip", "old_workflow": "modify_l3_core_service", "product_type": "GeantIP"},
+        {"new_workflow": "terminate_geant_ip", "old_workflow": "terminate_l3_core_service", "product_type": "GeantIP"},
+        {"new_workflow": "migrate_geant_ip", "old_workflow": "migrate_l3_core_service", "product_type": "GeantIP"},
+        {"new_workflow": "validate_geant_ip", "old_workflow": "validate_l3_core_service", "product_type": "GeantIP"},
+        {"new_workflow": "create_imported_geant_ip", "old_workflow": "create_imported_l3_core_service",
+         "product_type": "GeantIP"},
+        {"new_workflow": "import_geant_ip", "old_workflow": "import_l3_core_service",
+         "product_type": "GeantIP"},
+
+        # LHCOne updates
+        {"new_workflow": "create_lhcone", "old_workflow": "create_l3_core_service", "product_type": "LHCOne"},
+        {"new_workflow": "modify_lhcone", "old_workflow": "modify_l3_core_service", "product_type": "LHCOne"},
+        {"new_workflow": "terminate_lhcone", "old_workflow": "terminate_l3_core_service", "product_type": "LHCOne"},
+        {"new_workflow": "migrate_lhcone", "old_workflow": "migrate_l3_core_service", "product_type": "LHCOne"},
+        {"new_workflow": "validate_lhcone", "old_workflow": "validate_l3_core_service", "product_type": "LHCOne"},
+        {"new_workflow": "create_imported_lhcone", "old_workflow": "create_imported_l3_core_service",
+         "product_type": "LHCOne"},
+        {"new_workflow": "import_lhcone", "old_workflow": "import_l3_core_service", "product_type": "LHCOne"},
+
+        # Copernicus updates
+        {"new_workflow": "create_copernicus", "old_workflow": "create_l3_core_service", "product_type": "Copernicus"},
+        {"new_workflow": "modify_copernicus", "old_workflow": "modify_l3_core_service", "product_type": "Copernicus"},
+        {"new_workflow": "terminate_copernicus", "old_workflow": "terminate_l3_core_service",
+         "product_type": "Copernicus"},
+        {"new_workflow": "migrate_copernicus", "old_workflow": "migrate_l3_core_service", "product_type": "Copernicus"},
+        {"new_workflow": "validate_copernicus", "old_workflow": "validate_l3_core_service",
+         "product_type": "Copernicus"},
+        {"new_workflow": "create_imported_copernicus", "old_workflow": "create_imported_l3_core_service",
+         "product_type": "Copernicus"},
+        {"new_workflow": "import_copernicus", "old_workflow": "import_l3_core_service",
+         "product_type": "Copernicus"},
+    ]
+
+    # SQL template with parameters
+    sql_template = """
+UPDATE processes pr
+SET workflow_id = (
+    SELECT workflow_id
+    FROM workflows
+    WHERE name = :new_workflow
+)
+FROM processes_subscriptions ps
+JOIN subscriptions s ON ps.subscription_id = s.subscription_id
+JOIN products p ON s.product_id = p.product_id
+WHERE pr.pid = ps.pid
+  AND p.product_type = :product_type
+  AND pr.workflow_id = (
+      SELECT workflow_id
+      FROM workflows
+      WHERE name = :old_workflow
+  );
+    """
+
+    # Execute the update for each mapping by passing the mapping as a parameter dictionary.
+    for mapping in update_mappings:
+        conn.execute(sa.text(sql_template), mapping)
+
+
+    conn.execute(
+        sa.text(
+            """
+DELETE FROM workflows
+WHERE name IN (
+    'create_l3_core_service',
+    'modify_l3_core_service',
+    'terminate_l3_core_service',
+    'migrate_l3_core_service',
+    'validate_l3_core_service',
+    'create_imported_l3_core_service',
+    'import_l3_core_service'
+);
+            """
+        )
+    )
+
+
+def downgrade() -> None:
+    conn = op.get_bind()
diff --git a/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
index c8e3a36aa..1668c8943 100644
--- a/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
@@ -11,12 +11,11 @@ from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPType
 from gso.products.product_blocks.l3_core_service import AccessPortInactive
 from gso.products.product_blocks.service_binding_port import BFDSettings, ServiceBindingPortInactive
 from gso.products.product_types.edge_port import EdgePort
-from gso.products.product_types.l3_core_service import ImportedL3CoreServiceInactive
 from gso.utils.shared_enums import SBPType
 from gso.utils.types.geant_ids import IMPORTED_GS_ID
 from gso.utils.types.ip_address import IPAddress, IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask
 from gso.utils.types.virtual_identifiers import VLAN_ID
-from gso.workflows.l3_core_service.shared import L3ProductNameTypes
+from gso.workflows.l3_core_service.shared import L3ProductNameTypes, l3ImportedInactiveProductTypes
 
 
 def base_initial_input_form_generator() -> FormGenerator:
@@ -71,7 +70,7 @@ def base_initial_input_form_generator() -> FormGenerator:
 
 
 @step("Initialize subscription")
-def initialize_subscription(subscription: ImportedL3CoreServiceInactive, service_binding_ports: list) -> dict:
+def initialize_subscription(subscription: l3ImportedInactiveProductTypes, service_binding_ports: list) -> dict:
     """Initialize the subscription with the user input."""
     for service_binding_port in service_binding_ports:
         edge_port_subscription = EdgePort.from_subscription(service_binding_port.pop("edge_port"))
diff --git a/gso/workflows/l3_core_service/geant_ip/import_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/import_geant_ip.py
index 886fc3065..6ad0b34b1 100644
--- a/gso/workflows/l3_core_service/geant_ip/import_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/import_geant_ip.py
@@ -13,7 +13,7 @@ from gso.services.subscriptions import get_product_id_by_name
 
 
 @step("Create imported subscription")
-def import_l3_core_service_subscription(subscription_id: UUIDstr) -> State:
+def import_geant_ip_subscription(subscription_id: UUIDstr) -> State:
     """Take an imported subscription, and turn it into a GÉANT IP subscription."""
     old_l3_core_service = ImportedGeantIP.from_subscription(subscription_id)
     new_product_id = get_product_id_by_name(ProductName.GEANT_IP)
@@ -27,11 +27,4 @@ def import_l3_core_service_subscription(subscription_id: UUIDstr) -> State:
 @workflow("Import GÉANT IP Core Service", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None))
 def import_geant_ip() -> StepList:
     """Modify an imported subscription into a GÉANT IP subscription to complete the import."""
-    return (
-        init
-        >> store_process_subscription(Target.MODIFY)
-        >> unsync
-        >> import_l3_core_service_subscription
-        >> resync
-        >> done
-    )
+    return init >> store_process_subscription(Target.MODIFY) >> unsync >> import_geant_ip_subscription >> resync >> done
diff --git a/gso/workflows/l3_core_service/shared.py b/gso/workflows/l3_core_service/shared.py
index 3e44a3685..700e8b7be 100644
--- a/gso/workflows/l3_core_service/shared.py
+++ b/gso/workflows/l3_core_service/shared.py
@@ -3,13 +3,16 @@
 from typing import Literal
 
 from gso.products import ProductName
-from gso.products.product_types.copernicus import Copernicus, CopernicusInactive
-from gso.products.product_types.geant_ip import GeantIP, GeantIPInactive
-from gso.products.product_types.ias import IAS, IASInactive
-from gso.products.product_types.lhcone import LHCOne, LHCOneInactive
+from gso.products.product_types.copernicus import Copernicus, CopernicusInactive, ImportedCopernicusInactive
+from gso.products.product_types.geant_ip import GeantIP, GeantIPInactive, ImportedGeantIPInactive
+from gso.products.product_types.ias import IAS, IASInactive, ImportedIASInactive
+from gso.products.product_types.lhcone import ImportedLHCOneInactive, LHCOne, LHCOneInactive
 
 L3ProductTypes = IAS | LHCOne | Copernicus | GeantIP
 L3InactiveProductTypes = IASInactive | LHCOneInactive | CopernicusInactive | GeantIPInactive
+l3ImportedInactiveProductTypes = (
+    ImportedIASInactive | ImportedLHCOneInactive | ImportedCopernicusInactive | ImportedGeantIPInactive
+)
 L3ProductNameTypes = Literal[ProductName.IAS, ProductName.LHCONE, ProductName.COPERNICUS, ProductName.GEANT_IP]
 L3_PRODUCT_NAMES = [
     ProductName.GEANT_IP,
@@ -18,4 +21,4 @@ L3_PRODUCT_NAMES = [
     ProductName.COPERNICUS,
 ]
 L3_CORE_SERVICE_PRODUCT_TYPES = [GeantIP.__name__, IAS.__name__, LHCOne.__name__, Copernicus.__name__]
-L3CoreServiceNameTypes: Literal["geant_ip", "ias", "lhcone", "copernicus"]
+L3CoreServiceNameTypes = Literal["geant_ip", "ias", "lhcone", "copernicus"]
-- 
GitLab


From 6adaa0dc291efd7aafab00baa0f5dcf2a92a32bf Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Thu, 27 Mar 2025 14:27:17 +0100
Subject: [PATCH 45/87] Add l3 block creation in base create WF for l3 services

---
 .../l3_core_service/base_create_l3_core_service.py    | 11 +++++------
 1 file changed, 5 insertions(+), 6 deletions(-)

diff --git a/gso/workflows/l3_core_service/base_create_l3_core_service.py b/gso/workflows/l3_core_service/base_create_l3_core_service.py
index 576be6ce2..06f9154bb 100644
--- a/gso/workflows/l3_core_service/base_create_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_l3_core_service.py
@@ -11,7 +11,7 @@ from pydantic_forms.types import FormGenerator, State, UUIDstr
 from pydantic_forms.validators import Divider
 
 from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPTypes
-from gso.products.product_blocks.l3_core_service import AccessPortInactive
+from gso.products.product_blocks.l3_core_service import AccessPortInactive, L3CoreServiceBlockInactive
 from gso.products.product_blocks.service_binding_port import BFDSettings, ServiceBindingPortInactive
 from gso.products.product_types.edge_port import EdgePort
 from gso.services.lso_client import LSOState
@@ -196,15 +196,14 @@ def initialize_service_binding(
     )
 
     service = getattr(subscription, service_name)
-    assert service is not None, f"{service_name} is not set on subscription"
-
-    service.l3_core.ap_list.append(
-        AccessPortInactive.new(
+    service.l3_core = L3CoreServiceBlockInactive.new(
+        subscription_id=uuid4(),
+        ap_list = [AccessPortInactive.new(
             subscription_id=uuid4(),
             ap_type=edge_port["ap_type"],
             sbp=service_binding_port,
             custom_service_name=edge_port.get("custom_service_name"),
-        )
+        )]
     )
     edge_port_fqdn_list.append(edge_port_subscription.edge_port.node.router_fqdn)
 
-- 
GitLab


From 2c30208bab93a095e6e1e24d07c5ed6bd7e91be3 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 27 Mar 2025 14:32:49 +0100
Subject: [PATCH 46/87] rebase with origin

---
 .../2025-03-18_c9c9fdf624b5_re_model_ias.py   | 198 ++++++++++++++++++
 gso/products/product_blocks/gws.py            |  30 +++
 gso/products/product_types/gws.py             |  36 ++++
 gso/products/product_types/l3_core_service.py |   0
 4 files changed, 264 insertions(+)
 create mode 100644 gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py
 create mode 100644 gso/products/product_blocks/gws.py
 create mode 100644 gso/products/product_types/gws.py
 create mode 100644 gso/products/product_types/l3_core_service.py

diff --git a/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py b/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py
new file mode 100644
index 000000000..5ff448768
--- /dev/null
+++ b/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py
@@ -0,0 +1,198 @@
+"""Re-model L3CoreServices
+
+    GEANT_IP = "GÉANT IP"
+    IMPORTED_GEANT_IP = "IMPORTED GÉANT IP"
+    GWS = "GWS"
+    IMPORTED_GWS = "IMPORTED GWS"
+    LHCONE = "LHCONE"
+    IMPORTED_LHCONE = "IMPORTED LHCONE"
+    COPERNICUS = "COPERNICUS"
+    IMPORTED_COPERNICUS = "IMPORTED COPERNICUS"
+
+Revision ID: e1afa3790f32
+Revises: b96b0ecf6906
+Create Date: 2025-03-17 10:23:45.917222
+
+"""
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = 'e1afa3790f32'
+down_revision = 'b96b0ecf6906'
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    conn = op.get_bind()
+    conn.execute(
+        sa.text(
+            """
+DELETE FROM fixed_inputs
+WHERE fixed_inputs.product_id IN (
+    SELECT products.product_id FROM products WHERE products.name IN ('IAS', 'Imported IAS')
+)
+  AND fixed_inputs.name = 'l3_core_service_type'
+            """
+        )
+    )
+    conn.execute(
+        sa.text(
+            """
+INSERT INTO product_blocks (name, description, tag, status)
+VALUES ('IASProductBlock', 'An Internet Access Service for general internet access', 'IAS', 'active')
+RETURNING product_blocks.product_block_id
+            """
+        )
+    )
+    conn.execute(
+        sa.text(
+            """
+INSERT INTO resource_types (resource_type, description)
+VALUES ('ias_flavor', 'The flavor of the IAS service')
+RETURNING resource_types.resource_type_id
+            """
+        )
+    )
+    conn.execute(
+        sa.text(
+            """ 
+    INSERT INTO product_block_relations
+VALUES (
+    (SELECT product_blocks.product_block_id FROM product_blocks WHERE name = 'IASProductBlock'),
+    (SELECT product_blocks.product_block_id FROM product_blocks WHERE name = 'L3CoreServiceBlock'),
+    NULL,
+    NULL
+);
+            """
+        )
+    )
+
+    conn.execute(
+        sa.text(
+            """
+UPDATE product_product_blocks
+SET product_block_id = (
+    SELECT product_block_id FROM product_blocks WHERE name = 'IASProductBlock'
+)
+WHERE product_block_id = (
+    SELECT product_block_id FROM product_blocks WHERE name = 'L3CoreServiceBlock'
+)
+  AND product_id IN (
+    SELECT product_id FROM products WHERE name IN ('IAS', 'Imported IAS')
+);
+            """
+        )
+    )
+
+    conn.execute(
+        sa.text(
+            """
+UPDATE products
+SET product_type = 'IAS'
+WHERE product_type = 'L3CoreService'
+  AND name = 'IAS';
+            """
+        )
+    )
+
+    conn.execute(
+        sa.text(
+            """
+UPDATE products
+SET product_type = 'ImportedIAS'
+WHERE product_type = 'ImportedL3CoreService'
+  AND name = 'Imported IAS';
+            """
+        )
+    )
+
+    conn.execute(
+        sa.text(
+            """
+-- Step 1: Insert new subscription_instances for 'IASProductBlock' and return their IDs
+WITH inserted AS (
+    INSERT INTO subscription_instances (subscription_id, product_block_id)
+    SELECT
+        s.subscription_id,
+        (
+            SELECT product_block_id
+            FROM product_blocks
+            WHERE name = 'IASProductBlock'
+        ) AS product_block_id
+    FROM subscriptions s
+    WHERE s.product_id = (
+        SELECT product_id
+        FROM products
+        WHERE products.name = 'IAS'
+    )
+    RETURNING subscription_instance_id, subscription_id
+)
+
+-- Step 2: Link newly inserted "IASProductBlock" instances to the existing "L3CoreServiceBlock" instance
+INSERT INTO subscription_instance_relations (
+    in_use_by_id,
+    depends_on_id,
+    order_id,
+    domain_model_attr
+)
+SELECT
+    i.subscription_instance_id AS in_use_by_id,
+    e.subscription_instance_id AS depends_on_id,
+    0 AS order_id,
+    'l3_core' AS domain_model_attr
+FROM inserted i
+JOIN subscription_instances e
+  ON e.subscription_id = i.subscription_id
+  AND e.product_block_id = (
+      SELECT product_block_id
+      FROM product_blocks
+      WHERE name = 'L3CoreServiceBlock'
+  );
+            """
+        )
+    )
+    
+    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 ('IASProductBlock')),
+    (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ias_flavor'))
+)
+            """
+        )
+    )
+
+    conn.execute(
+        sa.text(
+            """
+
+                WITH subscription_instance_ids AS (
+                    SELECT subscription_instances.subscription_instance_id
+                    FROM subscription_instances
+                    WHERE subscription_instances.product_block_id IN (
+                        SELECT product_blocks.product_block_id
+                        FROM product_blocks
+                        WHERE product_blocks.name = 'IASProductBlock'
+                    )
+                )
+                INSERT INTO subscription_instance_values (subscription_instance_id, resource_type_id, value)
+                SELECT
+                    subscription_instance_ids.subscription_instance_id,
+                    resource_types.resource_type_id,
+                    'None'
+                    -- TODO we need to check this with Simone to see what the default value should be, in case it is empty string, this wont be show up in the GUI
+                FROM resource_types
+                CROSS JOIN subscription_instance_ids
+                WHERE resource_types.resource_type = 'ias_flavor'
+
+            """
+        )
+    )
+
+
+def downgrade() -> None:
+    conn = op.get_bind()
diff --git a/gso/products/product_blocks/gws.py b/gso/products/product_blocks/gws.py
new file mode 100644
index 000000000..fd2a00b6d
--- /dev/null
+++ b/gso/products/product_blocks/gws.py
@@ -0,0 +1,30 @@
+"""Product blocks for GWS products."""
+
+from orchestrator.domain.base import ProductBlockModel
+from orchestrator.types import SubscriptionLifecycle
+
+from gso.products.product_blocks.l3_core_service import (
+    L3CoreServiceBlock,
+    L3CoreServiceBlockInactive,
+    L3CoreServiceBlockProvisioning,
+)
+
+
+class GWSProductBlockInactive(
+    ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="GWSProductBlock"
+):
+    """A GWS product block. See `GWSProductBlock`."""
+
+    l3_core: L3CoreServiceBlockInactive | None = None
+
+
+class GWSProductBlockProvisioning(GWSProductBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
+    """A provisioning GWS product block. See `GWSProductBlock`."""
+
+    l3_core: L3CoreServiceBlockProvisioning
+
+
+class GWSProductBlock(GWSProductBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
+    """An active GWS product block."""
+
+    l3_core: L3CoreServiceBlock
diff --git a/gso/products/product_types/gws.py b/gso/products/product_types/gws.py
new file mode 100644
index 000000000..b79337ee5
--- /dev/null
+++ b/gso/products/product_types/gws.py
@@ -0,0 +1,36 @@
+"""Product type for IAS."""
+
+from orchestrator.domain.base import SubscriptionModel
+from orchestrator.types import SubscriptionLifecycle
+
+from gso.products.product_blocks.gws import GWSProductBlock, GWSProductBlockInactive, GWSProductBlockProvisioning
+
+
+class GWSInactive(SubscriptionModel, is_base=True):
+    """A GWS product that is inactive."""
+
+    gws: GWSProductBlockInactive
+
+
+class GWSProvisioning(GWSInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
+    """An GWS product that is being provisioned."""
+
+    gws: GWSProductBlockProvisioning
+
+
+class GWS(GWSProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
+    """An GWS product that is active."""
+
+    gws: GWSProductBlock
+
+
+class ImportedGWSInactive(SubscriptionModel, is_base=True):
+    """An imported GWS product that is inactive."""
+
+    gws: GWSProductBlockInactive
+
+
+class ImportedGWS(ImportedGWSInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE]):
+    """An imported GWS product that is active."""
+
+    gws: GWSProductBlock
diff --git a/gso/products/product_types/l3_core_service.py b/gso/products/product_types/l3_core_service.py
new file mode 100644
index 000000000..e69de29bb
-- 
GitLab


From 196be8f2e0a9d9d0e5a9824defd0041707937d26 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 27 Mar 2025 14:25:36 +0100
Subject: [PATCH 47/87] fix tests and types

---
 gso/products/product_blocks/gws.py            | 30 -------
 gso/products/product_types/copernicus.py      |  2 +-
 gso/products/product_types/geant_ip.py        |  2 +-
 gso/products/product_types/gws.py             | 36 --------
 gso/products/product_types/l3_core_service.py | 87 +++++++++++++++++++
 gso/products/product_types/lhcone.py          |  2 +-
 gso/workflows/__init__.py                     |  4 +
 .../base_create_imported_l3_core_service.py   | 10 ++-
 .../base_create_l3_core_service.py            |  9 +-
 .../base_migrate_l3_core_service.py           | 23 ++---
 .../base_modify_l3_core_service.py            |  8 +-
 .../base_validate_l3_core_service.py          |  5 +-
 .../l3_core_service/ias/migrate_ias.py        |  4 +-
 .../l3_core_service/lhcone/migrate_lhcone.py  |  4 +-
 gso/workflows/l3_core_service/shared.py       | 13 ++-
 test/fixtures/__init__.py                     |  4 +
 test/fixtures/l3_core_service_fixtures.py     | 42 +++++++--
 .../test_create_imported_l3_core_service.py   |  9 +-
 .../test_validate_l3_core_service.py          | 28 +++---
 .../test_validate_prefix_list.py              | 33 +++----
 20 files changed, 211 insertions(+), 144 deletions(-)
 delete mode 100644 gso/products/product_blocks/gws.py
 delete mode 100644 gso/products/product_types/gws.py

diff --git a/gso/products/product_blocks/gws.py b/gso/products/product_blocks/gws.py
deleted file mode 100644
index fd2a00b6d..000000000
--- a/gso/products/product_blocks/gws.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""Product blocks for GWS products."""
-
-from orchestrator.domain.base import ProductBlockModel
-from orchestrator.types import SubscriptionLifecycle
-
-from gso.products.product_blocks.l3_core_service import (
-    L3CoreServiceBlock,
-    L3CoreServiceBlockInactive,
-    L3CoreServiceBlockProvisioning,
-)
-
-
-class GWSProductBlockInactive(
-    ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="GWSProductBlock"
-):
-    """A GWS product block. See `GWSProductBlock`."""
-
-    l3_core: L3CoreServiceBlockInactive | None = None
-
-
-class GWSProductBlockProvisioning(GWSProductBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
-    """A provisioning GWS product block. See `GWSProductBlock`."""
-
-    l3_core: L3CoreServiceBlockProvisioning
-
-
-class GWSProductBlock(GWSProductBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
-    """An active GWS product block."""
-
-    l3_core: L3CoreServiceBlock
diff --git a/gso/products/product_types/copernicus.py b/gso/products/product_types/copernicus.py
index 348f9baa3..8b6bb242f 100644
--- a/gso/products/product_types/copernicus.py
+++ b/gso/products/product_types/copernicus.py
@@ -31,7 +31,7 @@ class Copernicus(CopernicusProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE
 class ImportedCopernicusInactive(SubscriptionModel, is_base=True):
     """An imported Copernicus product that is inactive."""
 
-    copernicus: CopernicusInactive
+    copernicus: CopernicusBlockInactive
 
 
 class ImportedCopernicus(
diff --git a/gso/products/product_types/geant_ip.py b/gso/products/product_types/geant_ip.py
index 0d7f4858b..a986623e6 100644
--- a/gso/products/product_types/geant_ip.py
+++ b/gso/products/product_types/geant_ip.py
@@ -31,7 +31,7 @@ class GeantIP(GeantIPProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
 class ImportedGeantIPInactive(SubscriptionModel, is_base=True):
     """An imported GeantIP product that is inactive."""
 
-    geant_ip: GeantIPInactive
+    geant_ip: GeantIPBlockInactive
 
 
 class ImportedGeantIP(
diff --git a/gso/products/product_types/gws.py b/gso/products/product_types/gws.py
deleted file mode 100644
index b79337ee5..000000000
--- a/gso/products/product_types/gws.py
+++ /dev/null
@@ -1,36 +0,0 @@
-"""Product type for IAS."""
-
-from orchestrator.domain.base import SubscriptionModel
-from orchestrator.types import SubscriptionLifecycle
-
-from gso.products.product_blocks.gws import GWSProductBlock, GWSProductBlockInactive, GWSProductBlockProvisioning
-
-
-class GWSInactive(SubscriptionModel, is_base=True):
-    """A GWS product that is inactive."""
-
-    gws: GWSProductBlockInactive
-
-
-class GWSProvisioning(GWSInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
-    """An GWS product that is being provisioned."""
-
-    gws: GWSProductBlockProvisioning
-
-
-class GWS(GWSProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
-    """An GWS product that is active."""
-
-    gws: GWSProductBlock
-
-
-class ImportedGWSInactive(SubscriptionModel, is_base=True):
-    """An imported GWS product that is inactive."""
-
-    gws: GWSProductBlockInactive
-
-
-class ImportedGWS(ImportedGWSInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE]):
-    """An imported GWS product that is active."""
-
-    gws: GWSProductBlock
diff --git a/gso/products/product_types/l3_core_service.py b/gso/products/product_types/l3_core_service.py
index e69de29bb..34f9beccd 100644
--- a/gso/products/product_types/l3_core_service.py
+++ b/gso/products/product_types/l3_core_service.py
@@ -0,0 +1,87 @@
+"""L3 Core Service product type."""
+
+from orchestrator.domain import SubscriptionModel
+from orchestrator.types import SubscriptionLifecycle
+from pydantic_forms.types import strEnum
+
+from gso.products.product_blocks.l3_core_service import (
+    L3CoreServiceBlock,
+    L3CoreServiceBlockInactive,
+    L3CoreServiceBlockProvisioning,
+)
+
+
+class L3CoreServiceType(strEnum):
+    """Available types of Layer 3 Core Services.
+
+    The core services offered include GÉANT IP for R&E access, and the Internet Access Service.
+    """
+
+    GEANT_IP = "GÉANT IP"
+    """GÉANT IP."""
+    IMPORTED_GEANT_IP = "IMPORTED GÉANT IP"
+    IAS = "IAS"
+    """Internet Access Serive."""
+    IMPORTED_IAS = "IMPORTED IAS"
+    GWS = "GWS"
+    """GÉANT Web Services."""
+    IMPORTED_GWS = "IMPORTED GWS"
+    LHCONE = "LHCONE"
+    """LHCOne."""
+    IMPORTED_LHCONE = "IMPORTED LHCONE"
+    COPERNICUS = "COPERNICUS"
+    """Copernicus."""
+    IMPORTED_COPERNICUS = "IMPORTED COPERNICUS"
+
+
+L3_CORE_SERVICE_TYPES = [
+    L3CoreServiceType.GEANT_IP,
+    L3CoreServiceType.IAS,
+    L3CoreServiceType.GWS,
+    L3CoreServiceType.LHCONE,
+    L3CoreServiceType.COPERNICUS,
+]
+IMPORTED_L3_CORE_SERVICE_TYPES = [
+    L3CoreServiceType.IMPORTED_GEANT_IP,
+    L3CoreServiceType.IMPORTED_IAS,
+    L3CoreServiceType.IMPORTED_GWS,
+    L3CoreServiceType.IMPORTED_LHCONE,
+    L3CoreServiceType.IMPORTED_COPERNICUS,
+]
+
+
+class L3CoreServiceInactive(SubscriptionModel, is_base=True):
+    """An inactive L3 Core Service subscription."""
+
+    l3_core_service_type: L3CoreServiceType
+    l3_core_service: L3CoreServiceBlockInactive
+
+
+class L3CoreServiceProvisioning(L3CoreServiceInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
+    """A L3 Core Service subscription that's being provisioned."""
+
+    l3_core_service_type: L3CoreServiceType
+    l3_core_service: L3CoreServiceBlockProvisioning
+
+
+class L3CoreService(L3CoreServiceProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
+    """An active L3 Core Service subscription."""
+
+    l3_core_service_type: L3CoreServiceType
+    l3_core_service: L3CoreServiceBlock
+
+
+class ImportedL3CoreServiceInactive(SubscriptionModel, is_base=True):
+    """An imported, inactive L3 Core Service subscription."""
+
+    l3_core_service_type: L3CoreServiceType
+    l3_core_service: L3CoreServiceBlockInactive
+
+
+class ImportedL3CoreService(
+    ImportedL3CoreServiceInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE]
+):
+    """An imported L3 Core Service subscription."""
+
+    l3_core_service_type: L3CoreServiceType
+    l3_core_service: L3CoreServiceBlock
diff --git a/gso/products/product_types/lhcone.py b/gso/products/product_types/lhcone.py
index ab4f8ea8e..84374aaf4 100644
--- a/gso/products/product_types/lhcone.py
+++ b/gso/products/product_types/lhcone.py
@@ -31,7 +31,7 @@ class LHCOne(LHCOneProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
 class ImportedLHCOneInactive(SubscriptionModel, is_base=True):
     """An imported LHCOne product that is inactive."""
 
-    lhcone: LHCOneInactive
+    lhcone: LHCOneBlockInactive
 
 
 class ImportedLHCOne(
diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py
index fb4e97116..9157b9e4a 100644
--- a/gso/workflows/__init__.py
+++ b/gso/workflows/__init__.py
@@ -169,3 +169,7 @@ LazyWorkflowInstance("gso.workflows.vrf.create_vrf", "create_vrf")
 LazyWorkflowInstance("gso.workflows.vrf.modify_vrf_router_list", "modify_vrf_router_list")
 LazyWorkflowInstance("gso.workflows.vrf.redeploy_vrf", "redeploy_vrf")
 LazyWorkflowInstance("gso.workflows.vrf.terminate_vrf", "terminate_vrf")
+
+# Validate workflow for L3 Core Service
+LazyWorkflowInstance("gso.workflows.l3_core_service.validate_prefix_list", "validate_prefix_list")
+LazyWorkflowInstance("gso.workflows.l3_core_service.validate_l3_core_service", "validate_l3_core_service")
diff --git a/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
index 1668c8943..5dba7a46f 100644
--- a/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
@@ -2,6 +2,7 @@
 
 from uuid import uuid4
 
+from orchestrator.domain import SubscriptionModel
 from orchestrator.forms import SubmitFormPage
 from orchestrator.workflow import step
 from pydantic import BaseModel, NonNegativeInt
@@ -15,7 +16,7 @@ from gso.utils.shared_enums import SBPType
 from gso.utils.types.geant_ids import IMPORTED_GS_ID
 from gso.utils.types.ip_address import IPAddress, IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask
 from gso.utils.types.virtual_identifiers import VLAN_ID
-from gso.workflows.l3_core_service.shared import L3ProductNameTypes, l3ImportedInactiveProductTypes
+from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes, L3ProductNameTypes
 
 
 def base_initial_input_form_generator() -> FormGenerator:
@@ -70,7 +71,9 @@ def base_initial_input_form_generator() -> FormGenerator:
 
 
 @step("Initialize subscription")
-def initialize_subscription(subscription: l3ImportedInactiveProductTypes, service_binding_ports: list) -> dict:
+def initialize_subscription(
+    subscription: SubscriptionModel, service_binding_ports: list, service_name: L3CoreServiceNameTypes
+) -> dict:
     """Initialize the subscription with the user input."""
     for service_binding_port in service_binding_ports:
         edge_port_subscription = EdgePort.from_subscription(service_binding_port.pop("edge_port"))
@@ -93,7 +96,8 @@ def initialize_subscription(subscription: l3ImportedInactiveProductTypes, servic
             v6_bfd_settings=BFDSettings.new(subscription_id=uuid4(), **(service_binding_port.pop("v6_bfd_settings"))),
             **service_binding_port,
         )
-        subscription.l3_core_service.ap_list.append(
+        ap_list = getattr(subscription, service_name).ap_list
+        ap_list.append(
             AccessPortInactive.new(
                 subscription_id=uuid4(),
                 ap_type=service_binding_port["ap_type"],
diff --git a/gso/workflows/l3_core_service/base_create_l3_core_service.py b/gso/workflows/l3_core_service/base_create_l3_core_service.py
index 06f9154bb..abf246675 100644
--- a/gso/workflows/l3_core_service/base_create_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_l3_core_service.py
@@ -3,6 +3,7 @@
 from typing import Any
 from uuid import uuid4
 
+from orchestrator.domain import SubscriptionModel
 from orchestrator.forms import FormPage, SubmitFormPage
 from orchestrator.forms.validators import Label
 from orchestrator.workflow import step
@@ -28,7 +29,7 @@ from gso.utils.types.geant_ids import IMPORTED_GS_ID
 from gso.utils.types.ip_address import IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask
 from gso.utils.types.tt_number import TTNumber
 from gso.utils.types.virtual_identifiers import VLAN_ID
-from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes, L3InactiveProductTypes, L3ProductTypes
+from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes, L3InactiveProductTypes
 
 
 def initial_input_form_generator(product_name: str) -> FormGenerator:
@@ -315,7 +316,7 @@ def deploy_bgp_peers_real(
 
 @step("Check BGP peers")
 def check_bgp_peers(subscription: dict[str, Any], edge_port_fqdn_list: list[str]) -> LSOState:
-    """Check correct deployment of BGP peers."""
+    """Check the correct deployment of BGP peers."""
     extra_vars = {"subscription": subscription, "verb": "check", "object": "bgp"}
 
     return {
@@ -326,7 +327,7 @@ def check_bgp_peers(subscription: dict[str, Any], edge_port_fqdn_list: list[str]
 
 
 @step("Update Infoblox")
-def update_dns_records(subscription: L3ProductTypes) -> State:
+def update_dns_records(subscription: SubscriptionModel) -> State:
     """Update DNS records in Infoblox."""
     #  TODO: implement
     return {"subscription": subscription}
@@ -334,7 +335,7 @@ def update_dns_records(subscription: L3ProductTypes) -> State:
 
 @step("Create a new SharePoint checklist item")
 def create_new_sharepoint_checklist(
-    subscription: L3ProductTypes,
+    subscription: SubscriptionModel,
     tt_number: TTNumber,
     process_id: UUIDstr,
     service_name: str,
diff --git a/gso/workflows/l3_core_service/base_migrate_l3_core_service.py b/gso/workflows/l3_core_service/base_migrate_l3_core_service.py
index a1bcedb8f..32a1dd82d 100644
--- a/gso/workflows/l3_core_service/base_migrate_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_migrate_l3_core_service.py
@@ -12,6 +12,7 @@ import json
 from typing import Any
 
 from orchestrator.config.assignee import Assignee
+from orchestrator.domain import SubscriptionModel
 from orchestrator.forms import FormPage, SubmitFormPage
 from orchestrator.utils.errors import ProcessFailureError
 from orchestrator.utils.json import json_dumps
@@ -26,11 +27,11 @@ from gso.services.partners import get_partner_by_id
 from gso.services.subscriptions import get_active_edge_port_subscriptions
 from gso.utils.types.tt_number import TTNumber
 from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, SKIP_MOODI_KEY
-from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes, L3ProductTypes
+from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes
 from gso.workflows.shared import create_summary_form
 
 
-def initial_input_form(subscription: L3ProductTypes, service_name: L3CoreServiceNameTypes) -> FormGenerator:
+def initial_input_form(subscription: SubscriptionModel, service_name: L3CoreServiceNameTypes) -> FormGenerator:
     partner_id = subscription.customer_id
     ap_list = getattr(subscription, service_name).l3_core.ap_list
 
@@ -96,7 +97,7 @@ def initial_input_form(subscription: L3ProductTypes, service_name: L3CoreService
 
 
 @step("Inject Partner Name")
-def inject_partner_name(subscription: L3ProductTypes) -> LSOState:
+def inject_partner_name(subscription: SubscriptionModel) -> LSOState:
     """Resolve and inject partner name into the state."""
     partner_name = get_partner_by_id(subscription.customer_id).name
 
@@ -105,7 +106,7 @@ def inject_partner_name(subscription: L3ProductTypes) -> LSOState:
 
 @step("Show BGP neighbors")
 def show_bgp_neighbors(
-    subscription: L3ProductTypes,
+    subscription: SubscriptionModel,
     process_id: UUIDstr,
     tt_number: TTNumber,
     source_edge_port: EdgePort,
@@ -131,7 +132,7 @@ def show_bgp_neighbors(
 
 @step("[DRY RUN] Deactivate BGP session on the source router")
 def deactivate_bgp_dry(
-    subscription: L3ProductTypes,
+    subscription: SubscriptionModel,
     process_id: UUIDstr,
     tt_number: TTNumber,
     source_access_port_fqdn: str,
@@ -154,7 +155,7 @@ def deactivate_bgp_dry(
 
 @step("[FOR REAL] Deactivate BGP session on the source router")
 def deactivate_bgp_real(
-    subscription: L3ProductTypes,
+    subscription: SubscriptionModel,
     process_id: UUIDstr,
     tt_number: TTNumber,
     source_access_port_fqdn: str,
@@ -197,7 +198,7 @@ def inform_operator_traffic_check() -> FormGenerator:
 
 @step("[DRY RUN] Deactivate SBP config on the source router")
 def deactivate_sbp_dry(
-    subscription: L3ProductTypes,
+    subscription: SubscriptionModel,
     process_id: UUIDstr,
     tt_number: TTNumber,
     source_access_port_fqdn: str,
@@ -219,7 +220,7 @@ def deactivate_sbp_dry(
 
 @step("[FOR REAL] Deactivate SBP config on the source router")
 def deactivate_sbp_real(
-    subscription: L3ProductTypes,
+    subscription: SubscriptionModel,
     process_id: UUIDstr,
     tt_number: TTNumber,
     source_access_port_fqdn: str,
@@ -242,7 +243,7 @@ def deactivate_sbp_real(
 
 @step("Generate updated subscription model")
 def generate_scoped_subscription_model(
-    subscription: L3ProductTypes,
+    subscription: SubscriptionModel,
     source_edge_port: EdgePort,
     destination_edge_port: EdgePort,
     service_name: L3CoreServiceNameTypes,
@@ -369,7 +370,7 @@ def deploy_bgp_session_real(
 
 
 @step("Update Infoblox")
-def update_dns_records(subscription: L3ProductTypes) -> State:
+def update_dns_records(subscription: SubscriptionModel) -> State:
     """Update DNS records in Infoblox."""
     #  TODO: implement
     return {"subscription": subscription}
@@ -377,7 +378,7 @@ def update_dns_records(subscription: L3ProductTypes) -> State:
 
 @step("Update subscription model")
 def update_subscription_model(
-    subscription: L3ProductTypes,
+    subscription: SubscriptionModel,
     destination_edge_port: EdgePort,
     replaced_ap_index: int,
     service_name: L3CoreServiceNameTypes,
diff --git a/gso/workflows/l3_core_service/base_modify_l3_core_service.py b/gso/workflows/l3_core_service/base_modify_l3_core_service.py
index c051f495f..02b763bf8 100644
--- a/gso/workflows/l3_core_service/base_modify_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_modify_l3_core_service.py
@@ -20,7 +20,7 @@ from gso.utils.types.geant_ids import IMPORTED_GS_ID
 from gso.utils.types.ip_address import IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask
 from gso.utils.types.tt_number import TTNumber
 from gso.utils.types.virtual_identifiers import VLAN_ID
-from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes, L3ProductTypes
+from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes
 
 
 class Operation(strEnum):
@@ -369,7 +369,7 @@ def initial_input_form_generator(subscription_id: UUIDstr, service_name: L3CoreS
 
 @step("Instantiate new Service Binding Ports")
 def create_new_sbp(
-    subscription: L3ProductTypes, added_access_port: dict[str, Any], service_name: L3CoreServiceNameTypes
+    subscription: SubscriptionModel, added_access_port: dict[str, Any], service_name: L3CoreServiceNameTypes
 ) -> State:
     """Add new SBP to the L3 Core Service subscription."""
     edge_port = EdgePort.from_subscription(added_access_port.pop("edge_port"))
@@ -410,7 +410,7 @@ def create_new_sbp(
 
 
 @step("Clean up removed Edge Ports")
-def remove_old_sbp(subscription: L3ProductTypes, removed_access_port: UUIDstr, service_name: str) -> State:
+def remove_old_sbp(subscription: SubscriptionModel, removed_access_port: UUIDstr, service_name: str) -> State:
     """Remove old SBP product blocks from the specific L3 core service subscription."""
     service = getattr(subscription, service_name)
     service.l3_core.ap_list.remove(AccessPort.from_db(UUID(removed_access_port)))
@@ -420,7 +420,7 @@ def remove_old_sbp(subscription: L3ProductTypes, removed_access_port: UUIDstr, s
 
 @step("Modify existing Service Binding Ports")
 def modify_existing_sbp(
-    subscription: L3ProductTypes,
+    subscription: SubscriptionModel,
     modified_access_port: UUIDstr,
     modified_sbp: dict[str, Any],
     service_name: L3CoreServiceNameTypes,
diff --git a/gso/workflows/l3_core_service/base_validate_l3_core_service.py b/gso/workflows/l3_core_service/base_validate_l3_core_service.py
index 9c0cef044..47aaef4e2 100644
--- a/gso/workflows/l3_core_service/base_validate_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_validate_l3_core_service.py
@@ -2,6 +2,7 @@
 
 from typing import Any
 
+from orchestrator.domain import SubscriptionModel
 from orchestrator.targets import Target
 from orchestrator.workflow import StepList, begin, done, step, workflow
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
@@ -10,11 +11,11 @@ from pydantic_forms.types import UUIDstr
 
 from gso.services.lso_client import LSOState, anonymous_lso_interaction
 from gso.services.partners import get_partner_by_id
-from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes, L3ProductTypes
+from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes
 
 
 @step("Prepare list of all Access Ports")
-def build_fqdn_list(subscription: L3ProductTypes, service_name: L3CoreServiceNameTypes) -> dict[str, list[str]]:
+def build_fqdn_list(subscription: SubscriptionModel, service_name: L3CoreServiceNameTypes) -> dict[str, list[str]]:
     """Build the list of all FQDNs that are in the list of access ports of a L3 Core Service subscription."""
     ap_list = getattr(subscription, service_name).l3_core.ap_list
     ap_fqdn_list = [ap.sbp.edge_port.node.router_fqdn for ap in ap_list]
diff --git a/gso/workflows/l3_core_service/ias/migrate_ias.py b/gso/workflows/l3_core_service/ias/migrate_ias.py
index 70cae3281..16c16bddc 100644
--- a/gso/workflows/l3_core_service/ias/migrate_ias.py
+++ b/gso/workflows/l3_core_service/ias/migrate_ias.py
@@ -1,8 +1,8 @@
 """A modification workflow that migrates an IAS Service to a new Edge Port.
 
 In one run of a migration workflow, only a single Access Port can be replaced. This is due to the nature of these
-services. When a service is dually homed, for example, only one of the Access Ports should be migrated. The other one will
-remain the way it is.
+services. When a service is dually homed, for example, only one of the Access Ports should be migrated. The other one
+will remain the way it is.
 
 At the start of the workflow, the operator will select one Access Port that is to be migrated, and will then select a
 destination Edge Port that this service should be placed on. All other Access Ports will be left as-is.
diff --git a/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py b/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py
index f5923d7ad..d53d7cdb5 100644
--- a/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py
@@ -1,8 +1,8 @@
 """A modification workflow that migrates an LHCOne Service to a new Edge Port.
 
 In one run of a migration workflow, only a single Access Port can be replaced. This is due to the nature of these
-services. When a service is dually homed, for example, only one of the Access Ports should be migrated. The other one will
-remain the way it is.
+services. When a service is dually homed, for example, only one of the Access Ports should be migrated. The other one
+will remain the way it is.
 
 At the start of the workflow, the operator will select one Access Port that is to be migrated, and will then select a
 destination Edge Port that this service should be placed on. All other Access Ports will be left as-is.
diff --git a/gso/workflows/l3_core_service/shared.py b/gso/workflows/l3_core_service/shared.py
index 700e8b7be..4a1f57772 100644
--- a/gso/workflows/l3_core_service/shared.py
+++ b/gso/workflows/l3_core_service/shared.py
@@ -3,16 +3,13 @@
 from typing import Literal
 
 from gso.products import ProductName
-from gso.products.product_types.copernicus import Copernicus, CopernicusInactive, ImportedCopernicusInactive
-from gso.products.product_types.geant_ip import GeantIP, GeantIPInactive, ImportedGeantIPInactive
-from gso.products.product_types.ias import IAS, IASInactive, ImportedIASInactive
-from gso.products.product_types.lhcone import ImportedLHCOneInactive, LHCOne, LHCOneInactive
+from gso.products.product_types.copernicus import Copernicus, CopernicusInactive
+from gso.products.product_types.geant_ip import GeantIP, GeantIPInactive
+from gso.products.product_types.ias import IAS, IASInactive
+from gso.products.product_types.lhcone import LHCOne, LHCOneInactive
 
-L3ProductTypes = IAS | LHCOne | Copernicus | GeantIP
 L3InactiveProductTypes = IASInactive | LHCOneInactive | CopernicusInactive | GeantIPInactive
-l3ImportedInactiveProductTypes = (
-    ImportedIASInactive | ImportedLHCOneInactive | ImportedCopernicusInactive | ImportedGeantIPInactive
-)
+
 L3ProductNameTypes = Literal[ProductName.IAS, ProductName.LHCONE, ProductName.COPERNICUS, ProductName.GEANT_IP]
 L3_PRODUCT_NAMES = [
     ProductName.GEANT_IP,
diff --git a/test/fixtures/__init__.py b/test/fixtures/__init__.py
index bba69807a..41c65f94b 100644
--- a/test/fixtures/__init__.py
+++ b/test/fixtures/__init__.py
@@ -9,6 +9,8 @@ from test.fixtures.l3_core_service_fixtures import (
     ias_subscription_factory,
     copernicus_subscription_factory,
     service_binding_port_factory,
+    l3_core_service_subscription_factory,
+    save_l3_core_subscription,
 )
 from test.fixtures.lan_switch_interconnect_fixtures import lan_switch_interconnect_subscription_factory
 from test.fixtures.layer_2_circuit_fixtures import layer_2_circuit_subscription_factory
@@ -31,6 +33,8 @@ __all__ = [
     "geant_ip_subscription_factory",
     "ias_subscription_factory",
     "copernicus_subscription_factory",
+    "l3_core_service_subscription_factory",
+    "save_l3_core_subscription",
     "lan_switch_interconnect_subscription_factory",
     "layer_2_circuit_subscription_factory",
     "office_router_subscription_factory",
diff --git a/test/fixtures/l3_core_service_fixtures.py b/test/fixtures/l3_core_service_fixtures.py
index e45ce4cbb..ff9e0745d 100644
--- a/test/fixtures/l3_core_service_fixtures.py
+++ b/test/fixtures/l3_core_service_fixtures.py
@@ -7,7 +7,17 @@ from orchestrator.domain import SubscriptionModel
 from orchestrator.types import SubscriptionLifecycle
 from pydantic import NonNegativeInt
 
-from gso.products import ProductName, ImportedIAS, IAS, GeantIP, ImportedGeantIP
+from gso.products import (
+    ProductName,
+    ImportedIAS,
+    IAS,
+    GeantIP,
+    ImportedGeantIP,
+    Copernicus,
+    ImportedLHCOne,
+    ImportedCopernicus,
+    LHCOne,
+)
 from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPTypes
 from gso.products.product_blocks.ias import IASFlavor
 from gso.products.product_blocks.l3_core_service import AccessPort
@@ -16,7 +26,7 @@ from gso.products.product_types.edge_port import EdgePort
 from gso.services import subscriptions
 from gso.utils.shared_enums import APType, SBPType
 from gso.utils.types.ip_address import IPAddress
-from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes
+from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes, L3ProductNameTypes
 
 
 @pytest.fixture()
@@ -172,7 +182,7 @@ def save_l3_core_subscription(access_port_factory, faker):
         ap_list, description, subscription, start_date, status, service_name: L3CoreServiceNameTypes
     ):
         # Default ap_list creation with primary and backup access ports
-        getattr(subscription, service_name).ap_list = ap_list or [
+        getattr(subscription, service_name).l3_core.ap_list = ap_list or [
             access_port_factory(ap_type=APType.PRIMARY),
             access_port_factory(ap_type=APType.BACKUP),
         ]
@@ -230,8 +240,8 @@ def geant_ip_subscription_factory(faker, partner_factory, access_port_factory, s
 def copernicus_subscription_factory(faker, partner_factory, access_port_factory, save_l3_core_subscription):
     return make_subscription_factory(
         product_name=ProductName.COPERNICUS,
-        imported_class=ImportedGeantIP,
-        native_class=GeantIP,
+        imported_class=ImportedCopernicus,
+        native_class=Copernicus,
         service_name="copernicus",
     )(faker, partner_factory, access_port_factory, save_l3_core_subscription)
 
@@ -239,5 +249,25 @@ def copernicus_subscription_factory(faker, partner_factory, access_port_factory,
 @pytest.fixture()
 def lhcone_subscription_factory(faker, partner_factory, access_port_factory, save_l3_core_subscription):
     return make_subscription_factory(
-        product_name=ProductName.LHCONE, imported_class=ImportedGeantIP, native_class=GeantIP, service_name="lhcone"
+        product_name=ProductName.LHCONE, imported_class=ImportedLHCOne, native_class=LHCOne, service_name="lhcone"
     )(faker, partner_factory, access_port_factory, save_l3_core_subscription)
+
+
+@pytest.fixture()
+def l3_core_service_subscription_factory(
+    ias_subscription_factory,
+    geant_ip_subscription_factory,
+    copernicus_subscription_factory,
+    lhcone_subscription_factory,
+) -> callable:
+    def factory(product_name: ProductName, *args, **kwargs):
+        if product_name == ProductName.IAS:
+            return ias_subscription_factory(*args, **kwargs)
+        if product_name == ProductName.GEANT_IP:
+            return geant_ip_subscription_factory(*args, **kwargs)
+        if product_name == ProductName.COPERNICUS:
+            return copernicus_subscription_factory(*args, **kwargs)
+        if product_name == ProductName.LHCONE:
+            return lhcone_subscription_factory(*args, **kwargs)
+
+    return factory
diff --git a/test/workflows/l3_core_service/test_create_imported_l3_core_service.py b/test/workflows/l3_core_service/test_create_imported_l3_core_service.py
index b44227741..1a96c23d5 100644
--- a/test/workflows/l3_core_service/test_create_imported_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_create_imported_l3_core_service.py
@@ -1,10 +1,10 @@
 import pytest
+from orchestrator.domain import SubscriptionModel
 from orchestrator.types import SubscriptionLifecycle
 
 from gso.products import ProductName
 from gso.products.product_blocks.bgp_session import IPFamily
 from gso.products.product_blocks.ias import IASFlavor
-from gso.products.product_types.l3_core_service import L3_CORE_SERVICE_TYPES, ImportedL3CoreService
 from gso.utils.shared_enums import SBPType
 from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES
 from test.workflows import assert_complete, extract_state, run_workflow
@@ -80,10 +80,11 @@ def test_create_imported_l3_core_service_success(faker, partner_factory, edge_po
         ],
     }
 
-    result, _, _ = run_workflow(
-        f"create_imported_{_L3_CORE_MIGRATION_MAP[product_name]}", [extra_ias_data, creation_form_input_data]
+    input_data = (
+        [extra_ias_data, creation_form_input_data] if product_name == ProductName.IAS else [creation_form_input_data]
     )
+    result, _, _ = run_workflow(f"{_L3_CORE_MIGRATION_MAP[product_name]}", input_data)
     state = extract_state(result)
-    subscription = ImportedL3CoreService.from_subscription(state["subscription_id"])
+    subscription = SubscriptionModel.from_subscription(state["subscription_id"])
     assert_complete(result)
     assert subscription.status == SubscriptionLifecycle.ACTIVE
diff --git a/test/workflows/l3_core_service/test_validate_l3_core_service.py b/test/workflows/l3_core_service/test_validate_l3_core_service.py
index cd6db8369..738932255 100644
--- a/test/workflows/l3_core_service/test_validate_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_validate_l3_core_service.py
@@ -1,29 +1,37 @@
 from unittest.mock import patch
 
 import pytest
+from orchestrator.domain import SubscriptionModel
 
-from gso.products.product_types.l3_core_service import L3_CORE_SERVICE_TYPES, L3CoreService
+from gso.products import ProductName
+from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES
 from test.workflows import assert_complete, assert_lso_success, extract_state, run_workflow
 
 
+_PRODUCT_NAME_VALIDATION_WF_MAP = {
+    ProductName.GEANT_IP: "validate_geant_ip",
+    ProductName.IAS: "validate_ias",
+    ProductName.LHCONE: "validate_lhcone",
+    ProductName.COPERNICUS: "validate_copernicus",
+}
+
+
 @pytest.mark.workflow()
 @patch("gso.services.lso_client._send_request")
-@pytest.mark.parametrize("l3_core_service_type", L3_CORE_SERVICE_TYPES)
-def test_validate_l3_core_service(
-    mock_lso_interaction, l3_core_service_subscription_factory, faker, l3_core_service_type
-):
-    subscription_id = str(
-        l3_core_service_subscription_factory(l3_core_service_type=l3_core_service_type).subscription_id
-    )
+@pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES)
+def test_validate_l3_core_service(mock_lso_interaction, l3_core_service_subscription_factory, faker, product_name):
+    subscription_id = str(l3_core_service_subscription_factory(product_name=product_name).subscription_id)
     initial_l3_core_service_data = [{"subscription_id": subscription_id}]
-    result, process_stat, step_log = run_workflow("validate_l3_core_service", initial_l3_core_service_data)
+    result, process_stat, step_log = run_workflow(
+        _PRODUCT_NAME_VALIDATION_WF_MAP[product_name], initial_l3_core_service_data
+    )
     result, step_log = assert_lso_success(result, process_stat, step_log)
     result, _ = assert_lso_success(result, process_stat, step_log)
     assert_complete(result)
 
     state = extract_state(result)
     subscription_id = state["subscription_id"]
-    subscription = L3CoreService.from_subscription(subscription_id)
+    subscription = SubscriptionModel.from_subscription(subscription_id)
     assert subscription.status == "active"
     assert subscription.insync is True
     assert mock_lso_interaction.call_count == 2
diff --git a/test/workflows/l3_core_service/test_validate_prefix_list.py b/test/workflows/l3_core_service/test_validate_prefix_list.py
index 4ef8872bd..f3c9d4a74 100644
--- a/test/workflows/l3_core_service/test_validate_prefix_list.py
+++ b/test/workflows/l3_core_service/test_validate_prefix_list.py
@@ -1,7 +1,10 @@
 from unittest.mock import patch
 
 import pytest
+from orchestrator.domain import SubscriptionModel
 
+from gso.products import GeantIP, ProductName
+from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES
 from test import USER_CONFIRM_EMPTY_FORM
 from gso.utils.shared_enums import Vendor
 from test.workflows import (
@@ -18,39 +21,31 @@ from test.workflows import (
 
 @pytest.mark.workflow()
 @patch("gso.services.lso_client._send_request")
-@pytest.mark.parametrize("l3_core_service_type", L3_CORE_SERVICE_TYPES)
-def test_validate_prefix_list_success(
-    mock_lso_interaction, l3_core_service_subscription_factory, faker, l3_core_service_type
-):
-    should_run_validation = l3_core_service_type == L3CoreServiceType.GEANT_IP
-    subscription_id = str(
-        l3_core_service_subscription_factory(l3_core_service_type=l3_core_service_type).subscription_id
-    )
+@pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES)
+def test_validate_prefix_list_success(mock_lso_interaction, l3_core_service_subscription_factory, faker, product_name):
+    subscription_id = str(l3_core_service_subscription_factory(product_name=product_name).subscription_id)
     initial_l3_core_service_data = [{"subscription_id": subscription_id}]
     # Run the workflow and extract results
     result, process_stat, step_log = run_workflow("validate_prefix_list", initial_l3_core_service_data)
 
     # If validation should run, assert LSO success
-    if should_run_validation:
-        result, step_log = assert_lso_success(result, process_stat, step_log)
-        assert_complete(result)
+    result, step_log = assert_lso_success(result, process_stat, step_log)
+    assert_complete(result)
     # Extract the state and validate subscription attributes
     state = extract_state(result)
     subscription_id = state["subscription_id"]
-    subscription = L3CoreService.from_subscription(subscription_id)
+    subscription = SubscriptionModel.from_subscription(subscription_id)
     assert subscription.status == "active"
     assert subscription.insync is True
     # Verify the number of LSO interactions
-    assert mock_lso_interaction.call_count == (1 if should_run_validation else 0)
+    assert mock_lso_interaction.call_count == 1
 
 
 @pytest.mark.workflow()
 @patch("gso.services.lso_client._send_request")
-def test_validate_prefix_list_with_diff(mock_lso_interaction, l3_core_service_subscription_factory, faker):
+def test_validate_prefix_list_with_diff(mock_lso_interaction, geant_ip_subscription_factory, faker):
     """Test case where playbook_has_diff qualifies and additional steps are executed."""
-    subscription_id = str(
-        l3_core_service_subscription_factory(l3_core_service_type=L3CoreServiceType.GEANT_IP).subscription_id
-    )
+    subscription_id = str(geant_ip_subscription_factory().subscription_id)
     initial_l3_core_service_data = [{"subscription_id": subscription_id}]
     # Run the workflow and extract results
     result, process_stat, step_log = run_workflow("validate_prefix_list", initial_l3_core_service_data)
@@ -59,7 +54,7 @@ def test_validate_prefix_list_with_diff(mock_lso_interaction, l3_core_service_su
     # Interaction has failed, we will need to redeploy this prefix list
     state = extract_state(result)
     subscription_id = state["subscription_id"]
-    subscription = L3CoreService.from_subscription(subscription_id)
+    subscription = GeantIP.from_subscription(subscription_id)
     assert not subscription.insync
 
     assert_suspended(result)
@@ -71,7 +66,7 @@ def test_validate_prefix_list_with_diff(mock_lso_interaction, l3_core_service_su
     # Extract the state and validate subscription attributes
     state = extract_state(result)
     subscription_id = state["subscription_id"]
-    subscription = L3CoreService.from_subscription(subscription_id)
+    subscription = GeantIP.from_subscription(subscription_id)
     assert subscription.status == "active"
     assert subscription.insync is True
     # Verify the number of LSO interactions
-- 
GitLab


From e25e6a39e49f153016185781b6ac372578bd58ab Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 27 Mar 2025 14:43:08 +0100
Subject: [PATCH 48/87] remove duplicate file from rebase

---
 .../2025-03-18_c9c9fdf624b5_re_model_ias.py   | 198 ------------------
 1 file changed, 198 deletions(-)
 delete mode 100644 gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py

diff --git a/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py b/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py
deleted file mode 100644
index 5ff448768..000000000
--- a/gso/migrations/versions/2025-03-18_c9c9fdf624b5_re_model_ias.py
+++ /dev/null
@@ -1,198 +0,0 @@
-"""Re-model L3CoreServices
-
-    GEANT_IP = "GÉANT IP"
-    IMPORTED_GEANT_IP = "IMPORTED GÉANT IP"
-    GWS = "GWS"
-    IMPORTED_GWS = "IMPORTED GWS"
-    LHCONE = "LHCONE"
-    IMPORTED_LHCONE = "IMPORTED LHCONE"
-    COPERNICUS = "COPERNICUS"
-    IMPORTED_COPERNICUS = "IMPORTED COPERNICUS"
-
-Revision ID: e1afa3790f32
-Revises: b96b0ecf6906
-Create Date: 2025-03-17 10:23:45.917222
-
-"""
-import sqlalchemy as sa
-from alembic import op
-
-# revision identifiers, used by Alembic.
-revision = 'e1afa3790f32'
-down_revision = 'b96b0ecf6906'
-branch_labels = None
-depends_on = None
-
-
-def upgrade() -> None:
-    conn = op.get_bind()
-    conn.execute(
-        sa.text(
-            """
-DELETE FROM fixed_inputs
-WHERE fixed_inputs.product_id IN (
-    SELECT products.product_id FROM products WHERE products.name IN ('IAS', 'Imported IAS')
-)
-  AND fixed_inputs.name = 'l3_core_service_type'
-            """
-        )
-    )
-    conn.execute(
-        sa.text(
-            """
-INSERT INTO product_blocks (name, description, tag, status)
-VALUES ('IASProductBlock', 'An Internet Access Service for general internet access', 'IAS', 'active')
-RETURNING product_blocks.product_block_id
-            """
-        )
-    )
-    conn.execute(
-        sa.text(
-            """
-INSERT INTO resource_types (resource_type, description)
-VALUES ('ias_flavor', 'The flavor of the IAS service')
-RETURNING resource_types.resource_type_id
-            """
-        )
-    )
-    conn.execute(
-        sa.text(
-            """ 
-    INSERT INTO product_block_relations
-VALUES (
-    (SELECT product_blocks.product_block_id FROM product_blocks WHERE name = 'IASProductBlock'),
-    (SELECT product_blocks.product_block_id FROM product_blocks WHERE name = 'L3CoreServiceBlock'),
-    NULL,
-    NULL
-);
-            """
-        )
-    )
-
-    conn.execute(
-        sa.text(
-            """
-UPDATE product_product_blocks
-SET product_block_id = (
-    SELECT product_block_id FROM product_blocks WHERE name = 'IASProductBlock'
-)
-WHERE product_block_id = (
-    SELECT product_block_id FROM product_blocks WHERE name = 'L3CoreServiceBlock'
-)
-  AND product_id IN (
-    SELECT product_id FROM products WHERE name IN ('IAS', 'Imported IAS')
-);
-            """
-        )
-    )
-
-    conn.execute(
-        sa.text(
-            """
-UPDATE products
-SET product_type = 'IAS'
-WHERE product_type = 'L3CoreService'
-  AND name = 'IAS';
-            """
-        )
-    )
-
-    conn.execute(
-        sa.text(
-            """
-UPDATE products
-SET product_type = 'ImportedIAS'
-WHERE product_type = 'ImportedL3CoreService'
-  AND name = 'Imported IAS';
-            """
-        )
-    )
-
-    conn.execute(
-        sa.text(
-            """
--- Step 1: Insert new subscription_instances for 'IASProductBlock' and return their IDs
-WITH inserted AS (
-    INSERT INTO subscription_instances (subscription_id, product_block_id)
-    SELECT
-        s.subscription_id,
-        (
-            SELECT product_block_id
-            FROM product_blocks
-            WHERE name = 'IASProductBlock'
-        ) AS product_block_id
-    FROM subscriptions s
-    WHERE s.product_id = (
-        SELECT product_id
-        FROM products
-        WHERE products.name = 'IAS'
-    )
-    RETURNING subscription_instance_id, subscription_id
-)
-
--- Step 2: Link newly inserted "IASProductBlock" instances to the existing "L3CoreServiceBlock" instance
-INSERT INTO subscription_instance_relations (
-    in_use_by_id,
-    depends_on_id,
-    order_id,
-    domain_model_attr
-)
-SELECT
-    i.subscription_instance_id AS in_use_by_id,
-    e.subscription_instance_id AS depends_on_id,
-    0 AS order_id,
-    'l3_core' AS domain_model_attr
-FROM inserted i
-JOIN subscription_instances e
-  ON e.subscription_id = i.subscription_id
-  AND e.product_block_id = (
-      SELECT product_block_id
-      FROM product_blocks
-      WHERE name = 'L3CoreServiceBlock'
-  );
-            """
-        )
-    )
-    
-    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 ('IASProductBlock')),
-    (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ias_flavor'))
-)
-            """
-        )
-    )
-
-    conn.execute(
-        sa.text(
-            """
-
-                WITH subscription_instance_ids AS (
-                    SELECT subscription_instances.subscription_instance_id
-                    FROM subscription_instances
-                    WHERE subscription_instances.product_block_id IN (
-                        SELECT product_blocks.product_block_id
-                        FROM product_blocks
-                        WHERE product_blocks.name = 'IASProductBlock'
-                    )
-                )
-                INSERT INTO subscription_instance_values (subscription_instance_id, resource_type_id, value)
-                SELECT
-                    subscription_instance_ids.subscription_instance_id,
-                    resource_types.resource_type_id,
-                    'None'
-                    -- TODO we need to check this with Simone to see what the default value should be, in case it is empty string, this wont be show up in the GUI
-                FROM resource_types
-                CROSS JOIN subscription_instance_ids
-                WHERE resource_types.resource_type = 'ias_flavor'
-
-            """
-        )
-    )
-
-
-def downgrade() -> None:
-    conn = op.get_bind()
-- 
GitLab


From 9855560d017465fc27955a1331f1c420dab97a4f Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 27 Mar 2025 16:10:20 +0100
Subject: [PATCH 49/87] pass test_create_imported_l3_core_service.py and
 test_create_l3_core_service.py

---
 .../base_create_imported_l3_core_service.py   | 21 +++++----
 .../base_create_l3_core_service.py            | 14 +++---
 .../ias/create_imported_ias.py                |  2 +-
 .../lhcone/create_imported_lhcone.py          |  4 +-
 .../test_create_imported_l3_core_service.py   |  6 +--
 .../test_create_l3_core_service.py            | 45 +++++++++++++------
 6 files changed, 58 insertions(+), 34 deletions(-)

diff --git a/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
index 5dba7a46f..a689bc912 100644
--- a/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
@@ -9,7 +9,7 @@ from pydantic import BaseModel, NonNegativeInt
 from pydantic_forms.types import FormGenerator, UUIDstr
 
 from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPTypes
-from gso.products.product_blocks.l3_core_service import AccessPortInactive
+from gso.products.product_blocks.l3_core_service import AccessPortInactive, L3CoreServiceBlockInactive
 from gso.products.product_blocks.service_binding_port import BFDSettings, ServiceBindingPortInactive
 from gso.products.product_types.edge_port import EdgePort
 from gso.utils.shared_enums import SBPType
@@ -96,14 +96,17 @@ def initialize_subscription(
             v6_bfd_settings=BFDSettings.new(subscription_id=uuid4(), **(service_binding_port.pop("v6_bfd_settings"))),
             **service_binding_port,
         )
-        ap_list = getattr(subscription, service_name).ap_list
-        ap_list.append(
-            AccessPortInactive.new(
-                subscription_id=uuid4(),
-                ap_type=service_binding_port["ap_type"],
-                sbp=service_binding_port_subscription,
-                custom_service_name=service_binding_port.get("custom_service_name"),
-            )
+        service = getattr(subscription, service_name)
+        service.l3_core = L3CoreServiceBlockInactive.new(
+            subscription_id=uuid4(),
+            ap_list=[
+                AccessPortInactive.new(
+                    subscription_id=uuid4(),
+                    ap_type=service_binding_port["ap_type"],
+                    sbp=service_binding_port_subscription,
+                    custom_service_name=service_binding_port.get("custom_service_name"),
+                )
+            ],
         )
 
     return {"subscription": subscription}
diff --git a/gso/workflows/l3_core_service/base_create_l3_core_service.py b/gso/workflows/l3_core_service/base_create_l3_core_service.py
index abf246675..34e2d78ee 100644
--- a/gso/workflows/l3_core_service/base_create_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_l3_core_service.py
@@ -199,12 +199,14 @@ def initialize_service_binding(
     service = getattr(subscription, service_name)
     service.l3_core = L3CoreServiceBlockInactive.new(
         subscription_id=uuid4(),
-        ap_list = [AccessPortInactive.new(
-            subscription_id=uuid4(),
-            ap_type=edge_port["ap_type"],
-            sbp=service_binding_port,
-            custom_service_name=edge_port.get("custom_service_name"),
-        )]
+        ap_list=[
+            AccessPortInactive.new(
+                subscription_id=uuid4(),
+                ap_type=edge_port["ap_type"],
+                sbp=service_binding_port,
+                custom_service_name=edge_port.get("custom_service_name"),
+            )
+        ],
     )
     edge_port_fqdn_list.append(edge_port_subscription.edge_port.node.router_fqdn)
 
diff --git a/gso/workflows/l3_core_service/ias/create_imported_ias.py b/gso/workflows/l3_core_service/ias/create_imported_ias.py
index 463371076..805ab14e6 100644
--- a/gso/workflows/l3_core_service/ias/create_imported_ias.py
+++ b/gso/workflows/l3_core_service/ias/create_imported_ias.py
@@ -36,7 +36,7 @@ def initial_input_form_generator() -> FormGenerator:
 def create_subscription(partner: str) -> dict:
     """Create a new subscription object in the database."""
     partner_id = get_partner_by_name(partner).partner_id
-    product_id = get_product_id_by_name(ProductName.IMPORTED_GEANT_IP)
+    product_id = get_product_id_by_name(ProductName.IMPORTED_IAS)
     subscription = ImportedIASInactive.from_product_id(product_id, partner_id)
     return {"subscription": subscription, "subscription_id": subscription.subscription_id, "service_name": "ias"}
 
diff --git a/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py b/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py
index 0e53a2250..a13f8f3d0 100644
--- a/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py
@@ -20,7 +20,7 @@ from gso.workflows.l3_core_service.base_create_imported_l3_core_service import (
 def create_subscription(partner: str) -> dict:
     """Create a new subscription object in the database."""
     partner_id = get_partner_by_name(partner).partner_id
-    product_id = get_product_id_by_name(ProductName.IMPORTED_GEANT_IP)
+    product_id = get_product_id_by_name(ProductName.IMPORTED_LHCONE)
     subscription = ImportedLHCOneInactive.from_product_id(product_id, partner_id)
     return {"subscription": subscription, "subscription_id": subscription.subscription_id, "service_name": "lhcone"}
 
@@ -30,7 +30,7 @@ def create_subscription(partner: str) -> dict:
     initial_input_form=base_initial_input_form_generator,
     target=Target.CREATE,
 )
-def create_lhcone() -> StepList:
+def create_imported_lhcone() -> StepList:
     """Import a LHCOne without provisioning it."""
     return (
         begin
diff --git a/test/workflows/l3_core_service/test_create_imported_l3_core_service.py b/test/workflows/l3_core_service/test_create_imported_l3_core_service.py
index 1a96c23d5..d16740ae2 100644
--- a/test/workflows/l3_core_service/test_create_imported_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_create_imported_l3_core_service.py
@@ -9,7 +9,7 @@ from gso.utils.shared_enums import SBPType
 from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES
 from test.workflows import assert_complete, extract_state, run_workflow
 
-_L3_CORE_MIGRATION_MAP = {
+_L3_CORE_MIGRATION_WF_MAP = {
     ProductName.COPERNICUS: "create_imported_copernicus",
     ProductName.GEANT_IP: "create_imported_geant_ip",
     ProductName.IAS: "create_imported_ias",
@@ -81,9 +81,9 @@ def test_create_imported_l3_core_service_success(faker, partner_factory, edge_po
     }
 
     input_data = (
-        [extra_ias_data, creation_form_input_data] if product_name == ProductName.IAS else [creation_form_input_data]
+        [creation_form_input_data, extra_ias_data] if product_name == ProductName.IAS else [creation_form_input_data]
     )
-    result, _, _ = run_workflow(f"{_L3_CORE_MIGRATION_MAP[product_name]}", input_data)
+    result, _, _ = run_workflow(f"{_L3_CORE_MIGRATION_WF_MAP[product_name]}", input_data)
     state = extract_state(result)
     subscription = SubscriptionModel.from_subscription(state["subscription_id"])
     assert_complete(result)
diff --git a/test/workflows/l3_core_service/test_create_l3_core_service.py b/test/workflows/l3_core_service/test_create_l3_core_service.py
index 2a5b3af07..084710225 100644
--- a/test/workflows/l3_core_service/test_create_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_create_l3_core_service.py
@@ -1,10 +1,11 @@
 from unittest.mock import patch
 
 import pytest
+from orchestrator.domain import SubscriptionModel
 from orchestrator.types import SubscriptionLifecycle
 
 from gso.products import ProductName
-from gso.products.product_types.l3_core_service import L3CoreService
+from gso.products.product_blocks.ias import IASFlavor
 from gso.services.subscriptions import get_product_id_by_name
 from gso.utils.shared_enums import APType
 from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES
@@ -19,6 +20,20 @@ from test.workflows import (
     run_workflow,
 )
 
+_L3_CORE_CREATION_WF_MAP = {
+    ProductName.COPERNICUS: "create_copernicus",
+    ProductName.GEANT_IP: "create_geant_ip",
+    ProductName.IAS: "create_ias",
+    ProductName.LHCONE: "create_lhcone",
+}
+
+_L3_ATTRIBUTES_MAP = {
+    ProductName.COPERNICUS: "copernicus",
+    ProductName.GEANT_IP: "geant_ip",
+    ProductName.IAS: "ias",
+    ProductName.LHCONE: "lhcone",
+}
+
 
 @pytest.fixture()
 def base_bgp_peer_input(faker):
@@ -36,21 +51,21 @@ def base_bgp_peer_input(faker):
     return _base_bgp_peer_input
 
 
-@pytest.mark.parametrize("l3_product_name", L3_PRODUCT_NAMES)
+@pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES)
 @pytest.mark.workflow()
 @patch("gso.services.lso_client._send_request")
-@patch("gso.workflows.l3_core_service.create_l3_core_service.SharePointClient")
+@patch("gso.workflows.l3_core_service.base_create_l3_core_service.SharePointClient")
 def test_create_l3_core_service_success(
     mock_sharepoint_client,
     mock_lso_client,
-    l3_product_name,
+    product_name,
     faker,
     partner_factory,
     edge_port_subscription_factory,
     base_bgp_peer_input,
 ):
     partner = partner_factory()
-    product_id = get_product_id_by_name(l3_product_name)
+    product_id = get_product_id_by_name(product_name)
     edge_port_a = str(edge_port_subscription_factory(partner=partner).subscription_id)
     mock_sharepoint_client.return_value = MockedSharePointClient
 
@@ -84,9 +99,15 @@ def test_create_l3_core_service_success(
             "v6_bgp_peer": base_bgp_peer_input() | {"add_v6_multicast": faker.boolean(), "peer_address": faker.ipv6()},
         },
     ]
+    extra_ias_data = {
+        "ias_flavor": IASFlavor.IASGWS,
+    }
+    if product_name == ProductName.IAS:
+        form_input_data.append(extra_ias_data)
+
     lso_interaction_count = 7
 
-    result, process_stat, step_log = run_workflow("create_l3_core_service", form_input_data)
+    result, process_stat, step_log = run_workflow(_L3_CORE_CREATION_WF_MAP[product_name], form_input_data)
 
     for _ in range(lso_interaction_count):
         result, step_log = assert_lso_interaction_success(result, process_stat, step_log)
@@ -97,13 +118,11 @@ def test_create_l3_core_service_success(
 
     assert_complete(result)
     state = extract_state(result)
-    subscription = L3CoreService.from_subscription(state["subscription_id"])
+    subscription = SubscriptionModel.from_subscription(state["subscription_id"])
     assert mock_lso_client.call_count == lso_interaction_count + 1
     assert subscription.status == SubscriptionLifecycle.ACTIVE
-    assert len(subscription.l3_core_service.ap_list) == 1
-    assert (
-        str(subscription.l3_core_service.ap_list[0].sbp.edge_port.owner_subscription_id)
-        == form_input_data[2]["edge_port"]["edge_port"]
-    )
-    assert subscription.l3_core_service.ap_list[0].sbp.gs_id == "GS-12345"
+    l3_core = getattr(subscription, _L3_ATTRIBUTES_MAP[product_name]).l3_core
+    assert len(l3_core.ap_list) == 1
+    assert str(l3_core.ap_list[0].sbp.edge_port.owner_subscription_id) == form_input_data[2]["edge_port"]["edge_port"]
+    assert l3_core.ap_list[0].sbp.gs_id == "GS-12345"
     assert mock_sharepoint_client.call_count == 1
-- 
GitLab


From 2da0608cee6910a853f8fedf67fa0a596440a0d0 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 27 Mar 2025 18:25:49 +0100
Subject: [PATCH 50/87] pass test_import_l3_core_service_success tests

---
 .../2025-03-25_e1afa3790f32_re_model_ias.py   | 23 +++++++-------
 test/fixtures/l3_core_service_fixtures.py     | 22 ++++++++++----
 .../test_create_l3_core_service.py            |  7 +++--
 .../test_import_l3_core_service.py            | 30 ++++++++++++-------
 4 files changed, 51 insertions(+), 31 deletions(-)

diff --git a/gso/migrations/versions/2025-03-25_e1afa3790f32_re_model_ias.py b/gso/migrations/versions/2025-03-25_e1afa3790f32_re_model_ias.py
index 1cec2203d..b25ef7920 100644
--- a/gso/migrations/versions/2025-03-25_e1afa3790f32_re_model_ias.py
+++ b/gso/migrations/versions/2025-03-25_e1afa3790f32_re_model_ias.py
@@ -103,7 +103,7 @@ def upgrade() -> None:
                         '{l3["imported_product"]["name"]}'
                     )
                 )
-                  AND fixed_inputs.name = 'l3_core_service_type'
+                  AND fixed_inputs.name = 'l3_core_service_type';
                 """
             )
         )
@@ -118,7 +118,7 @@ def upgrade() -> None:
                     '{l3["product_block"]["tag"]}',
                     'active'
                 )
-                RETURNING product_blocks.product_block_id
+                RETURNING product_blocks.product_block_id;
                 """
             )
         )
@@ -129,7 +129,7 @@ def upgrade() -> None:
                     """
                     INSERT INTO resource_types (resource_type, description)
                     VALUES ('ias_flavor', 'The flavor of the IAS service')
-                    RETURNING resource_types.resource_type_id
+                    RETURNING resource_types.resource_type_id;
                     """
                 )
             )
@@ -266,7 +266,7 @@ def upgrade() -> None:
                             FROM resource_types
                             WHERE resource_types.resource_type IN ('ias_flavor')
                         )
-                    )
+                    );
                     """
                 )
             )
@@ -294,7 +294,7 @@ def upgrade() -> None:
                         'IASPS Opt-OUT'
                     FROM resource_types
                     CROSS JOIN subscription_instance_ids
-                    WHERE resource_types.resource_type = 'ias_flavor'
+                    WHERE resource_types.resource_type = 'ias_flavor';
                     """
                 )
             )
@@ -315,7 +315,7 @@ def upgrade() -> None:
                         WHERE products.name IN ('Imported GWS', 'GWS')
                     )
                 )
-            )
+            );
             """
         )
     )
@@ -332,7 +332,7 @@ def upgrade() -> None:
                     FROM products
                     WHERE products.name IN ('Imported GWS', 'GWS')
                 )
-            )
+            );
             """
         )
     )
@@ -349,7 +349,7 @@ def upgrade() -> None:
                     FROM products
                     WHERE products.name IN ('Imported GWS', 'GWS')
                 )
-            )
+            );
             """
         )
     )
@@ -362,7 +362,7 @@ def upgrade() -> None:
                 SELECT products.product_id
                 FROM products
                 WHERE products.name IN ('Imported GWS', 'GWS')
-            )
+            );
             """
         )
     )
@@ -371,7 +371,7 @@ def upgrade() -> None:
         sa.text(
             """
             DELETE FROM products
-            WHERE products.name IN ('Imported GWS', 'GWS')
+            WHERE products.name IN ('Imported GWS', 'GWS');
             """
         )
     )
@@ -379,5 +379,4 @@ def upgrade() -> None:
 
 def downgrade() -> None:
     """Perform the downgrade (no actions defined)."""
-    conn = op.get_bind()
-    # No downgrade logic provided.
+    pass
diff --git a/test/fixtures/l3_core_service_fixtures.py b/test/fixtures/l3_core_service_fixtures.py
index ff9e0745d..874cea0a9 100644
--- a/test/fixtures/l3_core_service_fixtures.py
+++ b/test/fixtures/l3_core_service_fixtures.py
@@ -23,10 +23,17 @@ from gso.products.product_blocks.ias import IASFlavor
 from gso.products.product_blocks.l3_core_service import AccessPort
 from gso.products.product_blocks.service_binding_port import BFDSettings, ServiceBindingPort
 from gso.products.product_types.edge_port import EdgePort
-from gso.services import subscriptions
+from gso.services.subscriptions import get_product_id_by_name
 from gso.utils.shared_enums import APType, SBPType
 from gso.utils.types.ip_address import IPAddress
-from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes, L3ProductNameTypes
+from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes
+
+PRODUCT_IMPORTED_MAP = {
+    ProductName.IAS: ProductName.IMPORTED_IAS,
+    ProductName.GEANT_IP: ProductName.IMPORTED_GEANT_IP,
+    ProductName.COPERNICUS: ProductName.IMPORTED_COPERNICUS,
+    ProductName.LHCONE: ProductName.IMPORTED_LHCONE,
+}
 
 
 @pytest.fixture()
@@ -148,7 +155,10 @@ def access_port_factory(faker, service_binding_port_factory):
 
 
 def make_subscription_factory(
-    product_name: ProductName, imported_class, native_class, service_name: L3CoreServiceNameTypes
+    product_name: ProductName,
+    imported_class,
+    native_class,
+    service_name: L3CoreServiceNameTypes,
 ):
     def factory(faker, partner_factory, access_port_factory, save_l3_core_subscription):
         def create_subscription(
@@ -160,7 +170,7 @@ def make_subscription_factory(
             is_imported: bool | None = False,
         ) -> SubscriptionModel:
             partner = partner or partner_factory()
-            product_id = subscriptions.get_product_id_by_name(product_name)
+            product_id = get_product_id_by_name(PRODUCT_IMPORTED_MAP[product_name] if is_imported else product_name)
             subscription_class = imported_class if is_imported else native_class
 
             subscription = subscription_class.from_product_id(
@@ -216,10 +226,10 @@ def ias_subscription_factory(faker, partner_factory, access_port_factory, save_l
         partner = partner or partner_factory()
 
         if is_imported:
-            product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_IAS)
+            product_id = get_product_id_by_name(ProductName.IMPORTED_IAS)
             subscription = ImportedIAS.from_product_id(product_id, customer_id=partner["partner_id"], insync=True)
         else:
-            product_id = subscriptions.get_product_id_by_name(ProductName.IAS)
+            product_id = get_product_id_by_name(ProductName.IAS)
             subscription = IAS.from_product_id(product_id, customer_id=partner["partner_id"], insync=True)
 
         subscription.ias.ias_flavor = ias_flavor
diff --git a/test/workflows/l3_core_service/test_create_l3_core_service.py b/test/workflows/l3_core_service/test_create_l3_core_service.py
index 084710225..d74249d8f 100644
--- a/test/workflows/l3_core_service/test_create_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_create_l3_core_service.py
@@ -99,10 +99,11 @@ def test_create_l3_core_service_success(
             "v6_bgp_peer": base_bgp_peer_input() | {"add_v6_multicast": faker.boolean(), "peer_address": faker.ipv6()},
         },
     ]
-    extra_ias_data = {
-        "ias_flavor": IASFlavor.IASGWS,
-    }
+
     if product_name == ProductName.IAS:
+        extra_ias_data = {
+            "ias_flavor": IASFlavor.IASGWS,
+        }
         form_input_data.append(extra_ias_data)
 
     lso_interaction_count = 7
diff --git a/test/workflows/l3_core_service/test_import_l3_core_service.py b/test/workflows/l3_core_service/test_import_l3_core_service.py
index c04515cf8..8a1af3007 100644
--- a/test/workflows/l3_core_service/test_import_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_import_l3_core_service.py
@@ -1,21 +1,31 @@
 import pytest
+from orchestrator.domain import SubscriptionModel
 from orchestrator.types import SubscriptionLifecycle
 
-from gso.products.product_types.l3_core_service import IMPORTED_L3_CORE_SERVICE_TYPES, L3CoreService, L3CoreServiceType
+from gso.products import ProductName
+from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES
+from test.fixtures.l3_core_service_fixtures import PRODUCT_IMPORTED_MAP
 from test.workflows import assert_complete, run_workflow
 
+_L3_CORE_IMPORT_WF_MAP = {
+    ProductName.COPERNICUS: "import_copernicus",
+    ProductName.GEANT_IP: "import_geant_ip",
+    ProductName.IAS: "import_ias",
+    ProductName.LHCONE: "import_lhcone",
+}
 
-@pytest.mark.parametrize("l3_core_service_type", IMPORTED_L3_CORE_SERVICE_TYPES)
+
+@pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES)
 @pytest.mark.workflow()
-def test_import_l3_core_service_success(l3_core_service_subscription_factory, l3_core_service_type):
-    imported_l3_core_service = str(
-        l3_core_service_subscription_factory(l3_core_service_type=l3_core_service_type).subscription_id
-    )
-    result, _, _ = run_workflow("import_l3_core_service", [{"subscription_id": imported_l3_core_service}])
-    subscription = L3CoreService.from_subscription(imported_l3_core_service)
+def test_import_l3_core_service_success(l3_core_service_subscription_factory, product_name):
+    imported_subscription = l3_core_service_subscription_factory(product_name=product_name, is_imported=True)
+    assert imported_subscription.product.name == PRODUCT_IMPORTED_MAP[product_name]
+    imported_l3_core_service = str(imported_subscription.subscription_id)
 
+    result, _, _ = run_workflow(_L3_CORE_IMPORT_WF_MAP[product_name], [{"subscription_id": imported_l3_core_service}])
     assert_complete(result)
-    #  Remove the "IMPORTED_" prefix with ``[9:]``
-    assert subscription.l3_core_service_type == L3CoreServiceType(l3_core_service_type.value[9:])
+
+    subscription = SubscriptionModel.from_subscription(imported_l3_core_service)
+    assert subscription.product.name == product_name
     assert subscription.status == SubscriptionLifecycle.ACTIVE
     assert subscription.insync is True
-- 
GitLab


From 4ee1e506d39b7cdb579d6337afd63b0548b6bb9e Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 27 Mar 2025 20:07:36 +0100
Subject: [PATCH 51/87] pass test_import_l3_core_service_success

---
 .../base_create_imported_l3_core_service.py   |   4 +-
 test/fixtures/__init__.py                     |   4 +
 test/fixtures/l3_core_service_fixtures.py     | 145 +++++++++++-------
 3 files changed, 94 insertions(+), 59 deletions(-)

diff --git a/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
index a689bc912..2878ad3c7 100644
--- a/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
@@ -88,7 +88,7 @@ def initialize_subscription(
             )
             for session in bgp_peers
         ]
-        service_binding_port_subscription = ServiceBindingPortInactive.new(
+        service_binding_port_block = ServiceBindingPortInactive.new(
             subscription_id=uuid4(),
             edge_port=edge_port_subscription.edge_port,
             bgp_session_list=sbp_bgp_session_list,
@@ -103,7 +103,7 @@ def initialize_subscription(
                 AccessPortInactive.new(
                     subscription_id=uuid4(),
                     ap_type=service_binding_port["ap_type"],
-                    sbp=service_binding_port_subscription,
+                    sbp=service_binding_port_block,
                     custom_service_name=service_binding_port.get("custom_service_name"),
                 )
             ],
diff --git a/test/fixtures/__init__.py b/test/fixtures/__init__.py
index 41c65f94b..0bf505717 100644
--- a/test/fixtures/__init__.py
+++ b/test/fixtures/__init__.py
@@ -11,6 +11,8 @@ from test.fixtures.l3_core_service_fixtures import (
     service_binding_port_factory,
     l3_core_service_subscription_factory,
     save_l3_core_subscription,
+    l3_core_block_factory,
+    make_subscription_factory,
 )
 from test.fixtures.lan_switch_interconnect_fixtures import lan_switch_interconnect_subscription_factory
 from test.fixtures.layer_2_circuit_fixtures import layer_2_circuit_subscription_factory
@@ -35,6 +37,8 @@ __all__ = [
     "copernicus_subscription_factory",
     "l3_core_service_subscription_factory",
     "save_l3_core_subscription",
+    "l3_core_block_factory",
+    "make_subscription_factory",
     "lan_switch_interconnect_subscription_factory",
     "layer_2_circuit_subscription_factory",
     "office_router_subscription_factory",
diff --git a/test/fixtures/l3_core_service_fixtures.py b/test/fixtures/l3_core_service_fixtures.py
index 874cea0a9..fe4e75aa9 100644
--- a/test/fixtures/l3_core_service_fixtures.py
+++ b/test/fixtures/l3_core_service_fixtures.py
@@ -9,20 +9,16 @@ from pydantic import NonNegativeInt
 
 from gso.products import (
     ProductName,
-    ImportedIAS,
-    IAS,
-    GeantIP,
-    ImportedGeantIP,
-    Copernicus,
-    ImportedLHCOne,
-    ImportedCopernicus,
-    LHCOne,
 )
 from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPTypes
 from gso.products.product_blocks.ias import IASFlavor
-from gso.products.product_blocks.l3_core_service import AccessPort
+from gso.products.product_blocks.l3_core_service import AccessPort, L3CoreServiceBlockInactive
 from gso.products.product_blocks.service_binding_port import BFDSettings, ServiceBindingPort
+from gso.products.product_types.copernicus import ImportedCopernicusInactive, CopernicusInactive
 from gso.products.product_types.edge_port import EdgePort
+from gso.products.product_types.geant_ip import ImportedGeantIPInactive, GeantIPInactive
+from gso.products.product_types.ias import ImportedIASInactive, IASInactive
+from gso.products.product_types.lhcone import ImportedLHCOneInactive, LHCOneInactive
 from gso.services.subscriptions import get_product_id_by_name
 from gso.utils.shared_enums import APType, SBPType
 from gso.utils.types.ip_address import IPAddress
@@ -137,6 +133,23 @@ def service_binding_port_factory(
     return create_service_binding_port
 
 
+@pytest.fixture()
+def l3_core_block_factory(access_port_factory):
+    def factory(ap_list: list[AccessPort] | None = None):
+        # Default ap_list creation with primary and backup access ports
+        ap_list = ap_list or [
+            access_port_factory(ap_type=APType.PRIMARY),
+            access_port_factory(ap_type=APType.BACKUP),
+        ]
+
+        return L3CoreServiceBlockInactive.new(
+            subscription_id=uuid4(),
+            ap_list=ap_list,
+        )
+
+    return factory
+
+
 @pytest.fixture()
 def access_port_factory(faker, service_binding_port_factory):
     def create_access_port(
@@ -154,48 +167,43 @@ def access_port_factory(faker, service_binding_port_factory):
     return create_access_port
 
 
+@pytest.fixture()
 def make_subscription_factory(
-    product_name: ProductName,
-    imported_class,
-    native_class,
-    service_name: L3CoreServiceNameTypes,
+    partner_factory,
+    save_l3_core_subscription,
 ):
-    def factory(faker, partner_factory, access_port_factory, save_l3_core_subscription):
-        def create_subscription(
-            description=None,
-            partner: dict | None = None,
-            ap_list: list[AccessPort] | None = None,
-            start_date="2023-05-24T00:00:00+00:00",
-            status: SubscriptionLifecycle | None = None,
-            is_imported: bool | None = False,
-        ) -> SubscriptionModel:
-            partner = partner or partner_factory()
-            product_id = get_product_id_by_name(PRODUCT_IMPORTED_MAP[product_name] if is_imported else product_name)
-            subscription_class = imported_class if is_imported else native_class
-
-            subscription = subscription_class.from_product_id(
-                product_id, customer_id=partner["partner_id"], insync=True
-            )
+    def create_subscription(
+        product_name: ProductName,
+        imported_class,
+        native_class,
+        service_name: L3CoreServiceNameTypes,
+        description=None,
+        partner: dict | None = None,
+        ap_list: list[AccessPort] | None = None,
+        start_date="2023-05-24T00:00:00+00:00",
+        status: SubscriptionLifecycle | None = None,
+        is_imported: bool | None = False,
+    ) -> SubscriptionModel:
+        partner = partner or partner_factory()
+        product_id = get_product_id_by_name(PRODUCT_IMPORTED_MAP[product_name] if is_imported else product_name)
+        subscription_class = imported_class if is_imported else native_class
 
-            return save_l3_core_subscription(
-                ap_list, description, subscription, start_date, status, service_name=service_name
-            )
+        subscription = subscription_class.from_product_id(product_id, customer_id=partner["partner_id"], insync=True)
 
-        return create_subscription
+        return save_l3_core_subscription(
+            ap_list, description, subscription, start_date, status, service_name=service_name
+        )
 
-    return factory
+    return create_subscription
 
 
 @pytest.fixture()
-def save_l3_core_subscription(access_port_factory, faker):
+def save_l3_core_subscription(access_port_factory, faker, l3_core_block_factory):
     def _save_subscription(
         ap_list, description, subscription, start_date, status, service_name: L3CoreServiceNameTypes
     ):
-        # Default ap_list creation with primary and backup access ports
-        getattr(subscription, service_name).l3_core.ap_list = ap_list or [
-            access_port_factory(ap_type=APType.PRIMARY),
-            access_port_factory(ap_type=APType.BACKUP),
-        ]
+        getattr(subscription, service_name).l3_core = l3_core_block_factory(ap_list)
+
         # Update subscription with description, start date, and status
         subscription = SubscriptionModel.from_other_lifecycle(
             subscription,
@@ -227,10 +235,12 @@ def ias_subscription_factory(faker, partner_factory, access_port_factory, save_l
 
         if is_imported:
             product_id = get_product_id_by_name(ProductName.IMPORTED_IAS)
-            subscription = ImportedIAS.from_product_id(product_id, customer_id=partner["partner_id"], insync=True)
+            subscription = ImportedIASInactive.from_product_id(
+                product_id, customer_id=partner["partner_id"], insync=True
+            )
         else:
             product_id = get_product_id_by_name(ProductName.IAS)
-            subscription = IAS.from_product_id(product_id, customer_id=partner["partner_id"], insync=True)
+            subscription = IASInactive.from_product_id(product_id, customer_id=partner["partner_id"], insync=True)
 
         subscription.ias.ias_flavor = ias_flavor
 
@@ -240,27 +250,48 @@ def ias_subscription_factory(faker, partner_factory, access_port_factory, save_l
 
 
 @pytest.fixture()
-def geant_ip_subscription_factory(faker, partner_factory, access_port_factory, save_l3_core_subscription):
-    return make_subscription_factory(
-        product_name=ProductName.GEANT_IP, imported_class=ImportedGeantIP, native_class=GeantIP, service_name="geant_ip"
-    )(faker, partner_factory, access_port_factory, save_l3_core_subscription)
+def geant_ip_subscription_factory(make_subscription_factory):
+    def factory(*args, **kwargs):
+        return make_subscription_factory(
+            product_name=ProductName.GEANT_IP,
+            imported_class=ImportedGeantIPInactive,
+            native_class=GeantIPInactive,
+            service_name="geant_ip",
+            *args,
+            **kwargs,
+        )
+
+    return factory
 
 
 @pytest.fixture()
-def copernicus_subscription_factory(faker, partner_factory, access_port_factory, save_l3_core_subscription):
-    return make_subscription_factory(
-        product_name=ProductName.COPERNICUS,
-        imported_class=ImportedCopernicus,
-        native_class=Copernicus,
-        service_name="copernicus",
-    )(faker, partner_factory, access_port_factory, save_l3_core_subscription)
+def copernicus_subscription_factory(make_subscription_factory):
+    def factory(*args, **kwargs):
+        return make_subscription_factory(
+            product_name=ProductName.COPERNICUS,
+            imported_class=ImportedCopernicusInactive,
+            native_class=CopernicusInactive,
+            service_name="copernicus",
+            *args,
+            **kwargs,
+        )
+
+    return factory
 
 
 @pytest.fixture()
-def lhcone_subscription_factory(faker, partner_factory, access_port_factory, save_l3_core_subscription):
-    return make_subscription_factory(
-        product_name=ProductName.LHCONE, imported_class=ImportedLHCOne, native_class=LHCOne, service_name="lhcone"
-    )(faker, partner_factory, access_port_factory, save_l3_core_subscription)
+def lhcone_subscription_factory(make_subscription_factory):
+    def factory(*args, **kwargs):
+        return make_subscription_factory(
+            product_name=ProductName.LHCONE,
+            imported_class=ImportedLHCOneInactive,
+            native_class=LHCOneInactive,
+            service_name="lhcone",
+            *args,
+            **kwargs,
+        )
+
+    return factory
 
 
 @pytest.fixture()
-- 
GitLab


From 7cc3782457a4a5afe76605b86e0b60601ba968a8 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 27 Mar 2025 21:07:17 +0100
Subject: [PATCH 52/87] pass test_migrate_l3_core_service_success

---
 .../base_migrate_l3_core_service.py           |  2 +
 .../copernicus/migrate_copernicus.py          |  5 +-
 .../geant_ip/migrate_geant_ip.py              |  9 ++-
 .../l3_core_service/ias/migrate_ias.py        |  5 +-
 .../l3_core_service/lhcone/migrate_lhcone.py  |  5 +-
 .../test_create_imported_l3_core_service.py   |  4 +-
 .../test_migrate_l3_core_service.py           | 66 ++++++++++++-------
 7 files changed, 63 insertions(+), 33 deletions(-)

diff --git a/gso/workflows/l3_core_service/base_migrate_l3_core_service.py b/gso/workflows/l3_core_service/base_migrate_l3_core_service.py
index 32a1dd82d..346b24a0d 100644
--- a/gso/workflows/l3_core_service/base_migrate_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_migrate_l3_core_service.py
@@ -32,6 +32,7 @@ from gso.workflows.shared import create_summary_form
 
 
 def initial_input_form(subscription: SubscriptionModel, service_name: L3CoreServiceNameTypes) -> FormGenerator:
+    """Gather input from the operator on what destination Edge Ports this L3 Core Service should be migrated to."""
     partner_id = subscription.customer_id
     ap_list = getattr(subscription, service_name).l3_core.ap_list
 
@@ -84,6 +85,7 @@ def initial_input_form(subscription: SubscriptionModel, service_name: L3CoreServ
             ).description,
         }
         yield from create_summary_form(summary_input, subscription.product.name, list(summary_input.keys()))
+
     return (
         {"subscription_id": subscription.subscription_id, "subscription": subscription}
         | source_ep_user_input.model_dump()
diff --git a/gso/workflows/l3_core_service/copernicus/migrate_copernicus.py b/gso/workflows/l3_core_service/copernicus/migrate_copernicus.py
index b4e9d9346..41f55b6b9 100644
--- a/gso/workflows/l3_core_service/copernicus/migrate_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/migrate_copernicus.py
@@ -41,7 +41,10 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     """Gather input from the operator on what destination Edge Ports this Copernicus Service should be migrated to."""
     subscription = Copernicus.from_subscription(subscription_id)
 
-    return initial_input_form(subscription, service_name="copernicus")
+    initial_generator = initial_input_form(subscription, service_name="copernicus")
+    initial_user_input = yield from initial_generator
+
+    return initial_user_input
 
 
 @workflow(
diff --git a/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py
index 170bc37a8..ca690d60d 100644
--- a/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py
@@ -1,8 +1,8 @@
 """A modification workflow that migrates a GÉANT IP Service to a new Edge Port.
 
 In one run of a migration workflow, only a single Access Port can be replaced. This is due to the nature of these
-services. When a service is dually homed, for example, only one of the Access Ports should be migrated. The other one will
-remain the way it is.
+services. When a service is dually homed, for example, only one of the Access Ports should be migrated. The other one
+will remain the way it is.
 
 At the start of the workflow, the operator will select one Access Port that is to be migrated, and will then select a
 destination Edge Port that this service should be placed on. All other Access Ports will be left as-is.
@@ -41,7 +41,10 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     """Gather input from the operator on what destination Edge Ports this GÉANT IP Service should be migrated to."""
     subscription = GeantIP.from_subscription(subscription_id)
 
-    return initial_input_form(subscription, service_name="geant_ip")
+    initial_generator = initial_input_form(subscription, service_name="geant_ip")
+    initial_user_input = yield from initial_generator
+
+    return initial_user_input
 
 
 @workflow(
diff --git a/gso/workflows/l3_core_service/ias/migrate_ias.py b/gso/workflows/l3_core_service/ias/migrate_ias.py
index 16c16bddc..0fb01bbfb 100644
--- a/gso/workflows/l3_core_service/ias/migrate_ias.py
+++ b/gso/workflows/l3_core_service/ias/migrate_ias.py
@@ -41,7 +41,10 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     """Gather input from the operator on what destination Edge Ports this IAS Service should be migrated to."""
     subscription = IAS.from_subscription(subscription_id)
 
-    return initial_input_form(subscription, service_name="ias")
+    initial_generator = initial_input_form(subscription, service_name="ias")
+    initial_user_input = yield from initial_generator
+
+    return initial_user_input
 
 
 @workflow(
diff --git a/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py b/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py
index d53d7cdb5..67b9a5782 100644
--- a/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py
@@ -41,7 +41,10 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     """Gather input from the operator on what destination Edge Ports this LHCOne Service should be migrated to."""
     subscription = LHCOne.from_subscription(subscription_id)
 
-    return initial_input_form(subscription, service_name="lhcone")
+    initial_generator = initial_input_form(subscription, service_name="lhcone")
+    initial_user_input = yield from initial_generator
+
+    return initial_user_input
 
 
 @workflow(
diff --git a/test/workflows/l3_core_service/test_create_imported_l3_core_service.py b/test/workflows/l3_core_service/test_create_imported_l3_core_service.py
index d16740ae2..bc1f3633f 100644
--- a/test/workflows/l3_core_service/test_create_imported_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_create_imported_l3_core_service.py
@@ -9,7 +9,7 @@ from gso.utils.shared_enums import SBPType
 from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES
 from test.workflows import assert_complete, extract_state, run_workflow
 
-_L3_CORE_MIGRATION_WF_MAP = {
+_L3_CORE_CREATE_IMPORTED_WF_MAP = {
     ProductName.COPERNICUS: "create_imported_copernicus",
     ProductName.GEANT_IP: "create_imported_geant_ip",
     ProductName.IAS: "create_imported_ias",
@@ -83,7 +83,7 @@ def test_create_imported_l3_core_service_success(faker, partner_factory, edge_po
     input_data = (
         [creation_form_input_data, extra_ias_data] if product_name == ProductName.IAS else [creation_form_input_data]
     )
-    result, _, _ = run_workflow(f"{_L3_CORE_MIGRATION_WF_MAP[product_name]}", input_data)
+    result, _, _ = run_workflow(f"{_L3_CORE_CREATE_IMPORTED_WF_MAP[product_name]}", input_data)
     state = extract_state(result)
     subscription = SubscriptionModel.from_subscription(state["subscription_id"])
     assert_complete(result)
diff --git a/test/workflows/l3_core_service/test_migrate_l3_core_service.py b/test/workflows/l3_core_service/test_migrate_l3_core_service.py
index 04a69df97..8680a7af9 100644
--- a/test/workflows/l3_core_service/test_migrate_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_migrate_l3_core_service.py
@@ -1,10 +1,12 @@
 from unittest.mock import patch
 
 import pytest
+from orchestrator.domain import SubscriptionModel
 
+from gso.products import ProductName
 from gso.products.product_types.edge_port import EdgePort
-from gso.products.product_types.l3_core_service import L3_CORE_SERVICE_TYPES, L3CoreService
 from gso.utils.shared_enums import APType
+from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES
 from test import USER_CONFIRM_EMPTY_FORM
 from test.workflows import (
     assert_complete,
@@ -15,9 +17,23 @@ from test.workflows import (
     run_workflow,
 )
 
+_L3_CORE_MIGRATE_WF_MAP = {
+    ProductName.COPERNICUS: "migrate_copernicus",
+    ProductName.GEANT_IP: "migrate_geant_ip",
+    ProductName.IAS: "migrate_ias",
+    ProductName.LHCONE: "migrate_lhcone",
+}
+
+_L3_ATTRIBUTES_MAP = {
+    ProductName.COPERNICUS: "copernicus",
+    ProductName.GEANT_IP: "geant_ip",
+    ProductName.IAS: "ias",
+    ProductName.LHCONE: "lhcone",
+}
+
 
 @pytest.mark.workflow()
-@pytest.mark.parametrize("l3_core_service_type", L3_CORE_SERVICE_TYPES)
+@pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES)
 @patch("gso.services.lso_client._send_request")
 def test_migrate_l3_core_service_success(
     mock_execute_playbook,
@@ -25,29 +41,29 @@ def test_migrate_l3_core_service_success(
     edge_port_subscription_factory,
     partner_factory,
     l3_core_service_subscription_factory,
-    l3_core_service_type,
+    product_name,
     access_port_factory,
 ):
     partner = partner_factory()
     subscription_id = str(
         l3_core_service_subscription_factory(
-            partner=partner, l3_core_service_type=l3_core_service_type, ap_list=[access_port_factory()]
+            partner=partner, product_name=product_name, ap_list=[access_port_factory()]
         ).subscription_id
     )
     destination_edge_port = str(edge_port_subscription_factory(partner=partner).subscription_id)
-    subscription = L3CoreService.from_subscription(subscription_id)
-
+    subscription = SubscriptionModel.from_subscription(subscription_id)
+    l3_core = getattr(subscription, _L3_ATTRIBUTES_MAP[product_name]).l3_core
     form_input_data = [
         {"subscription_id": subscription_id},
         {
             "tt_number": faker.tt_number(),
-            "source_edge_port": subscription.l3_core_service.ap_list[0].sbp.edge_port.owner_subscription_id,
+            "source_edge_port": l3_core.ap_list[0].sbp.edge_port.owner_subscription_id,
         },
         {"destination_edge_port": destination_edge_port},
         {},
     ]
 
-    result, process_stat, step_log = run_workflow("migrate_l3_core_service", form_input_data)
+    result, process_stat, step_log = run_workflow(_L3_CORE_MIGRATE_WF_MAP[product_name], form_input_data)
 
     for _ in range(5):
         result, step_log = assert_lso_interaction_success(result, process_stat, step_log)
@@ -61,15 +77,16 @@ def test_migrate_l3_core_service_success(
 
     assert_complete(result)
     state = extract_state(result)
-    subscription = L3CoreService.from_subscription(state["subscription_id"])
+    subscription = SubscriptionModel.from_subscription(state["subscription_id"])
     assert mock_execute_playbook.call_count == 11
     assert subscription.insync
-    assert len(subscription.l3_core_service.ap_list) == 1
-    assert str(subscription.l3_core_service.ap_list[0].sbp.edge_port.owner_subscription_id) == destination_edge_port
+    l3_core = getattr(subscription, _L3_ATTRIBUTES_MAP[product_name]).l3_core
+    assert len(l3_core.ap_list) == 1
+    assert str(l3_core.ap_list[0].sbp.edge_port.owner_subscription_id) == destination_edge_port
 
 
 @pytest.mark.workflow()
-@pytest.mark.parametrize("l3_core_service_type", L3_CORE_SERVICE_TYPES)
+@pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES)
 @patch("gso.services.lso_client._send_request")
 def test_migrate_l3_core_service_scoped_emission(
     mock_execute_playbook,
@@ -78,26 +95,23 @@ def test_migrate_l3_core_service_scoped_emission(
     access_port_factory,
     partner_factory,
     l3_core_service_subscription_factory,
-    l3_core_service_type,
+    product_name,
 ):
     partner = partner_factory()
     custom_ap_list = [access_port_factory(ap_type=APType.LOAD_BALANCED) for _ in range(5)]
-    subscription_id = str(
-        l3_core_service_subscription_factory(
-            partner=partner, l3_core_service_type=l3_core_service_type, ap_list=custom_ap_list
-        ).subscription_id
+    subscription = l3_core_service_subscription_factory(
+        partner=partner, product_name=product_name, ap_list=custom_ap_list
     )
-    destination_edge_port = str(edge_port_subscription_factory(partner=partner).subscription_id)
-    subscription = L3CoreService.from_subscription(subscription_id)
+    destination_edge_port_id = str(edge_port_subscription_factory(partner=partner).subscription_id)
     source_edge_port = subscription.l3_core_service.ap_list[3].sbp.edge_port.owner_subscription_id
 
     form_input_data = [
-        {"subscription_id": subscription_id},
+        {"subscription_id": str(subscription.subscription_id)},
         {
             "tt_number": faker.tt_number(),
             "source_edge_port": source_edge_port,
         },
-        {"destination_edge_port": destination_edge_port},
+        {"destination_edge_port": destination_edge_port_id},
         {},
     ]
 
@@ -124,10 +138,12 @@ def test_migrate_l3_core_service_scoped_emission(
     assert len(state["inventory"]["all"]["hosts"].keys()) == 1
     transmitted_destination_ep_fqdn = next(iter(state["inventory"]["all"]["hosts"].keys()))
     assert (
-        EdgePort.from_subscription(destination_edge_port).edge_port.node.router_fqdn == transmitted_destination_ep_fqdn
+        EdgePort.from_subscription(destination_edge_port_id).edge_port.node.router_fqdn
+        == transmitted_destination_ep_fqdn
     )
     assert (
-        state["subscription"]["l3_core_service"] == state["__old_subscriptions__"][subscription_id]["l3_core_service"]
+        state["subscription"]["l3_core_service"]
+        == state["__old_subscriptions__"][str(subscription.subscription_id)]["l3_core_service"]
     )  # Subscription is unchanged for now
 
     for _ in range(4):
@@ -137,8 +153,8 @@ def test_migrate_l3_core_service_scoped_emission(
 
     assert_complete(result)
     state = extract_state(result)
-    subscription = L3CoreService.from_subscription(state["subscription_id"])
+    subscription = SubscriptionModel.from_subscription(state["subscription_id"])
     assert mock_execute_playbook.call_count == 11
     assert subscription.insync
     assert len(subscription.l3_core_service.ap_list) == 5
-    assert str(subscription.l3_core_service.ap_list[3].sbp.edge_port.owner_subscription_id) == destination_edge_port
+    assert str(subscription.l3_core_service.ap_list[3].sbp.edge_port.owner_subscription_id) == destination_edge_port_id
-- 
GitLab


From 7b30ecae6d3e4f8474b4f198720445f237f2816d Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Fri, 28 Mar 2025 09:25:56 +0100
Subject: [PATCH 53/87] pass test_modify_l3_core_service.py

---
 .../base_modify_l3_core_service.py            |   2 +-
 .../copernicus/modify_copernicus.py           |  14 +-
 .../geant_ip/modify_geant_ip.py               |  16 +-
 .../l3_core_service/ias/modify_ias.py         |  14 +-
 .../l3_core_service/lhcone/modify_lhcone.py   |  14 +-
 .../test_modify_l3_core_service.py            | 195 +++++++-----------
 6 files changed, 114 insertions(+), 141 deletions(-)

diff --git a/gso/workflows/l3_core_service/base_modify_l3_core_service.py b/gso/workflows/l3_core_service/base_modify_l3_core_service.py
index 02b763bf8..e38b0061d 100644
--- a/gso/workflows/l3_core_service/base_modify_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_modify_l3_core_service.py
@@ -31,7 +31,7 @@ class Operation(strEnum):
     EDIT = "Edit an existing Access Port"
 
 
-def initial_input_form_generator(subscription_id: UUIDstr, service_name: L3CoreServiceNameTypes) -> FormGenerator:
+def base_initial_input_form_generator(subscription_id: UUIDstr, service_name: L3CoreServiceNameTypes) -> FormGenerator:
     """Get input about added, removed, and modified Access Ports."""
     subscription = SubscriptionModel.from_subscription(subscription_id)
     product_name = subscription.product.name
diff --git a/gso/workflows/l3_core_service/copernicus/modify_copernicus.py b/gso/workflows/l3_core_service/copernicus/modify_copernicus.py
index 8f7cbb23b..fbd2b6d66 100644
--- a/gso/workflows/l3_core_service/copernicus/modify_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/modify_copernicus.py
@@ -5,21 +5,27 @@ from orchestrator.targets import Target
 from orchestrator.workflow import StepList
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from pydantic_forms.types import FormGenerator, UUIDstr
 
 from gso.workflows.l3_core_service.base_modify_l3_core_service import (
     Operation,
+    base_initial_input_form_generator,
     create_new_sbp,
     modify_existing_sbp,
     remove_old_sbp,
 )
-from gso.workflows.l3_core_service.base_modify_l3_core_service import (
-    initial_input_form_generator as base_initial_input_form_generator,
-)
+
+
+def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+    initial_generator = base_initial_input_form_generator(subscription_id, service_name="copernicus")
+    initial_user_input = yield from initial_generator
+
+    return initial_user_input
 
 
 @workflow(
     "Modify Copernicus ",
-    initial_input_form=wrap_modify_initial_input_form(base_initial_input_form_generator),
+    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
     target=Target.MODIFY,
 )
 def modify_copernicus() -> StepList:
diff --git a/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py
index 0fe8b0b89..45ef17dce 100644
--- a/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py
@@ -5,27 +5,27 @@ from orchestrator.targets import Target
 from orchestrator.workflow import StepList
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
-from pydantic_forms.types import FormGenerator
+from pydantic_forms.types import FormGenerator, UUIDstr
 
 from gso.workflows.l3_core_service.base_modify_l3_core_service import (
     Operation,
+    base_initial_input_form_generator,
     create_new_sbp,
     modify_existing_sbp,
     remove_old_sbp,
 )
-from gso.workflows.l3_core_service.base_modify_l3_core_service import (
-    initial_input_form_generator as base_initial_input_form_generator,
-)
 
 
-def initial_input_form_generator(subscription_id: str) -> FormGenerator:
-    """Gather input from the operator on what operation to perform on the GÉANT IP subscription."""
-    return base_initial_input_form_generator(subscription_id, service_name="geant_ip")
+def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+    initial_generator = base_initial_input_form_generator(subscription_id, service_name="geant_ip")
+    initial_user_input = yield from initial_generator
+
+    return initial_user_input
 
 
 @workflow(
     "Modify GÉANT IP ",
-    initial_input_form=wrap_modify_initial_input_form(base_initial_input_form_generator),
+    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
     target=Target.MODIFY,
 )
 def modify_geant_ip() -> StepList:
diff --git a/gso/workflows/l3_core_service/ias/modify_ias.py b/gso/workflows/l3_core_service/ias/modify_ias.py
index d4f062290..4c7212028 100644
--- a/gso/workflows/l3_core_service/ias/modify_ias.py
+++ b/gso/workflows/l3_core_service/ias/modify_ias.py
@@ -5,22 +5,22 @@ from orchestrator.targets import Target
 from orchestrator.workflow import StepList
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
-from pydantic_forms.types import FormGenerator
+from pydantic_forms.types import FormGenerator, UUIDstr
 
 from gso.workflows.l3_core_service.base_modify_l3_core_service import (
     Operation,
+    base_initial_input_form_generator,
     create_new_sbp,
     modify_existing_sbp,
     remove_old_sbp,
 )
-from gso.workflows.l3_core_service.base_modify_l3_core_service import (
-    initial_input_form_generator as base_initial_input_form_generator,
-)
 
 
-def initial_input_form_generator(subscription_id: str) -> FormGenerator:
-    """Gather input from the operator on what operation to perform on the IAS subscription."""
-    return base_initial_input_form_generator(subscription_id, service_name="ias")
+def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+    initial_generator = base_initial_input_form_generator(subscription_id, service_name="ias")
+    initial_user_input = yield from initial_generator
+
+    return initial_user_input
 
 
 @workflow(
diff --git a/gso/workflows/l3_core_service/lhcone/modify_lhcone.py b/gso/workflows/l3_core_service/lhcone/modify_lhcone.py
index 14d9e9a7b..5aac61f88 100644
--- a/gso/workflows/l3_core_service/lhcone/modify_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/modify_lhcone.py
@@ -5,21 +5,27 @@ from orchestrator.targets import Target
 from orchestrator.workflow import StepList
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from pydantic_forms.types import FormGenerator, UUIDstr
 
 from gso.workflows.l3_core_service.base_modify_l3_core_service import (
     Operation,
+    base_initial_input_form_generator,
     create_new_sbp,
     modify_existing_sbp,
     remove_old_sbp,
 )
-from gso.workflows.l3_core_service.base_modify_l3_core_service import (
-    initial_input_form_generator as base_initial_input_form_generator,
-)
+
+
+def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+    initial_generator = base_initial_input_form_generator(subscription_id, service_name="lhcone")
+    initial_user_input = yield from initial_generator
+
+    return initial_user_input
 
 
 @workflow(
     "Modify LHCOne ",
-    initial_input_form=wrap_modify_initial_input_form(base_initial_input_form_generator),
+    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
     target=Target.MODIFY,
 )
 def modify_lhcone() -> StepList:
diff --git a/test/workflows/l3_core_service/test_modify_l3_core_service.py b/test/workflows/l3_core_service/test_modify_l3_core_service.py
index 51d9b8aef..fc9999415 100644
--- a/test/workflows/l3_core_service/test_modify_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_modify_l3_core_service.py
@@ -1,44 +1,60 @@
 import pytest
+from orchestrator.domain import SubscriptionModel
 
+from gso.products import ProductName
 from gso.products.product_blocks.bgp_session import IPFamily
 from gso.products.product_types.l3_core_service import L3_CORE_SERVICE_TYPES, L3CoreService
 from gso.utils.shared_enums import APType
 from gso.workflows.l3_core_service.modify_l3_core_service import Operation
+from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES
 from test.workflows import extract_state, run_workflow
 
+_L3_CORE_MODIFY_WF_MAP = {
+    ProductName.COPERNICUS: "modify_copernicus",
+    ProductName.GEANT_IP: "modify_geant_ip",
+    ProductName.IAS: "modify_ias",
+    ProductName.LHCONE: "modify_lhcone",
+}
 
-@pytest.mark.parametrize("l3_core_service_type", L3_CORE_SERVICE_TYPES)
+_L3_ATTRIBUTES_MAP = {
+    ProductName.COPERNICUS: "copernicus",
+    ProductName.GEANT_IP: "geant_ip",
+    ProductName.IAS: "ias",
+    ProductName.LHCONE: "lhcone",
+}
+
+
+@pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES)
 @pytest.mark.workflow()
-def test_modify_l3_core_service_remove_edge_port_success(
-    faker, l3_core_service_subscription_factory, l3_core_service_type
-):
-    subscription = l3_core_service_subscription_factory(l3_core_service_type=l3_core_service_type)
-    access_port = subscription.l3_core_service.ap_list[0]
+def test_modify_l3_core_service_remove_edge_port_success(faker, l3_core_service_subscription_factory, product_name):
+    subscription = l3_core_service_subscription_factory(product_name=product_name)
+    access_port = getattr(subscription, _L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list[0]
     input_form_data = [
         {"subscription_id": str(subscription.subscription_id)},
         {"tt_number": faker.tt_number(), "operation": Operation.REMOVE},
         {"access_port": str(access_port.subscription_instance_id)},
     ]
 
-    result, _, _ = run_workflow("modify_l3_core_service", input_form_data)
+    result, _, _ = run_workflow(_L3_CORE_MODIFY_WF_MAP[product_name], input_form_data)
 
     state = extract_state(result)
-    subscription = L3CoreService.from_subscription(state["subscription_id"])
-    assert len(subscription.l3_core_service.ap_list) == 1
-    assert subscription.l3_core_service.ap_list[0].ap_type == APType.BACKUP
+    subscription = SubscriptionModel.from_subscription(state["subscription_id"])
+    ap_list = getattr(subscription, _L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
+    assert len(ap_list) == 1
+    assert ap_list[0].ap_type == APType.BACKUP
 
 
-@pytest.mark.parametrize("l3_core_service_type", L3_CORE_SERVICE_TYPES)
+@pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES)
 @pytest.mark.workflow()
 def test_modify_l3_core_service_add_new_edge_port_success(
     l3_core_service_subscription_factory,
     edge_port_subscription_factory,
     partner_factory,
     faker,
-    l3_core_service_type,
+    product_name,
 ):
     partner = partner_factory()
-    subscription = l3_core_service_subscription_factory(partner=partner, l3_core_service_type=l3_core_service_type)
+    subscription = l3_core_service_subscription_factory(partner=partner, product_name=product_name)
     new_edge_port = edge_port_subscription_factory(partner=partner).subscription_id
     input_form_data = [
         {"subscription_id": str(subscription.subscription_id)},
@@ -70,11 +86,12 @@ def test_modify_l3_core_service_add_new_edge_port_success(
         },
     ]
 
-    result, _, _ = run_workflow("modify_l3_core_service", input_form_data)
+    result, _, _ = run_workflow(_L3_CORE_MODIFY_WF_MAP[product_name], input_form_data)
 
     state = extract_state(result)
-    subscription = L3CoreService.from_subscription(state["subscription_id"])
-    new_ap = subscription.l3_core_service.ap_list[-1]
+    subscription = SubscriptionModel.from_subscription(state["subscription_id"])
+    ap_list = getattr(subscription, _L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
+    new_ap = ap_list[-1]
     assert new_ap.ap_type == APType.BACKUP
     assert new_ap.sbp.gs_id == input_form_data[2]["gs_id"]
     assert new_ap.sbp.vlan_id == input_form_data[2]["vlan_id"]
@@ -82,7 +99,7 @@ def test_modify_l3_core_service_add_new_edge_port_success(
     assert new_ap.sbp.ipv4_mask == input_form_data[2]["ipv4_mask"]
     assert str(new_ap.sbp.ipv6_address) == input_form_data[2]["ipv6_address"]
     assert new_ap.sbp.ipv6_mask == input_form_data[2]["ipv6_mask"]
-    assert len(subscription.l3_core_service.ap_list) == 3
+    assert len(ap_list) == 3
 
 
 @pytest.fixture()
@@ -129,121 +146,65 @@ def sbp_input_form_data(faker):
     return _generate_form_data
 
 
-@pytest.mark.parametrize("l3_core_service_type", L3_CORE_SERVICE_TYPES)
+@pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES)
 @pytest.mark.workflow()
 def test_modify_l3_core_service_modify_edge_port_success(
-    faker, l3_core_service_subscription_factory, l3_core_service_type, sbp_input_form_data
+    faker, l3_core_service_subscription_factory, product_name, sbp_input_form_data
 ):
-    subscription = l3_core_service_subscription_factory(l3_core_service_type=l3_core_service_type)
+    subscription = l3_core_service_subscription_factory(product_name=product_name)
     new_sbp_data = sbp_input_form_data()
+    ap_list = getattr(subscription, _L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
     input_form_data = [
         {"subscription_id": str(subscription.subscription_id)},
         {"tt_number": faker.tt_number(), "operation": Operation.EDIT},
-        {"access_port": subscription.l3_core_service.ap_list[0].subscription_instance_id},
+        {"access_port": ap_list[0].subscription_instance_id},
         {**new_sbp_data},
     ]
 
-    result, _, _ = run_workflow("modify_l3_core_service", input_form_data)
+    result, _, _ = run_workflow(_L3_CORE_MODIFY_WF_MAP[product_name], input_form_data)
 
     state = extract_state(result)
-    subscription = L3CoreService.from_subscription(state["subscription_id"])
-    assert len(subscription.l3_core_service.ap_list) == 2
-
-    assert subscription.l3_core_service.ap_list[0].sbp.gs_id == new_sbp_data["gs_id"]
-    assert subscription.l3_core_service.ap_list[0].sbp.is_tagged == new_sbp_data["is_tagged"]
-    assert subscription.l3_core_service.ap_list[0].sbp.vlan_id == new_sbp_data["vlan_id"]
-    assert str(subscription.l3_core_service.ap_list[0].sbp.ipv4_address) == new_sbp_data["ipv4_address"]
-    assert subscription.l3_core_service.ap_list[0].sbp.ipv4_mask == new_sbp_data["ipv4_mask"]
-    assert str(subscription.l3_core_service.ap_list[0].sbp.ipv6_address) == new_sbp_data["ipv6_address"]
-    assert subscription.l3_core_service.ap_list[0].sbp.ipv6_mask == new_sbp_data["ipv6_mask"]
-    assert (
-        subscription.l3_core_service.ap_list[0].sbp.custom_firewall_filters == new_sbp_data["custom_firewall_filters"]
-    )
-
-    assert (
-        subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[0].bfd_enabled
-        == new_sbp_data["v4_bgp_bfd_enabled"]
-    )
-    assert (
-        subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[0].has_custom_policies
-        == new_sbp_data["v4_bgp_has_custom_policies"]
-    )
-    assert (
-        subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[0].authentication_key
-        == new_sbp_data["v4_bgp_authentication_key"]
-    )
-    assert (
-        subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[0].multipath_enabled
-        == new_sbp_data["v4_bgp_multipath_enabled"]
-    )
-    assert (
-        subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[0].send_default_route
-        == new_sbp_data["v4_bgp_send_default_route"]
-    )
-    assert (
-        subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[0].is_passive == new_sbp_data["v4_bgp_is_passive"]
-    )
-    assert (
-        str(subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[0].peer_address)
-        == new_sbp_data["v4_bgp_peer_address"]
-    )
-    assert (
-        bool(IPFamily.V4MULTICAST in subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[0].families)
+    subscription = SubscriptionModel.from_subscription(state["subscription_id"])
+    ap_list = getattr(subscription, _L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
+    assert len(ap_list) == 2
+
+    assert ap_list[0].sbp.gs_id == new_sbp_data["gs_id"]
+    assert ap_list[0].sbp.is_tagged == new_sbp_data["is_tagged"]
+    assert ap_list[0].sbp.vlan_id == new_sbp_data["vlan_id"]
+    assert str(ap_list[0].sbp.ipv4_address) == new_sbp_data["ipv4_address"]
+    assert ap_list[0].sbp.ipv4_mask == new_sbp_data["ipv4_mask"]
+    assert str(ap_list[0].sbp.ipv6_address) == new_sbp_data["ipv6_address"]
+    assert ap_list[0].sbp.ipv6_mask == new_sbp_data["ipv6_mask"]
+    assert ap_list[0].sbp.custom_firewall_filters == new_sbp_data["custom_firewall_filters"]
+
+    assert ap_list[0].sbp.bgp_session_list[0].bfd_enabled == new_sbp_data["v4_bgp_bfd_enabled"]
+    assert ap_list[0].sbp.bgp_session_list[0].has_custom_policies == new_sbp_data["v4_bgp_has_custom_policies"]
+    assert ap_list[0].sbp.bgp_session_list[0].authentication_key == new_sbp_data["v4_bgp_authentication_key"]
+    assert ap_list[0].sbp.bgp_session_list[0].multipath_enabled == new_sbp_data["v4_bgp_multipath_enabled"]
+    assert ap_list[0].sbp.bgp_session_list[0].send_default_route == new_sbp_data["v4_bgp_send_default_route"]
+    assert ap_list[0].sbp.bgp_session_list[0].is_passive == new_sbp_data["v4_bgp_is_passive"]
+    assert str(ap_list[0].sbp.bgp_session_list[0].peer_address) == new_sbp_data["v4_bgp_peer_address"]
+    assert (
+        bool(IPFamily.V4MULTICAST in ap_list[0].sbp.bgp_session_list[0].families)
         == new_sbp_data["v4_bgp_add_v4_multicast"]
     )
 
+    assert ap_list[0].sbp.bgp_session_list[1].bfd_enabled == new_sbp_data["v6_bgp_bfd_enabled"]
+    assert ap_list[0].sbp.bgp_session_list[1].has_custom_policies == new_sbp_data["v6_bgp_has_custom_policies"]
+    assert ap_list[0].sbp.bgp_session_list[1].authentication_key == new_sbp_data["v6_bgp_authentication_key"]
+    assert ap_list[0].sbp.bgp_session_list[1].multipath_enabled == new_sbp_data["v6_bgp_multipath_enabled"]
+    assert ap_list[0].sbp.bgp_session_list[1].send_default_route == new_sbp_data["v6_bgp_send_default_route"]
+    assert ap_list[0].sbp.bgp_session_list[1].is_passive == new_sbp_data["v6_bgp_is_passive"]
+    assert str(ap_list[0].sbp.bgp_session_list[1].peer_address) == new_sbp_data["v6_bgp_peer_address"]
     assert (
-        subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[1].bfd_enabled
-        == new_sbp_data["v6_bgp_bfd_enabled"]
-    )
-    assert (
-        subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[1].has_custom_policies
-        == new_sbp_data["v6_bgp_has_custom_policies"]
-    )
-    assert (
-        subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[1].authentication_key
-        == new_sbp_data["v6_bgp_authentication_key"]
-    )
-    assert (
-        subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[1].multipath_enabled
-        == new_sbp_data["v6_bgp_multipath_enabled"]
-    )
-    assert (
-        subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[1].send_default_route
-        == new_sbp_data["v6_bgp_send_default_route"]
-    )
-    assert (
-        subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[1].is_passive == new_sbp_data["v6_bgp_is_passive"]
-    )
-    assert (
-        str(subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[1].peer_address)
-        == new_sbp_data["v6_bgp_peer_address"]
-    )
-    assert (
-        bool(IPFamily.V6MULTICAST in subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[1].families)
+        bool(IPFamily.V6MULTICAST in ap_list[0].sbp.bgp_session_list[1].families)
         == new_sbp_data["v6_bgp_add_v6_multicast"]
     )
-    assert subscription.l3_core_service.ap_list[0].sbp.v4_bfd_settings.bfd_enabled == new_sbp_data["v4_bfd_enabled"]
-    assert (
-        subscription.l3_core_service.ap_list[0].sbp.v4_bfd_settings.bfd_interval_rx
-        == new_sbp_data["v4_bfd_interval_rx"]
-    )
-    assert (
-        subscription.l3_core_service.ap_list[0].sbp.v4_bfd_settings.bfd_interval_tx
-        == new_sbp_data["v4_bfd_interval_tx"]
-    )
-    assert (
-        subscription.l3_core_service.ap_list[0].sbp.v4_bfd_settings.bfd_multiplier == new_sbp_data["v4_bfd_multiplier"]
-    )
-    assert subscription.l3_core_service.ap_list[0].sbp.v6_bfd_settings.bfd_enabled == new_sbp_data["v6_bfd_enabled"]
-    assert (
-        subscription.l3_core_service.ap_list[0].sbp.v6_bfd_settings.bfd_interval_rx
-        == new_sbp_data["v6_bfd_interval_rx"]
-    )
-    assert (
-        subscription.l3_core_service.ap_list[0].sbp.v6_bfd_settings.bfd_interval_tx
-        == new_sbp_data["v6_bfd_interval_tx"]
-    )
-    assert (
-        subscription.l3_core_service.ap_list[0].sbp.v6_bfd_settings.bfd_multiplier == new_sbp_data["v6_bfd_multiplier"]
-    )
+    assert ap_list[0].sbp.v4_bfd_settings.bfd_enabled == new_sbp_data["v4_bfd_enabled"]
+    assert ap_list[0].sbp.v4_bfd_settings.bfd_interval_rx == new_sbp_data["v4_bfd_interval_rx"]
+    assert ap_list[0].sbp.v4_bfd_settings.bfd_interval_tx == new_sbp_data["v4_bfd_interval_tx"]
+    assert ap_list[0].sbp.v4_bfd_settings.bfd_multiplier == new_sbp_data["v4_bfd_multiplier"]
+    assert ap_list[0].sbp.v6_bfd_settings.bfd_enabled == new_sbp_data["v6_bfd_enabled"]
+    assert ap_list[0].sbp.v6_bfd_settings.bfd_interval_rx == new_sbp_data["v6_bfd_interval_rx"]
+    assert ap_list[0].sbp.v6_bfd_settings.bfd_interval_tx == new_sbp_data["v6_bfd_interval_tx"]
+    assert ap_list[0].sbp.v6_bfd_settings.bfd_multiplier == new_sbp_data["v6_bfd_multiplier"]
-- 
GitLab


From 1fd14aff13d926309c0719178ba58347d532a307 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Fri, 28 Mar 2025 09:51:20 +0100
Subject: [PATCH 54/87] pass test_terminate_l3_core_service.py

---
 .../test_modify_l3_core_service.py            | 68 +++++++++----------
 .../test_terminate_l3_core_service.py         | 23 ++++---
 2 files changed, 49 insertions(+), 42 deletions(-)

diff --git a/test/workflows/l3_core_service/test_modify_l3_core_service.py b/test/workflows/l3_core_service/test_modify_l3_core_service.py
index fc9999415..7022ea300 100644
--- a/test/workflows/l3_core_service/test_modify_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_modify_l3_core_service.py
@@ -3,7 +3,6 @@ from orchestrator.domain import SubscriptionModel
 
 from gso.products import ProductName
 from gso.products.product_blocks.bgp_session import IPFamily
-from gso.products.product_types.l3_core_service import L3_CORE_SERVICE_TYPES, L3CoreService
 from gso.utils.shared_enums import APType
 from gso.workflows.l3_core_service.modify_l3_core_service import Operation
 from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES
@@ -168,43 +167,44 @@ def test_modify_l3_core_service_modify_edge_port_success(
     ap_list = getattr(subscription, _L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
     assert len(ap_list) == 2
 
-    assert ap_list[0].sbp.gs_id == new_sbp_data["gs_id"]
-    assert ap_list[0].sbp.is_tagged == new_sbp_data["is_tagged"]
-    assert ap_list[0].sbp.vlan_id == new_sbp_data["vlan_id"]
-    assert str(ap_list[0].sbp.ipv4_address) == new_sbp_data["ipv4_address"]
-    assert ap_list[0].sbp.ipv4_mask == new_sbp_data["ipv4_mask"]
-    assert str(ap_list[0].sbp.ipv6_address) == new_sbp_data["ipv6_address"]
-    assert ap_list[0].sbp.ipv6_mask == new_sbp_data["ipv6_mask"]
-    assert ap_list[0].sbp.custom_firewall_filters == new_sbp_data["custom_firewall_filters"]
-
-    assert ap_list[0].sbp.bgp_session_list[0].bfd_enabled == new_sbp_data["v4_bgp_bfd_enabled"]
-    assert ap_list[0].sbp.bgp_session_list[0].has_custom_policies == new_sbp_data["v4_bgp_has_custom_policies"]
-    assert ap_list[0].sbp.bgp_session_list[0].authentication_key == new_sbp_data["v4_bgp_authentication_key"]
-    assert ap_list[0].sbp.bgp_session_list[0].multipath_enabled == new_sbp_data["v4_bgp_multipath_enabled"]
-    assert ap_list[0].sbp.bgp_session_list[0].send_default_route == new_sbp_data["v4_bgp_send_default_route"]
-    assert ap_list[0].sbp.bgp_session_list[0].is_passive == new_sbp_data["v4_bgp_is_passive"]
-    assert str(ap_list[0].sbp.bgp_session_list[0].peer_address) == new_sbp_data["v4_bgp_peer_address"]
+    access_port = ap_list[0]
+    assert access_port.sbp.gs_id == new_sbp_data["gs_id"]
+    assert access_port.sbp.is_tagged == new_sbp_data["is_tagged"]
+    assert access_port.sbp.vlan_id == new_sbp_data["vlan_id"]
+    assert str(access_port.sbp.ipv4_address) == new_sbp_data["ipv4_address"]
+    assert access_port.sbp.ipv4_mask == new_sbp_data["ipv4_mask"]
+    assert str(access_port.sbp.ipv6_address) == new_sbp_data["ipv6_address"]
+    assert access_port.sbp.ipv6_mask == new_sbp_data["ipv6_mask"]
+    assert access_port.sbp.custom_firewall_filters == new_sbp_data["custom_firewall_filters"]
+
+    assert access_port.sbp.bgp_session_list[0].bfd_enabled == new_sbp_data["v4_bgp_bfd_enabled"]
+    assert access_port.sbp.bgp_session_list[0].has_custom_policies == new_sbp_data["v4_bgp_has_custom_policies"]
+    assert access_port.sbp.bgp_session_list[0].authentication_key == new_sbp_data["v4_bgp_authentication_key"]
+    assert access_port.sbp.bgp_session_list[0].multipath_enabled == new_sbp_data["v4_bgp_multipath_enabled"]
+    assert access_port.sbp.bgp_session_list[0].send_default_route == new_sbp_data["v4_bgp_send_default_route"]
+    assert access_port.sbp.bgp_session_list[0].is_passive == new_sbp_data["v4_bgp_is_passive"]
+    assert str(access_port.sbp.bgp_session_list[0].peer_address) == new_sbp_data["v4_bgp_peer_address"]
     assert (
-        bool(IPFamily.V4MULTICAST in ap_list[0].sbp.bgp_session_list[0].families)
+        bool(IPFamily.V4MULTICAST in access_port.sbp.bgp_session_list[0].families)
         == new_sbp_data["v4_bgp_add_v4_multicast"]
     )
 
-    assert ap_list[0].sbp.bgp_session_list[1].bfd_enabled == new_sbp_data["v6_bgp_bfd_enabled"]
-    assert ap_list[0].sbp.bgp_session_list[1].has_custom_policies == new_sbp_data["v6_bgp_has_custom_policies"]
-    assert ap_list[0].sbp.bgp_session_list[1].authentication_key == new_sbp_data["v6_bgp_authentication_key"]
-    assert ap_list[0].sbp.bgp_session_list[1].multipath_enabled == new_sbp_data["v6_bgp_multipath_enabled"]
-    assert ap_list[0].sbp.bgp_session_list[1].send_default_route == new_sbp_data["v6_bgp_send_default_route"]
-    assert ap_list[0].sbp.bgp_session_list[1].is_passive == new_sbp_data["v6_bgp_is_passive"]
-    assert str(ap_list[0].sbp.bgp_session_list[1].peer_address) == new_sbp_data["v6_bgp_peer_address"]
+    assert access_port.sbp.bgp_session_list[1].bfd_enabled == new_sbp_data["v6_bgp_bfd_enabled"]
+    assert access_port.sbp.bgp_session_list[1].has_custom_policies == new_sbp_data["v6_bgp_has_custom_policies"]
+    assert access_port.sbp.bgp_session_list[1].authentication_key == new_sbp_data["v6_bgp_authentication_key"]
+    assert access_port.sbp.bgp_session_list[1].multipath_enabled == new_sbp_data["v6_bgp_multipath_enabled"]
+    assert access_port.sbp.bgp_session_list[1].send_default_route == new_sbp_data["v6_bgp_send_default_route"]
+    assert access_port.sbp.bgp_session_list[1].is_passive == new_sbp_data["v6_bgp_is_passive"]
+    assert str(access_port.sbp.bgp_session_list[1].peer_address) == new_sbp_data["v6_bgp_peer_address"]
     assert (
-        bool(IPFamily.V6MULTICAST in ap_list[0].sbp.bgp_session_list[1].families)
+        bool(IPFamily.V6MULTICAST in access_port.sbp.bgp_session_list[1].families)
         == new_sbp_data["v6_bgp_add_v6_multicast"]
     )
-    assert ap_list[0].sbp.v4_bfd_settings.bfd_enabled == new_sbp_data["v4_bfd_enabled"]
-    assert ap_list[0].sbp.v4_bfd_settings.bfd_interval_rx == new_sbp_data["v4_bfd_interval_rx"]
-    assert ap_list[0].sbp.v4_bfd_settings.bfd_interval_tx == new_sbp_data["v4_bfd_interval_tx"]
-    assert ap_list[0].sbp.v4_bfd_settings.bfd_multiplier == new_sbp_data["v4_bfd_multiplier"]
-    assert ap_list[0].sbp.v6_bfd_settings.bfd_enabled == new_sbp_data["v6_bfd_enabled"]
-    assert ap_list[0].sbp.v6_bfd_settings.bfd_interval_rx == new_sbp_data["v6_bfd_interval_rx"]
-    assert ap_list[0].sbp.v6_bfd_settings.bfd_interval_tx == new_sbp_data["v6_bfd_interval_tx"]
-    assert ap_list[0].sbp.v6_bfd_settings.bfd_multiplier == new_sbp_data["v6_bfd_multiplier"]
+    assert access_port.sbp.v4_bfd_settings.bfd_enabled == new_sbp_data["v4_bfd_enabled"]
+    assert access_port.sbp.v4_bfd_settings.bfd_interval_rx == new_sbp_data["v4_bfd_interval_rx"]
+    assert access_port.sbp.v4_bfd_settings.bfd_interval_tx == new_sbp_data["v4_bfd_interval_tx"]
+    assert access_port.sbp.v4_bfd_settings.bfd_multiplier == new_sbp_data["v4_bfd_multiplier"]
+    assert access_port.sbp.v6_bfd_settings.bfd_enabled == new_sbp_data["v6_bfd_enabled"]
+    assert access_port.sbp.v6_bfd_settings.bfd_interval_rx == new_sbp_data["v6_bfd_interval_rx"]
+    assert access_port.sbp.v6_bfd_settings.bfd_interval_tx == new_sbp_data["v6_bfd_interval_tx"]
+    assert access_port.sbp.v6_bfd_settings.bfd_multiplier == new_sbp_data["v6_bfd_multiplier"]
diff --git a/test/workflows/l3_core_service/test_terminate_l3_core_service.py b/test/workflows/l3_core_service/test_terminate_l3_core_service.py
index 3ac23351e..3639fd6ec 100644
--- a/test/workflows/l3_core_service/test_terminate_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_terminate_l3_core_service.py
@@ -1,20 +1,27 @@
 import pytest
+from orchestrator.domain import SubscriptionModel
 
-from gso.products.product_types.l3_core_service import L3_CORE_SERVICE_TYPES, L3CoreService
+from gso.products import ProductName
+from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES
 from test.workflows import assert_complete, extract_state, run_workflow
 
+_L3_CORE_MODIFY_WF_MAP = {
+    ProductName.COPERNICUS: "terminate_copernicus",
+    ProductName.GEANT_IP: "terminate_geant_ip",
+    ProductName.IAS: "terminate_ias",
+    ProductName.LHCONE: "terminate_lhcone",
+}
+
 
 @pytest.mark.workflow()
-@pytest.mark.parametrize("l3_core_service_type", L3_CORE_SERVICE_TYPES)
-def test_terminate_l3_core_service(l3_core_service_type, l3_core_service_subscription_factory, faker):
-    subscription_id = str(
-        l3_core_service_subscription_factory(l3_core_service_type=l3_core_service_type).subscription_id
-    )
+@pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES)
+def test_terminate_l3_core_service(product_name, l3_core_service_subscription_factory, faker):
+    subscription_id = str(l3_core_service_subscription_factory(product_name=product_name).subscription_id)
     initial_form_data = [{"subscription_id": subscription_id}, {"tt_number": faker.tt_number()}]
-    result, _, _ = run_workflow("terminate_l3_core_service", initial_form_data)
+    result, _, _ = run_workflow(_L3_CORE_MODIFY_WF_MAP[product_name], initial_form_data)
     assert_complete(result)
 
     state = extract_state(result)
     subscription_id = state["subscription_id"]
-    subscription = L3CoreService.from_subscription(subscription_id)
+    subscription = SubscriptionModel.from_subscription(subscription_id)
     assert subscription.status == "terminated"
-- 
GitLab


From 67478018e39b82d81eaf5e2b0096afb014f3c36f Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Fri, 28 Mar 2025 11:29:36 +0100
Subject: [PATCH 55/87] =?UTF-8?q?add=20Validate=20Prefix-List=20for=20G?=
 =?UTF-8?q?=C3=89ANT=20IP=20and=20IAS=20and=20pass=20tests?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 ...bb3c4411ea_add_new_l3_core_services_wfs.py |  12 ++
 ...c38adde1a18e_update_wf_in_process_table.py |  29 +++--
 gso/workflows/__init__.py                     |   7 +-
 .../base_validate_l3_core_service.py          |  23 +---
 .../base_validate_prefix_list.py              | 103 ++++++++++++++++++
 .../geant_ip/validate_prefix_list.py          |  52 +++++++++
 .../ias/validate_prefix_list.py               |  52 +++++++++
 gso/workflows/l3_core_service/shared.py       |   3 +
 .../test_validate_prefix_list.py              |  16 ++-
 9 files changed, 251 insertions(+), 46 deletions(-)
 create mode 100644 gso/workflows/l3_core_service/base_validate_prefix_list.py
 create mode 100644 gso/workflows/l3_core_service/geant_ip/validate_prefix_list.py
 create mode 100644 gso/workflows/l3_core_service/ias/validate_prefix_list.py

diff --git a/gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py b/gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py
index a39be4d5c..72155294b 100644
--- a/gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py
+++ b/gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py
@@ -58,6 +58,12 @@ new_workflows = [
         "description": "Validate IAS Configuration",
         "product_type": "IAS"
     },
+    {
+        "name": "validate_ias_prefix_list",
+        "target": "SYSTEM",
+        "description": "Validate Prefix-List for IAS",
+        "product_type": "IAS"
+    },
     {
         "name": "create_geant_ip",
         "target": "CREATE",
@@ -100,6 +106,12 @@ new_workflows = [
         "description": "Validate GÉANT IP Configuration",
         "product_type": "GeantIP"
     },
+    {
+        "name": "validate_geant_ip_prefix_list",
+        "target": "SYSTEM",
+        "description": "Validate Prefix-List for GÉANT IP",
+        "product_type": "GeantIP"
+    },
     {
         "name": "create_lhcone",
         "target": "CREATE",
diff --git a/gso/migrations/versions/2025-03-26_c38adde1a18e_update_wf_in_process_table.py b/gso/migrations/versions/2025-03-26_c38adde1a18e_update_wf_in_process_table.py
index a1878359d..b370e3fcc 100644
--- a/gso/migrations/versions/2025-03-26_c38adde1a18e_update_wf_in_process_table.py
+++ b/gso/migrations/versions/2025-03-26_c38adde1a18e_update_wf_in_process_table.py
@@ -28,9 +28,9 @@ def upgrade() -> None:
         {"new_workflow": "terminate_ias", "old_workflow": "terminate_l3_core_service", "product_type": "IAS"},
         {"new_workflow": "migrate_ias", "old_workflow": "migrate_l3_core_service", "product_type": "IAS"},
         {"new_workflow": "validate_ias", "old_workflow": "validate_l3_core_service", "product_type": "IAS"},
-        {"new_workflow": "create_imported_ias", "old_workflow": "create_imported_l3_core_service",
-         "product_type": "IAS"},
+        {"new_workflow": "create_imported_ias", "old_workflow": "create_imported_l3_core_service", "product_type": "IAS"},
         {"new_workflow": "import_ias", "old_workflow": "import_l3_core_service", "product_type": "IAS"},
+        {"new_workflow": "validate_ias_prefix_list", "old_workflow": "validate_prefix_list", "product_type": "IAS"},
 
         # GeantIP updates
         {"new_workflow": "create_geant_ip", "old_workflow": "create_l3_core_service", "product_type": "GeantIP"},
@@ -38,10 +38,9 @@ def upgrade() -> None:
         {"new_workflow": "terminate_geant_ip", "old_workflow": "terminate_l3_core_service", "product_type": "GeantIP"},
         {"new_workflow": "migrate_geant_ip", "old_workflow": "migrate_l3_core_service", "product_type": "GeantIP"},
         {"new_workflow": "validate_geant_ip", "old_workflow": "validate_l3_core_service", "product_type": "GeantIP"},
-        {"new_workflow": "create_imported_geant_ip", "old_workflow": "create_imported_l3_core_service",
-         "product_type": "GeantIP"},
-        {"new_workflow": "import_geant_ip", "old_workflow": "import_l3_core_service",
-         "product_type": "GeantIP"},
+        {"new_workflow": "create_imported_geant_ip", "old_workflow": "create_imported_l3_core_service", "product_type": "GeantIP"},
+        {"new_workflow": "import_geant_ip", "old_workflow": "import_l3_core_service", "product_type": "GeantIP"},
+        {"new_workflow": "validate_geant_ip_prefix_list", "old_workflow": "validate_prefix_list", "product_type": "GeantIP"},
 
         # LHCOne updates
         {"new_workflow": "create_lhcone", "old_workflow": "create_l3_core_service", "product_type": "LHCOne"},
@@ -56,15 +55,11 @@ def upgrade() -> None:
         # Copernicus updates
         {"new_workflow": "create_copernicus", "old_workflow": "create_l3_core_service", "product_type": "Copernicus"},
         {"new_workflow": "modify_copernicus", "old_workflow": "modify_l3_core_service", "product_type": "Copernicus"},
-        {"new_workflow": "terminate_copernicus", "old_workflow": "terminate_l3_core_service",
-         "product_type": "Copernicus"},
+        {"new_workflow": "terminate_copernicus", "old_workflow": "terminate_l3_core_service", "product_type": "Copernicus"},
         {"new_workflow": "migrate_copernicus", "old_workflow": "migrate_l3_core_service", "product_type": "Copernicus"},
-        {"new_workflow": "validate_copernicus", "old_workflow": "validate_l3_core_service",
-         "product_type": "Copernicus"},
-        {"new_workflow": "create_imported_copernicus", "old_workflow": "create_imported_l3_core_service",
-         "product_type": "Copernicus"},
-        {"new_workflow": "import_copernicus", "old_workflow": "import_l3_core_service",
-         "product_type": "Copernicus"},
+        {"new_workflow": "validate_copernicus", "old_workflow": "validate_l3_core_service","product_type": "Copernicus"},
+        {"new_workflow": "create_imported_copernicus", "old_workflow": "create_imported_l3_core_service", "product_type": "Copernicus"},
+        {"new_workflow": "import_copernicus", "old_workflow": "import_l3_core_service", "product_type": "Copernicus"},
     ]
 
     # SQL template with parameters
@@ -103,7 +98,8 @@ WHERE name IN (
     'migrate_l3_core_service',
     'validate_l3_core_service',
     'create_imported_l3_core_service',
-    'import_l3_core_service'
+    'import_l3_core_service',
+    'validate_prefix_list'
 );
             """
         )
@@ -111,4 +107,5 @@ WHERE name IN (
 
 
 def downgrade() -> None:
-    conn = op.get_bind()
+    """No downgrade available."""
+    pass
diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py
index 9157b9e4a..7dd0ebf53 100644
--- a/gso/workflows/__init__.py
+++ b/gso/workflows/__init__.py
@@ -126,6 +126,8 @@ LazyWorkflowInstance("gso.workflows.l3_core_service.ias.migrate_ias", "migrate_i
 LazyWorkflowInstance("gso.workflows.l3_core_service.ias.create_imported_ias", "create_imported_ias")
 LazyWorkflowInstance("gso.workflows.l3_core_service.ias.import_ias", "import_ias")
 LazyWorkflowInstance("gso.workflows.l3_core_service.ias.validate_ias", "validate_ias")
+LazyWorkflowInstance("gso.workflows.l3_core_service.ias.validate_prefix_list", "validate_ias_prefix_list")
+
 
 #  Copernicus workflows
 LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.create_copernicus", "create_copernicus")
@@ -155,6 +157,7 @@ LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.migrate_geant_ip",
 LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.create_imported_geant_ip", "create_imported_geant_ip")
 LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.import_geant_ip", "import_geant_ip")
 LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.validate_geant_ip", "validate_geant_ip")
+LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.validate_prefix_list", "validate_geant_ip_prefix_list")
 
 # Layer 2 Circuit workflows
 LazyWorkflowInstance("gso.workflows.l2_circuit.create_layer_2_circuit", "create_layer_2_circuit")
@@ -169,7 +172,3 @@ LazyWorkflowInstance("gso.workflows.vrf.create_vrf", "create_vrf")
 LazyWorkflowInstance("gso.workflows.vrf.modify_vrf_router_list", "modify_vrf_router_list")
 LazyWorkflowInstance("gso.workflows.vrf.redeploy_vrf", "redeploy_vrf")
 LazyWorkflowInstance("gso.workflows.vrf.terminate_vrf", "terminate_vrf")
-
-# Validate workflow for L3 Core Service
-LazyWorkflowInstance("gso.workflows.l3_core_service.validate_prefix_list", "validate_prefix_list")
-LazyWorkflowInstance("gso.workflows.l3_core_service.validate_l3_core_service", "validate_l3_core_service")
diff --git a/gso/workflows/l3_core_service/base_validate_l3_core_service.py b/gso/workflows/l3_core_service/base_validate_l3_core_service.py
index 47aaef4e2..e1af96f44 100644
--- a/gso/workflows/l3_core_service/base_validate_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_validate_l3_core_service.py
@@ -3,13 +3,10 @@
 from typing import Any
 
 from orchestrator.domain import SubscriptionModel
-from orchestrator.targets import Target
-from orchestrator.workflow import StepList, begin, done, step, workflow
-from orchestrator.workflows.steps import resync, store_process_subscription, unsync
-from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from orchestrator.workflow import step
 from pydantic_forms.types import UUIDstr
 
-from gso.services.lso_client import LSOState, anonymous_lso_interaction
+from gso.services.lso_client import LSOState
 from gso.services.partners import get_partner_by_id
 from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes
 
@@ -66,19 +63,3 @@ def validate_bgp_peers(subscription: dict[str, Any], process_id: UUIDstr, ap_fqd
 def validate_dns_records() -> None:
     """Validate DNS records in Infoblox."""
     # TODO: implement
-
-
-@workflow("Validate L3 Core Service", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None)))
-def validate_l3_core_service() -> StepList:
-    """Validate an existing L3 Core Service subscription."""
-    return (
-        begin
-        >> store_process_subscription(Target.SYSTEM)
-        >> unsync
-        >> build_fqdn_list
-        >> anonymous_lso_interaction(validate_sbp_config)
-        >> anonymous_lso_interaction(validate_bgp_peers)
-        >> validate_dns_records
-        >> resync
-        >> done
-    )
diff --git a/gso/workflows/l3_core_service/base_validate_prefix_list.py b/gso/workflows/l3_core_service/base_validate_prefix_list.py
new file mode 100644
index 000000000..a798d9053
--- /dev/null
+++ b/gso/workflows/l3_core_service/base_validate_prefix_list.py
@@ -0,0 +1,103 @@
+"""Prefix Validation workflow for L3 Core Service subscription objects."""
+
+from typing import Any
+
+from orchestrator.config.assignee import Assignee
+from orchestrator.domain import SubscriptionModel
+from orchestrator.forms import SubmitFormPage
+from orchestrator.workflow import inputstep, step
+from pydantic import Field
+from pydantic_forms.types import FormGenerator, State, UUIDstr
+from pydantic_forms.validators import Label
+
+from gso.services.lso_client import LSOState
+from gso.services.partners import get_partner_by_id
+from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes
+
+
+@step("Prepare list of all Access Ports")
+def build_fqdn_list(subscription_id: UUIDstr, service_name: L3CoreServiceNameTypes) -> State:
+    """Build the list of all FQDNs that are in the list of access ports of a L3 Core Service subscription."""
+    subscription = SubscriptionModel.from_subscription(subscription_id)
+    ap_list = getattr(subscription, service_name).l3_core.ap_list
+    ap_fqdn_list = [ap.sbp.edge_port.node.router_fqdn for ap in ap_list]
+    return {"ap_fqdn_list": ap_fqdn_list, "subscription": subscription}
+
+
+@step("[DRY RUN] Validate Prefix-Lists")
+def validate_prefix_lists_dry(subscription: dict[str, Any], process_id: UUIDstr, ap_fqdn_list: list[str]) -> LSOState:
+    """Workflow step for running a playbook that validates prefix-lists in dry run mode."""
+    extra_vars = {
+        "subscription": subscription,
+        "partner_name": get_partner_by_id(subscription["customer_id"]).name,
+        "dry_run": True,
+        "verb": "deploy",
+        "object": "prefix_list",
+        "is_verification_workflow": "true",
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - Validate prefix-lists for {subscription["description"]}",
+    }
+
+    return {
+        "playbook_name": "gap_ansible/playbooks/validate_prefix_list.yaml",
+        "inventory": {"all": {"hosts": dict.fromkeys(ap_fqdn_list)}},
+        "extra_vars": extra_vars,
+    }
+
+
+@step("Evaluate validation of Prefix-Lists")
+def evaluate_result_has_diff(callback_result: dict) -> State:
+    """Evaluate the result of the playbook that validates prefix-lists."""
+    return {"callback_result": callback_result, "prefix_list_drift": bool(callback_result["return_code"] != 0)}
+
+
+@inputstep("Await operator confirmation", assignee=Assignee.SYSTEM)
+def await_operator() -> FormGenerator:
+    """Show a form for the operator to start redeploying the prefix list that has drifted."""
+
+    class AwaitOperatorForm(SubmitFormPage):
+        info_label_a: Label = Field("A drift has been detected for this prefix list!", exclude=True)
+        info_label_b: Label = Field("Please continue this workflow to redeploy the drifted prefix list.", exclude=True)
+
+    yield AwaitOperatorForm
+
+    return {}
+
+
+@step("[DRY RUN] Deploy Prefix-Lists")
+def deploy_prefix_lists_dry(subscription: dict[str, Any], process_id: UUIDstr, ap_fqdn_list: list[str]) -> LSOState:
+    """Workflow step for running a playbook that deploys prefix-lists in dry run mode."""
+    extra_vars = {
+        "subscription": subscription,
+        "partner_name": get_partner_by_id(subscription["customer_id"]).name,
+        "dry_run": True,
+        "verb": "deploy",
+        "object": "prefix_list",
+        "is_verification_workflow": "false",
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - Deploy prefix-lists for {subscription["description"]}",
+    }
+
+    return {
+        "playbook_name": "gap_ansible/playbooks/deploy_prefix_list.yaml",
+        "inventory": {"all": {"hosts": dict.fromkeys(ap_fqdn_list)}},
+        "extra_vars": extra_vars,
+    }
+
+
+@step("[REAL] Deploy Prefix-Lists")
+def deploy_prefix_lists_real(subscription: dict[str, Any], process_id: UUIDstr, ap_fqdn_list: list[str]) -> LSOState:
+    """Workflow step for running a playbook that deploys prefix-lists."""
+    extra_vars = {
+        "subscription": subscription,
+        "partner_name": get_partner_by_id(subscription["customer_id"]).name,
+        "dry_run": False,
+        "verb": "deploy",
+        "object": "prefix_list",
+        "is_verification_workflow": "false",
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - Deploy prefix-lists for {subscription["description"]}",
+    }
+
+    return {
+        "playbook_name": "gap_ansible/playbooks/deploy_prefix_list.yaml",
+        "inventory": {"all": {"hosts": dict.fromkeys(ap_fqdn_list)}},
+        "extra_vars": extra_vars,
+    }
diff --git a/gso/workflows/l3_core_service/geant_ip/validate_prefix_list.py b/gso/workflows/l3_core_service/geant_ip/validate_prefix_list.py
new file mode 100644
index 000000000..d081273ae
--- /dev/null
+++ b/gso/workflows/l3_core_service/geant_ip/validate_prefix_list.py
@@ -0,0 +1,52 @@
+"""Prefix Validation workflow for GÉANT IP subscription objects."""
+
+from orchestrator.targets import Target
+from orchestrator.workflow import StepList, begin, conditional, done, step, workflow
+from orchestrator.workflows.steps import resync, store_process_subscription, unsync
+from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from pydantic_forms.types import State
+
+from gso.services.lso_client import anonymous_lso_interaction, lso_interaction
+from gso.workflows.l3_core_service.base_validate_prefix_list import (
+    await_operator,
+    build_fqdn_list,
+    deploy_prefix_lists_dry,
+    deploy_prefix_lists_real,
+    evaluate_result_has_diff,
+    validate_prefix_lists_dry,
+)
+
+
+@step("Inject service name")
+def inject_srvice_name() -> State:
+    """Inject the service name into the subscription."""
+    return {"service_name": "geant_ip"}
+
+
+@workflow("Validate Prefix-List", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None)))
+def validate_geant_ip_prefix_list() -> StepList:
+    """Validate prefix-lists for an existing GÉANT IP subscription."""
+    prefix_list_has_drifted = conditional(lambda state: bool(state["prefix_list_drift"]))
+
+    redeploy_prefix_list_steps = (
+        begin
+        >> unsync
+        >> await_operator
+        >> lso_interaction(deploy_prefix_lists_dry)
+        >> lso_interaction(deploy_prefix_lists_real)
+        >> resync
+    )
+    prefix_list_validation_steps = (
+        begin
+        >> anonymous_lso_interaction(validate_prefix_lists_dry, evaluate_result_has_diff)
+        >> prefix_list_has_drifted(redeploy_prefix_list_steps)
+    )
+
+    return (
+        begin
+        >> inject_srvice_name
+        >> store_process_subscription(Target.SYSTEM)
+        >> build_fqdn_list
+        >> prefix_list_validation_steps
+        >> done
+    )
diff --git a/gso/workflows/l3_core_service/ias/validate_prefix_list.py b/gso/workflows/l3_core_service/ias/validate_prefix_list.py
new file mode 100644
index 000000000..c85876d3f
--- /dev/null
+++ b/gso/workflows/l3_core_service/ias/validate_prefix_list.py
@@ -0,0 +1,52 @@
+"""Prefix Validation workflow for IAS subscription objects."""
+
+from orchestrator.targets import Target
+from orchestrator.workflow import StepList, begin, conditional, done, step, workflow
+from orchestrator.workflows.steps import resync, store_process_subscription, unsync
+from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from pydantic_forms.types import State
+
+from gso.services.lso_client import anonymous_lso_interaction, lso_interaction
+from gso.workflows.l3_core_service.base_validate_prefix_list import (
+    await_operator,
+    build_fqdn_list,
+    deploy_prefix_lists_dry,
+    deploy_prefix_lists_real,
+    evaluate_result_has_diff,
+    validate_prefix_lists_dry,
+)
+
+
+@step("Inject service name")
+def inject_srvice_name() -> State:
+    """Inject the service name into the subscription."""
+    return {"service_name": "ias"}
+
+
+@workflow("Validate Prefix-List", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None)))
+def validate_ias_prefix_list() -> StepList:
+    """Validate prefix-lists for an existing IAS subscription."""
+    prefix_list_has_drifted = conditional(lambda state: bool(state["prefix_list_drift"]))
+
+    redeploy_prefix_list_steps = (
+        begin
+        >> unsync
+        >> await_operator
+        >> lso_interaction(deploy_prefix_lists_dry)
+        >> lso_interaction(deploy_prefix_lists_real)
+        >> resync
+    )
+    prefix_list_validation_steps = (
+        begin
+        >> anonymous_lso_interaction(validate_prefix_lists_dry, evaluate_result_has_diff)
+        >> prefix_list_has_drifted(redeploy_prefix_list_steps)
+    )
+
+    return (
+        begin
+        >> inject_srvice_name
+        >> store_process_subscription(Target.SYSTEM)
+        >> build_fqdn_list
+        >> prefix_list_validation_steps
+        >> done
+    )
diff --git a/gso/workflows/l3_core_service/shared.py b/gso/workflows/l3_core_service/shared.py
index 4a1f57772..d3d6fc536 100644
--- a/gso/workflows/l3_core_service/shared.py
+++ b/gso/workflows/l3_core_service/shared.py
@@ -11,6 +11,7 @@ from gso.products.product_types.lhcone import LHCOne, LHCOneInactive
 L3InactiveProductTypes = IASInactive | LHCOneInactive | CopernicusInactive | GeantIPInactive
 
 L3ProductNameTypes = Literal[ProductName.IAS, ProductName.LHCONE, ProductName.COPERNICUS, ProductName.GEANT_IP]
+
 L3_PRODUCT_NAMES = [
     ProductName.GEANT_IP,
     ProductName.IAS,
@@ -19,3 +20,5 @@ L3_PRODUCT_NAMES = [
 ]
 L3_CORE_SERVICE_PRODUCT_TYPES = [GeantIP.__name__, IAS.__name__, LHCOne.__name__, Copernicus.__name__]
 L3CoreServiceNameTypes = Literal["geant_ip", "ias", "lhcone", "copernicus"]
+L3_CORE_SERVICE_NAME_ATTRIBUTES = {"geant_ip", "ias", "lhcone", "copernicus"}
+assert len(L3_PRODUCT_NAMES) == len(L3_CORE_SERVICE_PRODUCT_TYPES) == len(L3_CORE_SERVICE_NAME_ATTRIBUTES)
diff --git a/test/workflows/l3_core_service/test_validate_prefix_list.py b/test/workflows/l3_core_service/test_validate_prefix_list.py
index f3c9d4a74..48d0ca30b 100644
--- a/test/workflows/l3_core_service/test_validate_prefix_list.py
+++ b/test/workflows/l3_core_service/test_validate_prefix_list.py
@@ -4,7 +4,6 @@ import pytest
 from orchestrator.domain import SubscriptionModel
 
 from gso.products import GeantIP, ProductName
-from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES
 from test import USER_CONFIRM_EMPTY_FORM
 from gso.utils.shared_enums import Vendor
 from test.workflows import (
@@ -19,16 +18,23 @@ from test.workflows import (
 )
 
 
+_PRODUCT_NAME_VALIDATION_WF_MAP = {
+    ProductName.GEANT_IP: "validate_geant_ip_prefix_list",
+    ProductName.IAS: "validate_ias_prefix_list",
+}
+
+
 @pytest.mark.workflow()
 @patch("gso.services.lso_client._send_request")
-@pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES)
+@pytest.mark.parametrize("product_name", [ProductName.IAS, ProductName.GEANT_IP])
 def test_validate_prefix_list_success(mock_lso_interaction, l3_core_service_subscription_factory, faker, product_name):
     subscription_id = str(l3_core_service_subscription_factory(product_name=product_name).subscription_id)
     initial_l3_core_service_data = [{"subscription_id": subscription_id}]
     # Run the workflow and extract results
-    result, process_stat, step_log = run_workflow("validate_prefix_list", initial_l3_core_service_data)
+    result, process_stat, step_log = run_workflow(
+        _PRODUCT_NAME_VALIDATION_WF_MAP[product_name], initial_l3_core_service_data
+    )
 
-    # If validation should run, assert LSO success
     result, step_log = assert_lso_success(result, process_stat, step_log)
     assert_complete(result)
     # Extract the state and validate subscription attributes
@@ -89,7 +95,7 @@ def test_validate_prefix_list_without_diff(mock_lso_interaction, l3_core_service
     # Extract the state and validate subscription attributes
     state = extract_state(result)
     subscription_id = state["subscription_id"]
-    subscription = L3CoreService.from_subscription(subscription_id)
+    subscription = SubscriptionModel.from_subscription(subscription_id)
     assert subscription.status == "active"
     assert subscription.insync is True
     # Verify the number of LSO interactions
-- 
GitLab


From 5f631f039612459e90944bc6c162f22d8960bd2e Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Fri, 28 Mar 2025 14:04:09 +0100
Subject: [PATCH 56/87] add new workflow translations

---
 ...bb3c4411ea_add_new_l3_core_services_wfs.py | 12 +++---
 gso/translations/en-GB.json                   | 38 +++++++++++++++----
 .../copernicus/create_imported_copernicus.py  |  2 +-
 .../copernicus/import_copernicus.py           |  2 +-
 .../copernicus/migrate_copernicus.py          |  2 +-
 .../copernicus/modify_copernicus.py           |  2 +-
 .../geant_ip/create_imported_geant_ip.py      |  2 +-
 .../geant_ip/import_geant_ip.py               |  2 +-
 .../geant_ip/migrate_geant_ip.py              |  2 +-
 .../geant_ip/modify_geant_ip.py               |  2 +-
 .../geant_ip/validate_prefix_list.py          |  4 +-
 .../ias/create_imported_ias.py                |  2 +-
 .../l3_core_service/ias/import_ias.py         |  2 +-
 .../l3_core_service/ias/migrate_ias.py        |  2 +-
 .../ias/validate_prefix_list.py               |  2 +-
 .../lhcone/create_imported_lhcone.py          |  2 +-
 .../l3_core_service/lhcone/create_lhcone.py   |  2 +-
 .../l3_core_service/lhcone/import_lhcone.py   |  2 +-
 .../l3_core_service/lhcone/migrate_lhcone.py  |  2 +-
 .../l3_core_service/lhcone/modify_lhcone.py   |  2 +-
 .../edge_port/test_migrate_edge_port.py       | 22 ++++++++---
 .../test_migrate_l3_core_service.py           | 16 +++++---
 .../test_validate_prefix_list.py              |  8 ++--
 23 files changed, 85 insertions(+), 49 deletions(-)

diff --git a/gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py b/gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py
index 72155294b..2a4242983 100644
--- a/gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py
+++ b/gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py
@@ -55,13 +55,13 @@ new_workflows = [
     {
         "name": "validate_ias",
         "target": "SYSTEM",
-        "description": "Validate IAS Configuration",
+        "description": "Validate IAS",
         "product_type": "IAS"
     },
     {
         "name": "validate_ias_prefix_list",
         "target": "SYSTEM",
-        "description": "Validate Prefix-List for IAS",
+        "description": "Validate IAS Prefix-List",
         "product_type": "IAS"
     },
     {
@@ -103,13 +103,13 @@ new_workflows = [
     {
         "name": "validate_geant_ip",
         "target": "SYSTEM",
-        "description": "Validate GÉANT IP Configuration",
+        "description": "Validate GÉANT IP",
         "product_type": "GeantIP"
     },
     {
         "name": "validate_geant_ip_prefix_list",
         "target": "SYSTEM",
-        "description": "Validate Prefix-List for GÉANT IP",
+        "description": "Validate GÉANT IP Prefix-List",
         "product_type": "GeantIP"
     },
     {
@@ -151,7 +151,7 @@ new_workflows = [
     {
         "name": "validate_lhcone",
         "target": "SYSTEM",
-        "description": "Validate LHCOne Configuration",
+        "description": "Validate LHCOne",
         "product_type": "LHCOne"
     },
     {
@@ -193,7 +193,7 @@ new_workflows = [
     {
         "name": "validate_copernicus",
         "target": "SYSTEM",
-        "description": "Validate Copernicus Configuration",
+        "description": "Validate Copernicus",
         "product_type": "Copernicus"
     },
 ]
diff --git a/gso/translations/en-GB.json b/gso/translations/en-GB.json
index 02ea8449f..c6c7208f9 100644
--- a/gso/translations/en-GB.json
+++ b/gso/translations/en-GB.json
@@ -83,7 +83,10 @@
         "create_edge_port": "Create Edge Port",
         "create_imported_edge_port": "NOT FOR HUMANS -- Import existing Edge Port",
         "create_imported_iptrunk": "NOT FOR HUMANS -- Import existing IP trunk",
-        "create_imported_l3_core_service": "NOT FOR HUMANS -- Import existing L3 Core Service",
+        "create_imported_geant_ip": "NOT FOR HUMANS -- Import existing GÉANT IP",
+        "create_imported_ias": "NOT FOR HUMANS -- Import existing IAS",
+        "create_imported_lhcone": "NOT FOR HUMANS -- Import existing LHCOne",
+        "create_imported_copernicus": "NOT FOR HUMANS -- Import existing Copernicus",
         "create_imported_lan_switch_interconnect": "NOT FOR HUMANS -- Import existing LAN Switch Interconnect",
         "create_imported_layer_2_circuit": "NOT FOR HUMANS -- Import existing Layer 2 Circuit",
         "create_imported_office_router": "NOT FOR HUMANS -- Import existing office router",
@@ -93,7 +96,10 @@
         "create_imported_super_pop_switch": "NOT FOR HUMANS -- Import existing super PoP switch",
         "create_imported_switch": "NOT FOR HUMANS -- Import existing Switch",
         "create_iptrunk": "Create IP Trunk",
-        "create_l3_core_service": "Create L3 Core Service",
+        "create_geant_ip": "Create GÉANT IP",
+        "create_lhcone": "Create LHCOne",
+        "create_copernicus": "Create Copernicus",
+        "create_ias": "Create IAS",
         "create_lan_switch_interconnect": "Create LAN Switch Interconnect",
         "create_layer_2_circuit": "Create Layer 2 Circuit",
         "create_router": "Create Router",
@@ -103,7 +109,10 @@
         "deploy_twamp": "Deploy TWAMP",
         "import_edge_port": "NOT FOR HUMANS -- Finalize import into an Edge Port",
         "import_iptrunk": "NOT FOR HUMANS -- Finalize import into an IP trunk product",
-        "import_l3_core_service": "NOT FOR HUMANS -- Finalize import into an L3 Core Service",
+        "import_geant_ip": "NOT FOR HUMANS -- Finalize import into a GÉANT IP product",
+        "import_ias": "NOT FOR HUMANS -- Finalize import into an IAS product",
+        "import_lhcone": "NOT FOR HUMANS -- Finalize import into a LHCOne product",
+        "import_copernicus": "NOT FOR HUMANS -- Finalize import into a Copernicus product",
         "import_lan_switch_interconnect": "NOT FOR HUMANS -- Finalize import into a LAN Switch Interconnect",
         "import_layer_2_circuit": "NOT FOR HUMANS -- Finalize import into a Layer 2 Circuit product",
         "import_office_router": "NOT FOR HUMANS -- Finalize import into an Office router product",
@@ -115,11 +124,17 @@
         "migrate_edge_port": "Migrate Edge Port",
         "migrate_iptrunk": "Migrate IP Trunk",
         "migrate_layer_2_circuit": "Migrate Layer 2 Circuit",
-        "migrate_l3_core_service": "Migrate L3 Core Service",
+        "migrate_ias": "Migrate IAS",
+        "migrate_lhcone": "Migrate LHCOne",
+        "migrate_copernicus": "Migrate Copernicus",
+        "migrate_geant_ip": "Migrate GÉANT IP",
         "modify_connection_strategy": "Modify connection strategy",
         "modify_edge_port": "Modify Edge Port",
         "modify_isis_metric": "Modify the ISIS metric",
-        "modify_l3_core_service": "Modify L3 Core Service",
+        "modify_ias": "Modify IAS",
+        "modify_lhcone": "Modify LHCOne",
+        "modify_copernicus": "Modify Copernicus",
+        "modify_geant_ip": "Modify GÉANT IP",
         "modify_layer_2_circuit": "Modify Layer 2 Circuit",
         "modify_router_kentik_license": "Modify device license in Kentik",
         "modify_site": "Modify Site",
@@ -137,7 +152,10 @@
         "task_validate_geant_products": "Validation task for GEANT products",
         "terminate_edge_port": "Terminate Edge Port",
         "terminate_iptrunk": "Terminate IP Trunk",
-        "terminate_l3_core_service": "Terminate L3 Core Service",
+        "terminate_geant_ip": "Terminate GÉANT IP",
+        "terminate_ias": "Terminate IAS",
+        "terminate_lhcone": "Terminate LHCOne",
+        "terminate_copernicus": "Terminate Copernicus",
         "terminate_lan_switch_interconnect": "Terminate LAN Switch Interconnect",
         "terminate_layer_2_circuit": "Terminate Layer 2 Circuit",
         "terminate_router": "Terminate Router",
@@ -147,9 +165,13 @@
         "update_ibgp_mesh": "Update iBGP mesh",
         "validate_edge_port": "Validate Edge Port",
         "validate_iptrunk": "Validate IP Trunk configuration",
-        "validate_l3_core_service": "Validate L3 Core Service",
+        "validate_geant_ip": "Validate GÉANT IP",
+        "validate_ias": "Validate IAS",
+        "validate_lhcone": "Validate LHCOne",
+        "validate_copernicus": "Validate Copernicus",
         "validate_lan_switch_interconnect": "Validate LAN Switch Interconnect",
-        "validate_prefix_list": "Validate Prefix-List",
+        "validate_geant_ip_prefix_list": "Validate GÉANT IP Prefix-List",
+        "validate_ias_prefix_list": "Validate IAS Prefix-List",
         "validate_router": "Validate Router configuration",
         "validate_switch": "Validate Switch configuration"
     }
diff --git a/gso/workflows/l3_core_service/copernicus/create_imported_copernicus.py b/gso/workflows/l3_core_service/copernicus/create_imported_copernicus.py
index f2970ecaa..4042c42be 100644
--- a/gso/workflows/l3_core_service/copernicus/create_imported_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/create_imported_copernicus.py
@@ -26,7 +26,7 @@ def create_subscription(partner: str) -> dict:
 
 
 @workflow(
-    "Create imported Copernicus",
+    "Create Imported Copernicus",
     initial_input_form=base_initial_input_form_generator,
     target=Target.CREATE,
 )
diff --git a/gso/workflows/l3_core_service/copernicus/import_copernicus.py b/gso/workflows/l3_core_service/copernicus/import_copernicus.py
index 85cf9b305..8ba33221c 100644
--- a/gso/workflows/l3_core_service/copernicus/import_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/import_copernicus.py
@@ -24,7 +24,7 @@ def import_copernicus_subscription(subscription_id: UUIDstr) -> State:
     return {"subscription": new_subscription}
 
 
-@workflow("Import Copernicus Service", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None))
+@workflow("Import Copernicus", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None))
 def import_copernicus() -> StepList:
     """Modify an imported subscription into a Copernicus subscription to complete the import."""
     return (
diff --git a/gso/workflows/l3_core_service/copernicus/migrate_copernicus.py b/gso/workflows/l3_core_service/copernicus/migrate_copernicus.py
index 41f55b6b9..8159b3814 100644
--- a/gso/workflows/l3_core_service/copernicus/migrate_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/migrate_copernicus.py
@@ -48,7 +48,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
 
 @workflow(
-    "Migrate Copernicus Service",
+    "Migrate Copernicus",
     initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
     target=Target.MODIFY,
 )
diff --git a/gso/workflows/l3_core_service/copernicus/modify_copernicus.py b/gso/workflows/l3_core_service/copernicus/modify_copernicus.py
index fbd2b6d66..b8b9e5c1f 100644
--- a/gso/workflows/l3_core_service/copernicus/modify_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/modify_copernicus.py
@@ -24,7 +24,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
 
 @workflow(
-    "Modify Copernicus ",
+    "Modify Copernicus",
     initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
     target=Target.MODIFY,
 )
diff --git a/gso/workflows/l3_core_service/geant_ip/create_imported_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/create_imported_geant_ip.py
index 3d1bef513..f6056ea1e 100644
--- a/gso/workflows/l3_core_service/geant_ip/create_imported_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/create_imported_geant_ip.py
@@ -26,7 +26,7 @@ def create_subscription(partner: str) -> dict:
 
 
 @workflow(
-    "Create imported GÉANT IP",
+    "Create Imported GÉANT IP",
     initial_input_form=base_initial_input_form_generator,
     target=Target.CREATE,
 )
diff --git a/gso/workflows/l3_core_service/geant_ip/import_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/import_geant_ip.py
index 6ad0b34b1..cf32d55de 100644
--- a/gso/workflows/l3_core_service/geant_ip/import_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/import_geant_ip.py
@@ -24,7 +24,7 @@ def import_geant_ip_subscription(subscription_id: UUIDstr) -> State:
     return {"subscription": new_subscription}
 
 
-@workflow("Import GÉANT IP Core Service", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None))
+@workflow("Import GÉANT IP", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None))
 def import_geant_ip() -> StepList:
     """Modify an imported subscription into a GÉANT IP subscription to complete the import."""
     return init >> store_process_subscription(Target.MODIFY) >> unsync >> import_geant_ip_subscription >> resync >> done
diff --git a/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py
index ca690d60d..202fff374 100644
--- a/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py
@@ -48,7 +48,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
 
 @workflow(
-    "Migrate GÉANT IP Service",
+    "Migrate GÉANT IP",
     initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
     target=Target.MODIFY,
 )
diff --git a/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py
index 45ef17dce..286447712 100644
--- a/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py
@@ -24,7 +24,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
 
 @workflow(
-    "Modify GÉANT IP ",
+    "Modify GÉANT IP",
     initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
     target=Target.MODIFY,
 )
diff --git a/gso/workflows/l3_core_service/geant_ip/validate_prefix_list.py b/gso/workflows/l3_core_service/geant_ip/validate_prefix_list.py
index d081273ae..1b807ee15 100644
--- a/gso/workflows/l3_core_service/geant_ip/validate_prefix_list.py
+++ b/gso/workflows/l3_core_service/geant_ip/validate_prefix_list.py
@@ -23,7 +23,9 @@ def inject_srvice_name() -> State:
     return {"service_name": "geant_ip"}
 
 
-@workflow("Validate Prefix-List", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None)))
+@workflow(
+    "Validate GÉANT IP Prefix-List", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None))
+)
 def validate_geant_ip_prefix_list() -> StepList:
     """Validate prefix-lists for an existing GÉANT IP subscription."""
     prefix_list_has_drifted = conditional(lambda state: bool(state["prefix_list_drift"]))
diff --git a/gso/workflows/l3_core_service/ias/create_imported_ias.py b/gso/workflows/l3_core_service/ias/create_imported_ias.py
index 805ab14e6..e796f1f53 100644
--- a/gso/workflows/l3_core_service/ias/create_imported_ias.py
+++ b/gso/workflows/l3_core_service/ias/create_imported_ias.py
@@ -42,7 +42,7 @@ def create_subscription(partner: str) -> dict:
 
 
 @workflow(
-    "Create imported IAS",
+    "Create Imported IAS",
     initial_input_form=initial_input_form_generator,
     target=Target.CREATE,
 )
diff --git a/gso/workflows/l3_core_service/ias/import_ias.py b/gso/workflows/l3_core_service/ias/import_ias.py
index 7b0018c70..9a85513db 100644
--- a/gso/workflows/l3_core_service/ias/import_ias.py
+++ b/gso/workflows/l3_core_service/ias/import_ias.py
@@ -24,7 +24,7 @@ def import_ias_subscription(subscription_id: UUIDstr) -> State:
     return {"subscription": new_subscription}
 
 
-@workflow("Import IAS Core Service", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None))
+@workflow("Import IAS", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None))
 def import_ias() -> StepList:
     """Modify an imported subscription into an IAS subscription to complete the import."""
     return init >> store_process_subscription(Target.MODIFY) >> unsync >> import_ias_subscription >> resync >> done
diff --git a/gso/workflows/l3_core_service/ias/migrate_ias.py b/gso/workflows/l3_core_service/ias/migrate_ias.py
index 0fb01bbfb..fa0ea8ffd 100644
--- a/gso/workflows/l3_core_service/ias/migrate_ias.py
+++ b/gso/workflows/l3_core_service/ias/migrate_ias.py
@@ -48,7 +48,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
 
 @workflow(
-    "Migrate IAS Service",
+    "Migrate IAS",
     initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
     target=Target.MODIFY,
 )
diff --git a/gso/workflows/l3_core_service/ias/validate_prefix_list.py b/gso/workflows/l3_core_service/ias/validate_prefix_list.py
index c85876d3f..fe00af651 100644
--- a/gso/workflows/l3_core_service/ias/validate_prefix_list.py
+++ b/gso/workflows/l3_core_service/ias/validate_prefix_list.py
@@ -23,7 +23,7 @@ def inject_srvice_name() -> State:
     return {"service_name": "ias"}
 
 
-@workflow("Validate Prefix-List", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None)))
+@workflow("Validate IAS Prefix-List", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None)))
 def validate_ias_prefix_list() -> StepList:
     """Validate prefix-lists for an existing IAS subscription."""
     prefix_list_has_drifted = conditional(lambda state: bool(state["prefix_list_drift"]))
diff --git a/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py b/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py
index a13f8f3d0..9bc09ed41 100644
--- a/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py
@@ -26,7 +26,7 @@ def create_subscription(partner: str) -> dict:
 
 
 @workflow(
-    "Create imported LHCOne",
+    "Create Imported LHCOne",
     initial_input_form=base_initial_input_form_generator,
     target=Target.CREATE,
 )
diff --git a/gso/workflows/l3_core_service/lhcone/create_lhcone.py b/gso/workflows/l3_core_service/lhcone/create_lhcone.py
index c39252268..9baabf314 100644
--- a/gso/workflows/l3_core_service/lhcone/create_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/create_lhcone.py
@@ -46,7 +46,7 @@ def initialize_subscription(
 
 
 @workflow(
-    "Create LHCONE",
+    "Create LHCOne",
     initial_input_form=wrap_create_initial_input_form(initial_input_form_generator),
     target=Target.CREATE,
 )
diff --git a/gso/workflows/l3_core_service/lhcone/import_lhcone.py b/gso/workflows/l3_core_service/lhcone/import_lhcone.py
index 749bf2e7b..6bd753770 100644
--- a/gso/workflows/l3_core_service/lhcone/import_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/import_lhcone.py
@@ -24,7 +24,7 @@ def import_lhcone_subscription(subscription_id: UUIDstr) -> State:
     return {"subscription": new_subscription}
 
 
-@workflow("Import LHCOne Service", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None))
+@workflow("Import LHCOne", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None))
 def import_lhcone() -> StepList:
     """Modify an imported subscription into a LHCOne subscription to complete the import."""
     return init >> store_process_subscription(Target.MODIFY) >> unsync >> import_lhcone_subscription >> resync >> done
diff --git a/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py b/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py
index 67b9a5782..8c6c2758a 100644
--- a/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py
@@ -48,7 +48,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
 
 @workflow(
-    "Migrate LHCOne Service",
+    "Migrate LHCOne",
     initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
     target=Target.MODIFY,
 )
diff --git a/gso/workflows/l3_core_service/lhcone/modify_lhcone.py b/gso/workflows/l3_core_service/lhcone/modify_lhcone.py
index 5aac61f88..600682264 100644
--- a/gso/workflows/l3_core_service/lhcone/modify_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/modify_lhcone.py
@@ -24,7 +24,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
 
 @workflow(
-    "Modify LHCOne ",
+    "Modify LHCOne",
     initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
     target=Target.MODIFY,
 )
diff --git a/test/workflows/edge_port/test_migrate_edge_port.py b/test/workflows/edge_port/test_migrate_edge_port.py
index 6c7c55d79..0f50af987 100644
--- a/test/workflows/edge_port/test_migrate_edge_port.py
+++ b/test/workflows/edge_port/test_migrate_edge_port.py
@@ -2,11 +2,12 @@ from unittest.mock import patch
 
 import pytest
 
-from gso.products import Layer2CircuitServiceType
+from gso.products import Layer2CircuitServiceType, ProductName
 from gso.products.product_types.edge_port import EdgePort
 from gso.products.product_types.l3_core_service import L3CoreServiceType
 from gso.products.product_types.router import Router
 from gso.utils.shared_enums import Vendor
+from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES
 from test import USER_CONFIRM_EMPTY_FORM
 from test.services.conftest import MockedNetboxClient
 from test.workflows import (
@@ -19,6 +20,14 @@ from test.workflows import (
 )
 
 
+_L3_ATTRIBUTES_MAP = {
+    ProductName.COPERNICUS: "copernicus",
+    ProductName.GEANT_IP: "geant_ip",
+    ProductName.IAS: "ias",
+    ProductName.LHCONE: "lhcone",
+}
+
+
 @pytest.fixture()
 def _netbox_client_mock():
     with (
@@ -89,16 +98,17 @@ def test_successful_edge_port_migration(
     layer_2_circuit_subscription_factory,
 ):
     edge_port = edge_port_subscription_factory(partner=partner)
-    for service_type in [service_type for service_type in L3CoreServiceType if not service_type.startswith("IMPORTED")]:
-        l3_core_service = l3_core_service_subscription_factory(partner=partner, l3_core_service_type=service_type)
-        l3_core_service.l3_core_service.ap_list[0].sbp.edge_port = edge_port.edge_port
+    for product_name in L3_PRODUCT_NAMES:
+        l3_core_service = l3_core_service_subscription_factory(partner=partner, product_name=product_name)
+        sbp = getattr(l3_core_service, _L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list[0].sbp
+        sbp.edge_port = edge_port.edge_port
         l3_core_service.save()
 
-    for service_type in [
+    for product_name in [
         service_type for service_type in Layer2CircuitServiceType if not service_type.startswith("IMPORTED")
     ]:
         layer_2_circuit_subscription_factory(
-            partner=partner, layer_2_circuit_side_a_edgeport=edge_port, layer_2_circuit_service_type=service_type
+            partner=partner, layer_2_circuit_side_a_edgeport=edge_port, layer_2_circuit_service_type=product_name
         )
 
     initial_data = [{"subscription_id": str(edge_port.subscription_id)}, *input_form_wizard_data]
diff --git a/test/workflows/l3_core_service/test_migrate_l3_core_service.py b/test/workflows/l3_core_service/test_migrate_l3_core_service.py
index 8680a7af9..fdb98b609 100644
--- a/test/workflows/l3_core_service/test_migrate_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_migrate_l3_core_service.py
@@ -103,7 +103,8 @@ def test_migrate_l3_core_service_scoped_emission(
         partner=partner, product_name=product_name, ap_list=custom_ap_list
     )
     destination_edge_port_id = str(edge_port_subscription_factory(partner=partner).subscription_id)
-    source_edge_port = subscription.l3_core_service.ap_list[3].sbp.edge_port.owner_subscription_id
+    ap_list = getattr(subscription, _L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
+    source_edge_port = ap_list[3].sbp.edge_port.owner_subscription_id
 
     form_input_data = [
         {"subscription_id": str(subscription.subscription_id)},
@@ -115,7 +116,7 @@ def test_migrate_l3_core_service_scoped_emission(
         {},
     ]
 
-    result, process_stat, step_log = run_workflow("migrate_l3_core_service", form_input_data)
+    result, process_stat, step_log = run_workflow(_L3_CORE_MIGRATE_WF_MAP[product_name], form_input_data)
 
     result, step_log = assert_lso_interaction_success(result, process_stat, step_log)
 
@@ -142,8 +143,10 @@ def test_migrate_l3_core_service_scoped_emission(
         == transmitted_destination_ep_fqdn
     )
     assert (
-        state["subscription"]["l3_core_service"]
-        == state["__old_subscriptions__"][str(subscription.subscription_id)]["l3_core_service"]
+        state["subscription"][_L3_ATTRIBUTES_MAP[product_name]]["l3_core"]
+        == state["__old_subscriptions__"][str(subscription.subscription_id)][_L3_ATTRIBUTES_MAP[product_name]][
+            "l3_core"
+        ]
     )  # Subscription is unchanged for now
 
     for _ in range(4):
@@ -156,5 +159,6 @@ def test_migrate_l3_core_service_scoped_emission(
     subscription = SubscriptionModel.from_subscription(state["subscription_id"])
     assert mock_execute_playbook.call_count == 11
     assert subscription.insync
-    assert len(subscription.l3_core_service.ap_list) == 5
-    assert str(subscription.l3_core_service.ap_list[3].sbp.edge_port.owner_subscription_id) == destination_edge_port_id
+    ap_list = getattr(subscription, _L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
+    assert len(ap_list) == 5
+    assert str(ap_list[3].sbp.edge_port.owner_subscription_id) == destination_edge_port_id
diff --git a/test/workflows/l3_core_service/test_validate_prefix_list.py b/test/workflows/l3_core_service/test_validate_prefix_list.py
index 48d0ca30b..12e0c561d 100644
--- a/test/workflows/l3_core_service/test_validate_prefix_list.py
+++ b/test/workflows/l3_core_service/test_validate_prefix_list.py
@@ -54,7 +54,7 @@ def test_validate_prefix_list_with_diff(mock_lso_interaction, geant_ip_subscript
     subscription_id = str(geant_ip_subscription_factory().subscription_id)
     initial_l3_core_service_data = [{"subscription_id": subscription_id}]
     # Run the workflow and extract results
-    result, process_stat, step_log = run_workflow("validate_prefix_list", initial_l3_core_service_data)
+    result, process_stat, step_log = run_workflow("validate_geant_ip_prefix_list", initial_l3_core_service_data)
     # Assert LSO success and workflow completion
     result, step_log = assert_lso_failure(result, process_stat, step_log)
     # Interaction has failed, we will need to redeploy this prefix list
@@ -83,12 +83,10 @@ def test_validate_prefix_list_with_diff(mock_lso_interaction, geant_ip_subscript
 @patch("gso.services.lso_client._send_request")
 def test_validate_prefix_list_without_diff(mock_lso_interaction, l3_core_service_subscription_factory, faker):
     """Test case where playbook_has_diff does not qualify and skips additional steps."""
-    subscription_id = str(
-        l3_core_service_subscription_factory(l3_core_service_type=L3CoreServiceType.GEANT_IP).subscription_id
-    )
+    subscription_id = str(l3_core_service_subscription_factory(product_name=ProductName.GEANT_IP).subscription_id)
     initial_l3_core_service_data = [{"subscription_id": subscription_id}]
     # Run the workflow and extract results
-    result, process_stat, step_log = run_workflow("validate_prefix_list", initial_l3_core_service_data)
+    result, process_stat, step_log = run_workflow("validate_ias_prefix_list", initial_l3_core_service_data)
     # Assert LSO success and workflow completion
     result, step_log = assert_lso_success(result, process_stat, step_log)
     assert_complete(result)
-- 
GitLab


From 2fe2e2449601feb3da04fb7ab5c7cc804951dcc9 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Fri, 28 Mar 2025 16:31:18 +0100
Subject: [PATCH 57/87] pass mypy and refactor code

---
 gso/cli/imports.py                            | 17 ++--
 gso/products/__init__.py                      |  2 +-
 .../base_create_imported_l3_core_service.py   |  4 +
 .../base_create_l3_core_service.py            | 13 +++-
 .../base_modify_l3_core_service.py            | 18 ++++-
 .../copernicus/import_copernicus.py           |  2 +-
 .../copernicus/migrate_copernicus.py          |  2 +-
 .../copernicus/modify_copernicus.py           |  1 +
 .../create_imported_l3_core_service.py        |  2 -
 .../geant_ip/import_geant_ip.py               |  2 +-
 .../geant_ip/migrate_geant_ip.py              |  2 +-
 .../geant_ip/modify_geant_ip.py               |  1 +
 .../l3_core_service/ias/create_ias.py         |  4 +-
 .../l3_core_service/ias/import_ias.py         |  2 +-
 .../l3_core_service/ias/migrate_ias.py        |  2 +-
 .../l3_core_service/ias/modify_ias.py         |  1 +
 .../l3_core_service/import_l3_core_service.py |  2 -
 .../l3_core_service/lhcone/import_lhcone.py   |  2 +-
 .../l3_core_service/lhcone/migrate_lhcone.py  |  2 +-
 .../l3_core_service/lhcone/modify_lhcone.py   |  1 +
 gso/workflows/l3_core_service/shared.py       | 78 +++++++++++++++++--
 test/fixtures/__init__.py                     | 24 +++---
 test/fixtures/l3_core_service_fixtures.py     | 19 +++--
 .../edge_port/test_migrate_edge_port.py       | 15 +---
 .../test_create_imported_l3_core_service.py   | 11 +--
 .../test_create_l3_core_service.py            | 20 +----
 .../test_import_l3_core_service.py            | 12 +--
 .../test_migrate_l3_core_service.py           | 35 +++------
 .../test_modify_l3_core_service.py            | 33 +++-----
 .../test_terminate_l3_core_service.py         | 12 +--
 .../test_validate_l3_core_service.py          | 15 +---
 .../test_validate_prefix_list.py              | 12 +--
 32 files changed, 180 insertions(+), 188 deletions(-)

diff --git a/gso/cli/imports.py b/gso/cli/imports.py
index 1a7740d7c..cdf78ca57 100644
--- a/gso/cli/imports.py
+++ b/gso/cli/imports.py
@@ -53,18 +53,11 @@ from gso.utils.types.ip_address import (
     PortNumber,
 )
 from gso.utils.types.virtual_identifiers import VC_ID, VLAN_ID
-from gso.workflows.l3_core_service.shared import L3ProductNameTypes
+from gso.workflows.l3_core_service.shared import L3_CREAT_IMPORTED_WF_MAP, L3ProductNameTypes
 
 app: typer.Typer = typer.Typer()
 IMPORT_WAIT_MESSAGE = "Waiting for the dust to settle before importing new products..."
 
-_L3_MIGRATION_MAP = {
-    ProductName.COPERNICUS: "create_imported_copernicus",
-    ProductName.GEANT_IP: "create_imported_geant_ip",
-    ProductName.IAS: "create_imported_ias",
-    ProductName.LHCONE: "create_imported_lhcone",
-}
-
 
 class CreatePartner(BaseModel):
     """Required inputs for creating a partner."""
@@ -323,6 +316,8 @@ class L3CoreServiceImportModel(BaseModel):
 
 
 class IASImportModel(L3CoreServiceImportModel):
+    """Import IAS model."""
+
     ias_flavor: IASFlavor | None = IASFlavor.IAS_PS_OPT_OUT
 
 
@@ -682,11 +677,11 @@ def import_l3_core_service(filepath: str = common_filepath_option) -> None:
 
         try:
             if product_name == ProductName.IAS.value:
-                initial_data = IASImportModel(**l3_core_service[0], **l3_core_service[1])
+                initial_data = IASImportModel(**l3_core_service[0], **l3_core_service[1]).model_dump()
             else:
-                initial_data = L3CoreServiceImportModel(**l3_core_service[0], **l3_core_service[1])
+                initial_data = L3CoreServiceImportModel(**l3_core_service[0], **l3_core_service[1]).model_dump()
 
-            start_process(_L3_MIGRATION_MAP[product_name], [initial_data.model_dump()])
+            start_process(L3_CREAT_IMPORTED_WF_MAP[product_name], [initial_data])
             edge_ports = [sbp["edge_port"] for sbp in l3_core_service[0]["service_binding_ports"]]
             successfully_imported_data.append(edge_ports)
             typer.echo(f"Successfully created imported {product_name} for {partner}")
diff --git a/gso/products/__init__.py b/gso/products/__init__.py
index 22fa171bb..fff581523 100644
--- a/gso/products/__init__.py
+++ b/gso/products/__init__.py
@@ -157,4 +157,4 @@ SUBSCRIPTION_MODEL_REGISTRY.update(
     },
 )
 
-__all__ = ["ProductName", "ProductType"]
+__all__ = ["L2_CIRCUIT_PRODUCT_TYPE", "ProductName", "ProductType"]
diff --git a/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
index 2878ad3c7..a35a6a168 100644
--- a/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
@@ -97,6 +97,10 @@ def initialize_subscription(
             **service_binding_port,
         )
         service = getattr(subscription, service_name)
+        if service is None:
+            msg = f"{service_name} is not set on subscription"
+            raise AttributeError(msg)
+
         service.l3_core = L3CoreServiceBlockInactive.new(
             subscription_id=uuid4(),
             ap_list=[
diff --git a/gso/workflows/l3_core_service/base_create_l3_core_service.py b/gso/workflows/l3_core_service/base_create_l3_core_service.py
index 34e2d78ee..22381866e 100644
--- a/gso/workflows/l3_core_service/base_create_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_l3_core_service.py
@@ -29,7 +29,7 @@ from gso.utils.types.geant_ids import IMPORTED_GS_ID
 from gso.utils.types.ip_address import IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask
 from gso.utils.types.tt_number import TTNumber
 from gso.utils.types.virtual_identifiers import VLAN_ID
-from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes, L3InactiveProductTypes
+from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes
 
 
 def initial_input_form_generator(product_name: str) -> FormGenerator:
@@ -162,7 +162,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
 
 
 def initialize_service_binding(
-    subscription: L3InactiveProductTypes,
+    subscription: SubscriptionModel,
     edge_port: dict,
     binding_port_input: dict,
     product_name: str,
@@ -197,6 +197,10 @@ def initialize_service_binding(
     )
 
     service = getattr(subscription, service_name)
+    if service is None:
+        msg = f"{service_name} is not set on subscription"
+        raise ValueError(msg)
+
     service.l3_core = L3CoreServiceBlockInactive.new(
         subscription_id=uuid4(),
         ap_list=[
@@ -344,7 +348,10 @@ def create_new_sharepoint_checklist(
 ) -> State:
     """Create a new checklist item in SharePoint for approving this L3 Core Service."""
     service = getattr(subscription, service_name)
-    assert service is not None, f"{service_name} is not set on subscription"
+    if service is None:
+        msg = f"{service_name} is not set on subscription"
+        raise ValueError(msg)
+
     new_ep = service.l3_core.ap_list[0].sbp.edge_port
     new_list_item_url = SharePointClient().add_list_item(
         list_name="l3_core_service",
diff --git a/gso/workflows/l3_core_service/base_modify_l3_core_service.py b/gso/workflows/l3_core_service/base_modify_l3_core_service.py
index e38b0061d..250550189 100644
--- a/gso/workflows/l3_core_service/base_modify_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_modify_l3_core_service.py
@@ -35,8 +35,11 @@ def base_initial_input_form_generator(subscription_id: UUIDstr, service_name: L3
     """Get input about added, removed, and modified Access Ports."""
     subscription = SubscriptionModel.from_subscription(subscription_id)
     product_name = subscription.product.name
+
     service = getattr(subscription, service_name)
-    assert service is not None, f"{service_name} is not set on subscription"
+    if service is None:
+        msg = f"{service_name} is not set on subscription"
+        raise ValueError(msg)
 
     class OperationSelectionForm(FormPage):
         model_config = ConfigDict(title="Modify Edge Port")
@@ -396,7 +399,10 @@ def create_new_sbp(
         gs_id=sbp_gs_id,
     )
     service = getattr(subscription, service_name)
-    assert service is not None, f"{service_name} is not set on subscription"
+    if service is None:
+        msg = f"{service_name} is not set on subscription"
+        raise ValueError(msg)
+
     service.l3_core.ap_list.append(
         AccessPort.new(
             subscription_id=uuid4(),
@@ -413,6 +419,10 @@ def create_new_sbp(
 def remove_old_sbp(subscription: SubscriptionModel, removed_access_port: UUIDstr, service_name: str) -> State:
     """Remove old SBP product blocks from the specific L3 core service subscription."""
     service = getattr(subscription, service_name)
+    if service is None:
+        msg = f"{service_name} is not set on subscription"
+        raise ValueError(msg)
+
     service.l3_core.ap_list.remove(AccessPort.from_db(UUID(removed_access_port)))
 
     return {"subscription": subscription}
@@ -427,6 +437,10 @@ def modify_existing_sbp(
 ) -> State:
     """Update the subscription model."""
     service = getattr(subscription, service_name)
+    if service is None:
+        msg = f"{service_name} is not set on subscription"
+        raise ValueError(msg)
+
     current_ap = next(ap for ap in service.l3_core.ap_list if str(ap.subscription_instance_id) == modified_access_port)
     v4_peer = next(peer for peer in current_ap.sbp.bgp_session_list if IPFamily.V4UNICAST in peer.families)
     for attribute in modified_sbp["v4_bgp_peer"]:
diff --git a/gso/workflows/l3_core_service/copernicus/import_copernicus.py b/gso/workflows/l3_core_service/copernicus/import_copernicus.py
index 8ba33221c..10f179f3b 100644
--- a/gso/workflows/l3_core_service/copernicus/import_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/import_copernicus.py
@@ -17,7 +17,7 @@ def import_copernicus_subscription(subscription_id: UUIDstr) -> State:
     """Take an imported subscription, and turn it into a Copernicus Service subscription."""
     old_l3_core_service = ImportedCopernicus.from_subscription(subscription_id)
     new_product_id = get_product_id_by_name(ProductName.COPERNICUS)
-    new_subscription = Copernicus.from_other_product(old_l3_core_service, new_product_id)
+    new_subscription = Copernicus.from_other_product(old_l3_core_service, new_product_id)  # type: ignore[arg-type]
     new_subscription.description = (
         f"{new_subscription.product.name} service for {get_partner_by_id(new_subscription.customer_id).name}"
     )
diff --git a/gso/workflows/l3_core_service/copernicus/migrate_copernicus.py b/gso/workflows/l3_core_service/copernicus/migrate_copernicus.py
index 8159b3814..2719d4631 100644
--- a/gso/workflows/l3_core_service/copernicus/migrate_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/migrate_copernicus.py
@@ -15,7 +15,7 @@ from orchestrator.workflows.steps import resync, store_process_subscription, uns
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
 from pydantic_forms.types import FormGenerator, UUIDstr
 
-from gso.products import Copernicus  # TODO replace this everywhere with the correct product
+from gso.products.product_types.copernicus import Copernicus
 from gso.services.lso_client import lso_interaction
 from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, start_moodi, stop_moodi
 from gso.workflows.l3_core_service.base_migrate_l3_core_service import (
diff --git a/gso/workflows/l3_core_service/copernicus/modify_copernicus.py b/gso/workflows/l3_core_service/copernicus/modify_copernicus.py
index b8b9e5c1f..78b1982cf 100644
--- a/gso/workflows/l3_core_service/copernicus/modify_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/modify_copernicus.py
@@ -17,6 +17,7 @@ from gso.workflows.l3_core_service.base_modify_l3_core_service import (
 
 
 def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+    """Generate the initial input form for modifying a Copernicus subscription."""
     initial_generator = base_initial_input_form_generator(subscription_id, service_name="copernicus")
     initial_user_input = yield from initial_generator
 
diff --git a/gso/workflows/l3_core_service/create_imported_l3_core_service.py b/gso/workflows/l3_core_service/create_imported_l3_core_service.py
index 20ed8bb87..f7a7e86f7 100644
--- a/gso/workflows/l3_core_service/create_imported_l3_core_service.py
+++ b/gso/workflows/l3_core_service/create_imported_l3_core_service.py
@@ -84,8 +84,6 @@ def create_subscription(partner: str, service_type: L3CoreServiceType) -> dict:
     match service_type:
         case L3CoreServiceType.GEANT_IP:
             product_id = get_product_id_by_name(ProductName.IMPORTED_GEANT_IP)
-        case L3CoreServiceType.GWS:
-            product_id = get_product_id_by_name(ProductName.IMPORTED_GWS)
         case L3CoreServiceType.LHCONE:
             product_id = get_product_id_by_name(ProductName.IMPORTED_LHCONE)
         case L3CoreServiceType.COPERNICUS:
diff --git a/gso/workflows/l3_core_service/geant_ip/import_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/import_geant_ip.py
index cf32d55de..2de6b214f 100644
--- a/gso/workflows/l3_core_service/geant_ip/import_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/import_geant_ip.py
@@ -17,7 +17,7 @@ def import_geant_ip_subscription(subscription_id: UUIDstr) -> State:
     """Take an imported subscription, and turn it into a GÉANT IP subscription."""
     old_l3_core_service = ImportedGeantIP.from_subscription(subscription_id)
     new_product_id = get_product_id_by_name(ProductName.GEANT_IP)
-    new_subscription = GeantIP.from_other_product(old_l3_core_service, new_product_id)
+    new_subscription = GeantIP.from_other_product(old_l3_core_service, new_product_id)  # type: ignore[arg-type]
     new_subscription.description = (
         f"{new_subscription.product.name} service for {get_partner_by_id(new_subscription.customer_id).name}"
     )
diff --git a/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py
index 202fff374..ac20bb284 100644
--- a/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py
@@ -15,7 +15,7 @@ from orchestrator.workflows.steps import resync, store_process_subscription, uns
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
 from pydantic_forms.types import FormGenerator, UUIDstr
 
-from gso.products import GeantIP
+from gso.products.product_types.geant_ip import GeantIP
 from gso.services.lso_client import lso_interaction
 from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, start_moodi, stop_moodi
 from gso.workflows.l3_core_service.base_migrate_l3_core_service import (
diff --git a/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py
index 286447712..0ed723624 100644
--- a/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py
@@ -17,6 +17,7 @@ from gso.workflows.l3_core_service.base_modify_l3_core_service import (
 
 
 def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+    """Generate the initial input form for modifying a GÉANT IP subscription."""
     initial_generator = base_initial_input_form_generator(subscription_id, service_name="geant_ip")
     initial_user_input = yield from initial_generator
 
diff --git a/gso/workflows/l3_core_service/ias/create_ias.py b/gso/workflows/l3_core_service/ias/create_ias.py
index 53b54081f..b8c829bd2 100644
--- a/gso/workflows/l3_core_service/ias/create_ias.py
+++ b/gso/workflows/l3_core_service/ias/create_ias.py
@@ -35,7 +35,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
     initial_user_input = yield from initial_generator
 
     # Additional IAS step
-    class IASExtraForm(FormPage):  # TODO: Think about the order of this form when user is filling it
+    class IASExtraForm(FormPage):
         ias_flavor: IASFlavor | None = IASFlavor.IAS_PS_OPT_OUT
 
     ias_extra = yield IASExtraForm
@@ -90,4 +90,4 @@ def create_ias() -> StepList:
         >> prompt_sharepoint_checklist_url
         >> stop_moodi()
         >> done
-    )  # TODO think about making these steps as step list in base files
+    )
diff --git a/gso/workflows/l3_core_service/ias/import_ias.py b/gso/workflows/l3_core_service/ias/import_ias.py
index 9a85513db..703be98d2 100644
--- a/gso/workflows/l3_core_service/ias/import_ias.py
+++ b/gso/workflows/l3_core_service/ias/import_ias.py
@@ -17,7 +17,7 @@ def import_ias_subscription(subscription_id: UUIDstr) -> State:
     """Take an imported subscription, and turn it into an IAS subscription."""
     old_l3_core_service = ImportedIAS.from_subscription(subscription_id)
     new_product_id = get_product_id_by_name(ProductName.IAS)
-    new_subscription = IAS.from_other_product(old_l3_core_service, new_product_id)
+    new_subscription = IAS.from_other_product(old_l3_core_service, new_product_id)  # type: ignore[arg-type]
     new_subscription.description = (
         f"{new_subscription.product.name} service for {get_partner_by_id(new_subscription.customer_id).name}"
     )
diff --git a/gso/workflows/l3_core_service/ias/migrate_ias.py b/gso/workflows/l3_core_service/ias/migrate_ias.py
index fa0ea8ffd..ddc25ff64 100644
--- a/gso/workflows/l3_core_service/ias/migrate_ias.py
+++ b/gso/workflows/l3_core_service/ias/migrate_ias.py
@@ -15,7 +15,7 @@ from orchestrator.workflows.steps import resync, store_process_subscription, uns
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
 from pydantic_forms.types import FormGenerator, UUIDstr
 
-from gso.products import IAS
+from gso.products.product_types.ias import IAS
 from gso.services.lso_client import lso_interaction
 from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, start_moodi, stop_moodi
 from gso.workflows.l3_core_service.base_migrate_l3_core_service import (
diff --git a/gso/workflows/l3_core_service/ias/modify_ias.py b/gso/workflows/l3_core_service/ias/modify_ias.py
index 4c7212028..1b7dfb0c5 100644
--- a/gso/workflows/l3_core_service/ias/modify_ias.py
+++ b/gso/workflows/l3_core_service/ias/modify_ias.py
@@ -17,6 +17,7 @@ from gso.workflows.l3_core_service.base_modify_l3_core_service import (
 
 
 def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+    """Generate the initial input form for modifying an IAS subscription."""
     initial_generator = base_initial_input_form_generator(subscription_id, service_name="ias")
     initial_user_input = yield from initial_generator
 
diff --git a/gso/workflows/l3_core_service/import_l3_core_service.py b/gso/workflows/l3_core_service/import_l3_core_service.py
index bd68546af..fdb06170f 100644
--- a/gso/workflows/l3_core_service/import_l3_core_service.py
+++ b/gso/workflows/l3_core_service/import_l3_core_service.py
@@ -24,8 +24,6 @@ def import_l3_core_service_subscription(subscription_id: UUIDstr) -> State:
     match old_l3_core_service.l3_core_service_type:
         case L3CoreServiceType.IMPORTED_GEANT_IP:
             new_subscription_id = get_product_id_by_name(ProductName.GEANT_IP)
-        case L3CoreServiceType.IMPORTED_GWS:
-            new_subscription_id = get_product_id_by_name(ProductName.GWS)
         case L3CoreServiceType.IMPORTED_LHCONE:
             new_subscription_id = get_product_id_by_name(ProductName.LHCONE)
         case L3CoreServiceType.IMPORTED_COPERNICUS:
diff --git a/gso/workflows/l3_core_service/lhcone/import_lhcone.py b/gso/workflows/l3_core_service/lhcone/import_lhcone.py
index 6bd753770..8b90d0df9 100644
--- a/gso/workflows/l3_core_service/lhcone/import_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/import_lhcone.py
@@ -17,7 +17,7 @@ def import_lhcone_subscription(subscription_id: UUIDstr) -> State:
     """Take an imported subscription, and turn it into a LHCOne Service subscription."""
     old_l3_core_service = ImportedLHCOne.from_subscription(subscription_id)
     new_product_id = get_product_id_by_name(ProductName.LHCONE)
-    new_subscription = LHCOne.from_other_product(old_l3_core_service, new_product_id)
+    new_subscription = LHCOne.from_other_product(old_l3_core_service, new_product_id)  # type: ignore[arg-type]
     new_subscription.description = (
         f"{new_subscription.product.name} service for {get_partner_by_id(new_subscription.customer_id).name}"
     )
diff --git a/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py b/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py
index 8c6c2758a..15752a8e0 100644
--- a/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py
@@ -15,7 +15,7 @@ from orchestrator.workflows.steps import resync, store_process_subscription, uns
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
 from pydantic_forms.types import FormGenerator, UUIDstr
 
-from gso.products import LHCOne
+from gso.products.product_types.lhcone import LHCOne
 from gso.services.lso_client import lso_interaction
 from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, start_moodi, stop_moodi
 from gso.workflows.l3_core_service.base_migrate_l3_core_service import (
diff --git a/gso/workflows/l3_core_service/lhcone/modify_lhcone.py b/gso/workflows/l3_core_service/lhcone/modify_lhcone.py
index 600682264..c9e00b5ef 100644
--- a/gso/workflows/l3_core_service/lhcone/modify_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/modify_lhcone.py
@@ -17,6 +17,7 @@ from gso.workflows.l3_core_service.base_modify_l3_core_service import (
 
 
 def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+    """Generate the initial input form for modifying a LHCOne subscription."""
     initial_generator = base_initial_input_form_generator(subscription_id, service_name="lhcone")
     initial_user_input = yield from initial_generator
 
diff --git a/gso/workflows/l3_core_service/shared.py b/gso/workflows/l3_core_service/shared.py
index d3d6fc536..572884db4 100644
--- a/gso/workflows/l3_core_service/shared.py
+++ b/gso/workflows/l3_core_service/shared.py
@@ -3,12 +3,10 @@
 from typing import Literal
 
 from gso.products import ProductName
-from gso.products.product_types.copernicus import Copernicus, CopernicusInactive
-from gso.products.product_types.geant_ip import GeantIP, GeantIPInactive
-from gso.products.product_types.ias import IAS, IASInactive
-from gso.products.product_types.lhcone import LHCOne, LHCOneInactive
-
-L3InactiveProductTypes = IASInactive | LHCOneInactive | CopernicusInactive | GeantIPInactive
+from gso.products.product_types.copernicus import Copernicus
+from gso.products.product_types.geant_ip import GeantIP
+from gso.products.product_types.ias import IAS
+from gso.products.product_types.lhcone import LHCOne
 
 L3ProductNameTypes = Literal[ProductName.IAS, ProductName.LHCONE, ProductName.COPERNICUS, ProductName.GEANT_IP]
 
@@ -20,5 +18,69 @@ L3_PRODUCT_NAMES = [
 ]
 L3_CORE_SERVICE_PRODUCT_TYPES = [GeantIP.__name__, IAS.__name__, LHCOne.__name__, Copernicus.__name__]
 L3CoreServiceNameTypes = Literal["geant_ip", "ias", "lhcone", "copernicus"]
-L3_CORE_SERVICE_NAME_ATTRIBUTES = {"geant_ip", "ias", "lhcone", "copernicus"}
-assert len(L3_PRODUCT_NAMES) == len(L3_CORE_SERVICE_PRODUCT_TYPES) == len(L3_CORE_SERVICE_NAME_ATTRIBUTES)
+L3_SERVICE_NAME_ATTRIBUTES = {"geant_ip", "ias", "lhcone", "copernicus"}
+assert len(L3_PRODUCT_NAMES) == len(L3_CORE_SERVICE_PRODUCT_TYPES) == len(L3_SERVICE_NAME_ATTRIBUTES)  # noqa: S101
+
+L3_CREAT_IMPORTED_WF_MAP = {
+    ProductName.COPERNICUS: "create_imported_copernicus",
+    ProductName.GEANT_IP: "create_imported_geant_ip",
+    ProductName.IAS: "create_imported_ias",
+    ProductName.LHCONE: "create_imported_lhcone",
+}
+
+L3_CREATION_WF_MAP = {
+    ProductName.COPERNICUS: "create_copernicus",
+    ProductName.GEANT_IP: "create_geant_ip",
+    ProductName.IAS: "create_ias",
+    ProductName.LHCONE: "create_lhcone",
+}
+
+
+L3_ATTRIBUTES_MAP = {
+    ProductName.COPERNICUS: "copernicus",
+    ProductName.GEANT_IP: "geant_ip",
+    ProductName.IAS: "ias",
+    ProductName.LHCONE: "lhcone",
+}
+
+L3_MIGRATION_WF_MAP = {
+    ProductName.COPERNICUS: "migrate_copernicus",
+    ProductName.GEANT_IP: "migrate_geant_ip",
+    ProductName.IAS: "migrate_ias",
+    ProductName.LHCONE: "migrate_lhcone",
+}
+
+
+L3_MODIFICATION_WF_MAP = {
+    ProductName.COPERNICUS: "modify_copernicus",
+    ProductName.GEANT_IP: "modify_geant_ip",
+    ProductName.IAS: "modify_ias",
+    ProductName.LHCONE: "modify_lhcone",
+}
+
+
+L3_IMPORT_WF_MAP = {
+    ProductName.COPERNICUS: "import_copernicus",
+    ProductName.GEANT_IP: "import_geant_ip",
+    ProductName.IAS: "import_ias",
+    ProductName.LHCONE: "import_lhcone",
+}
+
+L3_VALIDATION_WF_MAP = {
+    ProductName.GEANT_IP: "validate_geant_ip",
+    ProductName.IAS: "validate_ias",
+    ProductName.LHCONE: "validate_lhcone",
+    ProductName.COPERNICUS: "validate_copernicus",
+}
+
+L3_PREFIX_VALIDATION_WF_MAP = {
+    ProductName.GEANT_IP: "validate_geant_ip_prefix_list",
+    ProductName.IAS: "validate_ias_prefix_list",
+}
+
+L3_TERMINATION_WF_MAP = {
+    ProductName.COPERNICUS: "terminate_copernicus",
+    ProductName.GEANT_IP: "terminate_geant_ip",
+    ProductName.IAS: "terminate_ias",
+    ProductName.LHCONE: "terminate_lhcone",
+}
diff --git a/test/fixtures/__init__.py b/test/fixtures/__init__.py
index 0bf505717..b2ee468ff 100644
--- a/test/fixtures/__init__.py
+++ b/test/fixtures/__init__.py
@@ -4,15 +4,15 @@ from test.fixtures.l3_core_service_fixtures import (
     access_port_factory,
     bfd_settings_factory,
     bgp_session_subscription_factory,
-    lhcone_subscription_factory,
+    copernicus_subscription_factory,
     geant_ip_subscription_factory,
     ias_subscription_factory,
-    copernicus_subscription_factory,
-    service_binding_port_factory,
-    l3_core_service_subscription_factory,
-    save_l3_core_subscription,
     l3_core_block_factory,
+    l3_core_service_subscription_factory,
+    lhcone_subscription_factory,
     make_subscription_factory,
+    save_l3_core_subscription,
+    service_binding_port_factory,
 )
 from test.fixtures.lan_switch_interconnect_fixtures import lan_switch_interconnect_subscription_factory
 from test.fixtures.layer_2_circuit_fixtures import layer_2_circuit_subscription_factory
@@ -28,22 +28,22 @@ __all__ = [
     "access_port_factory",
     "bfd_settings_factory",
     "bgp_session_subscription_factory",
+    "copernicus_subscription_factory",
     "edge_port_subscription_factory",
-    "iptrunk_side_subscription_factory",
-    "iptrunk_subscription_factory",
-    "lhcone_subscription_factory",
     "geant_ip_subscription_factory",
     "ias_subscription_factory",
-    "copernicus_subscription_factory",
-    "l3_core_service_subscription_factory",
-    "save_l3_core_subscription",
+    "iptrunk_side_subscription_factory",
+    "iptrunk_subscription_factory",
     "l3_core_block_factory",
-    "make_subscription_factory",
+    "l3_core_service_subscription_factory",
     "lan_switch_interconnect_subscription_factory",
     "layer_2_circuit_subscription_factory",
+    "lhcone_subscription_factory",
+    "make_subscription_factory",
     "office_router_subscription_factory",
     "opengear_subscription_factory",
     "router_subscription_factory",
+    "save_l3_core_subscription",
     "service_binding_port_factory",
     "site_subscription_factory",
     "super_pop_switch_subscription_factory",
diff --git a/test/fixtures/l3_core_service_fixtures.py b/test/fixtures/l3_core_service_fixtures.py
index fe4e75aa9..2c2c3bcba 100644
--- a/test/fixtures/l3_core_service_fixtures.py
+++ b/test/fixtures/l3_core_service_fixtures.py
@@ -14,10 +14,10 @@ from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPType
 from gso.products.product_blocks.ias import IASFlavor
 from gso.products.product_blocks.l3_core_service import AccessPort, L3CoreServiceBlockInactive
 from gso.products.product_blocks.service_binding_port import BFDSettings, ServiceBindingPort
-from gso.products.product_types.copernicus import ImportedCopernicusInactive, CopernicusInactive
+from gso.products.product_types.copernicus import CopernicusInactive, ImportedCopernicusInactive
 from gso.products.product_types.edge_port import EdgePort
-from gso.products.product_types.geant_ip import ImportedGeantIPInactive, GeantIPInactive
-from gso.products.product_types.ias import ImportedIASInactive, IASInactive
+from gso.products.product_types.geant_ip import GeantIPInactive, ImportedGeantIPInactive
+from gso.products.product_types.ias import IASInactive, ImportedIASInactive
 from gso.products.product_types.lhcone import ImportedLHCOneInactive, LHCOneInactive
 from gso.services.subscriptions import get_product_id_by_name
 from gso.utils.shared_enums import APType, SBPType
@@ -182,6 +182,7 @@ def make_subscription_factory(
         ap_list: list[AccessPort] | None = None,
         start_date="2023-05-24T00:00:00+00:00",
         status: SubscriptionLifecycle | None = None,
+        *,
         is_imported: bool | None = False,
     ) -> SubscriptionModel:
         partner = partner or partner_factory()
@@ -257,7 +258,7 @@ def geant_ip_subscription_factory(make_subscription_factory):
             imported_class=ImportedGeantIPInactive,
             native_class=GeantIPInactive,
             service_name="geant_ip",
-            *args,
+            *args,  # noqa: B026
             **kwargs,
         )
 
@@ -272,7 +273,7 @@ def copernicus_subscription_factory(make_subscription_factory):
             imported_class=ImportedCopernicusInactive,
             native_class=CopernicusInactive,
             service_name="copernicus",
-            *args,
+            *args,  # noqa: B026
             **kwargs,
         )
 
@@ -287,7 +288,7 @@ def lhcone_subscription_factory(make_subscription_factory):
             imported_class=ImportedLHCOneInactive,
             native_class=LHCOneInactive,
             service_name="lhcone",
-            *args,
+            *args,  # noqa: B026
             **kwargs,
         )
 
@@ -304,11 +305,13 @@ def l3_core_service_subscription_factory(
     def factory(product_name: ProductName, *args, **kwargs):
         if product_name == ProductName.IAS:
             return ias_subscription_factory(*args, **kwargs)
+
         if product_name == ProductName.GEANT_IP:
             return geant_ip_subscription_factory(*args, **kwargs)
+
         if product_name == ProductName.COPERNICUS:
             return copernicus_subscription_factory(*args, **kwargs)
-        if product_name == ProductName.LHCONE:
-            return lhcone_subscription_factory(*args, **kwargs)
+
+        return lhcone_subscription_factory(*args, **kwargs)
 
     return factory
diff --git a/test/workflows/edge_port/test_migrate_edge_port.py b/test/workflows/edge_port/test_migrate_edge_port.py
index 0f50af987..b6ee2f214 100644
--- a/test/workflows/edge_port/test_migrate_edge_port.py
+++ b/test/workflows/edge_port/test_migrate_edge_port.py
@@ -2,12 +2,11 @@ from unittest.mock import patch
 
 import pytest
 
-from gso.products import Layer2CircuitServiceType, ProductName
+from gso.products import Layer2CircuitServiceType
 from gso.products.product_types.edge_port import EdgePort
-from gso.products.product_types.l3_core_service import L3CoreServiceType
 from gso.products.product_types.router import Router
 from gso.utils.shared_enums import Vendor
-from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES
+from gso.workflows.l3_core_service.shared import L3_ATTRIBUTES_MAP, L3_PRODUCT_NAMES
 from test import USER_CONFIRM_EMPTY_FORM
 from test.services.conftest import MockedNetboxClient
 from test.workflows import (
@@ -20,14 +19,6 @@ from test.workflows import (
 )
 
 
-_L3_ATTRIBUTES_MAP = {
-    ProductName.COPERNICUS: "copernicus",
-    ProductName.GEANT_IP: "geant_ip",
-    ProductName.IAS: "ias",
-    ProductName.LHCONE: "lhcone",
-}
-
-
 @pytest.fixture()
 def _netbox_client_mock():
     with (
@@ -100,7 +91,7 @@ def test_successful_edge_port_migration(
     edge_port = edge_port_subscription_factory(partner=partner)
     for product_name in L3_PRODUCT_NAMES:
         l3_core_service = l3_core_service_subscription_factory(partner=partner, product_name=product_name)
-        sbp = getattr(l3_core_service, _L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list[0].sbp
+        sbp = getattr(l3_core_service, L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list[0].sbp
         sbp.edge_port = edge_port.edge_port
         l3_core_service.save()
 
diff --git a/test/workflows/l3_core_service/test_create_imported_l3_core_service.py b/test/workflows/l3_core_service/test_create_imported_l3_core_service.py
index bc1f3633f..b1190f4a3 100644
--- a/test/workflows/l3_core_service/test_create_imported_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_create_imported_l3_core_service.py
@@ -6,16 +6,9 @@ from gso.products import ProductName
 from gso.products.product_blocks.bgp_session import IPFamily
 from gso.products.product_blocks.ias import IASFlavor
 from gso.utils.shared_enums import SBPType
-from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES
+from gso.workflows.l3_core_service.shared import L3_CREAT_IMPORTED_WF_MAP, L3_PRODUCT_NAMES
 from test.workflows import assert_complete, extract_state, run_workflow
 
-_L3_CORE_CREATE_IMPORTED_WF_MAP = {
-    ProductName.COPERNICUS: "create_imported_copernicus",
-    ProductName.GEANT_IP: "create_imported_geant_ip",
-    ProductName.IAS: "create_imported_ias",
-    ProductName.LHCONE: "create_imported_lhcone",
-}
-
 
 @pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES)
 def test_create_imported_l3_core_service_success(faker, partner_factory, edge_port_subscription_factory, product_name):
@@ -83,7 +76,7 @@ def test_create_imported_l3_core_service_success(faker, partner_factory, edge_po
     input_data = (
         [creation_form_input_data, extra_ias_data] if product_name == ProductName.IAS else [creation_form_input_data]
     )
-    result, _, _ = run_workflow(f"{_L3_CORE_CREATE_IMPORTED_WF_MAP[product_name]}", input_data)
+    result, _, _ = run_workflow(f"{L3_CREAT_IMPORTED_WF_MAP[product_name]}", input_data)
     state = extract_state(result)
     subscription = SubscriptionModel.from_subscription(state["subscription_id"])
     assert_complete(result)
diff --git a/test/workflows/l3_core_service/test_create_l3_core_service.py b/test/workflows/l3_core_service/test_create_l3_core_service.py
index d74249d8f..e4e5fd391 100644
--- a/test/workflows/l3_core_service/test_create_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_create_l3_core_service.py
@@ -8,7 +8,7 @@ from gso.products import ProductName
 from gso.products.product_blocks.ias import IASFlavor
 from gso.services.subscriptions import get_product_id_by_name
 from gso.utils.shared_enums import APType
-from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES
+from gso.workflows.l3_core_service.shared import L3_ATTRIBUTES_MAP, L3_CREATION_WF_MAP, L3_PRODUCT_NAMES
 from test import USER_CONFIRM_EMPTY_FORM
 from test.services.conftest import MockedSharePointClient
 from test.workflows import (
@@ -20,20 +20,6 @@ from test.workflows import (
     run_workflow,
 )
 
-_L3_CORE_CREATION_WF_MAP = {
-    ProductName.COPERNICUS: "create_copernicus",
-    ProductName.GEANT_IP: "create_geant_ip",
-    ProductName.IAS: "create_ias",
-    ProductName.LHCONE: "create_lhcone",
-}
-
-_L3_ATTRIBUTES_MAP = {
-    ProductName.COPERNICUS: "copernicus",
-    ProductName.GEANT_IP: "geant_ip",
-    ProductName.IAS: "ias",
-    ProductName.LHCONE: "lhcone",
-}
-
 
 @pytest.fixture()
 def base_bgp_peer_input(faker):
@@ -108,7 +94,7 @@ def test_create_l3_core_service_success(
 
     lso_interaction_count = 7
 
-    result, process_stat, step_log = run_workflow(_L3_CORE_CREATION_WF_MAP[product_name], form_input_data)
+    result, process_stat, step_log = run_workflow(L3_CREATION_WF_MAP[product_name], form_input_data)
 
     for _ in range(lso_interaction_count):
         result, step_log = assert_lso_interaction_success(result, process_stat, step_log)
@@ -122,7 +108,7 @@ def test_create_l3_core_service_success(
     subscription = SubscriptionModel.from_subscription(state["subscription_id"])
     assert mock_lso_client.call_count == lso_interaction_count + 1
     assert subscription.status == SubscriptionLifecycle.ACTIVE
-    l3_core = getattr(subscription, _L3_ATTRIBUTES_MAP[product_name]).l3_core
+    l3_core = getattr(subscription, L3_ATTRIBUTES_MAP[product_name]).l3_core
     assert len(l3_core.ap_list) == 1
     assert str(l3_core.ap_list[0].sbp.edge_port.owner_subscription_id) == form_input_data[2]["edge_port"]["edge_port"]
     assert l3_core.ap_list[0].sbp.gs_id == "GS-12345"
diff --git a/test/workflows/l3_core_service/test_import_l3_core_service.py b/test/workflows/l3_core_service/test_import_l3_core_service.py
index 8a1af3007..7bb36d27f 100644
--- a/test/workflows/l3_core_service/test_import_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_import_l3_core_service.py
@@ -2,18 +2,10 @@ import pytest
 from orchestrator.domain import SubscriptionModel
 from orchestrator.types import SubscriptionLifecycle
 
-from gso.products import ProductName
-from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES
+from gso.workflows.l3_core_service.shared import L3_IMPORT_WF_MAP, L3_PRODUCT_NAMES
 from test.fixtures.l3_core_service_fixtures import PRODUCT_IMPORTED_MAP
 from test.workflows import assert_complete, run_workflow
 
-_L3_CORE_IMPORT_WF_MAP = {
-    ProductName.COPERNICUS: "import_copernicus",
-    ProductName.GEANT_IP: "import_geant_ip",
-    ProductName.IAS: "import_ias",
-    ProductName.LHCONE: "import_lhcone",
-}
-
 
 @pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES)
 @pytest.mark.workflow()
@@ -22,7 +14,7 @@ def test_import_l3_core_service_success(l3_core_service_subscription_factory, pr
     assert imported_subscription.product.name == PRODUCT_IMPORTED_MAP[product_name]
     imported_l3_core_service = str(imported_subscription.subscription_id)
 
-    result, _, _ = run_workflow(_L3_CORE_IMPORT_WF_MAP[product_name], [{"subscription_id": imported_l3_core_service}])
+    result, _, _ = run_workflow(L3_IMPORT_WF_MAP[product_name], [{"subscription_id": imported_l3_core_service}])
     assert_complete(result)
 
     subscription = SubscriptionModel.from_subscription(imported_l3_core_service)
diff --git a/test/workflows/l3_core_service/test_migrate_l3_core_service.py b/test/workflows/l3_core_service/test_migrate_l3_core_service.py
index fdb98b609..d4691e8df 100644
--- a/test/workflows/l3_core_service/test_migrate_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_migrate_l3_core_service.py
@@ -3,10 +3,9 @@ from unittest.mock import patch
 import pytest
 from orchestrator.domain import SubscriptionModel
 
-from gso.products import ProductName
 from gso.products.product_types.edge_port import EdgePort
 from gso.utils.shared_enums import APType
-from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES
+from gso.workflows.l3_core_service.shared import L3_ATTRIBUTES_MAP, L3_MIGRATION_WF_MAP, L3_PRODUCT_NAMES
 from test import USER_CONFIRM_EMPTY_FORM
 from test.workflows import (
     assert_complete,
@@ -17,20 +16,6 @@ from test.workflows import (
     run_workflow,
 )
 
-_L3_CORE_MIGRATE_WF_MAP = {
-    ProductName.COPERNICUS: "migrate_copernicus",
-    ProductName.GEANT_IP: "migrate_geant_ip",
-    ProductName.IAS: "migrate_ias",
-    ProductName.LHCONE: "migrate_lhcone",
-}
-
-_L3_ATTRIBUTES_MAP = {
-    ProductName.COPERNICUS: "copernicus",
-    ProductName.GEANT_IP: "geant_ip",
-    ProductName.IAS: "ias",
-    ProductName.LHCONE: "lhcone",
-}
-
 
 @pytest.mark.workflow()
 @pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES)
@@ -52,7 +37,7 @@ def test_migrate_l3_core_service_success(
     )
     destination_edge_port = str(edge_port_subscription_factory(partner=partner).subscription_id)
     subscription = SubscriptionModel.from_subscription(subscription_id)
-    l3_core = getattr(subscription, _L3_ATTRIBUTES_MAP[product_name]).l3_core
+    l3_core = getattr(subscription, L3_ATTRIBUTES_MAP[product_name]).l3_core
     form_input_data = [
         {"subscription_id": subscription_id},
         {
@@ -63,7 +48,7 @@ def test_migrate_l3_core_service_success(
         {},
     ]
 
-    result, process_stat, step_log = run_workflow(_L3_CORE_MIGRATE_WF_MAP[product_name], form_input_data)
+    result, process_stat, step_log = run_workflow(L3_MIGRATION_WF_MAP[product_name], form_input_data)
 
     for _ in range(5):
         result, step_log = assert_lso_interaction_success(result, process_stat, step_log)
@@ -80,7 +65,7 @@ def test_migrate_l3_core_service_success(
     subscription = SubscriptionModel.from_subscription(state["subscription_id"])
     assert mock_execute_playbook.call_count == 11
     assert subscription.insync
-    l3_core = getattr(subscription, _L3_ATTRIBUTES_MAP[product_name]).l3_core
+    l3_core = getattr(subscription, L3_ATTRIBUTES_MAP[product_name]).l3_core
     assert len(l3_core.ap_list) == 1
     assert str(l3_core.ap_list[0].sbp.edge_port.owner_subscription_id) == destination_edge_port
 
@@ -103,7 +88,7 @@ def test_migrate_l3_core_service_scoped_emission(
         partner=partner, product_name=product_name, ap_list=custom_ap_list
     )
     destination_edge_port_id = str(edge_port_subscription_factory(partner=partner).subscription_id)
-    ap_list = getattr(subscription, _L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
+    ap_list = getattr(subscription, L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
     source_edge_port = ap_list[3].sbp.edge_port.owner_subscription_id
 
     form_input_data = [
@@ -116,7 +101,7 @@ def test_migrate_l3_core_service_scoped_emission(
         {},
     ]
 
-    result, process_stat, step_log = run_workflow(_L3_CORE_MIGRATE_WF_MAP[product_name], form_input_data)
+    result, process_stat, step_log = run_workflow(L3_MIGRATION_WF_MAP[product_name], form_input_data)
 
     result, step_log = assert_lso_interaction_success(result, process_stat, step_log)
 
@@ -143,10 +128,8 @@ def test_migrate_l3_core_service_scoped_emission(
         == transmitted_destination_ep_fqdn
     )
     assert (
-        state["subscription"][_L3_ATTRIBUTES_MAP[product_name]]["l3_core"]
-        == state["__old_subscriptions__"][str(subscription.subscription_id)][_L3_ATTRIBUTES_MAP[product_name]][
-            "l3_core"
-        ]
+        state["subscription"][L3_ATTRIBUTES_MAP[product_name]]["l3_core"]
+        == state["__old_subscriptions__"][str(subscription.subscription_id)][L3_ATTRIBUTES_MAP[product_name]]["l3_core"]
     )  # Subscription is unchanged for now
 
     for _ in range(4):
@@ -159,6 +142,6 @@ def test_migrate_l3_core_service_scoped_emission(
     subscription = SubscriptionModel.from_subscription(state["subscription_id"])
     assert mock_execute_playbook.call_count == 11
     assert subscription.insync
-    ap_list = getattr(subscription, _L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
+    ap_list = getattr(subscription, L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
     assert len(ap_list) == 5
     assert str(ap_list[3].sbp.edge_port.owner_subscription_id) == destination_edge_port_id
diff --git a/test/workflows/l3_core_service/test_modify_l3_core_service.py b/test/workflows/l3_core_service/test_modify_l3_core_service.py
index 7022ea300..a819b6f9d 100644
--- a/test/workflows/l3_core_service/test_modify_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_modify_l3_core_service.py
@@ -1,44 +1,29 @@
 import pytest
 from orchestrator.domain import SubscriptionModel
 
-from gso.products import ProductName
 from gso.products.product_blocks.bgp_session import IPFamily
 from gso.utils.shared_enums import APType
 from gso.workflows.l3_core_service.modify_l3_core_service import Operation
-from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES
+from gso.workflows.l3_core_service.shared import L3_ATTRIBUTES_MAP, L3_MODIFICATION_WF_MAP, L3_PRODUCT_NAMES
 from test.workflows import extract_state, run_workflow
 
-_L3_CORE_MODIFY_WF_MAP = {
-    ProductName.COPERNICUS: "modify_copernicus",
-    ProductName.GEANT_IP: "modify_geant_ip",
-    ProductName.IAS: "modify_ias",
-    ProductName.LHCONE: "modify_lhcone",
-}
-
-_L3_ATTRIBUTES_MAP = {
-    ProductName.COPERNICUS: "copernicus",
-    ProductName.GEANT_IP: "geant_ip",
-    ProductName.IAS: "ias",
-    ProductName.LHCONE: "lhcone",
-}
-
 
 @pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES)
 @pytest.mark.workflow()
 def test_modify_l3_core_service_remove_edge_port_success(faker, l3_core_service_subscription_factory, product_name):
     subscription = l3_core_service_subscription_factory(product_name=product_name)
-    access_port = getattr(subscription, _L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list[0]
+    access_port = getattr(subscription, L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list[0]
     input_form_data = [
         {"subscription_id": str(subscription.subscription_id)},
         {"tt_number": faker.tt_number(), "operation": Operation.REMOVE},
         {"access_port": str(access_port.subscription_instance_id)},
     ]
 
-    result, _, _ = run_workflow(_L3_CORE_MODIFY_WF_MAP[product_name], input_form_data)
+    result, _, _ = run_workflow(L3_MODIFICATION_WF_MAP[product_name], input_form_data)
 
     state = extract_state(result)
     subscription = SubscriptionModel.from_subscription(state["subscription_id"])
-    ap_list = getattr(subscription, _L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
+    ap_list = getattr(subscription, L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
     assert len(ap_list) == 1
     assert ap_list[0].ap_type == APType.BACKUP
 
@@ -85,11 +70,11 @@ def test_modify_l3_core_service_add_new_edge_port_success(
         },
     ]
 
-    result, _, _ = run_workflow(_L3_CORE_MODIFY_WF_MAP[product_name], input_form_data)
+    result, _, _ = run_workflow(L3_MODIFICATION_WF_MAP[product_name], input_form_data)
 
     state = extract_state(result)
     subscription = SubscriptionModel.from_subscription(state["subscription_id"])
-    ap_list = getattr(subscription, _L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
+    ap_list = getattr(subscription, L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
     new_ap = ap_list[-1]
     assert new_ap.ap_type == APType.BACKUP
     assert new_ap.sbp.gs_id == input_form_data[2]["gs_id"]
@@ -152,7 +137,7 @@ def test_modify_l3_core_service_modify_edge_port_success(
 ):
     subscription = l3_core_service_subscription_factory(product_name=product_name)
     new_sbp_data = sbp_input_form_data()
-    ap_list = getattr(subscription, _L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
+    ap_list = getattr(subscription, L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
     input_form_data = [
         {"subscription_id": str(subscription.subscription_id)},
         {"tt_number": faker.tt_number(), "operation": Operation.EDIT},
@@ -160,11 +145,11 @@ def test_modify_l3_core_service_modify_edge_port_success(
         {**new_sbp_data},
     ]
 
-    result, _, _ = run_workflow(_L3_CORE_MODIFY_WF_MAP[product_name], input_form_data)
+    result, _, _ = run_workflow(L3_MODIFICATION_WF_MAP[product_name], input_form_data)
 
     state = extract_state(result)
     subscription = SubscriptionModel.from_subscription(state["subscription_id"])
-    ap_list = getattr(subscription, _L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
+    ap_list = getattr(subscription, L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
     assert len(ap_list) == 2
 
     access_port = ap_list[0]
diff --git a/test/workflows/l3_core_service/test_terminate_l3_core_service.py b/test/workflows/l3_core_service/test_terminate_l3_core_service.py
index 3639fd6ec..6fd3e68f3 100644
--- a/test/workflows/l3_core_service/test_terminate_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_terminate_l3_core_service.py
@@ -1,24 +1,16 @@
 import pytest
 from orchestrator.domain import SubscriptionModel
 
-from gso.products import ProductName
-from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES
+from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES, L3_TERMINATION_WF_MAP
 from test.workflows import assert_complete, extract_state, run_workflow
 
-_L3_CORE_MODIFY_WF_MAP = {
-    ProductName.COPERNICUS: "terminate_copernicus",
-    ProductName.GEANT_IP: "terminate_geant_ip",
-    ProductName.IAS: "terminate_ias",
-    ProductName.LHCONE: "terminate_lhcone",
-}
-
 
 @pytest.mark.workflow()
 @pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES)
 def test_terminate_l3_core_service(product_name, l3_core_service_subscription_factory, faker):
     subscription_id = str(l3_core_service_subscription_factory(product_name=product_name).subscription_id)
     initial_form_data = [{"subscription_id": subscription_id}, {"tt_number": faker.tt_number()}]
-    result, _, _ = run_workflow(_L3_CORE_MODIFY_WF_MAP[product_name], initial_form_data)
+    result, _, _ = run_workflow(L3_TERMINATION_WF_MAP[product_name], initial_form_data)
     assert_complete(result)
 
     state = extract_state(result)
diff --git a/test/workflows/l3_core_service/test_validate_l3_core_service.py b/test/workflows/l3_core_service/test_validate_l3_core_service.py
index 738932255..ad6a06ca2 100644
--- a/test/workflows/l3_core_service/test_validate_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_validate_l3_core_service.py
@@ -3,28 +3,17 @@ from unittest.mock import patch
 import pytest
 from orchestrator.domain import SubscriptionModel
 
-from gso.products import ProductName
-from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES
+from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES, L3_VALIDATION_WF_MAP
 from test.workflows import assert_complete, assert_lso_success, extract_state, run_workflow
 
 
-_PRODUCT_NAME_VALIDATION_WF_MAP = {
-    ProductName.GEANT_IP: "validate_geant_ip",
-    ProductName.IAS: "validate_ias",
-    ProductName.LHCONE: "validate_lhcone",
-    ProductName.COPERNICUS: "validate_copernicus",
-}
-
-
 @pytest.mark.workflow()
 @patch("gso.services.lso_client._send_request")
 @pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES)
 def test_validate_l3_core_service(mock_lso_interaction, l3_core_service_subscription_factory, faker, product_name):
     subscription_id = str(l3_core_service_subscription_factory(product_name=product_name).subscription_id)
     initial_l3_core_service_data = [{"subscription_id": subscription_id}]
-    result, process_stat, step_log = run_workflow(
-        _PRODUCT_NAME_VALIDATION_WF_MAP[product_name], initial_l3_core_service_data
-    )
+    result, process_stat, step_log = run_workflow(L3_VALIDATION_WF_MAP[product_name], initial_l3_core_service_data)
     result, step_log = assert_lso_success(result, process_stat, step_log)
     result, _ = assert_lso_success(result, process_stat, step_log)
     assert_complete(result)
diff --git a/test/workflows/l3_core_service/test_validate_prefix_list.py b/test/workflows/l3_core_service/test_validate_prefix_list.py
index 12e0c561d..c3bdbcaf1 100644
--- a/test/workflows/l3_core_service/test_validate_prefix_list.py
+++ b/test/workflows/l3_core_service/test_validate_prefix_list.py
@@ -3,7 +3,9 @@ from unittest.mock import patch
 import pytest
 from orchestrator.domain import SubscriptionModel
 
-from gso.products import GeantIP, ProductName
+from gso.products import ProductName
+from gso.products.product_types.geant_ip import GeantIP
+from gso.workflows.l3_core_service.shared import L3_PREFIX_VALIDATION_WF_MAP
 from test import USER_CONFIRM_EMPTY_FORM
 from gso.utils.shared_enums import Vendor
 from test.workflows import (
@@ -18,12 +20,6 @@ from test.workflows import (
 )
 
 
-_PRODUCT_NAME_VALIDATION_WF_MAP = {
-    ProductName.GEANT_IP: "validate_geant_ip_prefix_list",
-    ProductName.IAS: "validate_ias_prefix_list",
-}
-
-
 @pytest.mark.workflow()
 @patch("gso.services.lso_client._send_request")
 @pytest.mark.parametrize("product_name", [ProductName.IAS, ProductName.GEANT_IP])
@@ -32,7 +28,7 @@ def test_validate_prefix_list_success(mock_lso_interaction, l3_core_service_subs
     initial_l3_core_service_data = [{"subscription_id": subscription_id}]
     # Run the workflow and extract results
     result, process_stat, step_log = run_workflow(
-        _PRODUCT_NAME_VALIDATION_WF_MAP[product_name], initial_l3_core_service_data
+        L3_PREFIX_VALIDATION_WF_MAP[product_name], initial_l3_core_service_data
     )
 
     result, step_log = assert_lso_success(result, process_stat, step_log)
-- 
GitLab


From d65e3a41d611583319e3b4820bd1fc2f7ed51b51 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Fri, 28 Mar 2025 17:35:54 +0100
Subject: [PATCH 58/87] rebase with develop and add missing logic

---
 ...bb3c4411ea_add_new_l3_core_services_wfs.py |  6 ---
 ...c38adde1a18e_update_wf_in_process_table.py |  1 -
 gso/services/subscriptions.py                 |  2 -
 gso/workflows/__init__.py                     |  2 -
 .../base_validate_prefix_list.py              |  7 ++-
 .../ias/validate_prefix_list.py               | 52 -------------------
 gso/workflows/l3_core_service/shared.py       |  5 --
 .../test_validate_prefix_list.py              | 27 +++++-----
 8 files changed, 17 insertions(+), 85 deletions(-)
 delete mode 100644 gso/workflows/l3_core_service/ias/validate_prefix_list.py

diff --git a/gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py b/gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py
index 2a4242983..fb2cc89b1 100644
--- a/gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py
+++ b/gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py
@@ -58,12 +58,6 @@ new_workflows = [
         "description": "Validate IAS",
         "product_type": "IAS"
     },
-    {
-        "name": "validate_ias_prefix_list",
-        "target": "SYSTEM",
-        "description": "Validate IAS Prefix-List",
-        "product_type": "IAS"
-    },
     {
         "name": "create_geant_ip",
         "target": "CREATE",
diff --git a/gso/migrations/versions/2025-03-26_c38adde1a18e_update_wf_in_process_table.py b/gso/migrations/versions/2025-03-26_c38adde1a18e_update_wf_in_process_table.py
index b370e3fcc..d2dc29f82 100644
--- a/gso/migrations/versions/2025-03-26_c38adde1a18e_update_wf_in_process_table.py
+++ b/gso/migrations/versions/2025-03-26_c38adde1a18e_update_wf_in_process_table.py
@@ -30,7 +30,6 @@ def upgrade() -> None:
         {"new_workflow": "validate_ias", "old_workflow": "validate_l3_core_service", "product_type": "IAS"},
         {"new_workflow": "create_imported_ias", "old_workflow": "create_imported_l3_core_service", "product_type": "IAS"},
         {"new_workflow": "import_ias", "old_workflow": "import_l3_core_service", "product_type": "IAS"},
-        {"new_workflow": "validate_ias_prefix_list", "old_workflow": "validate_prefix_list", "product_type": "IAS"},
 
         # GeantIP updates
         {"new_workflow": "create_geant_ip", "old_workflow": "create_l3_core_service", "product_type": "GeantIP"},
diff --git a/gso/services/subscriptions.py b/gso/services/subscriptions.py
index 7cd1775a3..6ae731410 100644
--- a/gso/services/subscriptions.py
+++ b/gso/services/subscriptions.py
@@ -211,8 +211,6 @@ def get_active_l3_services_linked_to_edge_port(edge_port_id: UUIDstr) -> list[Su
 def get_active_layer_3_services_on_router(subscription_id: UUID) -> list[SubscriptionModel]:
     """Get all active Layer 3 services that insist on a given router `subscription_id`.
 
-    TODO: Update this method when refactoring layer 3 services.
-
     Args:
         subscription_id: Subscription ID of a Router.
 
diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py
index 7dd0ebf53..f69279021 100644
--- a/gso/workflows/__init__.py
+++ b/gso/workflows/__init__.py
@@ -126,8 +126,6 @@ LazyWorkflowInstance("gso.workflows.l3_core_service.ias.migrate_ias", "migrate_i
 LazyWorkflowInstance("gso.workflows.l3_core_service.ias.create_imported_ias", "create_imported_ias")
 LazyWorkflowInstance("gso.workflows.l3_core_service.ias.import_ias", "import_ias")
 LazyWorkflowInstance("gso.workflows.l3_core_service.ias.validate_ias", "validate_ias")
-LazyWorkflowInstance("gso.workflows.l3_core_service.ias.validate_prefix_list", "validate_ias_prefix_list")
-
 
 #  Copernicus workflows
 LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.create_copernicus", "create_copernicus")
diff --git a/gso/workflows/l3_core_service/base_validate_prefix_list.py b/gso/workflows/l3_core_service/base_validate_prefix_list.py
index a798d9053..b55e056eb 100644
--- a/gso/workflows/l3_core_service/base_validate_prefix_list.py
+++ b/gso/workflows/l3_core_service/base_validate_prefix_list.py
@@ -12,15 +12,18 @@ from pydantic_forms.validators import Label
 
 from gso.services.lso_client import LSOState
 from gso.services.partners import get_partner_by_id
+from gso.utils.shared_enums import Vendor
 from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes
 
 
 @step("Prepare list of all Access Ports")
 def build_fqdn_list(subscription_id: UUIDstr, service_name: L3CoreServiceNameTypes) -> State:
-    """Build the list of all FQDNs that are in the list of access ports of a L3 Core Service subscription."""
+    """Build the list of all FQDNs in the access ports of L3 Core Service subscription, excluding Juniper devices."""
     subscription = SubscriptionModel.from_subscription(subscription_id)
     ap_list = getattr(subscription, service_name).l3_core.ap_list
-    ap_fqdn_list = [ap.sbp.edge_port.node.router_fqdn for ap in ap_list]
+    ap_fqdn_list = [
+        ap.sbp.edge_port.node.router_fqdn for ap in ap_list if ap.sbp.edge_port.node.vendor != Vendor.JUNIPER
+    ]
     return {"ap_fqdn_list": ap_fqdn_list, "subscription": subscription}
 
 
diff --git a/gso/workflows/l3_core_service/ias/validate_prefix_list.py b/gso/workflows/l3_core_service/ias/validate_prefix_list.py
deleted file mode 100644
index fe00af651..000000000
--- a/gso/workflows/l3_core_service/ias/validate_prefix_list.py
+++ /dev/null
@@ -1,52 +0,0 @@
-"""Prefix Validation workflow for IAS subscription objects."""
-
-from orchestrator.targets import Target
-from orchestrator.workflow import StepList, begin, conditional, done, step, workflow
-from orchestrator.workflows.steps import resync, store_process_subscription, unsync
-from orchestrator.workflows.utils import wrap_modify_initial_input_form
-from pydantic_forms.types import State
-
-from gso.services.lso_client import anonymous_lso_interaction, lso_interaction
-from gso.workflows.l3_core_service.base_validate_prefix_list import (
-    await_operator,
-    build_fqdn_list,
-    deploy_prefix_lists_dry,
-    deploy_prefix_lists_real,
-    evaluate_result_has_diff,
-    validate_prefix_lists_dry,
-)
-
-
-@step("Inject service name")
-def inject_srvice_name() -> State:
-    """Inject the service name into the subscription."""
-    return {"service_name": "ias"}
-
-
-@workflow("Validate IAS Prefix-List", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None)))
-def validate_ias_prefix_list() -> StepList:
-    """Validate prefix-lists for an existing IAS subscription."""
-    prefix_list_has_drifted = conditional(lambda state: bool(state["prefix_list_drift"]))
-
-    redeploy_prefix_list_steps = (
-        begin
-        >> unsync
-        >> await_operator
-        >> lso_interaction(deploy_prefix_lists_dry)
-        >> lso_interaction(deploy_prefix_lists_real)
-        >> resync
-    )
-    prefix_list_validation_steps = (
-        begin
-        >> anonymous_lso_interaction(validate_prefix_lists_dry, evaluate_result_has_diff)
-        >> prefix_list_has_drifted(redeploy_prefix_list_steps)
-    )
-
-    return (
-        begin
-        >> inject_srvice_name
-        >> store_process_subscription(Target.SYSTEM)
-        >> build_fqdn_list
-        >> prefix_list_validation_steps
-        >> done
-    )
diff --git a/gso/workflows/l3_core_service/shared.py b/gso/workflows/l3_core_service/shared.py
index 572884db4..7d0fe6994 100644
--- a/gso/workflows/l3_core_service/shared.py
+++ b/gso/workflows/l3_core_service/shared.py
@@ -73,11 +73,6 @@ L3_VALIDATION_WF_MAP = {
     ProductName.COPERNICUS: "validate_copernicus",
 }
 
-L3_PREFIX_VALIDATION_WF_MAP = {
-    ProductName.GEANT_IP: "validate_geant_ip_prefix_list",
-    ProductName.IAS: "validate_ias_prefix_list",
-}
-
 L3_TERMINATION_WF_MAP = {
     ProductName.COPERNICUS: "terminate_copernicus",
     ProductName.GEANT_IP: "terminate_geant_ip",
diff --git a/test/workflows/l3_core_service/test_validate_prefix_list.py b/test/workflows/l3_core_service/test_validate_prefix_list.py
index c3bdbcaf1..e3a6bd8bb 100644
--- a/test/workflows/l3_core_service/test_validate_prefix_list.py
+++ b/test/workflows/l3_core_service/test_validate_prefix_list.py
@@ -1,13 +1,10 @@
 from unittest.mock import patch
 
 import pytest
-from orchestrator.domain import SubscriptionModel
 
-from gso.products import ProductName
 from gso.products.product_types.geant_ip import GeantIP
-from gso.workflows.l3_core_service.shared import L3_PREFIX_VALIDATION_WF_MAP
-from test import USER_CONFIRM_EMPTY_FORM
 from gso.utils.shared_enums import Vendor
+from test import USER_CONFIRM_EMPTY_FORM
 from test.workflows import (
     assert_complete,
     assert_lso_failure,
@@ -22,23 +19,23 @@ from test.workflows import (
 
 @pytest.mark.workflow()
 @patch("gso.services.lso_client._send_request")
-@pytest.mark.parametrize("product_name", [ProductName.IAS, ProductName.GEANT_IP])
-def test_validate_prefix_list_success(mock_lso_interaction, l3_core_service_subscription_factory, faker, product_name):
-    subscription_id = str(l3_core_service_subscription_factory(product_name=product_name).subscription_id)
+def test_validate_prefix_list_success(mock_lso_interaction, geant_ip_subscription_factory, faker):
+    subscription_id = str(geant_ip_subscription_factory().subscription_id)
     initial_l3_core_service_data = [{"subscription_id": subscription_id}]
     # Run the workflow and extract results
-    result, process_stat, step_log = run_workflow(
-        L3_PREFIX_VALIDATION_WF_MAP[product_name], initial_l3_core_service_data
-    )
+    result, process_stat, step_log = run_workflow("validate_geant_ip_prefix_list", initial_l3_core_service_data)
 
     result, step_log = assert_lso_success(result, process_stat, step_log)
     assert_complete(result)
     # Extract the state and validate subscription attributes
     state = extract_state(result)
     subscription_id = state["subscription_id"]
-    subscription = SubscriptionModel.from_subscription(subscription_id)
+    subscription = GeantIP.from_subscription(subscription_id)
     assert subscription.status == "active"
     assert subscription.insync is True
+    # Verify the subscription has no Juniper devices
+    for ap in subscription.geant_ip.l3_core.ap_list:
+        assert ap.sbp.edge_port.node.vendor != Vendor.JUNIPER
     # Verify the number of LSO interactions
     assert mock_lso_interaction.call_count == 1
 
@@ -77,19 +74,19 @@ def test_validate_prefix_list_with_diff(mock_lso_interaction, geant_ip_subscript
 
 @pytest.mark.workflow()
 @patch("gso.services.lso_client._send_request")
-def test_validate_prefix_list_without_diff(mock_lso_interaction, l3_core_service_subscription_factory, faker):
+def test_validate_prefix_list_without_diff(mock_lso_interaction, geant_ip_subscription_factory, faker):
     """Test case where playbook_has_diff does not qualify and skips additional steps."""
-    subscription_id = str(l3_core_service_subscription_factory(product_name=ProductName.GEANT_IP).subscription_id)
+    subscription_id = str(geant_ip_subscription_factory().subscription_id)
     initial_l3_core_service_data = [{"subscription_id": subscription_id}]
     # Run the workflow and extract results
-    result, process_stat, step_log = run_workflow("validate_ias_prefix_list", initial_l3_core_service_data)
+    result, process_stat, step_log = run_workflow("validate_geant_ip_prefix_list", initial_l3_core_service_data)
     # Assert LSO success and workflow completion
     result, step_log = assert_lso_success(result, process_stat, step_log)
     assert_complete(result)
     # Extract the state and validate subscription attributes
     state = extract_state(result)
     subscription_id = state["subscription_id"]
-    subscription = SubscriptionModel.from_subscription(subscription_id)
+    subscription = GeantIP.from_subscription(subscription_id)
     assert subscription.status == "active"
     assert subscription.insync is True
     # Verify the number of LSO interactions
-- 
GitLab


From 72a979839d6a4b94a9cf17bbb61c1897d2060ca7 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Fri, 28 Mar 2025 17:39:30 +0100
Subject: [PATCH 59/87] remove all no needed l3_core_* files

---
 .../create_imported_l3_core_service.py        | 149 ------
 .../l3_core_service/create_l3_core_service.py | 389 --------------
 .../l3_core_service/import_l3_core_service.py |  51 --
 .../migrate_l3_core_service.py                | 419 ----------------
 .../l3_core_service/modify_l3_core_service.py | 474 ------------------
 gso/workflows/l3_core_service/shared.py       |   6 +-
 .../test_modify_l3_core_service.py            |   2 +-
 7 files changed, 4 insertions(+), 1486 deletions(-)
 delete mode 100644 gso/workflows/l3_core_service/create_imported_l3_core_service.py
 delete mode 100644 gso/workflows/l3_core_service/create_l3_core_service.py
 delete mode 100644 gso/workflows/l3_core_service/import_l3_core_service.py
 delete mode 100644 gso/workflows/l3_core_service/migrate_l3_core_service.py
 delete mode 100644 gso/workflows/l3_core_service/modify_l3_core_service.py

diff --git a/gso/workflows/l3_core_service/create_imported_l3_core_service.py b/gso/workflows/l3_core_service/create_imported_l3_core_service.py
deleted file mode 100644
index f7a7e86f7..000000000
--- a/gso/workflows/l3_core_service/create_imported_l3_core_service.py
+++ /dev/null
@@ -1,149 +0,0 @@
-"""A creation workflow for adding an existing L3 Core Service to the service database."""
-
-from uuid import uuid4
-
-from orchestrator import workflow
-from orchestrator.forms import SubmitFormPage
-from orchestrator.targets import Target
-from orchestrator.types import SubscriptionLifecycle
-from orchestrator.utils.errors import ProcessFailureError
-from orchestrator.workflow import StepList, begin, done, step
-from orchestrator.workflows.steps import resync, set_status, store_process_subscription
-from pydantic import BaseModel, NonNegativeInt
-from pydantic_forms.types import FormGenerator, UUIDstr
-
-from gso.products import ProductName
-from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPTypes
-from gso.products.product_blocks.l3_core_service import AccessPortInactive
-from gso.products.product_blocks.service_binding_port import BFDSettings, ServiceBindingPortInactive
-from gso.products.product_types.edge_port import EdgePort
-from gso.products.product_types.l3_core_service import ImportedL3CoreServiceInactive, L3CoreServiceType
-from gso.services.partners import get_partner_by_name
-from gso.services.subscriptions import get_product_id_by_name
-from gso.utils.shared_enums import SBPType
-from gso.utils.types.geant_ids import IMPORTED_GS_ID
-from gso.utils.types.ip_address import IPAddress, IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask
-from gso.utils.types.virtual_identifiers import VLAN_ID
-
-
-def initial_input_form_generator() -> FormGenerator:
-    """Take all information passed to this workflow by the API endpoint that was called."""
-
-    class BFDSettingsModel(BaseModel):
-        bfd_enabled: bool = False
-        bfd_interval_rx: int | None = None
-        bfd_interval_tx: int | None = None
-        bfd_multiplier: int | None = None
-
-    class BaseBGPPeer(BaseModel):
-        bfd_enabled: bool = False
-        has_custom_policies: bool = False
-        authentication_key: str | None
-        multipath_enabled: bool = False
-        send_default_route: bool = False
-        is_passive: bool = False
-        peer_address: IPAddress
-        families: list[IPFamily]
-        is_multi_hop: bool
-        rtbh_enabled: bool
-        prefix_limit: NonNegativeInt | None = None
-
-    class ServiceBindingPort(BaseModel):
-        edge_port: UUIDstr
-        ap_type: str
-        custom_service_name: str | None = None
-        gs_id: IMPORTED_GS_ID
-        sbp_type: SBPType = SBPType.L3
-        is_tagged: bool = False
-        vlan_id: VLAN_ID
-        custom_firewall_filters: bool = False
-        ipv4_address: IPv4AddressType
-        ipv4_mask: IPv4Netmask
-        ipv6_address: IPv6AddressType
-        ipv6_mask: IPv6Netmask
-        rtbh_enabled: bool = True
-        is_multi_hop: bool = True
-        bgp_peers: list[BaseBGPPeer]
-        v4_bfd_settings: BFDSettingsModel
-        v6_bfd_settings: BFDSettingsModel
-
-    class ImportL3CoreServiceForm(SubmitFormPage):
-        partner: str
-        service_binding_ports: list[ServiceBindingPort]
-        service_type: L3CoreServiceType
-
-    user_input = yield ImportL3CoreServiceForm
-
-    return user_input.model_dump()
-
-
-@step("Create subscription")
-def create_subscription(partner: str, service_type: L3CoreServiceType) -> dict:
-    """Create a new subscription object in the database."""
-    partner_id = get_partner_by_name(partner).partner_id
-    match service_type:
-        case L3CoreServiceType.GEANT_IP:
-            product_id = get_product_id_by_name(ProductName.IMPORTED_GEANT_IP)
-        case L3CoreServiceType.LHCONE:
-            product_id = get_product_id_by_name(ProductName.IMPORTED_LHCONE)
-        case L3CoreServiceType.COPERNICUS:
-            product_id = get_product_id_by_name(ProductName.IMPORTED_COPERNICUS)
-        case _:
-            msg = "L3 Core service type not defined. Cannot create subscription."
-            raise ProcessFailureError(msg, details=service_type)
-    subscription = ImportedL3CoreServiceInactive.from_product_id(product_id, partner_id)
-    return {"subscription": subscription, "subscription_id": subscription.subscription_id}
-
-
-@step("Initialize subscription")
-def initialize_subscription(subscription: ImportedL3CoreServiceInactive, service_binding_ports: list) -> dict:
-    """Initialize the subscription with the user input."""
-    for service_binding_port in service_binding_ports:
-        edge_port_subscription = EdgePort.from_subscription(service_binding_port.pop("edge_port"))
-        bgp_peers = service_binding_port.pop("bgp_peers")
-        sbp_bgp_session_list = [
-            BGPSession.new(
-                subscription_id=uuid4(),
-                ip_type=IPTypes.IPV4
-                if any(family in {IPFamily.V4UNICAST, IPFamily.V4MULTICAST} for family in session["families"])
-                else IPTypes.IPV6,
-                **session,
-            )
-            for session in bgp_peers
-        ]
-        service_binding_port_subscription = ServiceBindingPortInactive.new(
-            subscription_id=uuid4(),
-            edge_port=edge_port_subscription.edge_port,
-            bgp_session_list=sbp_bgp_session_list,
-            v4_bfd_settings=BFDSettings.new(subscription_id=uuid4(), **(service_binding_port.pop("v4_bfd_settings"))),
-            v6_bfd_settings=BFDSettings.new(subscription_id=uuid4(), **(service_binding_port.pop("v6_bfd_settings"))),
-            **service_binding_port,
-        )
-        subscription.l3_core_service.ap_list.append(
-            AccessPortInactive.new(
-                subscription_id=uuid4(),
-                ap_type=service_binding_port["ap_type"],
-                sbp=service_binding_port_subscription,
-                custom_service_name=service_binding_port.get("custom_service_name"),
-            )
-        )
-
-    return {"subscription": subscription}
-
-
-@workflow(
-    "Create imported L3 Core Service",
-    initial_input_form=initial_input_form_generator,
-    target=Target.CREATE,
-)
-def create_imported_l3_core_service() -> StepList:
-    """Import a GÉANT IP without provisioning it."""
-    return (
-        begin
-        >> create_subscription
-        >> store_process_subscription(Target.CREATE)
-        >> initialize_subscription
-        >> set_status(SubscriptionLifecycle.ACTIVE)
-        >> resync
-        >> done
-    )
diff --git a/gso/workflows/l3_core_service/create_l3_core_service.py b/gso/workflows/l3_core_service/create_l3_core_service.py
deleted file mode 100644
index bea3b0e6b..000000000
--- a/gso/workflows/l3_core_service/create_l3_core_service.py
+++ /dev/null
@@ -1,389 +0,0 @@
-"""Create a new L3 Core Service subscription including GÉANT IP and IAS."""
-
-from typing import Any
-from uuid import uuid4
-
-from orchestrator.forms import FormPage, SubmitFormPage
-from orchestrator.forms.validators import Label
-from orchestrator.targets import Target
-from orchestrator.types import SubscriptionLifecycle
-from orchestrator.workflow import StepList, begin, done, 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 BaseModel, ConfigDict, Field, NonNegativeInt, computed_field, model_validator
-from pydantic_forms.types import FormGenerator, State, UUIDstr
-from pydantic_forms.validators import Divider
-
-from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPTypes
-from gso.products.product_blocks.l3_core_service import AccessPortInactive
-from gso.products.product_blocks.service_binding_port import BFDSettings, ServiceBindingPortInactive
-from gso.products.product_types.edge_port import EdgePort
-from gso.products.product_types.l3_core_service import L3CoreService, L3CoreServiceInactive
-from gso.services.lso_client import LSOState, lso_interaction
-from gso.services.partners import get_partner_by_id
-from gso.services.sharepoint import SharePointClient
-from gso.services.subscriptions import generate_unique_id
-from gso.settings import load_oss_params
-from gso.utils.helpers import (
-    active_edge_port_selector,
-    partner_choice,
-)
-from gso.utils.shared_enums import APType, SBPType
-from gso.utils.types.geant_ids import IMPORTED_GS_ID
-from gso.utils.types.ip_address import IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask
-from gso.utils.types.tt_number import TTNumber
-from gso.utils.types.virtual_identifiers import VLAN_ID
-from gso.utils.workflow_steps import prompt_sharepoint_checklist_url, start_moodi, stop_moodi
-
-
-def initial_input_form_generator(product_name: str) -> FormGenerator:
-    """Gather input from the operator to build a new subscription object."""
-
-    class CreateL3CoreServiceForm(FormPage):
-        model_config = ConfigDict(title=f"{product_name} - Select partner")
-
-        tt_number: TTNumber
-        partner: partner_choice()  # type: ignore[valid-type]
-
-    initial_user_input = yield CreateL3CoreServiceForm
-
-    class EdgePortSelection(BaseModel):
-        edge_port: active_edge_port_selector(partner_id=initial_user_input.partner)  # type: ignore[valid-type]
-        ap_type: APType
-        custom_service_name: str | None = None
-
-    class EdgePortSelectionForm(FormPage):
-        model_config = ConfigDict(title=f"{product_name} - Select Edge Ports")
-        info_label: Label = Field(
-            f"Please select the Edge Ports where this {product_name} service will terminate", exclude=True
-        )
-
-        edge_port: EdgePortSelection
-
-    selected_edge_port = yield EdgePortSelectionForm
-
-    class BFDSettingsForm(BaseModel):
-        bfd_enabled: bool = False
-        bfd_interval_rx: int | None = Field(default=None, examples=["BFD RX defaults"])
-        bfd_interval_tx: int | None = None
-        bfd_multiplier: int | None = None
-
-    class IPv4BGPPeer(BaseModel):
-        peer_address: IPv4AddressType
-        authentication_key: str | None = None
-        has_custom_policies: bool = False
-        bfd_enabled: bool = False
-        multipath_enabled: bool = False
-        prefix_limit: NonNegativeInt | None = None
-        is_passive: bool = False
-        add_v4_multicast: bool = Field(default=False, exclude=True)
-        send_default_route: bool = False
-
-        @computed_field  # type: ignore[prop-decorator]
-        @property
-        def families(self) -> list[IPFamily]:
-            return [IPFamily.V4UNICAST, IPFamily.V4MULTICAST] if self.add_v4_multicast else [IPFamily.V4UNICAST]
-
-        @computed_field  # type: ignore[prop-decorator]
-        @property
-        def ip_type(self) -> IPTypes:
-            return IPTypes.IPV4
-
-    class IPv6BGPPeer(BaseModel):
-        peer_address: IPv6AddressType
-        authentication_key: str | None = None
-        has_custom_policies: bool = False
-        bfd_enabled: bool = False
-        multipath_enabled: bool = False
-        prefix_limit: NonNegativeInt | None = None
-        is_passive: bool = False
-        add_v6_multicast: bool = Field(default=False, exclude=True)
-        send_default_route: bool = False
-
-        @computed_field  # type: ignore[prop-decorator]
-        @property
-        def families(self) -> list[IPFamily]:
-            return [IPFamily.V6UNICAST, IPFamily.V6MULTICAST] if self.add_v6_multicast else [IPFamily.V6UNICAST]
-
-        @computed_field  # type: ignore[prop-decorator]
-        @property
-        def ip_type(self) -> IPTypes:
-            return IPTypes.IPV6
-
-    class BindingPortInputForm(SubmitFormPage):
-        model_config = ConfigDict(title=f"{product_name} - Configure Edge Port")
-        info_label: Label = Field("Please configure the Service Binding Ports for the Edge Port.", exclude=True)
-        current_ep_label: Label = Field(
-            f"Currently configuring on {EdgePort.from_subscription(selected_edge_port.edge_port.edge_port).description}"
-            f" (Access Port type: {selected_edge_port.edge_port.ap_type})",
-            exclude=True,
-        )
-
-        generate_gs_id: bool = True
-        gs_id: IMPORTED_GS_ID | None = None
-        is_tagged: bool = False
-        vlan_id: VLAN_ID
-        custom_firewall_filters: bool = False
-        divider: Divider = Field(None, exclude=True)
-        v4_label: Label = Field("IPV4 SBP interface params", exclude=True)
-        ipv4_address: IPv4AddressType
-        ipv4_mask: IPv4Netmask
-        v4_bfd_settings: BFDSettingsForm
-        v4_bgp_peer: IPv4BGPPeer
-        divider2: Divider = Field(None, exclude=True)
-        v6_label: Label = Field("IPV6 SBP interface params", exclude=True)
-        ipv6_address: IPv6AddressType
-        ipv6_mask: IPv6Netmask
-        v6_bfd_settings: BFDSettingsForm
-        v6_bgp_peer: IPv6BGPPeer
-
-        @model_validator(mode="before")
-        def validate_gs_id(cls, input_data: dict[str, Any]) -> dict[str, Any]:
-            gs_id = input_data.get("gs_id")
-            generate_gs_id = input_data.get("generate_gs_id", True)
-
-            if generate_gs_id and gs_id:
-                error_message = (
-                    "You cannot provide a GS ID manually while the 'Auto-generate GS ID' option is enabled."
-                    "Please either uncheck 'Auto-generate GS ID' or remove the manual GS ID."
-                )
-                raise ValueError(error_message)
-            return input_data
-
-    binding_port_input_form = yield BindingPortInputForm
-    binding_port_input = binding_port_input_form.model_dump() | {
-        "bgp_peers": [
-            binding_port_input_form.v4_bgp_peer.model_dump(),
-            binding_port_input_form.v6_bgp_peer.model_dump(),
-        ]
-    }
-
-    return (
-        initial_user_input.model_dump()
-        | selected_edge_port.model_dump()
-        | {"binding_port_input": binding_port_input, "product_name": product_name}
-    )
-
-
-@step("Create subscription")
-def create_subscription(product: UUIDstr, partner: str) -> State:
-    """Create a new subscription object in the database."""
-    subscription = L3CoreServiceInactive.from_product_id(product, partner)
-
-    return {"subscription": subscription, "subscription_id": subscription.subscription_id}
-
-
-@step("Initialize subscription")
-def initialize_subscription(
-    subscription: L3CoreServiceInactive, edge_port: dict, binding_port_input: dict, product_name: str
-) -> State:
-    """Take all user inputs and use them to populate the subscription model."""
-    edge_port_fqdn_list = []
-    edge_port_subscription = EdgePort.from_subscription(edge_port["edge_port"])
-    sbp_bgp_session_list = [
-        BGPSession.new(subscription_id=uuid4(), rtbh_enabled=True, is_multi_hop=True, **session)
-        for session in binding_port_input["bgp_peers"]
-    ]
-    sbp_gs_id = (
-        generate_unique_id(prefix="GS")
-        if binding_port_input.pop("generate_gs_id", False)
-        else binding_port_input.pop("gs_id")
-    )
-    binding_port_input.pop("gs_id", None)
-    service_binding_port = ServiceBindingPortInactive.new(
-        subscription_id=uuid4(),
-        v4_bfd_settings=BFDSettings.new(subscription_id=uuid4(), **(binding_port_input.pop("v4_bfd_settings"))),
-        v6_bfd_settings=BFDSettings.new(subscription_id=uuid4(), **(binding_port_input.pop("v6_bfd_settings"))),
-        **binding_port_input,
-        bgp_session_list=sbp_bgp_session_list,
-        sbp_type=SBPType.L3,
-        edge_port=edge_port_subscription.edge_port,
-        gs_id=sbp_gs_id,
-    )
-    subscription.l3_core_service.ap_list.append(
-        AccessPortInactive.new(
-            subscription_id=uuid4(),
-            ap_type=edge_port["ap_type"],
-            sbp=service_binding_port,
-            custom_service_name=edge_port.get("custom_service_name"),
-        )
-    )
-    edge_port_fqdn_list.append(edge_port_subscription.edge_port.node.router_fqdn)
-
-    partner_name = get_partner_by_id(subscription.customer_id).name
-    subscription.description = f"{product_name} service for {partner_name}"
-    return {"subscription": subscription, "edge_port_fqdn_list": edge_port_fqdn_list, "partner_name": partner_name}
-
-
-@step("[DRY RUN] Deploy service binding port")
-def provision_sbp_dry(
-    subscription: dict[str, Any], process_id: UUIDstr, tt_number: str, edge_port_fqdn_list: list[str], partner_name: str
-) -> LSOState:
-    """Perform a dry run of deploying Service Binding Ports."""
-    extra_vars = {
-        "subscription": subscription,
-        "partner_name": partner_name,
-        "dry_run": True,
-        "verb": "deploy",
-        "object": "sbp",
-        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - "
-        f"Deploy config for {subscription["description"]}",
-    }
-
-    return {
-        "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml",
-        "inventory": {"all": {"hosts": dict.fromkeys(edge_port_fqdn_list)}},
-        "extra_vars": extra_vars,
-    }
-
-
-@step("[FOR REAL] Deploy service binding port")
-def provision_sbp_real(
-    subscription: dict[str, Any], process_id: UUIDstr, tt_number: str, edge_port_fqdn_list: list[str], partner_name: str
-) -> LSOState:
-    """Deploy Service Binding Ports."""
-    extra_vars = {
-        "subscription": subscription,
-        "partner_name": partner_name,
-        "dry_run": False,
-        "verb": "deploy",
-        "object": "sbp",
-        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - "
-        f"Deploy config for {subscription["description"]}",
-    }
-
-    return {
-        "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml",
-        "inventory": {"all": {"hosts": dict.fromkeys(edge_port_fqdn_list)}},
-        "extra_vars": extra_vars,
-    }
-
-
-@step("Check service binding port functionality")
-def check_sbp_functionality(subscription: dict[str, Any], edge_port_fqdn_list: list[str]) -> LSOState:
-    """Check functionality of deployed Service Binding Ports."""
-    extra_vars = {"subscription": subscription, "verb": "check", "object": "sbp"}
-
-    return {
-        "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml",
-        "inventory": {"all": {"hosts": dict.fromkeys(edge_port_fqdn_list)}},
-        "extra_vars": extra_vars,
-    }
-
-
-@step("[DRY RUN] Deploy BGP peers")
-def deploy_bgp_peers_dry(
-    subscription: dict[str, Any], edge_port_fqdn_list: list[str], tt_number: str, process_id: UUIDstr, partner_name: str
-) -> LSOState:
-    """Perform a dry run of deploying BGP peers."""
-    extra_vars = {
-        "subscription": subscription,
-        "partner_name": partner_name,
-        "verb": "deploy",
-        "object": "bgp",
-        "dry_run": True,
-        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - "
-        f"Deploying BGP peers for {subscription["description"]}",
-    }
-
-    return {
-        "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml",
-        "inventory": {"all": {"hosts": dict.fromkeys(edge_port_fqdn_list)}},
-        "extra_vars": extra_vars,
-    }
-
-
-@step("[FOR REAL] Deploy BGP peers")
-def deploy_bgp_peers_real(
-    subscription: dict[str, Any], edge_port_fqdn_list: list[str], tt_number: str, process_id: UUIDstr, partner_name: str
-) -> LSOState:
-    """Deploy BGP peers."""
-    extra_vars = {
-        "subscription": subscription,
-        "partner_name": partner_name,
-        "verb": "deploy",
-        "object": "bgp",
-        "dry_run": False,
-        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - "
-        f"Deploying BGP peers for {subscription["description"]}",
-    }
-
-    return {
-        "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml",
-        "inventory": {"all": {"hosts": dict.fromkeys(edge_port_fqdn_list)}},
-        "extra_vars": extra_vars,
-    }
-
-
-@step("Check BGP peers")
-def check_bgp_peers(subscription: dict[str, Any], edge_port_fqdn_list: list[str]) -> LSOState:
-    """Check correct deployment of BGP peers."""
-    extra_vars = {"subscription": subscription, "verb": "check", "object": "bgp"}
-
-    return {
-        "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml",
-        "inventory": {"all": {"hosts": dict.fromkeys(edge_port_fqdn_list)}},
-        "extra_vars": extra_vars,
-    }
-
-
-@step("Update Infoblox")
-def update_dns_records(subscription: L3CoreService) -> State:
-    """Update DNS records in Infoblox."""
-    #  TODO: implement
-    return {"subscription": subscription}
-
-
-@step("Create a new SharePoint checklist item")
-def create_new_sharepoint_checklist(subscription: L3CoreService, tt_number: TTNumber, process_id: UUIDstr) -> State:
-    """Create a new checklist item in SharePoint for approving this L3 Core Service."""
-    new_ep = subscription.l3_core_service.ap_list[0].sbp.edge_port
-    new_list_item_url = SharePointClient().add_list_item(
-        list_name="l3_core_service",
-        fields={
-            "Title": f"{subscription.description}",
-            "TT_NUMBER": tt_number,
-            "ACTIVITY_TYPE": "Creation",
-            "PRODUCT_TYPE": subscription.l3_core_service_type,
-            "LOCATION": f"{new_ep.edge_port_name} {new_ep.edge_port_description} on {new_ep.node.router_fqdn}",
-            "GAP_PROCESS_URL": f"{load_oss_params().GENERAL.public_hostname}/workflows/{process_id}",
-        },
-    )
-
-    return {"checklist_url": new_list_item_url}
-
-
-@workflow(
-    "Create L3 Core Service",
-    initial_input_form=wrap_create_initial_input_form(initial_input_form_generator),
-    target=Target.CREATE,
-)
-def create_l3_core_service() -> StepList:
-    """Create a new L3 Core Service subscription including GÉANT IP and IAS.
-
-    * Create subscription object in the service database
-    * Deploy service binding ports
-    * Deploy BGP peers
-    * Update DNS records
-    * Set the subscription in a provisioning state in the database
-    """
-    return (
-        begin
-        >> create_subscription
-        >> store_process_subscription(Target.CREATE)
-        >> initialize_subscription
-        >> start_moodi()
-        >> lso_interaction(provision_sbp_dry)
-        >> lso_interaction(provision_sbp_real)
-        >> lso_interaction(check_sbp_functionality)
-        >> lso_interaction(deploy_bgp_peers_dry)
-        >> lso_interaction(deploy_bgp_peers_real)
-        >> lso_interaction(check_bgp_peers)
-        >> update_dns_records
-        >> set_status(SubscriptionLifecycle.ACTIVE)
-        >> resync
-        >> create_new_sharepoint_checklist
-        >> prompt_sharepoint_checklist_url
-        >> stop_moodi()
-        >> done
-    )
diff --git a/gso/workflows/l3_core_service/import_l3_core_service.py b/gso/workflows/l3_core_service/import_l3_core_service.py
deleted file mode 100644
index fdb06170f..000000000
--- a/gso/workflows/l3_core_service/import_l3_core_service.py
+++ /dev/null
@@ -1,51 +0,0 @@
-"""A modification workflow for migrating an `ImportedGeantIP` to a `GeantIP` subscription."""
-
-from orchestrator.targets import Target
-from orchestrator.utils.errors import ProcessFailureError
-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 pydantic_forms.types import State, UUIDstr
-
-from gso.products import ProductName
-from gso.products.product_types.l3_core_service import (
-    ImportedL3CoreService,
-    L3CoreService,
-    L3CoreServiceType,
-)
-from gso.services.partners import get_partner_by_id
-from gso.services.subscriptions import get_product_id_by_name
-
-
-@step("Create imported subscription")
-def import_l3_core_service_subscription(subscription_id: UUIDstr) -> State:
-    """Take an imported subscription, and turn it into an L3 Core Service subscription."""
-    old_l3_core_service = ImportedL3CoreService.from_subscription(subscription_id)
-    match old_l3_core_service.l3_core_service_type:
-        case L3CoreServiceType.IMPORTED_GEANT_IP:
-            new_subscription_id = get_product_id_by_name(ProductName.GEANT_IP)
-        case L3CoreServiceType.IMPORTED_LHCONE:
-            new_subscription_id = get_product_id_by_name(ProductName.LHCONE)
-        case L3CoreServiceType.IMPORTED_COPERNICUS:
-            new_subscription_id = get_product_id_by_name(ProductName.COPERNICUS)
-        case _:
-            msg = f"This {old_l3_core_service.l3_core_service_type} is already imported, nothing to do."
-            raise ProcessFailureError(message=msg, details=old_l3_core_service)
-    new_subscription = L3CoreService.from_other_product(old_l3_core_service, new_subscription_id)  # type: ignore[arg-type]
-    new_subscription.description = (
-        f"{new_subscription.product.name} service for {get_partner_by_id(new_subscription.customer_id).name}"
-    )
-    return {"subscription": new_subscription}
-
-
-@workflow("Import L3 Core Service", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None))
-def import_l3_core_service() -> StepList:
-    """Modify an imported subscription into an L3 Core Service subscription to complete the import."""
-    return (
-        init
-        >> store_process_subscription(Target.MODIFY)
-        >> unsync
-        >> import_l3_core_service_subscription
-        >> resync
-        >> done
-    )
diff --git a/gso/workflows/l3_core_service/migrate_l3_core_service.py b/gso/workflows/l3_core_service/migrate_l3_core_service.py
deleted file mode 100644
index 3f97983c5..000000000
--- a/gso/workflows/l3_core_service/migrate_l3_core_service.py
+++ /dev/null
@@ -1,419 +0,0 @@
-"""A modification workflow that migrates a L3 Core Service to a new Edge Port.
-
-In one run of a migration workflow, only a single Access Port can be replaced. This is due to the nature of these
-services. When a service is dual homed for example, only one of the Access Ports should be migrated. The other one will
-remain the way it is.
-
-At the start of the workflow, the operator will select one Access Port that is to be migrated, and will then select a
-destination Edge Port that this service should be placed on. All other Access Ports will be left as-is.
-"""
-
-import json
-from typing import Any
-
-from orchestrator import workflow
-from orchestrator.config.assignee import Assignee
-from orchestrator.forms import FormPage, SubmitFormPage
-from orchestrator.targets import Target
-from orchestrator.utils.errors import ProcessFailureError
-from orchestrator.utils.json import json_dumps
-from orchestrator.workflow import StepList, begin, conditional, done, inputstep, step
-from orchestrator.workflows.steps import resync, store_process_subscription, unsync
-from orchestrator.workflows.utils import wrap_modify_initial_input_form
-from pydantic import ConfigDict, Field
-from pydantic_forms.types import FormGenerator, State, UUIDstr
-from pydantic_forms.validators import Choice, Divider, Label
-
-from gso.products.product_types.edge_port import EdgePort
-from gso.products.product_types.l3_core_service import L3CoreService
-from gso.services.lso_client import LSOState, lso_interaction
-from gso.services.partners import get_partner_by_id
-from gso.services.subscriptions import get_active_edge_port_subscriptions
-from gso.utils.types.tt_number import TTNumber
-from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, SKIP_MOODI_KEY, start_moodi, stop_moodi
-from gso.workflows.shared import create_summary_form
-
-
-def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
-    """Gather input from the operator on what destination Edge Ports this L3 Core Service should be migrated to."""
-    subscription = L3CoreService.from_subscription(subscription_id)
-    partner_id = subscription.customer_id
-    current_ep_list = {
-        str(
-            ap.sbp.edge_port.owner_subscription_id
-        ): f"{EdgePort.from_subscription(ap.sbp.edge_port.owner_subscription_id).description} ({ap.ap_type})"
-        for ap in subscription.l3_core_service.ap_list
-    }
-    source_edge_port_selector = Choice(
-        "Select an Edge Port",
-        zip(current_ep_list.keys(), current_ep_list.items(), strict=True),  # type: ignore[arg-type]
-    )
-
-    class L3CoreServiceSourceEdgePortSelectionForm(FormPage):
-        model_config = ConfigDict(title=f"Migrating a(n) {subscription.l3_core_service_type} AP to a new Edge Port")
-
-        tt_number: TTNumber
-        divider: Divider = Field(None, exclude=True)
-        skip_moodi: bool = False
-        is_human_initiated_wf: bool = True
-        source_edge_port: source_edge_port_selector | str  # type: ignore[valid-type]
-
-    source_ep_user_input = yield L3CoreServiceSourceEdgePortSelectionForm
-
-    def _destination_edge_port_selector(pid: UUIDstr) -> Choice:
-        existing_ep_list = [ap.sbp.edge_port.owner_subscription_id for ap in subscription.l3_core_service.ap_list]
-        edge_port_subscriptions = list(
-            filter(
-                lambda ep: bool(ep.customer_id == pid) and ep.subscription_id not in existing_ep_list,
-                get_active_edge_port_subscriptions(),
-            )
-        )
-        edge_ports = {str(port.subscription_id): port.description for port in edge_port_subscriptions}
-
-        return Choice(
-            "Select an Edge Port",
-            zip(edge_ports.keys(), edge_ports.items(), strict=True),  # type: ignore[arg-type]
-        )
-
-    class L3CoreServiceEdgePortSelectionForm(FormPage):
-        destination_edge_port: _destination_edge_port_selector(partner_id) | str  # type: ignore[valid-type]
-
-    destination_ep_user_input = yield L3CoreServiceEdgePortSelectionForm
-
-    if source_ep_user_input.is_human_initiated_wf:
-        summary_input = {
-            "source_edge_port": EdgePort.from_subscription(source_ep_user_input.source_edge_port).description,
-            "destination_edge_port": EdgePort.from_subscription(
-                destination_ep_user_input.destination_edge_port
-            ).description,
-        }
-        yield from create_summary_form(
-            summary_input, subscription.l3_core_service_type.value, list(summary_input.keys())
-        )
-
-    return (
-        {"subscription_id": subscription_id, "subscription": subscription}
-        | source_ep_user_input.model_dump()
-        | destination_ep_user_input.model_dump()
-        | {
-            IS_HUMAN_INITIATED_WF_KEY: source_ep_user_input.is_human_initiated_wf,
-            SKIP_MOODI_KEY: source_ep_user_input.skip_moodi,
-        }
-    )
-
-
-@step("Inject Partner Name")
-def inject_partner_name(subscription: L3CoreService) -> LSOState:
-    """Resolve and inject partner name into the state."""
-    partner_name = get_partner_by_id(subscription.customer_id).name
-
-    return {"partner_name": partner_name}
-
-
-@step("Show BGP neighbors")
-def show_bgp_neighbors(
-    subscription: L3CoreService, process_id: UUIDstr, tt_number: TTNumber, source_edge_port: EdgePort, partner_name: str
-) -> LSOState:
-    """List all BGP neighbors on the source router, to present an expected base-line for the new one."""
-    source_access_port_fqdn = source_edge_port.edge_port.node.router_fqdn
-
-    return {
-        "playbook_name": "gap_ansible/playbooks/manage_bgp_peers.yaml",
-        "inventory": {"all": {"hosts": {source_access_port_fqdn: None}}},
-        "extra_vars": {
-            "dry_run": True,
-            "verb": "check",
-            "object": "bgp",
-            "subscription": subscription,
-            "partner_name": partner_name,
-            "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Show BGP neighbors.",
-        },
-        "source_access_port_fqdn": source_access_port_fqdn,
-    }
-
-
-@step("[DRY RUN] Deactivate BGP session on the source router")
-def deactivate_bgp_dry(
-    subscription: L3CoreService,
-    process_id: UUIDstr,
-    tt_number: TTNumber,
-    source_access_port_fqdn: str,
-    partner_name: str,
-) -> LSOState:
-    """Perform a dry run of deactivating the BGP session on the source router."""
-    return {
-        "playbook_name": "gap_ansible/playbooks/manage_bgp_peers.yaml",
-        "inventory": {"all": {"hosts": {source_access_port_fqdn: None}}},
-        "extra_vars": {
-            "dry_run": True,
-            "verb": "deactivate",
-            "object": "bgp",
-            "subscription": subscription,
-            "partner_name": partner_name,
-            "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Deactivate BGP session.",
-        },
-    }
-
-
-@step("[FOR REAL] Deactivate BGP session on the source router")
-def deactivate_bgp_real(
-    subscription: L3CoreService,
-    process_id: UUIDstr,
-    tt_number: TTNumber,
-    source_access_port_fqdn: str,
-    partner_name: str,
-) -> LSOState:
-    """Deactivate the BGP session on the source router."""
-    return {
-        "playbook_name": "gap_ansible/playbooks/manage_bgp_peers.yaml",
-        "inventory": {"all": {"hosts": {source_access_port_fqdn: None}}},
-        "extra_vars": {
-            "dry_run": False,
-            "verb": "deactivate",
-            "object": "bgp",
-            "subscription": subscription,
-            "partner_name": partner_name,
-            "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Deactivate BGP session.",
-        },
-    }
-
-
-@inputstep("Verify pre-check results", assignee=Assignee.SYSTEM)
-def inform_operator_traffic_check() -> FormGenerator:
-    """Wait for confirmation from an operator that the results from the pre-checks look OK.
-
-    In case the results look OK, the workflow can continue. If the results don't look OK, the workflow can still be
-    aborted at this time, without the subscription going out of sync. Moodi will also not start, and the subscription
-    model has not been updated yet. Effectively, this prevents any changes inside the orchestrator from occurring. The
-    one thing that must be rolled back manually, is the deactivated configuration that sits on the source device.
-    """
-
-    class PreCheckPage(SubmitFormPage):
-        model_config = ConfigDict(title="Please confirm before continuing")
-
-        info_label_1: Label = "Please verify that traffic has moved as expected."
-        info_label_3: Label = "If traffic is misbehaving, this is your last chance to abort this workflow cleanly."
-
-    yield PreCheckPage
-    return {}
-
-
-@step("[DRY RUN] Deactivate SBP config on the source router")
-def deactivate_sbp_dry(
-    subscription: L3CoreService,
-    process_id: UUIDstr,
-    tt_number: TTNumber,
-    source_access_port_fqdn: str,
-    partner_name: str,
-) -> LSOState:
-    """Perform a dry run of deactivating SBP config on the source router."""
-    return {
-        "playbook_name": "gap_ansible/playbooks/manage_sbp.yaml",
-        "inventory": {"all": {"hosts": {source_access_port_fqdn: None}}},
-        "extra_vars": {
-            "dry_run": True,
-            "verb": "deactivate",
-            "subscription": subscription,
-            "partner_name": partner_name,
-            "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Deactivate BGP session.",
-        },
-    }
-
-
-@step("[FOR REAL] Deactivate SBP config on the source router")
-def deactivate_sbp_real(
-    subscription: L3CoreService,
-    process_id: UUIDstr,
-    tt_number: TTNumber,
-    source_access_port_fqdn: str,
-    partner_name: str,
-) -> LSOState:
-    """Deactivate the BGP session on the source router."""
-    return {
-        "playbook_name": "gap_ansible/playbooks/manage_sbp.yaml",
-        "inventory": {"all": {"hosts": {source_access_port_fqdn: None}}},
-        "extra_vars": {
-            "dry_run": False,
-            "verb": "deactivate",
-            "subscription": subscription,
-            "partner_name": partner_name,
-            "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Deactivate BGP session.",
-        },
-        "__remove_keys": ["source_access_port_fqdn"],
-    }
-
-
-@step("Generate updated subscription model")
-def generate_scoped_subscription_model(
-    subscription: L3CoreService, source_edge_port: EdgePort, destination_edge_port: EdgePort
-) -> State:
-    """Calculate what the updated subscription model will look like, but don't update the actual subscription yet.
-
-    The new subscription is used for running Ansible playbooks remotely, but the updated subscription model is not
-    stored yet, to avoid issues recovering when the workflow is aborted.
-    """
-    updated_subscription = json.loads(json_dumps(subscription))
-    for index, ap in enumerate(updated_subscription["l3_core_service"]["ap_list"]):
-        if ap["sbp"]["edge_port"]["owner_subscription_id"] == str(source_edge_port.subscription_id):
-            #  We have found the AP that is to be replaced, we can return all the necessary information to the state.
-            #  First, remove all unneeded unchanged APs that should not be included when executing a playbook.
-            updated_subscription["l3_core_service"]["ap_list"] = [
-                updated_subscription["l3_core_service"]["ap_list"][index]
-            ]
-            #  Then replace the AP that is migrated such that it includes the destination EP instead of the source one.
-            updated_subscription["l3_core_service"]["ap_list"][0]["sbp"]["edge_port"] = json.loads(
-                json_dumps(destination_edge_port.edge_port)
-            )
-            return {"scoped_subscription": updated_subscription, "replaced_ap_index": index}
-
-    msg = "Failed to find selected EP in current subscription."
-    raise ProcessFailureError(msg, details=source_edge_port)
-
-
-@step("[DRY RUN] Configure service on destination Edge Port")
-def deploy_destination_ep_dry(
-    scoped_subscription: dict[str, Any],
-    process_id: UUIDstr,
-    tt_number: TTNumber,
-    destination_edge_port: EdgePort,
-    partner_name: str,
-) -> LSOState:
-    """Deploy Access Port on the destination Edge Port, as a dry run.
-
-    Only the updated Access Port is sent as part of the subscription model, to reduce the scope of the playbook.
-    """
-    return {
-        "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml",
-        "inventory": {"all": {"hosts": {destination_edge_port.edge_port.node.router_fqdn: None}}},
-        "extra_vars": {
-            "dry_run": True,
-            "verb": "deploy",
-            "object": "sbp",
-            "subscription": scoped_subscription,
-            "partner_name": partner_name,
-            "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - "
-            "Deploying SBP and standard IDs.",
-        },
-    }
-
-
-@step("[FOR REAL] Configure service on destination Edge Port")
-def deploy_destination_ep_real(
-    scoped_subscription: dict[str, Any],
-    destination_edge_port: EdgePort,
-    process_id: UUIDstr,
-    tt_number: TTNumber,
-    partner_name: str,
-) -> LSOState:
-    """Deploy Access Port on the destination Edge Port."""
-    return {
-        "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml",
-        "inventory": {"all": {"hosts": {destination_edge_port.edge_port.node.router_fqdn: None}}},
-        "extra_vars": {
-            "dry_run": False,
-            "verb": "deploy",
-            "object": "sbp",
-            "subscription": scoped_subscription,
-            "partner_name": partner_name,
-            "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - "
-            "Deploying SBP and standard IDs.",
-        },
-    }
-
-
-@step("[DRY RUN] Deploy BGP session")
-def deploy_bgp_session_dry(
-    scoped_subscription: dict[str, Any],
-    destination_edge_port: EdgePort,
-    process_id: UUIDstr,
-    tt_number: TTNumber,
-    partner_name: str,
-) -> LSOState:
-    """Perform a dry run of deploying the destination BGP session."""
-    return {
-        "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml",
-        "inventory": {"all": {"hosts": {destination_edge_port.edge_port.node.router_fqdn: None}}},
-        "extra_vars": {
-            "dry_run": True,
-            "verb": "deploy",
-            "object": "bgp",
-            "subscription": scoped_subscription,
-            "partner_name": partner_name,
-            "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Deploying BGP session for SBP.",
-        },
-    }
-
-
-@step("[FOR REAL] Deploy BGP session")
-def deploy_bgp_session_real(
-    scoped_subscription: dict[str, Any],
-    destination_edge_port: EdgePort,
-    process_id: UUIDstr,
-    tt_number: TTNumber,
-    partner_name: str,
-) -> LSOState:
-    """Deploy the destination BGP session."""
-    return {
-        "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml",
-        "inventory": {"all": {"hosts": {destination_edge_port.edge_port.node.router_fqdn: None}}},
-        "extra_vars": {
-            "dry_run": False,
-            "verb": "deploy",
-            "object": "bgp",
-            "subscription": scoped_subscription,
-            "partner_name": partner_name,
-            "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Deploying BGP session for SBP.",
-        },
-        "__remove_keys": ["scoped_subscription"],
-    }
-
-
-@step("Update Infoblox")
-def update_dns_records(subscription: L3CoreService) -> State:
-    """Update DNS records in Infoblox."""
-    #  TODO: implement
-    return {"subscription": subscription}
-
-
-@step("Update subscription model")
-def update_subscription_model(
-    subscription: L3CoreService, destination_edge_port: EdgePort, replaced_ap_index: int
-) -> State:
-    """Update the subscription model with the destination Edge Port attached to the Access Port that is migrated."""
-    subscription.l3_core_service.ap_list[replaced_ap_index].sbp.edge_port = destination_edge_port.edge_port
-
-    return {"subscription": subscription, "__remove_keys": ["replaced_ap_index"]}
-
-
-@workflow(
-    "Migrate L3 Core Service",
-    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
-    target=Target.MODIFY,
-)
-def migrate_l3_core_service() -> StepList:
-    """Migrate a L3 Core Service to a destination Edge Port."""
-    is_human_initiated_wf = conditional(lambda state: bool(state.get(IS_HUMAN_INITIATED_WF_KEY)))
-
-    return (
-        begin
-        >> store_process_subscription(Target.MODIFY)
-        >> inject_partner_name
-        >> is_human_initiated_wf(lso_interaction(show_bgp_neighbors))  # TODO: send OTRS email with pre-check results
-        >> is_human_initiated_wf(lso_interaction(deactivate_bgp_dry))
-        >> is_human_initiated_wf(lso_interaction(deactivate_bgp_real))
-        >> is_human_initiated_wf(lso_interaction(deactivate_sbp_dry))
-        >> is_human_initiated_wf(lso_interaction(deactivate_sbp_real))
-        >> is_human_initiated_wf(inform_operator_traffic_check)
-        >> unsync
-        >> generate_scoped_subscription_model
-        >> start_moodi()  # TODO: include results from first LSO run
-        >> lso_interaction(deploy_destination_ep_dry)
-        >> lso_interaction(deploy_destination_ep_real)
-        >> lso_interaction(deploy_bgp_session_dry)
-        >> lso_interaction(deploy_bgp_session_real)
-        >> update_dns_records
-        >> update_subscription_model
-        >> resync
-        >> stop_moodi()
-        >> done
-    )
diff --git a/gso/workflows/l3_core_service/modify_l3_core_service.py b/gso/workflows/l3_core_service/modify_l3_core_service.py
deleted file mode 100644
index 427717fdf..000000000
--- a/gso/workflows/l3_core_service/modify_l3_core_service.py
+++ /dev/null
@@ -1,474 +0,0 @@
-"""A modification workflow for a L3 Core Service subscription.
-
-Only one operation can be performed per workflow run. This is enforced through the input form at the start of the
-workflow. One access port can either be added, removed, or modified. Every one of these operations requires a separate
-maintenance ticket and therefore should be in separate workflow runs.
-"""
-
-from typing import Any, TypeAlias, cast
-from uuid import UUID, uuid4
-
-from orchestrator import begin, conditional, done, step, workflow
-from orchestrator.forms import FormPage
-from orchestrator.targets import Target
-from orchestrator.workflow import StepList
-from orchestrator.workflows.steps import resync, store_process_subscription, unsync
-from orchestrator.workflows.utils import wrap_modify_initial_input_form
-from pydantic import BaseModel, ConfigDict, Field, NonNegativeInt, computed_field, field_validator, model_validator
-from pydantic_forms.types import FormGenerator, State, UUIDstr, strEnum
-from pydantic_forms.validators import Choice, Divider, Label, ReadOnlyField
-
-from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPTypes
-from gso.products.product_blocks.l3_core_service import AccessPort
-from gso.products.product_blocks.service_binding_port import BFDSettings, ServiceBindingPort
-from gso.products.product_types.edge_port import EdgePort
-from gso.products.product_types.l3_core_service import L3CoreService
-from gso.services.subscriptions import generate_unique_id, get_active_edge_port_subscriptions
-from gso.utils.shared_enums import APType, SBPType
-from gso.utils.types.geant_ids import IMPORTED_GS_ID
-from gso.utils.types.ip_address import IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask
-from gso.utils.types.tt_number import TTNumber
-from gso.utils.types.virtual_identifiers import VLAN_ID
-
-
-class Operation(strEnum):
-    """The three operations that can be performed to modify an L3 Core Service."""
-
-    ADD = "Add an Access Port"
-    REMOVE = "Remove an existing Access Port"
-    EDIT = "Edit an existing Access Port"
-
-
-def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
-    """Get input about added, removed, and modified Access Ports."""
-    subscription = L3CoreService.from_subscription(subscription_id)
-    product_name = subscription.product.name
-
-    class OperationSelectionForm(FormPage):
-        model_config = ConfigDict(title="Modify Edge Port")
-
-        tt_number: TTNumber
-        operation: Operation
-
-    def access_port_selector() -> TypeAlias:
-        """Generate a dropdown selector for choosing an Access Port in an input form."""
-        access_ports = subscription.l3_core_service.ap_list
-        options = {
-            str(access_port.subscription_instance_id): (
-                f"{access_port.sbp.gs_id} on "
-                f"{EdgePort.from_subscription(access_port.sbp.edge_port.owner_subscription_id).description} "
-                f"({access_port.ap_type})"
-            )
-            for access_port in access_ports
-        }
-
-        return cast(
-            type[Choice],
-            Choice.__call__(
-                "Select an Access Port",
-                zip(options.keys(), options.items(), strict=True),
-            ),
-        )
-
-    class BFDInputModel(BaseModel):
-        bfd_enabled: bool = False
-        bfd_interval_rx: int | None = None
-        bfd_interval_tx: int | None = None
-        bfd_multiplier: int | None = None
-
-    class IPv4BGPPeer(BaseModel):
-        peer_address: IPv4AddressType
-        authentication_key: str | None = None
-        has_custom_policies: bool = False
-        bfd_enabled: bool = False
-        multipath_enabled: bool = False
-        prefix_limit: NonNegativeInt | None = None
-        is_passive: bool = False
-        add_v4_multicast: bool = Field(default=False, exclude=True)
-        send_default_route: bool = False
-
-        @computed_field  # type: ignore[prop-decorator]
-        @property
-        def families(self) -> list[IPFamily]:
-            return [IPFamily.V4UNICAST, IPFamily.V4MULTICAST] if self.add_v4_multicast else [IPFamily.V4UNICAST]
-
-        @computed_field  # type: ignore[prop-decorator]
-        @property
-        def ip_type(self) -> IPTypes:
-            return IPTypes.IPV4
-
-    class IPv6BGPPeer(BaseModel):
-        peer_address: IPv6AddressType
-        authentication_key: str | None = None
-        has_custom_policies: bool = False
-        bfd_enabled: bool = False
-        multipath_enabled: bool = False
-        prefix_limit: NonNegativeInt | None = None
-        is_passive: bool = False
-        add_v6_multicast: bool = Field(default=False, exclude=True)
-        send_default_route: bool = False
-
-        @computed_field  # type: ignore[prop-decorator]
-        @property
-        def families(self) -> list[IPFamily]:
-            return [IPFamily.V6UNICAST, IPFamily.V6MULTICAST] if self.add_v6_multicast else [IPFamily.V6UNICAST]
-
-        @computed_field  # type: ignore[prop-decorator]
-        @property
-        def ip_type(self) -> IPTypes:
-            return IPTypes.IPV6
-
-    initial_input = yield OperationSelectionForm
-    match initial_input.operation:
-        case Operation.ADD:
-
-            class AccessPortListItem(BaseModel):
-                edge_port: str
-                ap_type: str
-                custom_service_name: str
-
-            def available_new_edge_port_selector() -> TypeAlias:
-                """Generate a dropdown selector for choosing an active Edge Port in an input form."""
-                edge_ports = get_active_edge_port_subscriptions(partner_id=subscription.customer_id)
-
-                options = {
-                    str(edge_port.subscription_id): edge_port.description
-                    for edge_port in edge_ports
-                    if edge_port.subscription_id
-                    not in [ap.sbp.edge_port.owner_subscription_id for ap in subscription.l3_core_service.ap_list]
-                }
-
-                return cast(
-                    type[Choice],
-                    Choice.__call__(
-                        "Select an Edge Port",
-                        zip(options.keys(), options.items(), strict=True),
-                    ),
-                )
-
-            def existing_ap_list() -> type[list]:
-                return cast(
-                    type[list],
-                    ReadOnlyField(
-                        [
-                            AccessPortListItem(
-                                edge_port=EdgePort.from_subscription(
-                                    access_port.sbp.edge_port.owner_subscription_id
-                                ).description,
-                                ap_type=access_port.ap_type.value,
-                                custom_service_name=access_port.custom_service_name or "",
-                            )
-                            for access_port in subscription.l3_core_service.ap_list
-                        ],
-                        default_type=list[AccessPortListItem],
-                    ),
-                )
-
-            class AddAccessPortForm(FormPage):
-                model_config = ConfigDict(title=f"Add an Edge Port to a {product_name}")
-                existing_access_ports: existing_ap_list()  # type: ignore[valid-type]
-
-                divider_a: Divider = Field(exclude=True)
-                label_a: Label = Field(
-                    "Please use the fields below to configure a new Access Port, in addition to the existing ones "
-                    "listed above.",
-                    exclude=True,
-                )
-                edge_port: available_new_edge_port_selector()  # type: ignore[valid-type]
-                ap_type: APType
-                generate_gs_id: bool = True
-                gs_id: IMPORTED_GS_ID | None = None
-                custom_service_name: str | None = None
-                is_tagged: bool = False
-                vlan_id: VLAN_ID
-                ipv4_address: IPv4AddressType
-                ipv4_mask: IPv4Netmask
-                ipv6_address: IPv6AddressType
-                ipv6_mask: IPv6Netmask
-                custom_firewall_filters: bool = False
-
-                divider_b: Divider = Field(None, exclude=True)
-                label_b: Label = Field("IPv4 settings for BFD and BGP", exclude=True)
-                v4_bfd_settings: BFDInputModel
-                v4_bgp_peer: IPv4BGPPeer
-
-                divider_c: Divider = Field(None, exclude=True)
-                label_c: Label = Field("IPv6 settings for BFD and BGP", exclude=True)
-                v6_bfd_settings: BFDInputModel
-                v6_bgp_peer: IPv6BGPPeer
-
-                @model_validator(mode="before")
-                def validate_gs_id(cls, input_data: dict[str, Any]) -> dict[str, Any]:
-                    gs_id = input_data.get("gs_id")
-                    generate_gs_id = input_data.get("generate_gs_id", True)
-
-                    if generate_gs_id and gs_id:
-                        error_message = (
-                            "You cannot provide a GS ID manually while the 'Auto-generate GS ID' option is enabled."
-                            "Please either uncheck 'Auto-generate GS ID' or remove the manual GS ID."
-                        )
-                        raise ValueError(error_message)
-                    return input_data
-
-                @field_validator("edge_port")
-                def selected_edge_port_is_new(cls, value: UUIDstr) -> UUIDstr:
-                    if value in [
-                        str(ap.sbp.edge_port.owner_subscription_id) for ap in subscription.l3_core_service.ap_list
-                    ]:
-                        error_message = (
-                            f"This {product_name} service is already deployed on "
-                            f"{EdgePort.from_subscription(value).description}."
-                        )
-                        raise ValueError(error_message)
-                    return value
-
-            user_input = yield AddAccessPortForm
-            return {"operation": initial_input.operation, "added_access_port": user_input}
-
-        case Operation.REMOVE:
-
-            class RemoveAccessPortForm(FormPage):
-                model_config = ConfigDict(title=f"Remove an Edge Port from a {product_name}")
-                label: Label = Field(
-                    f"Please select one of the Access Ports associated with this {product_name} that should get "
-                    f"removed.",
-                    exclude=True,
-                )
-                access_port: access_port_selector()  # type: ignore[valid-type]
-
-            user_input = yield RemoveAccessPortForm
-            return {"operation": initial_input.operation, "removed_access_port": user_input.access_port}
-
-        case Operation.EDIT:
-
-            class SelectModifyAccessPortForm(FormPage):
-                model_config = ConfigDict(title=f"Modify {product_name}")
-                label: Label = Field(
-                    f"Please select one of the Access Ports associated with this {product_name} to be modified.",
-                    exclude=True,
-                )
-                access_port: access_port_selector()  # type: ignore[valid-type]
-
-            user_input = yield SelectModifyAccessPortForm
-            current_ap = AccessPort.from_db(user_input.access_port)
-            v4_peer = next(peer for peer in current_ap.sbp.bgp_session_list if IPFamily.V4UNICAST in peer.families)
-            v6_peer = next(peer for peer in current_ap.sbp.bgp_session_list if IPFamily.V6UNICAST in peer.families)
-
-            class BindingPortModificationForm(FormPage):
-                model_config = ConfigDict(title=f"{product_name} - Modify Edge Port configuration")
-                current_ep_label: Label = Field(
-                    f'Currently configuring on Edge Port "{current_ap.sbp.edge_port.edge_port_description}"',
-                    exclude=True,
-                )
-
-                gs_id: str = current_ap.sbp.gs_id
-                custom_service_name: str | None = current_ap.custom_service_name
-                is_tagged: bool = current_ap.sbp.is_tagged
-                ap_type: APType | str = current_ap.ap_type  # FIXME: remove str workaround
-                # The SBP model does not require these five fields, but in the case of L3 Core Services this will never
-                # occur since it's a layer 3 service. The ignore statements are there to put our type checker at ease.
-                vlan_id: VLAN_ID = current_ap.sbp.vlan_id  # type: ignore[assignment]
-                ipv4_address: IPv4AddressType = current_ap.sbp.ipv4_address  # type: ignore[assignment]
-                ipv4_mask: IPv4Netmask = current_ap.sbp.ipv4_mask  # type: ignore[assignment]
-                ipv6_address: IPv6AddressType = current_ap.sbp.ipv6_address  # type: ignore[assignment]
-                ipv6_mask: IPv6Netmask = current_ap.sbp.ipv6_mask  # type: ignore[assignment]
-                custom_firewall_filters: bool = current_ap.sbp.custom_firewall_filters
-
-                divider_a: Divider = Field(None, exclude=True)
-                label_a: Label = Field("IPv4 settings for BFD and BGP", exclude=True)
-                v4_bfd_enabled: bool = Field(current_ap.sbp.v4_bfd_settings.bfd_enabled, exclude=True)
-                v4_bfd_multiplier: int | None = Field(current_ap.sbp.v4_bfd_settings.bfd_multiplier, exclude=True)
-                v4_bfd_interval_rx: int | None = Field(current_ap.sbp.v4_bfd_settings.bfd_interval_rx, exclude=True)
-                v4_bfd_interval_tx: int | None = Field(current_ap.sbp.v4_bfd_settings.bfd_interval_tx, exclude=True)
-
-                v4_bgp_peer_address: IPv4AddressType = Field(IPv4AddressType(v4_peer.peer_address), exclude=True)
-                v4_bgp_authentication_key: str | None = Field(v4_peer.authentication_key, exclude=True)
-                v4_bgp_has_custom_policies: bool = Field(v4_peer.has_custom_policies, exclude=True)
-                v4_bgp_bfd_enabled: bool = Field(v4_peer.bfd_enabled, exclude=True)
-                v4_bgp_multipath_enabled: bool = Field(v4_peer.multipath_enabled, exclude=True)
-                v4_bgp_prefix_limit: NonNegativeInt | None = Field(v4_peer.prefix_limit, exclude=True)
-                v4_bgp_is_passive: bool = Field(v4_peer.is_passive, exclude=True)
-                v4_bgp_send_default_route: bool = Field(v4_peer.send_default_route, exclude=True)
-                v4_bgp_add_v4_multicast: bool = Field(bool(IPFamily.V4MULTICAST in v4_peer.families), exclude=True)
-
-                divider_b: Divider = Field(None, exclude=True)
-                label_b: Label = Field("IPv6 settings for BFD and BGP", exclude=True)
-                v6_bfd_enabled: bool = Field(current_ap.sbp.v6_bfd_settings.bfd_enabled, exclude=True)
-                v6_bfd_multiplier: int | None = Field(current_ap.sbp.v6_bfd_settings.bfd_multiplier, exclude=True)
-                v6_bfd_interval_rx: int | None = Field(current_ap.sbp.v6_bfd_settings.bfd_interval_rx, exclude=True)
-                v6_bfd_interval_tx: int | None = Field(current_ap.sbp.v6_bfd_settings.bfd_interval_tx, exclude=True)
-
-                v6_bgp_peer_address: IPv6AddressType = Field(IPv6AddressType(v6_peer.peer_address), exclude=True)
-                v6_bgp_authentication_key: str | None = Field(v6_peer.authentication_key, exclude=True)
-                v6_bgp_has_custom_policies: bool = Field(v6_peer.has_custom_policies, exclude=True)
-                v6_bgp_bfd_enabled: bool = Field(v6_peer.bfd_enabled, exclude=True)
-                v6_bgp_multipath_enabled: bool = Field(v6_peer.multipath_enabled, exclude=True)
-                v6_bgp_prefix_limit: NonNegativeInt | None = Field(v6_peer.prefix_limit, exclude=True)
-                v6_bgp_is_passive: bool = Field(v6_peer.is_passive, exclude=True)
-                v6_bgp_send_default_route: bool = Field(v6_peer.send_default_route, exclude=True)
-                v6_bgp_add_v6_multicast: bool = Field(bool(IPFamily.V6MULTICAST in v6_peer.families), exclude=True)
-
-                @computed_field  # type: ignore[prop-decorator]
-                @property
-                def v4_bfd_settings(self) -> BFDInputModel:
-                    return BFDInputModel(
-                        bfd_enabled=self.v4_bfd_enabled,
-                        bfd_multiplier=self.v4_bfd_multiplier,
-                        bfd_interval_rx=self.v4_bfd_interval_rx,
-                        bfd_interval_tx=self.v4_bfd_interval_tx,
-                    )
-
-                @computed_field  # type: ignore[prop-decorator]
-                @property
-                def v4_bgp_peer(self) -> IPv4BGPPeer:
-                    return IPv4BGPPeer(
-                        peer_address=self.v4_bgp_peer_address,
-                        authentication_key=self.v4_bgp_authentication_key,
-                        has_custom_policies=self.v4_bgp_has_custom_policies,
-                        bfd_enabled=self.v4_bgp_bfd_enabled,
-                        multipath_enabled=self.v4_bgp_multipath_enabled,
-                        prefix_limit=self.v4_bgp_prefix_limit,
-                        is_passive=self.v4_bgp_is_passive,
-                        send_default_route=self.v4_bgp_send_default_route,
-                        add_v4_multicast=self.v4_bgp_add_v4_multicast,
-                    )
-
-                @computed_field  # type: ignore[prop-decorator]
-                @property
-                def v6_bfd_settings(self) -> BFDInputModel:
-                    return BFDInputModel(
-                        bfd_enabled=self.v6_bfd_enabled,
-                        bfd_multiplier=self.v6_bfd_multiplier,
-                        bfd_interval_rx=self.v6_bfd_interval_rx,
-                        bfd_interval_tx=self.v6_bfd_interval_tx,
-                    )
-
-                @computed_field  # type: ignore[prop-decorator]
-                @property
-                def v6_bgp_peer(self) -> IPv6BGPPeer:
-                    return IPv6BGPPeer(
-                        peer_address=self.v6_bgp_peer_address,
-                        authentication_key=self.v6_bgp_authentication_key,
-                        has_custom_policies=self.v6_bgp_has_custom_policies,
-                        bfd_enabled=self.v6_bgp_bfd_enabled,
-                        multipath_enabled=self.v6_bgp_multipath_enabled,
-                        prefix_limit=self.v6_bgp_prefix_limit,
-                        is_passive=self.v6_bgp_is_passive,
-                        send_default_route=self.v6_bgp_send_default_route,
-                        add_v6_multicast=self.v6_bgp_add_v6_multicast,
-                    )
-
-            binding_port_input_form = yield BindingPortModificationForm
-            return {
-                "operation": initial_input.operation,
-                "modified_access_port": user_input.access_port,
-                "modified_sbp": binding_port_input_form,
-            }
-
-        case _:
-            msg = f"Invalid operation selected: {initial_input.operation}"
-            raise ValueError(msg)
-
-
-@step("Instantiate new Service Binding Ports")
-def create_new_sbp(subscription: L3CoreService, added_access_port: dict[str, Any]) -> State:
-    """Add new SBP to the L3 Core Service subscription."""
-    edge_port = EdgePort.from_subscription(added_access_port.pop("edge_port"))
-    bgp_session_list = [
-        BGPSession.new(subscription_id=uuid4(), **session, rtbh_enabled=True, is_multi_hop=True)
-        for session in [added_access_port["v4_bgp_peer"], added_access_port["v6_bgp_peer"]]
-    ]
-    v4_bfd_settings = BFDSettings.new(subscription_id=uuid4(), **added_access_port.pop("v4_bfd_settings"))
-    v6_bfd_settings = BFDSettings.new(subscription_id=uuid4(), **added_access_port.pop("v6_bfd_settings"))
-    sbp_gs_id = (
-        generate_unique_id(prefix="GS")
-        if added_access_port.pop("generate_gs_id", False)
-        else added_access_port.pop("gs_id")
-    )
-    added_access_port.pop("gs_id", None)
-    service_binding_port = ServiceBindingPort.new(
-        subscription_id=uuid4(),
-        **added_access_port,
-        v4_bfd_settings=v4_bfd_settings,
-        v6_bfd_settings=v6_bfd_settings,
-        bgp_session_list=bgp_session_list,
-        sbp_type=SBPType.L3,
-        edge_port=edge_port.edge_port,
-        gs_id=sbp_gs_id,
-    )
-    subscription.l3_core_service.ap_list.append(
-        AccessPort.new(
-            subscription_id=uuid4(),
-            ap_type=added_access_port["ap_type"],
-            sbp=service_binding_port,
-            custom_service_name=added_access_port.get("custom_service_name"),
-        )
-    )
-
-    return {"subscription": subscription}
-
-
-@step("Clean up removed Edge Ports")
-def remove_old_sbp(subscription: L3CoreService, removed_access_port: UUIDstr) -> State:
-    """Remove old SBP product blocks from the GÉANT IP subscription."""
-    subscription.l3_core_service.ap_list.remove(AccessPort.from_db(UUID(removed_access_port)))
-
-    return {"subscription": subscription}
-
-
-@step("Modify existing Service Binding Ports")
-def modify_existing_sbp(
-    subscription: L3CoreService, modified_access_port: UUIDstr, modified_sbp: dict[str, Any]
-) -> State:
-    """Update the subscription model."""
-    current_ap = next(
-        ap for ap in subscription.l3_core_service.ap_list if str(ap.subscription_instance_id) == modified_access_port
-    )
-    v4_peer = next(peer for peer in current_ap.sbp.bgp_session_list if IPFamily.V4UNICAST in peer.families)
-    for attribute in modified_sbp["v4_bgp_peer"]:
-        setattr(v4_peer, attribute, modified_sbp["v4_bgp_peer"][attribute])
-    for attribute in modified_sbp["v4_bfd_settings"]:
-        setattr(current_ap.sbp.v4_bfd_settings, attribute, modified_sbp["v4_bfd_settings"][attribute])
-
-    v6_peer = next(peer for peer in current_ap.sbp.bgp_session_list if IPFamily.V6UNICAST in peer.families)
-    for attribute in modified_sbp["v6_bgp_peer"]:
-        setattr(v6_peer, attribute, modified_sbp["v6_bgp_peer"][attribute])
-    for attribute in modified_sbp["v6_bfd_settings"]:
-        setattr(current_ap.sbp.v6_bfd_settings, attribute, modified_sbp["v6_bfd_settings"][attribute])
-
-    current_ap.sbp.bgp_session_list = [v4_peer, v6_peer]
-    current_ap.sbp.vlan_id = modified_sbp["vlan_id"]
-    current_ap.sbp.gs_id = modified_sbp["gs_id"]
-    current_ap.sbp.is_tagged = modified_sbp["is_tagged"]
-    current_ap.sbp.ipv4_address = modified_sbp["ipv4_address"]
-    current_ap.sbp.ipv4_mask = modified_sbp["ipv4_mask"]
-    current_ap.sbp.ipv6_address = modified_sbp["ipv6_address"]
-    current_ap.sbp.ipv6_mask = modified_sbp["ipv6_mask"]
-    current_ap.sbp.custom_firewall_filters = modified_sbp["custom_firewall_filters"]
-    current_ap.ap_type = modified_sbp["ap_type"]
-    current_ap.custom_service_name = modified_sbp["custom_service_name"]
-
-    return {"subscription": subscription}
-
-
-@workflow(
-    "Modify L3 Core Service",
-    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
-    target=Target.MODIFY,
-)
-def modify_l3_core_service() -> StepList:
-    """Modify an L3 Core Service subscription."""
-    access_port_is_added = conditional(lambda state: state["operation"] == Operation.ADD)
-    access_port_is_removed = conditional(lambda state: state["operation"] == Operation.REMOVE)
-    access_port_is_modified = conditional(lambda state: state["operation"] == Operation.EDIT)
-
-    return (
-        begin
-        >> store_process_subscription(Target.MODIFY)
-        >> unsync
-        >> access_port_is_added(create_new_sbp)
-        >> access_port_is_removed(remove_old_sbp)
-        >> access_port_is_modified(modify_existing_sbp)
-        >> resync
-        >> done
-    )
diff --git a/gso/workflows/l3_core_service/shared.py b/gso/workflows/l3_core_service/shared.py
index 7d0fe6994..bdb4c0ef4 100644
--- a/gso/workflows/l3_core_service/shared.py
+++ b/gso/workflows/l3_core_service/shared.py
@@ -8,8 +8,6 @@ from gso.products.product_types.geant_ip import GeantIP
 from gso.products.product_types.ias import IAS
 from gso.products.product_types.lhcone import LHCOne
 
-L3ProductNameTypes = Literal[ProductName.IAS, ProductName.LHCONE, ProductName.COPERNICUS, ProductName.GEANT_IP]
-
 L3_PRODUCT_NAMES = [
     ProductName.GEANT_IP,
     ProductName.IAS,
@@ -17,10 +15,12 @@ L3_PRODUCT_NAMES = [
     ProductName.COPERNICUS,
 ]
 L3_CORE_SERVICE_PRODUCT_TYPES = [GeantIP.__name__, IAS.__name__, LHCOne.__name__, Copernicus.__name__]
-L3CoreServiceNameTypes = Literal["geant_ip", "ias", "lhcone", "copernicus"]
 L3_SERVICE_NAME_ATTRIBUTES = {"geant_ip", "ias", "lhcone", "copernicus"}
 assert len(L3_PRODUCT_NAMES) == len(L3_CORE_SERVICE_PRODUCT_TYPES) == len(L3_SERVICE_NAME_ATTRIBUTES)  # noqa: S101
 
+L3CoreServiceNameTypes = Literal["geant_ip", "ias", "lhcone", "copernicus"]
+L3ProductNameTypes = Literal[ProductName.IAS, ProductName.LHCONE, ProductName.COPERNICUS, ProductName.GEANT_IP]
+
 L3_CREAT_IMPORTED_WF_MAP = {
     ProductName.COPERNICUS: "create_imported_copernicus",
     ProductName.GEANT_IP: "create_imported_geant_ip",
diff --git a/test/workflows/l3_core_service/test_modify_l3_core_service.py b/test/workflows/l3_core_service/test_modify_l3_core_service.py
index a819b6f9d..b8e08976e 100644
--- a/test/workflows/l3_core_service/test_modify_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_modify_l3_core_service.py
@@ -3,7 +3,7 @@ from orchestrator.domain import SubscriptionModel
 
 from gso.products.product_blocks.bgp_session import IPFamily
 from gso.utils.shared_enums import APType
-from gso.workflows.l3_core_service.modify_l3_core_service import Operation
+from gso.workflows.l3_core_service.base_modify_l3_core_service import Operation
 from gso.workflows.l3_core_service.shared import L3_ATTRIBUTES_MAP, L3_MODIFICATION_WF_MAP, L3_PRODUCT_NAMES
 from test.workflows import extract_state, run_workflow
 
-- 
GitLab


From 7a6a78c7027dc896f9e581bb1a6257836b774e9d Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Mon, 31 Mar 2025 16:01:24 +0200
Subject: [PATCH 60/87] remove service_name from state

---
 gso/products/product_types/base_l3_core.py    | 30 ++++++++++
 gso/products/product_types/copernicus.py      | 45 ++++++++++++++-
 gso/products/product_types/geant_ip.py        | 45 ++++++++++++++-
 gso/products/product_types/ias.py             | 45 ++++++++++++++-
 gso/products/product_types/lhcone.py          | 45 ++++++++++++++-
 .../base_create_imported_l3_core_service.py   | 12 +---
 .../base_create_l3_core_service.py            | 20 ++-----
 .../base_migrate_l3_core_service.py           | 12 ++--
 .../base_modify_l3_core_service.py            | 55 ++++++-------------
 .../base_validate_l3_core_service.py          |  5 +-
 .../base_validate_prefix_list.py              |  5 +-
 .../copernicus/create_copernicus.py           | 17 +-----
 .../copernicus/create_imported_copernicus.py  |  2 +-
 .../copernicus/migrate_copernicus.py          | 14 +----
 .../copernicus/modify_copernicus.py           | 11 +---
 .../copernicus/validate_copernicus.py         | 10 +---
 .../geant_ip/create_geant_ip.py               | 17 +-----
 .../geant_ip/create_imported_geant_ip.py      |  2 +-
 .../geant_ip/migrate_geant_ip.py              | 14 +----
 .../geant_ip/modify_geant_ip.py               | 11 +---
 .../geant_ip/validate_geant_ip.py             | 10 +---
 .../geant_ip/validate_prefix_list.py          | 18 +-----
 .../l3_core_service/ias/create_ias.py         | 19 +------
 .../ias/create_imported_ias.py                |  2 +-
 .../l3_core_service/ias/migrate_ias.py        | 14 +----
 .../l3_core_service/ias/modify_ias.py         | 11 +---
 .../l3_core_service/ias/validate_ias.py       | 10 +---
 .../lhcone/create_imported_lhcone.py          |  2 +-
 .../l3_core_service/lhcone/create_lhcone.py   | 17 +-----
 .../l3_core_service/lhcone/migrate_lhcone.py  | 14 +----
 .../l3_core_service/lhcone/modify_lhcone.py   | 11 +---
 .../l3_core_service/lhcone/validate_lhcone.py | 10 +---
 gso/workflows/l3_core_service/shared.py       | 12 +---
 test/fixtures/l3_core_service_fixtures.py     | 17 ++----
 .../edge_port/test_migrate_edge_port.py       |  4 +-
 .../test_create_l3_core_service.py            | 12 ++--
 .../test_migrate_l3_core_service.py           | 19 +++----
 .../test_modify_l3_core_service.py            | 12 ++--
 38 files changed, 287 insertions(+), 344 deletions(-)
 create mode 100644 gso/products/product_types/base_l3_core.py

diff --git a/gso/products/product_types/base_l3_core.py b/gso/products/product_types/base_l3_core.py
new file mode 100644
index 000000000..b7b6937b9
--- /dev/null
+++ b/gso/products/product_types/base_l3_core.py
@@ -0,0 +1,30 @@
+"""Base class for L3 products."""
+
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from typing import TYPE_CHECKING
+
+from orchestrator.domain import SubscriptionModel
+
+if TYPE_CHECKING:
+    from gso.products.product_blocks.l3_core_service import L3CoreServiceBlockInactive
+
+
+class BaseL3SubscriptionModel(SubscriptionModel, ABC):
+    """Base class for L3 products."""
+
+    @property
+    @abstractmethod
+    def l3_core(self) -> L3CoreServiceBlockInactive | None:
+        """Getter: Should be implemented by subclasses."""
+
+    @l3_core.setter
+    @abstractmethod
+    def l3_core(self, value: L3CoreServiceBlockInactive) -> None:
+        """Setter: Should be implemented by subclasses."""
+
+    @property
+    @abstractmethod
+    def service_name_attribute(self) -> str:
+        """Get the service name."""
diff --git a/gso/products/product_types/copernicus.py b/gso/products/product_types/copernicus.py
index 8b6bb242f..70c904a18 100644
--- a/gso/products/product_types/copernicus.py
+++ b/gso/products/product_types/copernicus.py
@@ -1,6 +1,9 @@
 """Product type for LHCONE."""
 
-from orchestrator.domain.base import SubscriptionModel
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
 from orchestrator.types import SubscriptionLifecycle
 
 from gso.products.product_blocks.copernicus import (
@@ -8,13 +11,34 @@ from gso.products.product_blocks.copernicus import (
     CopernicusBlockInactive,
     CopernicusBlockProvisioning,
 )
+from gso.products.product_types.base_l3_core import BaseL3SubscriptionModel
+
+if TYPE_CHECKING:
+    from gso.products.product_blocks.l3_core_service import (
+        L3CoreServiceBlockInactive,
+    )
 
 
-class CopernicusInactive(SubscriptionModel, is_base=True):
+class CopernicusInactive(BaseL3SubscriptionModel, is_base=True):
     """A Copernicus product that is inactive."""
 
     copernicus: CopernicusBlockInactive
 
+    @property
+    def l3_core(self) -> L3CoreServiceBlockInactive | None:
+        """Getter: Retrieve the l3_core from the copernicus attribute."""
+        return self.copernicus.l3_core
+
+    @l3_core.setter
+    def l3_core(self, value: L3CoreServiceBlockInactive) -> None:
+        """Setter: Set the l3_core on the copernicus attribute."""
+        self.copernicus.l3_core = value
+
+    @property
+    def service_name_attribute(self) -> str:
+        """Get the service name."""
+        return "copernicus"
+
 
 class CopernicusProvisioning(CopernicusInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
     """A Copernicus product that is being provisioned."""
@@ -28,11 +52,26 @@ class Copernicus(CopernicusProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE
     copernicus: CopernicusBlock
 
 
-class ImportedCopernicusInactive(SubscriptionModel, is_base=True):
+class ImportedCopernicusInactive(BaseL3SubscriptionModel, is_base=True):
     """An imported Copernicus product that is inactive."""
 
     copernicus: CopernicusBlockInactive
 
+    @property
+    def l3_core(self) -> L3CoreServiceBlockInactive | None:
+        """Getter: Retrieve the l3_core from the copernicus attribute."""
+        return self.copernicus.l3_core
+
+    @l3_core.setter
+    def l3_core(self, value: L3CoreServiceBlockInactive) -> None:
+        """Setter: Set the l3_core on the copernicus attribute."""
+        self.copernicus.l3_core = value
+
+    @property
+    def service_name_attribute(self) -> str:
+        """Get the service name."""
+        return "copernicus"
+
 
 class ImportedCopernicus(
     ImportedCopernicusInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE]
diff --git a/gso/products/product_types/geant_ip.py b/gso/products/product_types/geant_ip.py
index a986623e6..c7c7bd383 100644
--- a/gso/products/product_types/geant_ip.py
+++ b/gso/products/product_types/geant_ip.py
@@ -1,6 +1,9 @@
 """Product type for IAS."""
 
-from orchestrator.domain.base import SubscriptionModel
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
 from orchestrator.types import SubscriptionLifecycle
 
 from gso.products.product_blocks.geant_ip import (
@@ -8,13 +11,34 @@ from gso.products.product_blocks.geant_ip import (
     GeantIPBlockInactive,
     GeantIPBlockProvisioning,
 )
+from gso.products.product_types.base_l3_core import BaseL3SubscriptionModel
+
+if TYPE_CHECKING:
+    from gso.products.product_blocks.l3_core_service import (
+        L3CoreServiceBlockInactive,
+    )
 
 
-class GeantIPInactive(SubscriptionModel, is_base=True):
+class GeantIPInactive(BaseL3SubscriptionModel, is_base=True):
     """A GeantIP product that is inactive."""
 
     geant_ip: GeantIPBlockInactive
 
+    @property
+    def l3_core(self) -> L3CoreServiceBlockInactive | None:
+        """Getter: Retrieve the l3_core from the geant_ip attribute."""
+        return self.geant_ip.l3_core
+
+    @l3_core.setter
+    def l3_core(self, value: L3CoreServiceBlockInactive) -> None:
+        """Setter: Set the l3_core on the geant_ip attribute."""
+        self.geant_ip.l3_core = value
+
+    @property
+    def service_name_attribute(self) -> str:
+        """Get the service name."""
+        return "geant_ip"
+
 
 class GeantIPProvisioning(GeantIPInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
     """A GeantIP product that is being provisioned."""
@@ -28,11 +52,26 @@ class GeantIP(GeantIPProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
     geant_ip: GeantIPBlock
 
 
-class ImportedGeantIPInactive(SubscriptionModel, is_base=True):
+class ImportedGeantIPInactive(BaseL3SubscriptionModel, is_base=True):
     """An imported GeantIP product that is inactive."""
 
     geant_ip: GeantIPBlockInactive
 
+    @property
+    def l3_core(self) -> L3CoreServiceBlockInactive | None:
+        """Getter: Retrieve the l3_core from the geant_ip attribute."""
+        return self.geant_ip.l3_core
+
+    @l3_core.setter
+    def l3_core(self, value: L3CoreServiceBlockInactive) -> None:
+        """Setter: Set the l3_core on the geant_ip attribute."""
+        self.geant_ip.l3_core = value
+
+    @property
+    def service_name_attribute(self) -> str:
+        """Get the service name."""
+        return "geant_ip"
+
 
 class ImportedGeantIP(
     ImportedGeantIPInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE]
diff --git a/gso/products/product_types/ias.py b/gso/products/product_types/ias.py
index f67a74c5b..e339ea8df 100644
--- a/gso/products/product_types/ias.py
+++ b/gso/products/product_types/ias.py
@@ -1,6 +1,9 @@
 """Product type for IAS."""
 
-from orchestrator.domain.base import SubscriptionModel
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
 from orchestrator.types import SubscriptionLifecycle
 
 from gso.products.product_blocks.ias import (
@@ -8,13 +11,34 @@ from gso.products.product_blocks.ias import (
     IASBlockInactive,
     IASBlockProvisioning,
 )
+from gso.products.product_types.base_l3_core import BaseL3SubscriptionModel
+
+if TYPE_CHECKING:
+    from gso.products.product_blocks.l3_core_service import (
+        L3CoreServiceBlockInactive,
+    )
 
 
-class IASInactive(SubscriptionModel, is_base=True):
+class IASInactive(BaseL3SubscriptionModel, is_base=True):
     """An IAS product that is inactive."""
 
     ias: IASBlockInactive
 
+    @property
+    def l3_core(self) -> L3CoreServiceBlockInactive | None:
+        """Getter: Retrieve the l3_core from the ias attribute."""
+        return self.ias.l3_core
+
+    @l3_core.setter
+    def l3_core(self, value: L3CoreServiceBlockInactive) -> None:
+        """Setter: Set the l3_core on the ias attribute."""
+        self.ias.l3_core = value
+
+    @property
+    def service_name_attribute(self) -> str:
+        """Get the service name."""
+        return "ias"
+
 
 class IASProvisioning(IASInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
     """An IAS product that is being provisioned."""
@@ -28,11 +52,26 @@ class IAS(IASProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
     ias: IASBlock
 
 
-class ImportedIASInactive(SubscriptionModel, is_base=True):
+class ImportedIASInactive(BaseL3SubscriptionModel, is_base=True):
     """An imported IAS product that is inactive."""
 
     ias: IASBlockInactive
 
+    @property
+    def l3_core(self) -> L3CoreServiceBlockInactive | None:
+        """Getter: Retrieve the l3_core from the ias attribute."""
+        return self.ias.l3_core
+
+    @l3_core.setter
+    def l3_core(self, value: L3CoreServiceBlockInactive) -> None:
+        """Setter: Set the l3_core on the ias attribute."""
+        self.ias.l3_core = value
+
+    @property
+    def service_name_attribute(self) -> str:
+        """Get the service name."""
+        return "ias"
+
 
 class ImportedIAS(ImportedIASInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE]):
     """An imported IAS product that is active."""
diff --git a/gso/products/product_types/lhcone.py b/gso/products/product_types/lhcone.py
index 84374aaf4..b65ed0c69 100644
--- a/gso/products/product_types/lhcone.py
+++ b/gso/products/product_types/lhcone.py
@@ -1,6 +1,9 @@
 """Product type for LHCONE."""
 
-from orchestrator.domain.base import SubscriptionModel
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
 from orchestrator.types import SubscriptionLifecycle
 
 from gso.products.product_blocks.lhcone import (
@@ -8,13 +11,34 @@ from gso.products.product_blocks.lhcone import (
     LHCOneBlockInactive,
     LHCOneBlockProvisioning,
 )
+from gso.products.product_types.base_l3_core import BaseL3SubscriptionModel
+
+if TYPE_CHECKING:
+    from gso.products.product_blocks.l3_core_service import (
+        L3CoreServiceBlockInactive,
+    )
 
 
-class LHCOneInactive(SubscriptionModel, is_base=True):
+class LHCOneInactive(BaseL3SubscriptionModel, is_base=True):
     """A LHCOne product that is inactive."""
 
     lhcone: LHCOneBlockInactive
 
+    @property
+    def l3_core(self) -> L3CoreServiceBlockInactive | None:
+        """Getter: Retrieve the l3_core from the lhcone attribute."""
+        return self.lhcone.l3_core
+
+    @l3_core.setter
+    def l3_core(self, value: L3CoreServiceBlockInactive) -> None:
+        """Setter: Set the l3_core on the lhcone attribute."""
+        self.lhcone.l3_core = value
+
+    @property
+    def service_name_attribute(self) -> str:
+        """Get the service name."""
+        return "lhcone"
+
 
 class LHCOneProvisioning(LHCOneInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
     """A LHCOne product that is being provisioned."""
@@ -28,11 +52,26 @@ class LHCOne(LHCOneProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
     lhcone: LHCOneBlock
 
 
-class ImportedLHCOneInactive(SubscriptionModel, is_base=True):
+class ImportedLHCOneInactive(BaseL3SubscriptionModel, is_base=True):
     """An imported LHCOne product that is inactive."""
 
     lhcone: LHCOneBlockInactive
 
+    @property
+    def l3_core(self) -> L3CoreServiceBlockInactive | None:
+        """Getter: Retrieve the l3_core from the lhcone attribute."""
+        return self.lhcone.l3_core
+
+    @l3_core.setter
+    def l3_core(self, value: L3CoreServiceBlockInactive) -> None:
+        """Setter: Set the l3_core on the lhcone attribute."""
+        self.lhcone.l3_core = value
+
+    @property
+    def service_name_attribute(self) -> str:
+        """Get the service name."""
+        return "lhcone"
+
 
 class ImportedLHCOne(
     ImportedLHCOneInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE]
diff --git a/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
index a35a6a168..556b5eaf8 100644
--- a/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
@@ -16,7 +16,7 @@ from gso.utils.shared_enums import SBPType
 from gso.utils.types.geant_ids import IMPORTED_GS_ID
 from gso.utils.types.ip_address import IPAddress, IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask
 from gso.utils.types.virtual_identifiers import VLAN_ID
-from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes, L3ProductNameTypes
+from gso.workflows.l3_core_service.shared import L3ProductNameTypes
 
 
 def base_initial_input_form_generator() -> FormGenerator:
@@ -71,9 +71,7 @@ def base_initial_input_form_generator() -> FormGenerator:
 
 
 @step("Initialize subscription")
-def initialize_subscription(
-    subscription: SubscriptionModel, service_binding_ports: list, service_name: L3CoreServiceNameTypes
-) -> dict:
+def initialize_subscription(subscription: SubscriptionModel, service_binding_ports: list) -> dict:
     """Initialize the subscription with the user input."""
     for service_binding_port in service_binding_ports:
         edge_port_subscription = EdgePort.from_subscription(service_binding_port.pop("edge_port"))
@@ -96,12 +94,8 @@ def initialize_subscription(
             v6_bfd_settings=BFDSettings.new(subscription_id=uuid4(), **(service_binding_port.pop("v6_bfd_settings"))),
             **service_binding_port,
         )
-        service = getattr(subscription, service_name)
-        if service is None:
-            msg = f"{service_name} is not set on subscription"
-            raise AttributeError(msg)
 
-        service.l3_core = L3CoreServiceBlockInactive.new(
+        subscription.l3_core = L3CoreServiceBlockInactive.new(  # type: ignore[attr-defined]
             subscription_id=uuid4(),
             ap_list=[
                 AccessPortInactive.new(
diff --git a/gso/workflows/l3_core_service/base_create_l3_core_service.py b/gso/workflows/l3_core_service/base_create_l3_core_service.py
index 22381866e..18a8c9d84 100644
--- a/gso/workflows/l3_core_service/base_create_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_l3_core_service.py
@@ -29,7 +29,6 @@ from gso.utils.types.geant_ids import IMPORTED_GS_ID
 from gso.utils.types.ip_address import IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask
 from gso.utils.types.tt_number import TTNumber
 from gso.utils.types.virtual_identifiers import VLAN_ID
-from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes
 
 
 def initial_input_form_generator(product_name: str) -> FormGenerator:
@@ -161,12 +160,12 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
     )
 
 
-def initialize_service_binding(
+@step("Initialize subscription")
+def initialize_subscription(
     subscription: SubscriptionModel,
     edge_port: dict,
     binding_port_input: dict,
     product_name: str,
-    service_name: L3CoreServiceNameTypes,
 ) -> dict:
     """Initialize a service binding port for a given service type in the subscription model."""
     edge_port_fqdn_list = []
@@ -196,12 +195,7 @@ def initialize_service_binding(
         gs_id=sbp_gs_id,
     )
 
-    service = getattr(subscription, service_name)
-    if service is None:
-        msg = f"{service_name} is not set on subscription"
-        raise ValueError(msg)
-
-    service.l3_core = L3CoreServiceBlockInactive.new(
+    subscription.l3_core = L3CoreServiceBlockInactive.new(  # type: ignore[attr-defined]
         subscription_id=uuid4(),
         ap_list=[
             AccessPortInactive.new(
@@ -344,15 +338,9 @@ def create_new_sharepoint_checklist(
     subscription: SubscriptionModel,
     tt_number: TTNumber,
     process_id: UUIDstr,
-    service_name: str,
 ) -> State:
     """Create a new checklist item in SharePoint for approving this L3 Core Service."""
-    service = getattr(subscription, service_name)
-    if service is None:
-        msg = f"{service_name} is not set on subscription"
-        raise ValueError(msg)
-
-    new_ep = service.l3_core.ap_list[0].sbp.edge_port
+    new_ep = subscription.l3_core.ap_list[0].sbp.edge_port  # type: ignore[attr-defined]
     new_list_item_url = SharePointClient().add_list_item(
         list_name="l3_core_service",
         fields={
diff --git a/gso/workflows/l3_core_service/base_migrate_l3_core_service.py b/gso/workflows/l3_core_service/base_migrate_l3_core_service.py
index 346b24a0d..3ae59b1fd 100644
--- a/gso/workflows/l3_core_service/base_migrate_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_migrate_l3_core_service.py
@@ -27,14 +27,14 @@ from gso.services.partners import get_partner_by_id
 from gso.services.subscriptions import get_active_edge_port_subscriptions
 from gso.utils.types.tt_number import TTNumber
 from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, SKIP_MOODI_KEY
-from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes
 from gso.workflows.shared import create_summary_form
 
 
-def initial_input_form(subscription: SubscriptionModel, service_name: L3CoreServiceNameTypes) -> FormGenerator:
+def initial_input_form(subscription_id: UUIDstr) -> FormGenerator:
     """Gather input from the operator on what destination Edge Ports this L3 Core Service should be migrated to."""
+    subscription = SubscriptionModel.from_subscription(subscription_id)
     partner_id = subscription.customer_id
-    ap_list = getattr(subscription, service_name).l3_core.ap_list
+    ap_list = subscription.l3_core.ap_list  # type: ignore[attr-defined]
 
     current_ep_list = {
         str(
@@ -93,7 +93,6 @@ def initial_input_form(subscription: SubscriptionModel, service_name: L3CoreServ
         | {
             IS_HUMAN_INITIATED_WF_KEY: source_ep_user_input.is_human_initiated_wf,
             SKIP_MOODI_KEY: source_ep_user_input.skip_moodi,
-            "service_name": service_name,
         }
     )
 
@@ -248,13 +247,13 @@ def generate_scoped_subscription_model(
     subscription: SubscriptionModel,
     source_edge_port: EdgePort,
     destination_edge_port: EdgePort,
-    service_name: L3CoreServiceNameTypes,
 ) -> State:
     """Calculate what the updated subscription model will look like, but don't update the actual subscription yet.
 
     The new subscription is used for running Ansible playbooks remotely, but the updated subscription model is not
     stored yet, to avoid issues recovering when the workflow is aborted.
     """
+    service_name = subscription.service_name_attribute  # type: ignore[attr-defined]
     updated_subscription = json.loads(json_dumps(subscription))
     for index, ap in enumerate(updated_subscription[service_name]["l3_core"]["ap_list"]):
         if ap["sbp"]["edge_port"]["owner_subscription_id"] == str(source_edge_port.subscription_id):
@@ -383,10 +382,9 @@ def update_subscription_model(
     subscription: SubscriptionModel,
     destination_edge_port: EdgePort,
     replaced_ap_index: int,
-    service_name: L3CoreServiceNameTypes,
 ) -> State:
     """Update the subscription model with the destination Edge Port attached to the Access Port that is migrated."""
-    ap_list = getattr(subscription, service_name).l3_core.ap_list
+    ap_list = subscription.l3_core.ap_list  # type: ignore[attr-defined]
     ap_list[replaced_ap_index].sbp.edge_port = destination_edge_port.edge_port
 
     return {"subscription": subscription, "__remove_keys": ["replaced_ap_index"]}
diff --git a/gso/workflows/l3_core_service/base_modify_l3_core_service.py b/gso/workflows/l3_core_service/base_modify_l3_core_service.py
index 250550189..b9b8c77e1 100644
--- a/gso/workflows/l3_core_service/base_modify_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_modify_l3_core_service.py
@@ -20,7 +20,6 @@ from gso.utils.types.geant_ids import IMPORTED_GS_ID
 from gso.utils.types.ip_address import IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask
 from gso.utils.types.tt_number import TTNumber
 from gso.utils.types.virtual_identifiers import VLAN_ID
-from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes
 
 
 class Operation(strEnum):
@@ -31,16 +30,11 @@ class Operation(strEnum):
     EDIT = "Edit an existing Access Port"
 
 
-def base_initial_input_form_generator(subscription_id: UUIDstr, service_name: L3CoreServiceNameTypes) -> FormGenerator:
+def base_initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     """Get input about added, removed, and modified Access Ports."""
     subscription = SubscriptionModel.from_subscription(subscription_id)
     product_name = subscription.product.name
 
-    service = getattr(subscription, service_name)
-    if service is None:
-        msg = f"{service_name} is not set on subscription"
-        raise ValueError(msg)
-
     class OperationSelectionForm(FormPage):
         model_config = ConfigDict(title="Modify Edge Port")
 
@@ -49,7 +43,7 @@ def base_initial_input_form_generator(subscription_id: UUIDstr, service_name: L3
 
     def access_port_selector() -> TypeAlias:
         """Generate a dropdown selector for choosing an Access Port in an input form."""
-        access_ports = service.l3_core.ap_list
+        access_ports = subscription.l3_core.ap_list  # type: ignore[attr-defined]
         options = {
             str(access_port.subscription_instance_id): (
                 f"{access_port.sbp.gs_id} on "
@@ -132,7 +126,7 @@ def base_initial_input_form_generator(subscription_id: UUIDstr, service_name: L3
                     str(edge_port.subscription_id): edge_port.description
                     for edge_port in edge_ports
                     if edge_port.subscription_id
-                    not in [ap.sbp.edge_port.owner_subscription_id for ap in service.l3_core.ap_list]
+                    not in [ap.sbp.edge_port.owner_subscription_id for ap in subscription.l3_core.ap_list]  # type: ignore[attr-defined]
                 }
 
                 return cast(
@@ -155,7 +149,7 @@ def base_initial_input_form_generator(subscription_id: UUIDstr, service_name: L3
                                 ap_type=access_port.ap_type.value,
                                 custom_service_name=access_port.custom_service_name or "",
                             )
-                            for access_port in service.l3_core.ap_list
+                            for access_port in subscription.l3_core.ap_list  # type: ignore[attr-defined]
                         ],
                         default_type=list[AccessPortListItem],
                     ),
@@ -209,7 +203,7 @@ def base_initial_input_form_generator(subscription_id: UUIDstr, service_name: L3
 
                 @field_validator("edge_port")
                 def selected_edge_port_is_new(cls, value: UUIDstr) -> UUIDstr:
-                    if value in [str(ap.sbp.edge_port.owner_subscription_id) for ap in service.l3_core.ap_list]:
+                    if value in [str(ap.sbp.edge_port.owner_subscription_id) for ap in subscription.l3_core.ap_list]:  # type: ignore[attr-defined]
                         error_message = (
                             f"This {product_name} service is already deployed on "
                             f"{EdgePort.from_subscription(value).description}."
@@ -218,7 +212,10 @@ def base_initial_input_form_generator(subscription_id: UUIDstr, service_name: L3
                     return value
 
             user_input = yield AddAccessPortForm
-            return {"operation": initial_input.operation, "added_access_port": user_input, "service_name": service_name}
+            return {
+                "operation": initial_input.operation,
+                "added_access_port": user_input,
+            }
 
         case Operation.REMOVE:
 
@@ -235,7 +232,6 @@ def base_initial_input_form_generator(subscription_id: UUIDstr, service_name: L3
             return {
                 "operation": initial_input.operation,
                 "removed_access_port": user_input.access_port,
-                "service_name": service_name,
             }
 
         case Operation.EDIT:
@@ -362,7 +358,6 @@ def base_initial_input_form_generator(subscription_id: UUIDstr, service_name: L3
                 "operation": initial_input.operation,
                 "modified_access_port": user_input.access_port,
                 "modified_sbp": binding_port_input_form,
-                "service_name": service_name,
             }
 
         case _:
@@ -371,9 +366,7 @@ def base_initial_input_form_generator(subscription_id: UUIDstr, service_name: L3
 
 
 @step("Instantiate new Service Binding Ports")
-def create_new_sbp(
-    subscription: SubscriptionModel, added_access_port: dict[str, Any], service_name: L3CoreServiceNameTypes
-) -> State:
+def create_new_sbp(subscription: SubscriptionModel, added_access_port: dict[str, Any]) -> State:
     """Add new SBP to the L3 Core Service subscription."""
     edge_port = EdgePort.from_subscription(added_access_port.pop("edge_port"))
     bgp_session_list = [
@@ -398,12 +391,7 @@ def create_new_sbp(
         edge_port=edge_port.edge_port,
         gs_id=sbp_gs_id,
     )
-    service = getattr(subscription, service_name)
-    if service is None:
-        msg = f"{service_name} is not set on subscription"
-        raise ValueError(msg)
-
-    service.l3_core.ap_list.append(
+    subscription.l3_core.ap_list.append(  # type: ignore[attr-defined]
         AccessPort.new(
             subscription_id=uuid4(),
             ap_type=added_access_port["ap_type"],
@@ -416,14 +404,9 @@ def create_new_sbp(
 
 
 @step("Clean up removed Edge Ports")
-def remove_old_sbp(subscription: SubscriptionModel, removed_access_port: UUIDstr, service_name: str) -> State:
+def remove_old_sbp(subscription: SubscriptionModel, removed_access_port: UUIDstr) -> State:
     """Remove old SBP product blocks from the specific L3 core service subscription."""
-    service = getattr(subscription, service_name)
-    if service is None:
-        msg = f"{service_name} is not set on subscription"
-        raise ValueError(msg)
-
-    service.l3_core.ap_list.remove(AccessPort.from_db(UUID(removed_access_port)))
+    subscription.l3_core.ap_list.remove(AccessPort.from_db(UUID(removed_access_port)))  # type: ignore[attr-defined]
 
     return {"subscription": subscription}
 
@@ -433,15 +416,13 @@ def modify_existing_sbp(
     subscription: SubscriptionModel,
     modified_access_port: UUIDstr,
     modified_sbp: dict[str, Any],
-    service_name: L3CoreServiceNameTypes,
 ) -> State:
     """Update the subscription model."""
-    service = getattr(subscription, service_name)
-    if service is None:
-        msg = f"{service_name} is not set on subscription"
-        raise ValueError(msg)
-
-    current_ap = next(ap for ap in service.l3_core.ap_list if str(ap.subscription_instance_id) == modified_access_port)
+    current_ap = next(
+        ap
+        for ap in subscription.l3_core.ap_list  # type: ignore[attr-defined]
+        if str(ap.subscription_instance_id) == modified_access_port
+    )
     v4_peer = next(peer for peer in current_ap.sbp.bgp_session_list if IPFamily.V4UNICAST in peer.families)
     for attribute in modified_sbp["v4_bgp_peer"]:
         setattr(v4_peer, attribute, modified_sbp["v4_bgp_peer"][attribute])
diff --git a/gso/workflows/l3_core_service/base_validate_l3_core_service.py b/gso/workflows/l3_core_service/base_validate_l3_core_service.py
index e1af96f44..f1e5f3a00 100644
--- a/gso/workflows/l3_core_service/base_validate_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_validate_l3_core_service.py
@@ -8,13 +8,12 @@ from pydantic_forms.types import UUIDstr
 
 from gso.services.lso_client import LSOState
 from gso.services.partners import get_partner_by_id
-from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes
 
 
 @step("Prepare list of all Access Ports")
-def build_fqdn_list(subscription: SubscriptionModel, service_name: L3CoreServiceNameTypes) -> dict[str, list[str]]:
+def build_fqdn_list(subscription: SubscriptionModel) -> dict[str, list[str]]:
     """Build the list of all FQDNs that are in the list of access ports of a L3 Core Service subscription."""
-    ap_list = getattr(subscription, service_name).l3_core.ap_list
+    ap_list = subscription.l3_core.ap_list  # type: ignore[attr-defined]
     ap_fqdn_list = [ap.sbp.edge_port.node.router_fqdn for ap in ap_list]
     return {"ap_fqdn_list": ap_fqdn_list}
 
diff --git a/gso/workflows/l3_core_service/base_validate_prefix_list.py b/gso/workflows/l3_core_service/base_validate_prefix_list.py
index b55e056eb..1771426c9 100644
--- a/gso/workflows/l3_core_service/base_validate_prefix_list.py
+++ b/gso/workflows/l3_core_service/base_validate_prefix_list.py
@@ -13,14 +13,13 @@ from pydantic_forms.validators import Label
 from gso.services.lso_client import LSOState
 from gso.services.partners import get_partner_by_id
 from gso.utils.shared_enums import Vendor
-from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes
 
 
 @step("Prepare list of all Access Ports")
-def build_fqdn_list(subscription_id: UUIDstr, service_name: L3CoreServiceNameTypes) -> State:
+def build_fqdn_list(subscription_id: UUIDstr) -> State:
     """Build the list of all FQDNs in the access ports of L3 Core Service subscription, excluding Juniper devices."""
     subscription = SubscriptionModel.from_subscription(subscription_id)
-    ap_list = getattr(subscription, service_name).l3_core.ap_list
+    ap_list = subscription.l3_core.ap_list  # type: ignore[attr-defined]
     ap_fqdn_list = [
         ap.sbp.edge_port.node.router_fqdn for ap in ap_list if ap.sbp.edge_port.node.vendor != Vendor.JUNIPER
     ]
diff --git a/gso/workflows/l3_core_service/copernicus/create_copernicus.py b/gso/workflows/l3_core_service/copernicus/create_copernicus.py
index 2504fe0b6..c011f1865 100644
--- a/gso/workflows/l3_core_service/copernicus/create_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/create_copernicus.py
@@ -17,12 +17,11 @@ from gso.workflows.l3_core_service.base_create_l3_core_service import (
     deploy_bgp_peers_dry,
     deploy_bgp_peers_real,
     initial_input_form_generator,
-    initialize_service_binding,
+    initialize_subscription,
     provision_sbp_dry,
     provision_sbp_real,
     update_dns_records,
 )
-from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes
 
 
 @step("Create subscription")
@@ -30,19 +29,7 @@ def create_subscription(product: UUIDstr, partner: str) -> State:
     """Create a new subscription object in the database."""
     subscription = CopernicusInactive.from_product_id(product, partner)
 
-    return {"subscription": subscription, "subscription_id": subscription.subscription_id, "service_name": "copernicus"}
-
-
-@step("Initialize subscription")
-def initialize_subscription(
-    subscription: CopernicusInactive,
-    edge_port: dict,
-    binding_port_input: dict,
-    product_name: str,
-    service_name: L3CoreServiceNameTypes,
-) -> State:
-    """Take all user inputs and use them to populate the subscription model."""
-    return initialize_service_binding(subscription, edge_port, binding_port_input, product_name, service_name)
+    return {"subscription": subscription, "subscription_id": subscription.subscription_id}
 
 
 @workflow(
diff --git a/gso/workflows/l3_core_service/copernicus/create_imported_copernicus.py b/gso/workflows/l3_core_service/copernicus/create_imported_copernicus.py
index 4042c42be..24a1bcba8 100644
--- a/gso/workflows/l3_core_service/copernicus/create_imported_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/create_imported_copernicus.py
@@ -22,7 +22,7 @@ def create_subscription(partner: str) -> dict:
     partner_id = get_partner_by_name(partner).partner_id
     product_id = get_product_id_by_name(ProductName.IMPORTED_COPERNICUS)
     subscription = ImportedCopernicusInactive.from_product_id(product_id, partner_id)
-    return {"subscription": subscription, "subscription_id": subscription.subscription_id, "service_name": "copernicus"}
+    return {"subscription": subscription, "subscription_id": subscription.subscription_id}
 
 
 @workflow(
diff --git a/gso/workflows/l3_core_service/copernicus/migrate_copernicus.py b/gso/workflows/l3_core_service/copernicus/migrate_copernicus.py
index 2719d4631..17b8156d8 100644
--- a/gso/workflows/l3_core_service/copernicus/migrate_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/migrate_copernicus.py
@@ -13,9 +13,7 @@ from orchestrator.targets import Target
 from orchestrator.workflow import StepList, begin, conditional, done
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
-from pydantic_forms.types import FormGenerator, UUIDstr
 
-from gso.products.product_types.copernicus import Copernicus
 from gso.services.lso_client import lso_interaction
 from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, start_moodi, stop_moodi
 from gso.workflows.l3_core_service.base_migrate_l3_core_service import (
@@ -37,19 +35,9 @@ from gso.workflows.l3_core_service.base_migrate_l3_core_service import (
 )
 
 
-def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
-    """Gather input from the operator on what destination Edge Ports this Copernicus Service should be migrated to."""
-    subscription = Copernicus.from_subscription(subscription_id)
-
-    initial_generator = initial_input_form(subscription, service_name="copernicus")
-    initial_user_input = yield from initial_generator
-
-    return initial_user_input
-
-
 @workflow(
     "Migrate Copernicus",
-    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
+    initial_input_form=wrap_modify_initial_input_form(initial_input_form),
     target=Target.MODIFY,
 )
 def migrate_copernicus() -> StepList:
diff --git a/gso/workflows/l3_core_service/copernicus/modify_copernicus.py b/gso/workflows/l3_core_service/copernicus/modify_copernicus.py
index 78b1982cf..37a263d79 100644
--- a/gso/workflows/l3_core_service/copernicus/modify_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/modify_copernicus.py
@@ -5,7 +5,6 @@ from orchestrator.targets import Target
 from orchestrator.workflow import StepList
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
-from pydantic_forms.types import FormGenerator, UUIDstr
 
 from gso.workflows.l3_core_service.base_modify_l3_core_service import (
     Operation,
@@ -16,17 +15,9 @@ from gso.workflows.l3_core_service.base_modify_l3_core_service import (
 )
 
 
-def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
-    """Generate the initial input form for modifying a Copernicus subscription."""
-    initial_generator = base_initial_input_form_generator(subscription_id, service_name="copernicus")
-    initial_user_input = yield from initial_generator
-
-    return initial_user_input
-
-
 @workflow(
     "Modify Copernicus",
-    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
+    initial_input_form=wrap_modify_initial_input_form(base_initial_input_form_generator),
     target=Target.MODIFY,
 )
 def modify_copernicus() -> StepList:
diff --git a/gso/workflows/l3_core_service/copernicus/validate_copernicus.py b/gso/workflows/l3_core_service/copernicus/validate_copernicus.py
index 873524f55..820329369 100644
--- a/gso/workflows/l3_core_service/copernicus/validate_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/validate_copernicus.py
@@ -1,10 +1,9 @@
 """Validation workflow for Copernicus subscription objects."""
 
 from orchestrator.targets import Target
-from orchestrator.workflow import StepList, begin, done, step, workflow
+from orchestrator.workflow import StepList, begin, done, workflow
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
-from pydantic_forms.types import State
 
 from gso.services.lso_client import anonymous_lso_interaction
 from gso.workflows.l3_core_service.base_validate_l3_core_service import (
@@ -15,18 +14,11 @@ from gso.workflows.l3_core_service.base_validate_l3_core_service import (
 )
 
 
-@step("Inject service name")
-def inject_srvice_name() -> State:
-    """Inject the service name into the subscription."""
-    return {"service_name": "copernicus"}
-
-
 @workflow("Validate Copernicus", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None)))
 def validate_copernicus() -> StepList:
     """Validate an existing Copernicus subscription."""
     return (
         begin
-        >> inject_srvice_name
         >> store_process_subscription(Target.SYSTEM)
         >> unsync
         >> build_fqdn_list
diff --git a/gso/workflows/l3_core_service/geant_ip/create_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/create_geant_ip.py
index 346480000..18315f20b 100644
--- a/gso/workflows/l3_core_service/geant_ip/create_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/create_geant_ip.py
@@ -17,12 +17,11 @@ from gso.workflows.l3_core_service.base_create_l3_core_service import (
     deploy_bgp_peers_dry,
     deploy_bgp_peers_real,
     initial_input_form_generator,
-    initialize_service_binding,
+    initialize_subscription,
     provision_sbp_dry,
     provision_sbp_real,
     update_dns_records,
 )
-from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes
 
 
 @step("Create subscription")
@@ -30,19 +29,7 @@ def create_subscription(product: UUIDstr, partner: str) -> State:
     """Create a new subscription object in the database."""
     subscription = GeantIPInactive.from_product_id(product, partner)
 
-    return {"subscription": subscription, "subscription_id": subscription.subscription_id, "service_name": "geant_ip"}
-
-
-@step("Initialize subscription")
-def initialize_subscription(
-    subscription: GeantIPInactive,
-    edge_port: dict,
-    binding_port_input: dict,
-    product_name: str,
-    service_name: L3CoreServiceNameTypes,
-) -> State:
-    """Take all user inputs and use them to populate the subscription model."""
-    return initialize_service_binding(subscription, edge_port, binding_port_input, product_name, service_name)
+    return {"subscription": subscription, "subscription_id": subscription.subscription_id}
 
 
 @workflow(
diff --git a/gso/workflows/l3_core_service/geant_ip/create_imported_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/create_imported_geant_ip.py
index f6056ea1e..e07766025 100644
--- a/gso/workflows/l3_core_service/geant_ip/create_imported_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/create_imported_geant_ip.py
@@ -22,7 +22,7 @@ def create_subscription(partner: str) -> dict:
     partner_id = get_partner_by_name(partner).partner_id
     product_id = get_product_id_by_name(ProductName.IMPORTED_GEANT_IP)
     subscription = ImportedGeantIPInactive.from_product_id(product_id, partner_id)
-    return {"subscription": subscription, "subscription_id": subscription.subscription_id, "service_name": "geant_ip"}
+    return {"subscription": subscription, "subscription_id": subscription.subscription_id}
 
 
 @workflow(
diff --git a/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py
index ac20bb284..b7c42e67e 100644
--- a/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py
@@ -13,9 +13,7 @@ from orchestrator.targets import Target
 from orchestrator.workflow import StepList, begin, conditional, done
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
-from pydantic_forms.types import FormGenerator, UUIDstr
 
-from gso.products.product_types.geant_ip import GeantIP
 from gso.services.lso_client import lso_interaction
 from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, start_moodi, stop_moodi
 from gso.workflows.l3_core_service.base_migrate_l3_core_service import (
@@ -37,19 +35,9 @@ from gso.workflows.l3_core_service.base_migrate_l3_core_service import (
 )
 
 
-def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
-    """Gather input from the operator on what destination Edge Ports this GÉANT IP Service should be migrated to."""
-    subscription = GeantIP.from_subscription(subscription_id)
-
-    initial_generator = initial_input_form(subscription, service_name="geant_ip")
-    initial_user_input = yield from initial_generator
-
-    return initial_user_input
-
-
 @workflow(
     "Migrate GÉANT IP",
-    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
+    initial_input_form=wrap_modify_initial_input_form(initial_input_form),
     target=Target.MODIFY,
 )
 def migrate_geant_ip() -> StepList:
diff --git a/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py
index 0ed723624..a7c7e67bf 100644
--- a/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py
@@ -5,7 +5,6 @@ from orchestrator.targets import Target
 from orchestrator.workflow import StepList
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
-from pydantic_forms.types import FormGenerator, UUIDstr
 
 from gso.workflows.l3_core_service.base_modify_l3_core_service import (
     Operation,
@@ -16,17 +15,9 @@ from gso.workflows.l3_core_service.base_modify_l3_core_service import (
 )
 
 
-def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
-    """Generate the initial input form for modifying a GÉANT IP subscription."""
-    initial_generator = base_initial_input_form_generator(subscription_id, service_name="geant_ip")
-    initial_user_input = yield from initial_generator
-
-    return initial_user_input
-
-
 @workflow(
     "Modify GÉANT IP",
-    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
+    initial_input_form=wrap_modify_initial_input_form(base_initial_input_form_generator),
     target=Target.MODIFY,
 )
 def modify_geant_ip() -> StepList:
diff --git a/gso/workflows/l3_core_service/geant_ip/validate_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/validate_geant_ip.py
index 514956162..217c3d6c6 100644
--- a/gso/workflows/l3_core_service/geant_ip/validate_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/validate_geant_ip.py
@@ -1,10 +1,9 @@
 """Validation workflow for GÉANT IP subscription objects."""
 
 from orchestrator.targets import Target
-from orchestrator.workflow import StepList, begin, done, step, workflow
+from orchestrator.workflow import StepList, begin, done, workflow
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
-from pydantic_forms.types import State
 
 from gso.services.lso_client import anonymous_lso_interaction
 from gso.workflows.l3_core_service.base_validate_l3_core_service import (
@@ -15,18 +14,11 @@ from gso.workflows.l3_core_service.base_validate_l3_core_service import (
 )
 
 
-@step("Inject service name")
-def inject_srvice_name() -> State:
-    """Inject the service name into the subscription."""
-    return {"service_name": "geant_ip"}
-
-
 @workflow("Validate GÉANT IP", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None)))
 def validate_geant_ip() -> StepList:
     """Validate an existing Copernicus subscription."""
     return (
         begin
-        >> inject_srvice_name
         >> store_process_subscription(Target.SYSTEM)
         >> unsync
         >> build_fqdn_list
diff --git a/gso/workflows/l3_core_service/geant_ip/validate_prefix_list.py b/gso/workflows/l3_core_service/geant_ip/validate_prefix_list.py
index 1b807ee15..c532d56d1 100644
--- a/gso/workflows/l3_core_service/geant_ip/validate_prefix_list.py
+++ b/gso/workflows/l3_core_service/geant_ip/validate_prefix_list.py
@@ -1,10 +1,9 @@
 """Prefix Validation workflow for GÉANT IP subscription objects."""
 
 from orchestrator.targets import Target
-from orchestrator.workflow import StepList, begin, conditional, done, step, workflow
+from orchestrator.workflow import StepList, begin, conditional, done, workflow
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
-from pydantic_forms.types import State
 
 from gso.services.lso_client import anonymous_lso_interaction, lso_interaction
 from gso.workflows.l3_core_service.base_validate_prefix_list import (
@@ -17,12 +16,6 @@ from gso.workflows.l3_core_service.base_validate_prefix_list import (
 )
 
 
-@step("Inject service name")
-def inject_srvice_name() -> State:
-    """Inject the service name into the subscription."""
-    return {"service_name": "geant_ip"}
-
-
 @workflow(
     "Validate GÉANT IP Prefix-List", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None))
 )
@@ -44,11 +37,4 @@ def validate_geant_ip_prefix_list() -> StepList:
         >> prefix_list_has_drifted(redeploy_prefix_list_steps)
     )
 
-    return (
-        begin
-        >> inject_srvice_name
-        >> store_process_subscription(Target.SYSTEM)
-        >> build_fqdn_list
-        >> prefix_list_validation_steps
-        >> done
-    )
+    return begin >> store_process_subscription(Target.SYSTEM) >> build_fqdn_list >> prefix_list_validation_steps >> done
diff --git a/gso/workflows/l3_core_service/ias/create_ias.py b/gso/workflows/l3_core_service/ias/create_ias.py
index b8c829bd2..21ae9addd 100644
--- a/gso/workflows/l3_core_service/ias/create_ias.py
+++ b/gso/workflows/l3_core_service/ias/create_ias.py
@@ -18,7 +18,7 @@ from gso.workflows.l3_core_service.base_create_l3_core_service import (
     create_new_sharepoint_checklist,
     deploy_bgp_peers_dry,
     deploy_bgp_peers_real,
-    initialize_service_binding,
+    initialize_subscription,
     provision_sbp_dry,
     provision_sbp_real,
     update_dns_records,
@@ -26,7 +26,6 @@ from gso.workflows.l3_core_service.base_create_l3_core_service import (
 from gso.workflows.l3_core_service.base_create_l3_core_service import (
     initial_input_form_generator as l3_initial_input_form_generator,
 )
-from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes
 
 
 def initial_input_form_generator(product_name: str) -> FormGenerator:
@@ -47,21 +46,7 @@ def create_subscription(product: UUIDstr, partner: str) -> State:
     """Create a new subscription object in the database."""
     subscription = IASInactive.from_product_id(product, partner)
 
-    return {"subscription": subscription, "subscription_id": subscription.subscription_id, "service_name": "ias"}
-
-
-@step("Initialize subscription")
-def initialize_subscription(
-    subscription: IASInactive,
-    edge_port: dict,
-    binding_port_input: dict,
-    product_name: str,
-    ias_flavor: IASFlavor,
-    service_name: L3CoreServiceNameTypes,
-) -> State:
-    """Take all user inputs and use them to populate the subscription model."""
-    subscription.ias.ias_flavor = ias_flavor
-    return initialize_service_binding(subscription, edge_port, binding_port_input, product_name, service_name)
+    return {"subscription": subscription, "subscription_id": subscription.subscription_id}
 
 
 @workflow(
diff --git a/gso/workflows/l3_core_service/ias/create_imported_ias.py b/gso/workflows/l3_core_service/ias/create_imported_ias.py
index e796f1f53..9061c6a74 100644
--- a/gso/workflows/l3_core_service/ias/create_imported_ias.py
+++ b/gso/workflows/l3_core_service/ias/create_imported_ias.py
@@ -38,7 +38,7 @@ def create_subscription(partner: str) -> dict:
     partner_id = get_partner_by_name(partner).partner_id
     product_id = get_product_id_by_name(ProductName.IMPORTED_IAS)
     subscription = ImportedIASInactive.from_product_id(product_id, partner_id)
-    return {"subscription": subscription, "subscription_id": subscription.subscription_id, "service_name": "ias"}
+    return {"subscription": subscription, "subscription_id": subscription.subscription_id}
 
 
 @workflow(
diff --git a/gso/workflows/l3_core_service/ias/migrate_ias.py b/gso/workflows/l3_core_service/ias/migrate_ias.py
index ddc25ff64..b430d832b 100644
--- a/gso/workflows/l3_core_service/ias/migrate_ias.py
+++ b/gso/workflows/l3_core_service/ias/migrate_ias.py
@@ -13,9 +13,7 @@ from orchestrator.targets import Target
 from orchestrator.workflow import StepList, begin, conditional, done
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
-from pydantic_forms.types import FormGenerator, UUIDstr
 
-from gso.products.product_types.ias import IAS
 from gso.services.lso_client import lso_interaction
 from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, start_moodi, stop_moodi
 from gso.workflows.l3_core_service.base_migrate_l3_core_service import (
@@ -37,19 +35,9 @@ from gso.workflows.l3_core_service.base_migrate_l3_core_service import (
 )
 
 
-def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
-    """Gather input from the operator on what destination Edge Ports this IAS Service should be migrated to."""
-    subscription = IAS.from_subscription(subscription_id)
-
-    initial_generator = initial_input_form(subscription, service_name="ias")
-    initial_user_input = yield from initial_generator
-
-    return initial_user_input
-
-
 @workflow(
     "Migrate IAS",
-    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
+    initial_input_form=wrap_modify_initial_input_form(initial_input_form),
     target=Target.MODIFY,
 )
 def migrate_ias() -> StepList:
diff --git a/gso/workflows/l3_core_service/ias/modify_ias.py b/gso/workflows/l3_core_service/ias/modify_ias.py
index 1b7dfb0c5..907cf400f 100644
--- a/gso/workflows/l3_core_service/ias/modify_ias.py
+++ b/gso/workflows/l3_core_service/ias/modify_ias.py
@@ -5,7 +5,6 @@ from orchestrator.targets import Target
 from orchestrator.workflow import StepList
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
-from pydantic_forms.types import FormGenerator, UUIDstr
 
 from gso.workflows.l3_core_service.base_modify_l3_core_service import (
     Operation,
@@ -16,17 +15,9 @@ from gso.workflows.l3_core_service.base_modify_l3_core_service import (
 )
 
 
-def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
-    """Generate the initial input form for modifying an IAS subscription."""
-    initial_generator = base_initial_input_form_generator(subscription_id, service_name="ias")
-    initial_user_input = yield from initial_generator
-
-    return initial_user_input
-
-
 @workflow(
     "Modify IAS",
-    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
+    initial_input_form=wrap_modify_initial_input_form(base_initial_input_form_generator),
     target=Target.MODIFY,
 )
 def modify_ias() -> StepList:
diff --git a/gso/workflows/l3_core_service/ias/validate_ias.py b/gso/workflows/l3_core_service/ias/validate_ias.py
index c9df01381..95c06042a 100644
--- a/gso/workflows/l3_core_service/ias/validate_ias.py
+++ b/gso/workflows/l3_core_service/ias/validate_ias.py
@@ -1,10 +1,9 @@
 """Validation workflow for IAS subscription objects."""
 
 from orchestrator.targets import Target
-from orchestrator.workflow import StepList, begin, done, step, workflow
+from orchestrator.workflow import StepList, begin, done, workflow
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
-from pydantic_forms.types import State
 
 from gso.services.lso_client import anonymous_lso_interaction
 from gso.workflows.l3_core_service.base_validate_l3_core_service import (
@@ -15,18 +14,11 @@ from gso.workflows.l3_core_service.base_validate_l3_core_service import (
 )
 
 
-@step("Inject service name")
-def inject_srvice_name() -> State:
-    """Inject the service name into the subscription."""
-    return {"service_name": "ias"}
-
-
 @workflow("Validate IAS", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None)))
 def validate_ias() -> StepList:
     """Validate an existing IAS subscription."""
     return (
         begin
-        >> inject_srvice_name
         >> store_process_subscription(Target.SYSTEM)
         >> unsync
         >> build_fqdn_list
diff --git a/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py b/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py
index 9bc09ed41..5291118c6 100644
--- a/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py
@@ -22,7 +22,7 @@ def create_subscription(partner: str) -> dict:
     partner_id = get_partner_by_name(partner).partner_id
     product_id = get_product_id_by_name(ProductName.IMPORTED_LHCONE)
     subscription = ImportedLHCOneInactive.from_product_id(product_id, partner_id)
-    return {"subscription": subscription, "subscription_id": subscription.subscription_id, "service_name": "lhcone"}
+    return {"subscription": subscription, "subscription_id": subscription.subscription_id}
 
 
 @workflow(
diff --git a/gso/workflows/l3_core_service/lhcone/create_lhcone.py b/gso/workflows/l3_core_service/lhcone/create_lhcone.py
index 9baabf314..7e90e04c0 100644
--- a/gso/workflows/l3_core_service/lhcone/create_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/create_lhcone.py
@@ -17,12 +17,11 @@ from gso.workflows.l3_core_service.base_create_l3_core_service import (
     deploy_bgp_peers_dry,
     deploy_bgp_peers_real,
     initial_input_form_generator,
-    initialize_service_binding,
+    initialize_subscription,
     provision_sbp_dry,
     provision_sbp_real,
     update_dns_records,
 )
-from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes
 
 
 @step("Create subscription")
@@ -30,19 +29,7 @@ def create_subscription(product: UUIDstr, partner: str) -> State:
     """Create a new subscription object in the database."""
     subscription = LHCOneInactive.from_product_id(product, partner)
 
-    return {"subscription": subscription, "subscription_id": subscription.subscription_id, "service_name": "lhcone"}
-
-
-@step("Initialize subscription")
-def initialize_subscription(
-    subscription: LHCOneInactive,
-    edge_port: dict,
-    binding_port_input: dict,
-    product_name: str,
-    service_name: L3CoreServiceNameTypes,
-) -> State:
-    """Take all user inputs and use them to populate the subscription model."""
-    return initialize_service_binding(subscription, edge_port, binding_port_input, product_name, service_name)
+    return {"subscription": subscription, "subscription_id": subscription.subscription_id}
 
 
 @workflow(
diff --git a/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py b/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py
index 15752a8e0..18d3a2ddc 100644
--- a/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py
@@ -13,9 +13,7 @@ from orchestrator.targets import Target
 from orchestrator.workflow import StepList, begin, conditional, done
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
-from pydantic_forms.types import FormGenerator, UUIDstr
 
-from gso.products.product_types.lhcone import LHCOne
 from gso.services.lso_client import lso_interaction
 from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, start_moodi, stop_moodi
 from gso.workflows.l3_core_service.base_migrate_l3_core_service import (
@@ -37,19 +35,9 @@ from gso.workflows.l3_core_service.base_migrate_l3_core_service import (
 )
 
 
-def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
-    """Gather input from the operator on what destination Edge Ports this LHCOne Service should be migrated to."""
-    subscription = LHCOne.from_subscription(subscription_id)
-
-    initial_generator = initial_input_form(subscription, service_name="lhcone")
-    initial_user_input = yield from initial_generator
-
-    return initial_user_input
-
-
 @workflow(
     "Migrate LHCOne",
-    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
+    initial_input_form=wrap_modify_initial_input_form(initial_input_form),
     target=Target.MODIFY,
 )
 def migrate_lhcone() -> StepList:
diff --git a/gso/workflows/l3_core_service/lhcone/modify_lhcone.py b/gso/workflows/l3_core_service/lhcone/modify_lhcone.py
index c9e00b5ef..cc5af5ed3 100644
--- a/gso/workflows/l3_core_service/lhcone/modify_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/modify_lhcone.py
@@ -5,7 +5,6 @@ from orchestrator.targets import Target
 from orchestrator.workflow import StepList
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
-from pydantic_forms.types import FormGenerator, UUIDstr
 
 from gso.workflows.l3_core_service.base_modify_l3_core_service import (
     Operation,
@@ -16,17 +15,9 @@ from gso.workflows.l3_core_service.base_modify_l3_core_service import (
 )
 
 
-def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
-    """Generate the initial input form for modifying a LHCOne subscription."""
-    initial_generator = base_initial_input_form_generator(subscription_id, service_name="lhcone")
-    initial_user_input = yield from initial_generator
-
-    return initial_user_input
-
-
 @workflow(
     "Modify LHCOne",
-    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
+    initial_input_form=wrap_modify_initial_input_form(base_initial_input_form_generator),
     target=Target.MODIFY,
 )
 def modify_lhcone() -> StepList:
diff --git a/gso/workflows/l3_core_service/lhcone/validate_lhcone.py b/gso/workflows/l3_core_service/lhcone/validate_lhcone.py
index 172f6caea..b379dc33d 100644
--- a/gso/workflows/l3_core_service/lhcone/validate_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/validate_lhcone.py
@@ -1,10 +1,9 @@
 """Validation workflow for LHCONE subscription objects."""
 
 from orchestrator.targets import Target
-from orchestrator.workflow import StepList, begin, done, step, workflow
+from orchestrator.workflow import StepList, begin, done, workflow
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
-from pydantic_forms.types import State
 
 from gso.services.lso_client import anonymous_lso_interaction
 from gso.workflows.l3_core_service.base_validate_l3_core_service import (
@@ -15,18 +14,11 @@ from gso.workflows.l3_core_service.base_validate_l3_core_service import (
 )
 
 
-@step("Inject service name")
-def inject_srvice_name() -> State:
-    """Inject the service name into the subscription."""
-    return {"service_name": "lhcone"}
-
-
 @workflow("Validate LHCOne", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None)))
 def validate_lhcone() -> StepList:
     """Validate an existing LHCone subscription."""
     return (
         begin
-        >> inject_srvice_name
         >> store_process_subscription(Target.SYSTEM)
         >> unsync
         >> build_fqdn_list
diff --git a/gso/workflows/l3_core_service/shared.py b/gso/workflows/l3_core_service/shared.py
index bdb4c0ef4..3ed5a0feb 100644
--- a/gso/workflows/l3_core_service/shared.py
+++ b/gso/workflows/l3_core_service/shared.py
@@ -15,10 +15,8 @@ L3_PRODUCT_NAMES = [
     ProductName.COPERNICUS,
 ]
 L3_CORE_SERVICE_PRODUCT_TYPES = [GeantIP.__name__, IAS.__name__, LHCOne.__name__, Copernicus.__name__]
-L3_SERVICE_NAME_ATTRIBUTES = {"geant_ip", "ias", "lhcone", "copernicus"}
-assert len(L3_PRODUCT_NAMES) == len(L3_CORE_SERVICE_PRODUCT_TYPES) == len(L3_SERVICE_NAME_ATTRIBUTES)  # noqa: S101
+assert len(L3_PRODUCT_NAMES) == len(L3_CORE_SERVICE_PRODUCT_TYPES)  # noqa: S101
 
-L3CoreServiceNameTypes = Literal["geant_ip", "ias", "lhcone", "copernicus"]
 L3ProductNameTypes = Literal[ProductName.IAS, ProductName.LHCONE, ProductName.COPERNICUS, ProductName.GEANT_IP]
 
 L3_CREAT_IMPORTED_WF_MAP = {
@@ -35,14 +33,6 @@ L3_CREATION_WF_MAP = {
     ProductName.LHCONE: "create_lhcone",
 }
 
-
-L3_ATTRIBUTES_MAP = {
-    ProductName.COPERNICUS: "copernicus",
-    ProductName.GEANT_IP: "geant_ip",
-    ProductName.IAS: "ias",
-    ProductName.LHCONE: "lhcone",
-}
-
 L3_MIGRATION_WF_MAP = {
     ProductName.COPERNICUS: "migrate_copernicus",
     ProductName.GEANT_IP: "migrate_geant_ip",
diff --git a/test/fixtures/l3_core_service_fixtures.py b/test/fixtures/l3_core_service_fixtures.py
index 2c2c3bcba..f41248bfa 100644
--- a/test/fixtures/l3_core_service_fixtures.py
+++ b/test/fixtures/l3_core_service_fixtures.py
@@ -22,7 +22,6 @@ from gso.products.product_types.lhcone import ImportedLHCOneInactive, LHCOneInac
 from gso.services.subscriptions import get_product_id_by_name
 from gso.utils.shared_enums import APType, SBPType
 from gso.utils.types.ip_address import IPAddress
-from gso.workflows.l3_core_service.shared import L3CoreServiceNameTypes
 
 PRODUCT_IMPORTED_MAP = {
     ProductName.IAS: ProductName.IMPORTED_IAS,
@@ -176,7 +175,6 @@ def make_subscription_factory(
         product_name: ProductName,
         imported_class,
         native_class,
-        service_name: L3CoreServiceNameTypes,
         description=None,
         partner: dict | None = None,
         ap_list: list[AccessPort] | None = None,
@@ -191,19 +189,15 @@ def make_subscription_factory(
 
         subscription = subscription_class.from_product_id(product_id, customer_id=partner["partner_id"], insync=True)
 
-        return save_l3_core_subscription(
-            ap_list, description, subscription, start_date, status, service_name=service_name
-        )
+        return save_l3_core_subscription(ap_list, description, subscription, start_date, status)
 
     return create_subscription
 
 
 @pytest.fixture()
 def save_l3_core_subscription(access_port_factory, faker, l3_core_block_factory):
-    def _save_subscription(
-        ap_list, description, subscription, start_date, status, service_name: L3CoreServiceNameTypes
-    ):
-        getattr(subscription, service_name).l3_core = l3_core_block_factory(ap_list)
+    def _save_subscription(ap_list, description, subscription, start_date, status):
+        subscription.l3_core = l3_core_block_factory(ap_list)
 
         # Update subscription with description, start date, and status
         subscription = SubscriptionModel.from_other_lifecycle(
@@ -245,7 +239,7 @@ def ias_subscription_factory(faker, partner_factory, access_port_factory, save_l
 
         subscription.ias.ias_flavor = ias_flavor
 
-        return save_l3_core_subscription(ap_list, description, subscription, start_date, status, service_name="ias")
+        return save_l3_core_subscription(ap_list, description, subscription, start_date, status)
 
     return create_ias_subscription
 
@@ -257,7 +251,6 @@ def geant_ip_subscription_factory(make_subscription_factory):
             product_name=ProductName.GEANT_IP,
             imported_class=ImportedGeantIPInactive,
             native_class=GeantIPInactive,
-            service_name="geant_ip",
             *args,  # noqa: B026
             **kwargs,
         )
@@ -272,7 +265,6 @@ def copernicus_subscription_factory(make_subscription_factory):
             product_name=ProductName.COPERNICUS,
             imported_class=ImportedCopernicusInactive,
             native_class=CopernicusInactive,
-            service_name="copernicus",
             *args,  # noqa: B026
             **kwargs,
         )
@@ -287,7 +279,6 @@ def lhcone_subscription_factory(make_subscription_factory):
             product_name=ProductName.LHCONE,
             imported_class=ImportedLHCOneInactive,
             native_class=LHCOneInactive,
-            service_name="lhcone",
             *args,  # noqa: B026
             **kwargs,
         )
diff --git a/test/workflows/edge_port/test_migrate_edge_port.py b/test/workflows/edge_port/test_migrate_edge_port.py
index b6ee2f214..821e7e82b 100644
--- a/test/workflows/edge_port/test_migrate_edge_port.py
+++ b/test/workflows/edge_port/test_migrate_edge_port.py
@@ -6,7 +6,7 @@ from gso.products import Layer2CircuitServiceType
 from gso.products.product_types.edge_port import EdgePort
 from gso.products.product_types.router import Router
 from gso.utils.shared_enums import Vendor
-from gso.workflows.l3_core_service.shared import L3_ATTRIBUTES_MAP, L3_PRODUCT_NAMES
+from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES
 from test import USER_CONFIRM_EMPTY_FORM
 from test.services.conftest import MockedNetboxClient
 from test.workflows import (
@@ -91,7 +91,7 @@ def test_successful_edge_port_migration(
     edge_port = edge_port_subscription_factory(partner=partner)
     for product_name in L3_PRODUCT_NAMES:
         l3_core_service = l3_core_service_subscription_factory(partner=partner, product_name=product_name)
-        sbp = getattr(l3_core_service, L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list[0].sbp
+        sbp = l3_core_service.l3_core.ap_list[0].sbp
         sbp.edge_port = edge_port.edge_port
         l3_core_service.save()
 
diff --git a/test/workflows/l3_core_service/test_create_l3_core_service.py b/test/workflows/l3_core_service/test_create_l3_core_service.py
index e4e5fd391..aacfd9cc0 100644
--- a/test/workflows/l3_core_service/test_create_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_create_l3_core_service.py
@@ -8,7 +8,7 @@ from gso.products import ProductName
 from gso.products.product_blocks.ias import IASFlavor
 from gso.services.subscriptions import get_product_id_by_name
 from gso.utils.shared_enums import APType
-from gso.workflows.l3_core_service.shared import L3_ATTRIBUTES_MAP, L3_CREATION_WF_MAP, L3_PRODUCT_NAMES
+from gso.workflows.l3_core_service.shared import L3_CREATION_WF_MAP, L3_PRODUCT_NAMES
 from test import USER_CONFIRM_EMPTY_FORM
 from test.services.conftest import MockedSharePointClient
 from test.workflows import (
@@ -108,8 +108,10 @@ def test_create_l3_core_service_success(
     subscription = SubscriptionModel.from_subscription(state["subscription_id"])
     assert mock_lso_client.call_count == lso_interaction_count + 1
     assert subscription.status == SubscriptionLifecycle.ACTIVE
-    l3_core = getattr(subscription, L3_ATTRIBUTES_MAP[product_name]).l3_core
-    assert len(l3_core.ap_list) == 1
-    assert str(l3_core.ap_list[0].sbp.edge_port.owner_subscription_id) == form_input_data[2]["edge_port"]["edge_port"]
-    assert l3_core.ap_list[0].sbp.gs_id == "GS-12345"
+    assert len(subscription.l3_core.ap_list) == 1
+    assert (
+        str(subscription.l3_core.ap_list[0].sbp.edge_port.owner_subscription_id)
+        == form_input_data[2]["edge_port"]["edge_port"]
+    )
+    assert subscription.l3_core.ap_list[0].sbp.gs_id == "GS-12345"
     assert mock_sharepoint_client.call_count == 1
diff --git a/test/workflows/l3_core_service/test_migrate_l3_core_service.py b/test/workflows/l3_core_service/test_migrate_l3_core_service.py
index d4691e8df..18c5682e7 100644
--- a/test/workflows/l3_core_service/test_migrate_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_migrate_l3_core_service.py
@@ -5,7 +5,7 @@ from orchestrator.domain import SubscriptionModel
 
 from gso.products.product_types.edge_port import EdgePort
 from gso.utils.shared_enums import APType
-from gso.workflows.l3_core_service.shared import L3_ATTRIBUTES_MAP, L3_MIGRATION_WF_MAP, L3_PRODUCT_NAMES
+from gso.workflows.l3_core_service.shared import L3_MIGRATION_WF_MAP, L3_PRODUCT_NAMES
 from test import USER_CONFIRM_EMPTY_FORM
 from test.workflows import (
     assert_complete,
@@ -37,12 +37,11 @@ def test_migrate_l3_core_service_success(
     )
     destination_edge_port = str(edge_port_subscription_factory(partner=partner).subscription_id)
     subscription = SubscriptionModel.from_subscription(subscription_id)
-    l3_core = getattr(subscription, L3_ATTRIBUTES_MAP[product_name]).l3_core
     form_input_data = [
         {"subscription_id": subscription_id},
         {
             "tt_number": faker.tt_number(),
-            "source_edge_port": l3_core.ap_list[0].sbp.edge_port.owner_subscription_id,
+            "source_edge_port": subscription.l3_core.ap_list[0].sbp.edge_port.owner_subscription_id,
         },
         {"destination_edge_port": destination_edge_port},
         {},
@@ -65,9 +64,8 @@ def test_migrate_l3_core_service_success(
     subscription = SubscriptionModel.from_subscription(state["subscription_id"])
     assert mock_execute_playbook.call_count == 11
     assert subscription.insync
-    l3_core = getattr(subscription, L3_ATTRIBUTES_MAP[product_name]).l3_core
-    assert len(l3_core.ap_list) == 1
-    assert str(l3_core.ap_list[0].sbp.edge_port.owner_subscription_id) == destination_edge_port
+    assert len(subscription.l3_core.ap_list) == 1
+    assert str(subscription.l3_core.ap_list[0].sbp.edge_port.owner_subscription_id) == destination_edge_port
 
 
 @pytest.mark.workflow()
@@ -87,8 +85,9 @@ def test_migrate_l3_core_service_scoped_emission(
     subscription = l3_core_service_subscription_factory(
         partner=partner, product_name=product_name, ap_list=custom_ap_list
     )
+    service_name = subscription.service_name_attribute
     destination_edge_port_id = str(edge_port_subscription_factory(partner=partner).subscription_id)
-    ap_list = getattr(subscription, L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
+    ap_list = subscription.l3_core.ap_list
     source_edge_port = ap_list[3].sbp.edge_port.owner_subscription_id
 
     form_input_data = [
@@ -128,8 +127,8 @@ def test_migrate_l3_core_service_scoped_emission(
         == transmitted_destination_ep_fqdn
     )
     assert (
-        state["subscription"][L3_ATTRIBUTES_MAP[product_name]]["l3_core"]
-        == state["__old_subscriptions__"][str(subscription.subscription_id)][L3_ATTRIBUTES_MAP[product_name]]["l3_core"]
+        state["subscription"][service_name]["l3_core"]
+        == state["__old_subscriptions__"][str(subscription.subscription_id)][service_name]["l3_core"]
     )  # Subscription is unchanged for now
 
     for _ in range(4):
@@ -142,6 +141,6 @@ def test_migrate_l3_core_service_scoped_emission(
     subscription = SubscriptionModel.from_subscription(state["subscription_id"])
     assert mock_execute_playbook.call_count == 11
     assert subscription.insync
-    ap_list = getattr(subscription, L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
+    ap_list = subscription.l3_core.ap_list
     assert len(ap_list) == 5
     assert str(ap_list[3].sbp.edge_port.owner_subscription_id) == destination_edge_port_id
diff --git a/test/workflows/l3_core_service/test_modify_l3_core_service.py b/test/workflows/l3_core_service/test_modify_l3_core_service.py
index b8e08976e..420ab84ea 100644
--- a/test/workflows/l3_core_service/test_modify_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_modify_l3_core_service.py
@@ -4,7 +4,7 @@ from orchestrator.domain import SubscriptionModel
 from gso.products.product_blocks.bgp_session import IPFamily
 from gso.utils.shared_enums import APType
 from gso.workflows.l3_core_service.base_modify_l3_core_service import Operation
-from gso.workflows.l3_core_service.shared import L3_ATTRIBUTES_MAP, L3_MODIFICATION_WF_MAP, L3_PRODUCT_NAMES
+from gso.workflows.l3_core_service.shared import L3_MODIFICATION_WF_MAP, L3_PRODUCT_NAMES
 from test.workflows import extract_state, run_workflow
 
 
@@ -12,7 +12,7 @@ from test.workflows import extract_state, run_workflow
 @pytest.mark.workflow()
 def test_modify_l3_core_service_remove_edge_port_success(faker, l3_core_service_subscription_factory, product_name):
     subscription = l3_core_service_subscription_factory(product_name=product_name)
-    access_port = getattr(subscription, L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list[0]
+    access_port = subscription.l3_core.ap_list[0]
     input_form_data = [
         {"subscription_id": str(subscription.subscription_id)},
         {"tt_number": faker.tt_number(), "operation": Operation.REMOVE},
@@ -23,7 +23,7 @@ def test_modify_l3_core_service_remove_edge_port_success(faker, l3_core_service_
 
     state = extract_state(result)
     subscription = SubscriptionModel.from_subscription(state["subscription_id"])
-    ap_list = getattr(subscription, L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
+    ap_list = subscription.l3_core.ap_list
     assert len(ap_list) == 1
     assert ap_list[0].ap_type == APType.BACKUP
 
@@ -74,7 +74,7 @@ def test_modify_l3_core_service_add_new_edge_port_success(
 
     state = extract_state(result)
     subscription = SubscriptionModel.from_subscription(state["subscription_id"])
-    ap_list = getattr(subscription, L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
+    ap_list = subscription.l3_core.ap_list
     new_ap = ap_list[-1]
     assert new_ap.ap_type == APType.BACKUP
     assert new_ap.sbp.gs_id == input_form_data[2]["gs_id"]
@@ -137,7 +137,7 @@ def test_modify_l3_core_service_modify_edge_port_success(
 ):
     subscription = l3_core_service_subscription_factory(product_name=product_name)
     new_sbp_data = sbp_input_form_data()
-    ap_list = getattr(subscription, L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
+    ap_list = subscription.l3_core.ap_list
     input_form_data = [
         {"subscription_id": str(subscription.subscription_id)},
         {"tt_number": faker.tt_number(), "operation": Operation.EDIT},
@@ -149,7 +149,7 @@ def test_modify_l3_core_service_modify_edge_port_success(
 
     state = extract_state(result)
     subscription = SubscriptionModel.from_subscription(state["subscription_id"])
-    ap_list = getattr(subscription, L3_ATTRIBUTES_MAP[product_name]).l3_core.ap_list
+    ap_list = subscription.l3_core.ap_list
     assert len(ap_list) == 2
 
     access_port = ap_list[0]
-- 
GitLab


From f29923dc21c4b5af7f243f85bd22c364bb915d9e Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Mon, 31 Mar 2025 16:14:44 +0200
Subject: [PATCH 61/87] remove unused codes

---
 gso/products/product_types/copernicus.py      |  2 +-
 gso/products/product_types/geant_ip.py        |  2 +-
 gso/products/product_types/l3_core_service.py | 87 -------------------
 .../terminate_l3_core_service.py              | 40 ---------
 .../validate_l3_core_service.py               | 82 -----------------
 .../l3_core_service/validate_prefix_list.py   |  3 +-
 6 files changed, 4 insertions(+), 212 deletions(-)
 delete mode 100644 gso/products/product_types/l3_core_service.py
 delete mode 100644 gso/workflows/l3_core_service/terminate_l3_core_service.py
 delete mode 100644 gso/workflows/l3_core_service/validate_l3_core_service.py

diff --git a/gso/products/product_types/copernicus.py b/gso/products/product_types/copernicus.py
index 70c904a18..6e395db03 100644
--- a/gso/products/product_types/copernicus.py
+++ b/gso/products/product_types/copernicus.py
@@ -1,4 +1,4 @@
-"""Product type for LHCONE."""
+"""Product type for Copernicus."""
 
 from __future__ import annotations
 
diff --git a/gso/products/product_types/geant_ip.py b/gso/products/product_types/geant_ip.py
index c7c7bd383..0eb34104f 100644
--- a/gso/products/product_types/geant_ip.py
+++ b/gso/products/product_types/geant_ip.py
@@ -1,4 +1,4 @@
-"""Product type for IAS."""
+"""Product type for GEANT IP."""
 
 from __future__ import annotations
 
diff --git a/gso/products/product_types/l3_core_service.py b/gso/products/product_types/l3_core_service.py
deleted file mode 100644
index 34f9beccd..000000000
--- a/gso/products/product_types/l3_core_service.py
+++ /dev/null
@@ -1,87 +0,0 @@
-"""L3 Core Service product type."""
-
-from orchestrator.domain import SubscriptionModel
-from orchestrator.types import SubscriptionLifecycle
-from pydantic_forms.types import strEnum
-
-from gso.products.product_blocks.l3_core_service import (
-    L3CoreServiceBlock,
-    L3CoreServiceBlockInactive,
-    L3CoreServiceBlockProvisioning,
-)
-
-
-class L3CoreServiceType(strEnum):
-    """Available types of Layer 3 Core Services.
-
-    The core services offered include GÉANT IP for R&E access, and the Internet Access Service.
-    """
-
-    GEANT_IP = "GÉANT IP"
-    """GÉANT IP."""
-    IMPORTED_GEANT_IP = "IMPORTED GÉANT IP"
-    IAS = "IAS"
-    """Internet Access Serive."""
-    IMPORTED_IAS = "IMPORTED IAS"
-    GWS = "GWS"
-    """GÉANT Web Services."""
-    IMPORTED_GWS = "IMPORTED GWS"
-    LHCONE = "LHCONE"
-    """LHCOne."""
-    IMPORTED_LHCONE = "IMPORTED LHCONE"
-    COPERNICUS = "COPERNICUS"
-    """Copernicus."""
-    IMPORTED_COPERNICUS = "IMPORTED COPERNICUS"
-
-
-L3_CORE_SERVICE_TYPES = [
-    L3CoreServiceType.GEANT_IP,
-    L3CoreServiceType.IAS,
-    L3CoreServiceType.GWS,
-    L3CoreServiceType.LHCONE,
-    L3CoreServiceType.COPERNICUS,
-]
-IMPORTED_L3_CORE_SERVICE_TYPES = [
-    L3CoreServiceType.IMPORTED_GEANT_IP,
-    L3CoreServiceType.IMPORTED_IAS,
-    L3CoreServiceType.IMPORTED_GWS,
-    L3CoreServiceType.IMPORTED_LHCONE,
-    L3CoreServiceType.IMPORTED_COPERNICUS,
-]
-
-
-class L3CoreServiceInactive(SubscriptionModel, is_base=True):
-    """An inactive L3 Core Service subscription."""
-
-    l3_core_service_type: L3CoreServiceType
-    l3_core_service: L3CoreServiceBlockInactive
-
-
-class L3CoreServiceProvisioning(L3CoreServiceInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
-    """A L3 Core Service subscription that's being provisioned."""
-
-    l3_core_service_type: L3CoreServiceType
-    l3_core_service: L3CoreServiceBlockProvisioning
-
-
-class L3CoreService(L3CoreServiceProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
-    """An active L3 Core Service subscription."""
-
-    l3_core_service_type: L3CoreServiceType
-    l3_core_service: L3CoreServiceBlock
-
-
-class ImportedL3CoreServiceInactive(SubscriptionModel, is_base=True):
-    """An imported, inactive L3 Core Service subscription."""
-
-    l3_core_service_type: L3CoreServiceType
-    l3_core_service: L3CoreServiceBlockInactive
-
-
-class ImportedL3CoreService(
-    ImportedL3CoreServiceInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE]
-):
-    """An imported L3 Core Service subscription."""
-
-    l3_core_service_type: L3CoreServiceType
-    l3_core_service: L3CoreServiceBlock
diff --git a/gso/workflows/l3_core_service/terminate_l3_core_service.py b/gso/workflows/l3_core_service/terminate_l3_core_service.py
deleted file mode 100644
index 8cae6c430..000000000
--- a/gso/workflows/l3_core_service/terminate_l3_core_service.py
+++ /dev/null
@@ -1,40 +0,0 @@
-"""Workflow for terminating a Layer 3 Core Service."""
-
-from orchestrator import begin, workflow
-from orchestrator.forms import SubmitFormPage
-from orchestrator.targets import Target
-from orchestrator.types import SubscriptionLifecycle
-from orchestrator.workflow import StepList, done
-from orchestrator.workflows.steps import resync, set_status, store_process_subscription, unsync
-from orchestrator.workflows.utils import wrap_modify_initial_input_form
-from pydantic_forms.types import FormGenerator, UUIDstr
-
-from gso.products.product_types.l3_core_service import L3CoreService
-from gso.utils.types.tt_number import TTNumber
-
-
-def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
-    subscription = L3CoreService.from_subscription(subscription_id)
-
-    class TerminateForm(SubmitFormPage):
-        tt_number: TTNumber
-
-    yield TerminateForm
-    return {"subscription": subscription}
-
-
-@workflow(
-    "Terminate Layer 3 Core Service",
-    initial_input_form=wrap_modify_initial_input_form(_input_form_generator),
-    target=Target.TERMINATE,
-)
-def terminate_l3_core_service() -> StepList:
-    """Terminate a Layer 3 Core Service subscription."""
-    return (
-        begin
-        >> store_process_subscription(Target.TERMINATE)
-        >> unsync
-        >> set_status(SubscriptionLifecycle.TERMINATED)
-        >> resync
-        >> done
-    )
diff --git a/gso/workflows/l3_core_service/validate_l3_core_service.py b/gso/workflows/l3_core_service/validate_l3_core_service.py
deleted file mode 100644
index 0b02e34f6..000000000
--- a/gso/workflows/l3_core_service/validate_l3_core_service.py
+++ /dev/null
@@ -1,82 +0,0 @@
-"""Validation workflow for L3 Core Service subscription objects."""
-
-from typing import Any
-
-from orchestrator.targets import Target
-from orchestrator.workflow import StepList, begin, done, step, workflow
-from orchestrator.workflows.steps import resync, store_process_subscription, unsync
-from orchestrator.workflows.utils import wrap_modify_initial_input_form
-from pydantic_forms.types import State, UUIDstr
-
-from gso.products.product_types.l3_core_service import L3CoreService
-from gso.services.lso_client import LSOState, anonymous_lso_interaction
-from gso.services.partners import get_partner_by_id
-
-
-@step("Prepare list of all Access Ports")
-def build_fqdn_list(subscription: L3CoreService) -> State:
-    """Build the list of all FQDNs that are in the list of access ports of a L3 Core Service subscription."""
-    ap_fqdn_list = [ap.sbp.edge_port.node.router_fqdn for ap in subscription.l3_core_service.ap_list]
-    return {"ap_fqdn_list": ap_fqdn_list}
-
-
-@step("Check SBP config for drift")
-def validate_sbp_config(subscription: dict[str, Any], process_id: UUIDstr, ap_fqdn_list: list[str]) -> LSOState:
-    """Workflow step for running a playbook that checks whether SBP config has drifted."""
-    extra_vars = {
-        "subscription": subscription,
-        "partner_name": get_partner_by_id(subscription["customer_id"]).name,
-        "dry_run": True,
-        "verb": "deploy",
-        "object": "sbp",
-        "is_verification_workflow": "true",
-        "commit_comment": f"GSO_PROCESS_ID: {process_id} - Validate config for {subscription["description"]}",
-    }
-
-    return {
-        "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml",
-        "inventory": {"all": {"hosts": dict.fromkeys(ap_fqdn_list)}},
-        "extra_vars": extra_vars,
-    }
-
-
-@step("Check BGP peer config for drift")
-def validate_bgp_peers(subscription: dict[str, Any], process_id: UUIDstr, ap_fqdn_list: list[str]) -> LSOState:
-    """Workflow step for running a playbook that checks whether BGP peer config has drifted."""
-    extra_vars = {
-        "subscription": subscription,
-        "partner_name": get_partner_by_id(subscription["customer_id"]).name,
-        "verb": "deploy",
-        "object": "bgp",
-        "dry_run": True,
-        "is_verification_workflow": "true",
-        "commit_comment": f"GSO_PROCESS_ID: {process_id} - Validate BGP peer config for {subscription["description"]}",
-    }
-
-    return {
-        "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml",
-        "inventory": {"all": {"hosts": dict.fromkeys(ap_fqdn_list)}},
-        "extra_vars": extra_vars,
-    }
-
-
-@step("Validate DNS records")
-def validate_dns_records() -> None:
-    """Validate DNS records in Infoblox."""
-    # TODO: implement
-
-
-@workflow("Validate L3 Core Service", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None)))
-def validate_l3_core_service() -> StepList:
-    """Validate an existing L3 Core Service subscription."""
-    return (
-        begin
-        >> store_process_subscription(Target.SYSTEM)
-        >> unsync
-        >> build_fqdn_list
-        >> anonymous_lso_interaction(validate_sbp_config)
-        >> anonymous_lso_interaction(validate_bgp_peers)
-        >> validate_dns_records
-        >> resync
-        >> done
-    )
diff --git a/gso/workflows/l3_core_service/validate_prefix_list.py b/gso/workflows/l3_core_service/validate_prefix_list.py
index 9c9b6cd43..f9e580b1c 100644
--- a/gso/workflows/l3_core_service/validate_prefix_list.py
+++ b/gso/workflows/l3_core_service/validate_prefix_list.py
@@ -1,3 +1,4 @@
+
 """Prefix Validation workflow for L3 Core Service subscription objects."""
 
 from typing import Any
@@ -138,4 +139,4 @@ def validate_prefix_list() -> StepList:
         >> fqdn_list_is_empty(done)
         >> prefix_list_should_be_validated(prefix_list_validation_steps)
         >> done
-    )
+    )
\ No newline at end of file
-- 
GitLab


From d3f329f9aacf65c81e4f5bcc23231d27b70b646a Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Mon, 31 Mar 2025 16:19:06 +0200
Subject: [PATCH 62/87] rename base_l3_core.py to l3_core_service.py

---
 gso/products/product_blocks/geant_ip.py                       | 2 +-
 gso/products/product_types/copernicus.py                      | 2 +-
 gso/products/product_types/geant_ip.py                        | 4 ++--
 gso/products/product_types/ias.py                             | 2 +-
 .../product_types/{base_l3_core.py => l3_core_service.py}     | 0
 gso/products/product_types/lhcone.py                          | 2 +-
 6 files changed, 6 insertions(+), 6 deletions(-)
 rename gso/products/product_types/{base_l3_core.py => l3_core_service.py} (100%)

diff --git a/gso/products/product_blocks/geant_ip.py b/gso/products/product_blocks/geant_ip.py
index 15865f8bc..2050794d1 100644
--- a/gso/products/product_blocks/geant_ip.py
+++ b/gso/products/product_blocks/geant_ip.py
@@ -1,4 +1,4 @@
-"""Product blocks for GEANT IP products."""
+"""Product blocks for GÉANT IP products."""
 
 from orchestrator.domain.base import ProductBlockModel
 from orchestrator.types import SubscriptionLifecycle
diff --git a/gso/products/product_types/copernicus.py b/gso/products/product_types/copernicus.py
index 6e395db03..9eee749ee 100644
--- a/gso/products/product_types/copernicus.py
+++ b/gso/products/product_types/copernicus.py
@@ -11,7 +11,7 @@ from gso.products.product_blocks.copernicus import (
     CopernicusBlockInactive,
     CopernicusBlockProvisioning,
 )
-from gso.products.product_types.base_l3_core import BaseL3SubscriptionModel
+from gso.products.product_types.l3_core_service import BaseL3SubscriptionModel
 
 if TYPE_CHECKING:
     from gso.products.product_blocks.l3_core_service import (
diff --git a/gso/products/product_types/geant_ip.py b/gso/products/product_types/geant_ip.py
index 0eb34104f..d5e07325d 100644
--- a/gso/products/product_types/geant_ip.py
+++ b/gso/products/product_types/geant_ip.py
@@ -1,4 +1,4 @@
-"""Product type for GEANT IP."""
+"""Product type for GÉANT IP."""
 
 from __future__ import annotations
 
@@ -11,7 +11,7 @@ from gso.products.product_blocks.geant_ip import (
     GeantIPBlockInactive,
     GeantIPBlockProvisioning,
 )
-from gso.products.product_types.base_l3_core import BaseL3SubscriptionModel
+from gso.products.product_types.l3_core_service import BaseL3SubscriptionModel
 
 if TYPE_CHECKING:
     from gso.products.product_blocks.l3_core_service import (
diff --git a/gso/products/product_types/ias.py b/gso/products/product_types/ias.py
index e339ea8df..5ba70c777 100644
--- a/gso/products/product_types/ias.py
+++ b/gso/products/product_types/ias.py
@@ -11,7 +11,7 @@ from gso.products.product_blocks.ias import (
     IASBlockInactive,
     IASBlockProvisioning,
 )
-from gso.products.product_types.base_l3_core import BaseL3SubscriptionModel
+from gso.products.product_types.l3_core_service import BaseL3SubscriptionModel
 
 if TYPE_CHECKING:
     from gso.products.product_blocks.l3_core_service import (
diff --git a/gso/products/product_types/base_l3_core.py b/gso/products/product_types/l3_core_service.py
similarity index 100%
rename from gso/products/product_types/base_l3_core.py
rename to gso/products/product_types/l3_core_service.py
diff --git a/gso/products/product_types/lhcone.py b/gso/products/product_types/lhcone.py
index b65ed0c69..ad48086a2 100644
--- a/gso/products/product_types/lhcone.py
+++ b/gso/products/product_types/lhcone.py
@@ -11,7 +11,7 @@ from gso.products.product_blocks.lhcone import (
     LHCOneBlockInactive,
     LHCOneBlockProvisioning,
 )
-from gso.products.product_types.base_l3_core import BaseL3SubscriptionModel
+from gso.products.product_types.l3_core_service import BaseL3SubscriptionModel
 
 if TYPE_CHECKING:
     from gso.products.product_blocks.l3_core_service import (
-- 
GitLab


From 47c2d6fd8c87bbb38849c7c7dc1ffae5d1d6a115 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Mon, 31 Mar 2025 16:21:43 +0200
Subject: [PATCH 63/87] rename L3ProductNameTypes to L3ProductNameType

---
 gso/cli/imports.py                                            | 4 ++--
 .../l3_core_service/base_create_imported_l3_core_service.py   | 4 ++--
 gso/workflows/l3_core_service/shared.py                       | 2 +-
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/gso/cli/imports.py b/gso/cli/imports.py
index cdf78ca57..cb0158842 100644
--- a/gso/cli/imports.py
+++ b/gso/cli/imports.py
@@ -53,7 +53,7 @@ from gso.utils.types.ip_address import (
     PortNumber,
 )
 from gso.utils.types.virtual_identifiers import VC_ID, VLAN_ID
-from gso.workflows.l3_core_service.shared import L3_CREAT_IMPORTED_WF_MAP, L3ProductNameTypes
+from gso.workflows.l3_core_service.shared import L3_CREAT_IMPORTED_WF_MAP, L3ProductNameType
 
 app: typer.Typer = typer.Typer()
 IMPORT_WAIT_MESSAGE = "Waiting for the dust to settle before importing new products..."
@@ -290,7 +290,7 @@ class L3CoreServiceImportModel(BaseModel):
 
     partner: str
     service_binding_ports: list[ServiceBindingPort]
-    product_name: L3ProductNameTypes
+    product_name: L3ProductNameType
 
     @field_validator("partner")
     def check_if_partner_exists(cls, value: str) -> str:
diff --git a/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
index 556b5eaf8..10a5ec01b 100644
--- a/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
@@ -16,7 +16,7 @@ from gso.utils.shared_enums import SBPType
 from gso.utils.types.geant_ids import IMPORTED_GS_ID
 from gso.utils.types.ip_address import IPAddress, IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask
 from gso.utils.types.virtual_identifiers import VLAN_ID
-from gso.workflows.l3_core_service.shared import L3ProductNameTypes
+from gso.workflows.l3_core_service.shared import L3ProductNameType
 
 
 def base_initial_input_form_generator() -> FormGenerator:
@@ -63,7 +63,7 @@ def base_initial_input_form_generator() -> FormGenerator:
     class ImportL3CoreServiceForm(SubmitFormPage):
         partner: str
         service_binding_ports: list[ServiceBindingPort]
-        product_name: L3ProductNameTypes
+        product_name: L3ProductNameType
 
     user_input = yield ImportL3CoreServiceForm
 
diff --git a/gso/workflows/l3_core_service/shared.py b/gso/workflows/l3_core_service/shared.py
index 3ed5a0feb..263dbb67c 100644
--- a/gso/workflows/l3_core_service/shared.py
+++ b/gso/workflows/l3_core_service/shared.py
@@ -17,7 +17,7 @@ L3_PRODUCT_NAMES = [
 L3_CORE_SERVICE_PRODUCT_TYPES = [GeantIP.__name__, IAS.__name__, LHCOne.__name__, Copernicus.__name__]
 assert len(L3_PRODUCT_NAMES) == len(L3_CORE_SERVICE_PRODUCT_TYPES)  # noqa: S101
 
-L3ProductNameTypes = Literal[ProductName.IAS, ProductName.LHCONE, ProductName.COPERNICUS, ProductName.GEANT_IP]
+L3ProductNameType = Literal[ProductName.IAS, ProductName.LHCONE, ProductName.COPERNICUS, ProductName.GEANT_IP]
 
 L3_CREAT_IMPORTED_WF_MAP = {
     ProductName.COPERNICUS: "create_imported_copernicus",
-- 
GitLab


From 7d0be0596c519333cb2ca4e6ab844b4fc1085787 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Mon, 31 Mar 2025 20:35:11 +0200
Subject: [PATCH 64/87] make naming a bit consistent

---
 gso/products/product_blocks/copernicus.py                    | 2 +-
 .../l3_core_service/base_create_imported_l3_core_service.py  | 2 +-
 gso/workflows/l3_core_service/base_modify_l3_core_service.py | 2 +-
 .../l3_core_service/copernicus/create_imported_copernicus.py | 4 ++--
 .../l3_core_service/copernicus/import_copernicus.py          | 4 ++--
 .../l3_core_service/copernicus/modify_copernicus.py          | 4 ++--
 .../l3_core_service/copernicus/terminate_copernicus.py       | 5 +++--
 .../l3_core_service/geant_ip/create_imported_geant_ip.py     | 4 ++--
 gso/workflows/l3_core_service/geant_ip/import_geant_ip.py    | 2 +-
 gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py    | 4 ++--
 gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py | 5 +++--
 gso/workflows/l3_core_service/ias/create_ias.py              | 4 ++--
 gso/workflows/l3_core_service/ias/create_imported_ias.py     | 4 +++-
 gso/workflows/l3_core_service/ias/import_ias.py              | 2 +-
 gso/workflows/l3_core_service/ias/modify_ias.py              | 4 ++--
 gso/workflows/l3_core_service/ias/terminate_ias.py           | 5 +++--
 .../l3_core_service/lhcone/create_imported_lhcone.py         | 4 ++--
 gso/workflows/l3_core_service/lhcone/import_lhcone.py        | 4 ++--
 gso/workflows/l3_core_service/lhcone/modify_lhcone.py        | 4 ++--
 gso/workflows/l3_core_service/lhcone/terminate_lhcone.py     | 5 +++--
 20 files changed, 40 insertions(+), 34 deletions(-)

diff --git a/gso/products/product_blocks/copernicus.py b/gso/products/product_blocks/copernicus.py
index 3b6a77cb2..e98263a48 100644
--- a/gso/products/product_blocks/copernicus.py
+++ b/gso/products/product_blocks/copernicus.py
@@ -1,4 +1,4 @@
-"""Product blocks for Copernicus AS products."""
+"""Product blocks for Copernicus products."""
 
 from orchestrator.domain.base import ProductBlockModel
 from orchestrator.types import SubscriptionLifecycle
diff --git a/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
index 10a5ec01b..3f8bf13f3 100644
--- a/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py
@@ -19,7 +19,7 @@ from gso.utils.types.virtual_identifiers import VLAN_ID
 from gso.workflows.l3_core_service.shared import L3ProductNameType
 
 
-def base_initial_input_form_generator() -> FormGenerator:
+def initial_input_form_generator() -> FormGenerator:
     """Take all information passed to this workflow by the API endpoint that was called."""
 
     class BFDSettingsModel(BaseModel):
diff --git a/gso/workflows/l3_core_service/base_modify_l3_core_service.py b/gso/workflows/l3_core_service/base_modify_l3_core_service.py
index b9b8c77e1..c55fb2c2b 100644
--- a/gso/workflows/l3_core_service/base_modify_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_modify_l3_core_service.py
@@ -30,7 +30,7 @@ class Operation(strEnum):
     EDIT = "Edit an existing Access Port"
 
 
-def base_initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     """Get input about added, removed, and modified Access Ports."""
     subscription = SubscriptionModel.from_subscription(subscription_id)
     product_name = subscription.product.name
diff --git a/gso/workflows/l3_core_service/copernicus/create_imported_copernicus.py b/gso/workflows/l3_core_service/copernicus/create_imported_copernicus.py
index 24a1bcba8..e5dad5809 100644
--- a/gso/workflows/l3_core_service/copernicus/create_imported_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/create_imported_copernicus.py
@@ -11,7 +11,7 @@ from gso.products.product_types.copernicus import ImportedCopernicusInactive
 from gso.services.partners import get_partner_by_name
 from gso.services.subscriptions import get_product_id_by_name
 from gso.workflows.l3_core_service.base_create_imported_l3_core_service import (
-    base_initial_input_form_generator,
+    initial_input_form_generator,
     initialize_subscription,
 )
 
@@ -27,7 +27,7 @@ def create_subscription(partner: str) -> dict:
 
 @workflow(
     "Create Imported Copernicus",
-    initial_input_form=base_initial_input_form_generator,
+    initial_input_form=initial_input_form_generator,
     target=Target.CREATE,
 )
 def create_imported_copernicus() -> StepList:
diff --git a/gso/workflows/l3_core_service/copernicus/import_copernicus.py b/gso/workflows/l3_core_service/copernicus/import_copernicus.py
index 10f179f3b..ff0eb1013 100644
--- a/gso/workflows/l3_core_service/copernicus/import_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/import_copernicus.py
@@ -12,9 +12,9 @@ from gso.services.partners import get_partner_by_id
 from gso.services.subscriptions import get_product_id_by_name
 
 
-@step("Create imported subscription")
+@step("Create a Copernicus subscription")
 def import_copernicus_subscription(subscription_id: UUIDstr) -> State:
-    """Take an imported subscription, and turn it into a Copernicus Service subscription."""
+    """Take an imported subscription, and turn it into a Copernicus subscription."""
     old_l3_core_service = ImportedCopernicus.from_subscription(subscription_id)
     new_product_id = get_product_id_by_name(ProductName.COPERNICUS)
     new_subscription = Copernicus.from_other_product(old_l3_core_service, new_product_id)  # type: ignore[arg-type]
diff --git a/gso/workflows/l3_core_service/copernicus/modify_copernicus.py b/gso/workflows/l3_core_service/copernicus/modify_copernicus.py
index 37a263d79..3d442f2dc 100644
--- a/gso/workflows/l3_core_service/copernicus/modify_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/modify_copernicus.py
@@ -8,8 +8,8 @@ from orchestrator.workflows.utils import wrap_modify_initial_input_form
 
 from gso.workflows.l3_core_service.base_modify_l3_core_service import (
     Operation,
-    base_initial_input_form_generator,
     create_new_sbp,
+    initial_input_form_generator,
     modify_existing_sbp,
     remove_old_sbp,
 )
@@ -17,7 +17,7 @@ from gso.workflows.l3_core_service.base_modify_l3_core_service import (
 
 @workflow(
     "Modify Copernicus",
-    initial_input_form=wrap_modify_initial_input_form(base_initial_input_form_generator),
+    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
     target=Target.MODIFY,
 )
 def modify_copernicus() -> StepList:
diff --git a/gso/workflows/l3_core_service/copernicus/terminate_copernicus.py b/gso/workflows/l3_core_service/copernicus/terminate_copernicus.py
index 532277e76..1a3cfc96d 100644
--- a/gso/workflows/l3_core_service/copernicus/terminate_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/terminate_copernicus.py
@@ -13,7 +13,8 @@ from gso.products.product_types.copernicus import Copernicus
 from gso.utils.types.tt_number import TTNumber
 
 
-def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+def input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+    """Initial input form generator for terminating a Copernicus subscription."""
     subscription = Copernicus.from_subscription(subscription_id)
 
     class TerminateForm(SubmitFormPage):
@@ -25,7 +26,7 @@ def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
 @workflow(
     "Terminate Copernicus",
-    initial_input_form=wrap_modify_initial_input_form(_input_form_generator),
+    initial_input_form=wrap_modify_initial_input_form(input_form_generator),
     target=Target.TERMINATE,
 )
 def terminate_copernicus() -> StepList:
diff --git a/gso/workflows/l3_core_service/geant_ip/create_imported_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/create_imported_geant_ip.py
index e07766025..7a1775034 100644
--- a/gso/workflows/l3_core_service/geant_ip/create_imported_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/create_imported_geant_ip.py
@@ -11,7 +11,7 @@ from gso.products.product_types.geant_ip import ImportedGeantIPInactive
 from gso.services.partners import get_partner_by_name
 from gso.services.subscriptions import get_product_id_by_name
 from gso.workflows.l3_core_service.base_create_imported_l3_core_service import (
-    base_initial_input_form_generator,
+    initial_input_form_generator,
     initialize_subscription,
 )
 
@@ -27,7 +27,7 @@ def create_subscription(partner: str) -> dict:
 
 @workflow(
     "Create Imported GÉANT IP",
-    initial_input_form=base_initial_input_form_generator,
+    initial_input_form=initial_input_form_generator,
     target=Target.CREATE,
 )
 def create_imported_geant_ip() -> StepList:
diff --git a/gso/workflows/l3_core_service/geant_ip/import_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/import_geant_ip.py
index 2de6b214f..d7bd7bef4 100644
--- a/gso/workflows/l3_core_service/geant_ip/import_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/import_geant_ip.py
@@ -12,7 +12,7 @@ from gso.services.partners import get_partner_by_id
 from gso.services.subscriptions import get_product_id_by_name
 
 
-@step("Create imported subscription")
+@step("Create a GÉANT IP subscription")
 def import_geant_ip_subscription(subscription_id: UUIDstr) -> State:
     """Take an imported subscription, and turn it into a GÉANT IP subscription."""
     old_l3_core_service = ImportedGeantIP.from_subscription(subscription_id)
diff --git a/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py
index a7c7e67bf..a2b07c08f 100644
--- a/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py
@@ -8,8 +8,8 @@ from orchestrator.workflows.utils import wrap_modify_initial_input_form
 
 from gso.workflows.l3_core_service.base_modify_l3_core_service import (
     Operation,
-    base_initial_input_form_generator,
     create_new_sbp,
+    initial_input_form_generator,
     modify_existing_sbp,
     remove_old_sbp,
 )
@@ -17,7 +17,7 @@ from gso.workflows.l3_core_service.base_modify_l3_core_service import (
 
 @workflow(
     "Modify GÉANT IP",
-    initial_input_form=wrap_modify_initial_input_form(base_initial_input_form_generator),
+    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
     target=Target.MODIFY,
 )
 def modify_geant_ip() -> StepList:
diff --git a/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py
index 9a73d9d9c..c8fc8c14f 100644
--- a/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py
@@ -13,7 +13,8 @@ from gso.products.product_types.geant_ip import GeantIP
 from gso.utils.types.tt_number import TTNumber
 
 
-def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+def input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+    """Initial input form generator for terminating a GÉANT IP subscription."""
     subscription = GeantIP.from_subscription(subscription_id)
 
     class TerminateForm(SubmitFormPage):
@@ -25,7 +26,7 @@ def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
 @workflow(
     "Terminate GÉANT IP",
-    initial_input_form=wrap_modify_initial_input_form(_input_form_generator),
+    initial_input_form=wrap_modify_initial_input_form(input_form_generator),
     target=Target.TERMINATE,
 )
 def terminate_geant_ip() -> StepList:
diff --git a/gso/workflows/l3_core_service/ias/create_ias.py b/gso/workflows/l3_core_service/ias/create_ias.py
index 21ae9addd..8243ed758 100644
--- a/gso/workflows/l3_core_service/ias/create_ias.py
+++ b/gso/workflows/l3_core_service/ias/create_ias.py
@@ -24,13 +24,13 @@ from gso.workflows.l3_core_service.base_create_l3_core_service import (
     update_dns_records,
 )
 from gso.workflows.l3_core_service.base_create_l3_core_service import (
-    initial_input_form_generator as l3_initial_input_form_generator,
+    initial_input_form_generator as base_initial_input_form_generator,
 )
 
 
 def initial_input_form_generator(product_name: str) -> FormGenerator:
     """Initial input form generator for creating a new IAS subscription."""
-    initial_generator = l3_initial_input_form_generator(product_name)
+    initial_generator = base_initial_input_form_generator(product_name)
     initial_user_input = yield from initial_generator
 
     # Additional IAS step
diff --git a/gso/workflows/l3_core_service/ias/create_imported_ias.py b/gso/workflows/l3_core_service/ias/create_imported_ias.py
index 9061c6a74..639fdf36a 100644
--- a/gso/workflows/l3_core_service/ias/create_imported_ias.py
+++ b/gso/workflows/l3_core_service/ias/create_imported_ias.py
@@ -14,7 +14,9 @@ from gso.products.product_types.ias import ImportedIASInactive
 from gso.services.partners import get_partner_by_name
 from gso.services.subscriptions import get_product_id_by_name
 from gso.workflows.l3_core_service.base_create_imported_l3_core_service import (
-    base_initial_input_form_generator,
+    initial_input_form_generator as base_initial_input_form_generator,
+)
+from gso.workflows.l3_core_service.base_create_imported_l3_core_service import (
     initialize_subscription,
 )
 
diff --git a/gso/workflows/l3_core_service/ias/import_ias.py b/gso/workflows/l3_core_service/ias/import_ias.py
index 703be98d2..130c01a84 100644
--- a/gso/workflows/l3_core_service/ias/import_ias.py
+++ b/gso/workflows/l3_core_service/ias/import_ias.py
@@ -12,7 +12,7 @@ from gso.services.partners import get_partner_by_id
 from gso.services.subscriptions import get_product_id_by_name
 
 
-@step("Create imported subscription")
+@step("Create an IAS subscription")
 def import_ias_subscription(subscription_id: UUIDstr) -> State:
     """Take an imported subscription, and turn it into an IAS subscription."""
     old_l3_core_service = ImportedIAS.from_subscription(subscription_id)
diff --git a/gso/workflows/l3_core_service/ias/modify_ias.py b/gso/workflows/l3_core_service/ias/modify_ias.py
index 907cf400f..9fd707808 100644
--- a/gso/workflows/l3_core_service/ias/modify_ias.py
+++ b/gso/workflows/l3_core_service/ias/modify_ias.py
@@ -8,8 +8,8 @@ from orchestrator.workflows.utils import wrap_modify_initial_input_form
 
 from gso.workflows.l3_core_service.base_modify_l3_core_service import (
     Operation,
-    base_initial_input_form_generator,
     create_new_sbp,
+    initial_input_form_generator,
     modify_existing_sbp,
     remove_old_sbp,
 )
@@ -17,7 +17,7 @@ from gso.workflows.l3_core_service.base_modify_l3_core_service import (
 
 @workflow(
     "Modify IAS",
-    initial_input_form=wrap_modify_initial_input_form(base_initial_input_form_generator),
+    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
     target=Target.MODIFY,
 )
 def modify_ias() -> StepList:
diff --git a/gso/workflows/l3_core_service/ias/terminate_ias.py b/gso/workflows/l3_core_service/ias/terminate_ias.py
index 3a59130fa..fffcb4146 100644
--- a/gso/workflows/l3_core_service/ias/terminate_ias.py
+++ b/gso/workflows/l3_core_service/ias/terminate_ias.py
@@ -13,7 +13,8 @@ from gso.products.product_types.ias import IAS
 from gso.utils.types.tt_number import TTNumber
 
 
-def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+def input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+    """Initial input form generator for terminating an IAS subscription."""
     subscription = IAS.from_subscription(subscription_id)
 
     class TerminateForm(SubmitFormPage):
@@ -25,7 +26,7 @@ def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
 @workflow(
     "Terminate IAS",
-    initial_input_form=wrap_modify_initial_input_form(_input_form_generator),
+    initial_input_form=wrap_modify_initial_input_form(input_form_generator),
     target=Target.TERMINATE,
 )
 def terminate_ias() -> StepList:
diff --git a/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py b/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py
index 5291118c6..adc0fb663 100644
--- a/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py
@@ -11,7 +11,7 @@ from gso.products.product_types.lhcone import ImportedLHCOneInactive
 from gso.services.partners import get_partner_by_name
 from gso.services.subscriptions import get_product_id_by_name
 from gso.workflows.l3_core_service.base_create_imported_l3_core_service import (
-    base_initial_input_form_generator,
+    initial_input_form_generator,
     initialize_subscription,
 )
 
@@ -27,7 +27,7 @@ def create_subscription(partner: str) -> dict:
 
 @workflow(
     "Create Imported LHCOne",
-    initial_input_form=base_initial_input_form_generator,
+    initial_input_form=initial_input_form_generator,
     target=Target.CREATE,
 )
 def create_imported_lhcone() -> StepList:
diff --git a/gso/workflows/l3_core_service/lhcone/import_lhcone.py b/gso/workflows/l3_core_service/lhcone/import_lhcone.py
index 8b90d0df9..bf4cdae26 100644
--- a/gso/workflows/l3_core_service/lhcone/import_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/import_lhcone.py
@@ -12,9 +12,9 @@ from gso.services.partners import get_partner_by_id
 from gso.services.subscriptions import get_product_id_by_name
 
 
-@step("Create imported subscription")
+@step("Create a LHCOne subscription")
 def import_lhcone_subscription(subscription_id: UUIDstr) -> State:
-    """Take an imported subscription, and turn it into a LHCOne Service subscription."""
+    """Take an imported subscription, and turn it into a LHCOne subscription."""
     old_l3_core_service = ImportedLHCOne.from_subscription(subscription_id)
     new_product_id = get_product_id_by_name(ProductName.LHCONE)
     new_subscription = LHCOne.from_other_product(old_l3_core_service, new_product_id)  # type: ignore[arg-type]
diff --git a/gso/workflows/l3_core_service/lhcone/modify_lhcone.py b/gso/workflows/l3_core_service/lhcone/modify_lhcone.py
index cc5af5ed3..da79e86bd 100644
--- a/gso/workflows/l3_core_service/lhcone/modify_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/modify_lhcone.py
@@ -8,8 +8,8 @@ from orchestrator.workflows.utils import wrap_modify_initial_input_form
 
 from gso.workflows.l3_core_service.base_modify_l3_core_service import (
     Operation,
-    base_initial_input_form_generator,
     create_new_sbp,
+    initial_input_form_generator,
     modify_existing_sbp,
     remove_old_sbp,
 )
@@ -17,7 +17,7 @@ from gso.workflows.l3_core_service.base_modify_l3_core_service import (
 
 @workflow(
     "Modify LHCOne",
-    initial_input_form=wrap_modify_initial_input_form(base_initial_input_form_generator),
+    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
     target=Target.MODIFY,
 )
 def modify_lhcone() -> StepList:
diff --git a/gso/workflows/l3_core_service/lhcone/terminate_lhcone.py b/gso/workflows/l3_core_service/lhcone/terminate_lhcone.py
index 4df83d02b..b1b05f395 100644
--- a/gso/workflows/l3_core_service/lhcone/terminate_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/terminate_lhcone.py
@@ -13,7 +13,8 @@ from gso.products.product_types.lhcone import LHCOne
 from gso.utils.types.tt_number import TTNumber
 
 
-def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+def input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+    """Initial input form generator for terminating an LHCOne subscription."""
     subscription = LHCOne.from_subscription(subscription_id)
 
     class TerminateForm(SubmitFormPage):
@@ -25,7 +26,7 @@ def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
 @workflow(
     "Terminate LHCOne",
-    initial_input_form=wrap_modify_initial_input_form(_input_form_generator),
+    initial_input_form=wrap_modify_initial_input_form(input_form_generator),
     target=Target.TERMINATE,
 )
 def terminate_lhcone() -> StepList:
-- 
GitLab


From 67b36af73382b9fb26129fdb9f4c3b7e8e8aef52 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Mon, 31 Mar 2025 20:44:50 +0200
Subject: [PATCH 65/87] make vale happy

---
 gso/products/product_types/ias.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/gso/products/product_types/ias.py b/gso/products/product_types/ias.py
index 5ba70c777..6eb6193fe 100644
--- a/gso/products/product_types/ias.py
+++ b/gso/products/product_types/ias.py
@@ -26,12 +26,12 @@ class IASInactive(BaseL3SubscriptionModel, is_base=True):
 
     @property
     def l3_core(self) -> L3CoreServiceBlockInactive | None:
-        """Getter: Retrieve the l3_core from the ias attribute."""
+        """Getter: Retrieve the l3_core from the 'ias' attribute."""
         return self.ias.l3_core
 
     @l3_core.setter
     def l3_core(self, value: L3CoreServiceBlockInactive) -> None:
-        """Setter: Set the l3_core on the ias attribute."""
+        """Setter: Set the l3_core on the 'ias' attribute."""
         self.ias.l3_core = value
 
     @property
@@ -59,12 +59,12 @@ class ImportedIASInactive(BaseL3SubscriptionModel, is_base=True):
 
     @property
     def l3_core(self) -> L3CoreServiceBlockInactive | None:
-        """Getter: Retrieve the l3_core from the ias attribute."""
+        """Getter: Retrieve the l3_core from the 'ias' attribute."""
         return self.ias.l3_core
 
     @l3_core.setter
     def l3_core(self, value: L3CoreServiceBlockInactive) -> None:
-        """Setter: Set the l3_core on the ias attribute."""
+        """Setter: Set the l3_core on the 'ias' attribute."""
         self.ias.l3_core = value
 
     @property
-- 
GitLab


From 051b4940facedb1d3a46245e6e3bdbe2ae54b4a3 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Mon, 31 Mar 2025 20:48:43 +0200
Subject: [PATCH 66/87] rename input_form_generator to
 initial_input_form_generator

---
 .../l3_core_service/copernicus/terminate_copernicus.py        | 4 ++--
 gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py  | 4 ++--
 gso/workflows/l3_core_service/ias/terminate_ias.py            | 4 ++--
 gso/workflows/l3_core_service/lhcone/terminate_lhcone.py      | 4 ++--
 4 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/gso/workflows/l3_core_service/copernicus/terminate_copernicus.py b/gso/workflows/l3_core_service/copernicus/terminate_copernicus.py
index 1a3cfc96d..d8452035b 100644
--- a/gso/workflows/l3_core_service/copernicus/terminate_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/terminate_copernicus.py
@@ -13,7 +13,7 @@ from gso.products.product_types.copernicus import Copernicus
 from gso.utils.types.tt_number import TTNumber
 
 
-def input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     """Initial input form generator for terminating a Copernicus subscription."""
     subscription = Copernicus.from_subscription(subscription_id)
 
@@ -26,7 +26,7 @@ def input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
 @workflow(
     "Terminate Copernicus",
-    initial_input_form=wrap_modify_initial_input_form(input_form_generator),
+    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
     target=Target.TERMINATE,
 )
 def terminate_copernicus() -> StepList:
diff --git a/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py
index c8fc8c14f..afae36f22 100644
--- a/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py
@@ -13,7 +13,7 @@ from gso.products.product_types.geant_ip import GeantIP
 from gso.utils.types.tt_number import TTNumber
 
 
-def input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     """Initial input form generator for terminating a GÉANT IP subscription."""
     subscription = GeantIP.from_subscription(subscription_id)
 
@@ -26,7 +26,7 @@ def input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
 @workflow(
     "Terminate GÉANT IP",
-    initial_input_form=wrap_modify_initial_input_form(input_form_generator),
+    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
     target=Target.TERMINATE,
 )
 def terminate_geant_ip() -> StepList:
diff --git a/gso/workflows/l3_core_service/ias/terminate_ias.py b/gso/workflows/l3_core_service/ias/terminate_ias.py
index fffcb4146..c6d2f2272 100644
--- a/gso/workflows/l3_core_service/ias/terminate_ias.py
+++ b/gso/workflows/l3_core_service/ias/terminate_ias.py
@@ -13,7 +13,7 @@ from gso.products.product_types.ias import IAS
 from gso.utils.types.tt_number import TTNumber
 
 
-def input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     """Initial input form generator for terminating an IAS subscription."""
     subscription = IAS.from_subscription(subscription_id)
 
@@ -26,7 +26,7 @@ def input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
 @workflow(
     "Terminate IAS",
-    initial_input_form=wrap_modify_initial_input_form(input_form_generator),
+    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
     target=Target.TERMINATE,
 )
 def terminate_ias() -> StepList:
diff --git a/gso/workflows/l3_core_service/lhcone/terminate_lhcone.py b/gso/workflows/l3_core_service/lhcone/terminate_lhcone.py
index b1b05f395..c82f7f7ae 100644
--- a/gso/workflows/l3_core_service/lhcone/terminate_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/terminate_lhcone.py
@@ -13,7 +13,7 @@ from gso.products.product_types.lhcone import LHCOne
 from gso.utils.types.tt_number import TTNumber
 
 
-def input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     """Initial input form generator for terminating an LHCOne subscription."""
     subscription = LHCOne.from_subscription(subscription_id)
 
@@ -26,7 +26,7 @@ def input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
 @workflow(
     "Terminate LHCOne",
-    initial_input_form=wrap_modify_initial_input_form(input_form_generator),
+    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
     target=Target.TERMINATE,
 )
 def terminate_lhcone() -> StepList:
-- 
GitLab


From 9a928f3dc48db8f826eef4c2c1fccea6034513c7 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Tue, 1 Apr 2025 10:03:29 +0200
Subject: [PATCH 67/87] add more asssert for L3 tests

---
 .../l3_core_service/test_create_imported_l3_core_service.py     | 2 ++
 test/workflows/l3_core_service/test_create_l3_core_service.py   | 1 +
 2 files changed, 3 insertions(+)

diff --git a/test/workflows/l3_core_service/test_create_imported_l3_core_service.py b/test/workflows/l3_core_service/test_create_imported_l3_core_service.py
index b1190f4a3..1798c3103 100644
--- a/test/workflows/l3_core_service/test_create_imported_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_create_imported_l3_core_service.py
@@ -7,6 +7,7 @@ from gso.products.product_blocks.bgp_session import IPFamily
 from gso.products.product_blocks.ias import IASFlavor
 from gso.utils.shared_enums import SBPType
 from gso.workflows.l3_core_service.shared import L3_CREAT_IMPORTED_WF_MAP, L3_PRODUCT_NAMES
+from test.fixtures.l3_core_service_fixtures import PRODUCT_IMPORTED_MAP
 from test.workflows import assert_complete, extract_state, run_workflow
 
 
@@ -81,3 +82,4 @@ def test_create_imported_l3_core_service_success(faker, partner_factory, edge_po
     subscription = SubscriptionModel.from_subscription(state["subscription_id"])
     assert_complete(result)
     assert subscription.status == SubscriptionLifecycle.ACTIVE
+    assert subscription.product.name == PRODUCT_IMPORTED_MAP[product_name]
diff --git a/test/workflows/l3_core_service/test_create_l3_core_service.py b/test/workflows/l3_core_service/test_create_l3_core_service.py
index aacfd9cc0..367d56920 100644
--- a/test/workflows/l3_core_service/test_create_l3_core_service.py
+++ b/test/workflows/l3_core_service/test_create_l3_core_service.py
@@ -106,6 +106,7 @@ def test_create_l3_core_service_success(
     assert_complete(result)
     state = extract_state(result)
     subscription = SubscriptionModel.from_subscription(state["subscription_id"])
+    assert subscription.product.name == product_name
     assert mock_lso_client.call_count == lso_interaction_count + 1
     assert subscription.status == SubscriptionLifecycle.ACTIVE
     assert len(subscription.l3_core.ap_list) == 1
-- 
GitLab


From 7a68db9963d48fc334c505078ed557283ff29a51 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 3 Apr 2025 07:17:50 +0000
Subject: [PATCH 68/87] Apply 1 suggestion(s) to 1 file(s)

Co-authored-by: Karel van Klink <karel.vanklink@geant.org>
---
 gso/products/product_blocks/ias.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gso/products/product_blocks/ias.py b/gso/products/product_blocks/ias.py
index 615fc58dd..fdf26d324 100644
--- a/gso/products/product_blocks/ias.py
+++ b/gso/products/product_blocks/ias.py
@@ -23,7 +23,7 @@ class IASBlockInactive(ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITI
     """An inactive IAS product block. See `IASBlock`."""
 
     l3_core: L3CoreServiceBlockInactive | None = None
-    ias_flavor: IASFlavor | None = IASFlavor.IAS_PS_OPT_OUT
+    ias_flavor: IASFlavor = IASFlavor.IAS_PS_OPT_OUT
 
 
 class IASBlockProvisioning(IASBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
-- 
GitLab


From fdf61c20acf74cd90f2195b389ff5914601d8e75 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 3 Apr 2025 07:18:10 +0000
Subject: [PATCH 69/87] Apply 1 suggestion(s) to 1 file(s)

Co-authored-by: Karel van Klink <karel.vanklink@geant.org>
---
 gso/products/product_types/copernicus.py | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/gso/products/product_types/copernicus.py b/gso/products/product_types/copernicus.py
index 9eee749ee..cb6c2e687 100644
--- a/gso/products/product_types/copernicus.py
+++ b/gso/products/product_types/copernicus.py
@@ -14,9 +14,7 @@ from gso.products.product_blocks.copernicus import (
 from gso.products.product_types.l3_core_service import BaseL3SubscriptionModel
 
 if TYPE_CHECKING:
-    from gso.products.product_blocks.l3_core_service import (
-        L3CoreServiceBlockInactive,
-    )
+    from gso.products.product_blocks.l3_core_service import L3CoreServiceBlockInactive
 
 
 class CopernicusInactive(BaseL3SubscriptionModel, is_base=True):
-- 
GitLab


From 1c963714d340659fd8413b139e4172ec59afb9ec Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 3 Apr 2025 07:18:37 +0000
Subject: [PATCH 70/87] Apply 1 suggestion(s) to 1 file(s)

Co-authored-by: Karel van Klink <karel.vanklink@geant.org>
---
 gso/translations/en-GB.json | 1 -
 1 file changed, 1 deletion(-)

diff --git a/gso/translations/en-GB.json b/gso/translations/en-GB.json
index c6c7208f9..98d936fa0 100644
--- a/gso/translations/en-GB.json
+++ b/gso/translations/en-GB.json
@@ -171,7 +171,6 @@
         "validate_copernicus": "Validate Copernicus",
         "validate_lan_switch_interconnect": "Validate LAN Switch Interconnect",
         "validate_geant_ip_prefix_list": "Validate GÉANT IP Prefix-List",
-        "validate_ias_prefix_list": "Validate IAS Prefix-List",
         "validate_router": "Validate Router configuration",
         "validate_switch": "Validate Switch configuration"
     }
-- 
GitLab


From 62285a3aacecd8260f02a1478468bfd0bc5c092b Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 3 Apr 2025 07:18:47 +0000
Subject: [PATCH 71/87] Apply 1 suggestion(s) to 1 file(s)

Co-authored-by: Karel van Klink <karel.vanklink@geant.org>
---
 gso/workflows/l3_core_service/copernicus/import_copernicus.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gso/workflows/l3_core_service/copernicus/import_copernicus.py b/gso/workflows/l3_core_service/copernicus/import_copernicus.py
index ff0eb1013..0b23e716f 100644
--- a/gso/workflows/l3_core_service/copernicus/import_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/import_copernicus.py
@@ -1,4 +1,4 @@
-"""A modification workflow for migrating an `ImportedCopernicus` to an `Copernicus` subscription."""
+"""A modification workflow for migrating an `ImportedCopernicus` to a `Copernicus` subscription."""
 
 from orchestrator.targets import Target
 from orchestrator.workflow import StepList, done, init, step, workflow
-- 
GitLab


From e43c37e924bfcace42ead1cbbc9f54cd2490e166 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 3 Apr 2025 07:18:52 +0000
Subject: [PATCH 72/87] Apply 1 suggestion(s) to 1 file(s)

Co-authored-by: Karel van Klink <karel.vanklink@geant.org>
---
 gso/workflows/l3_core_service/copernicus/modify_copernicus.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gso/workflows/l3_core_service/copernicus/modify_copernicus.py b/gso/workflows/l3_core_service/copernicus/modify_copernicus.py
index 3d442f2dc..6aacfa9df 100644
--- a/gso/workflows/l3_core_service/copernicus/modify_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/modify_copernicus.py
@@ -1,4 +1,4 @@
-"""Modification workflow for a Copernicus  subscription."""
+"""Modification workflow for a Copernicus subscription."""
 
 from orchestrator import begin, conditional, done, workflow
 from orchestrator.targets import Target
-- 
GitLab


From 086146c4db623bce68f9745c40ea38a6b3a36fb8 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 3 Apr 2025 07:19:35 +0000
Subject: [PATCH 73/87] Apply 1 suggestion(s) to 1 file(s)

Co-authored-by: Karel van Klink <karel.vanklink@geant.org>
---
 .../l3_core_service/copernicus/terminate_copernicus.py        | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/gso/workflows/l3_core_service/copernicus/terminate_copernicus.py b/gso/workflows/l3_core_service/copernicus/terminate_copernicus.py
index d8452035b..9d9539d6f 100644
--- a/gso/workflows/l3_core_service/copernicus/terminate_copernicus.py
+++ b/gso/workflows/l3_core_service/copernicus/terminate_copernicus.py
@@ -20,8 +20,8 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     class TerminateForm(SubmitFormPage):
         tt_number: TTNumber
 
-    yield TerminateForm
-    return {"subscription": subscription}
+    user_input = yield TerminateForm
+    return {"subscription": subscription} | user_input.model_dump()
 
 
 @workflow(
-- 
GitLab


From 4cd0391c724bf00a0c8b1e470cd5dc5e982cd9f5 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 3 Apr 2025 09:48:09 +0200
Subject: [PATCH 74/87] add TTNumber to state for L3 trmination wfs

---
 gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py | 5 +++--
 gso/workflows/l3_core_service/ias/terminate_ias.py           | 5 +++--
 gso/workflows/l3_core_service/lhcone/terminate_lhcone.py     | 5 +++--
 3 files changed, 9 insertions(+), 6 deletions(-)

diff --git a/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py
index afae36f22..b96474dd6 100644
--- a/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py
+++ b/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py
@@ -20,8 +20,9 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     class TerminateForm(SubmitFormPage):
         tt_number: TTNumber
 
-    yield TerminateForm
-    return {"subscription": subscription}
+    user_input = yield TerminateForm
+
+    return {"subscription": subscription} | user_input.model_dump()
 
 
 @workflow(
diff --git a/gso/workflows/l3_core_service/ias/terminate_ias.py b/gso/workflows/l3_core_service/ias/terminate_ias.py
index c6d2f2272..5cc25a6e2 100644
--- a/gso/workflows/l3_core_service/ias/terminate_ias.py
+++ b/gso/workflows/l3_core_service/ias/terminate_ias.py
@@ -20,8 +20,9 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     class TerminateForm(SubmitFormPage):
         tt_number: TTNumber
 
-    yield TerminateForm
-    return {"subscription": subscription}
+    user_input = yield TerminateForm
+
+    return {"subscription": subscription} | user_input.model_dump()
 
 
 @workflow(
diff --git a/gso/workflows/l3_core_service/lhcone/terminate_lhcone.py b/gso/workflows/l3_core_service/lhcone/terminate_lhcone.py
index c82f7f7ae..021e2b70b 100644
--- a/gso/workflows/l3_core_service/lhcone/terminate_lhcone.py
+++ b/gso/workflows/l3_core_service/lhcone/terminate_lhcone.py
@@ -20,8 +20,9 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     class TerminateForm(SubmitFormPage):
         tt_number: TTNumber
 
-    yield TerminateForm
-    return {"subscription": subscription}
+    user_input = yield TerminateForm
+
+    return {"subscription": subscription} | user_input.model_dump()
 
 
 @workflow(
-- 
GitLab


From fe10dfdb15b3e86fc3c156d7131919f574bbbd45 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 3 Apr 2025 09:49:25 +0200
Subject: [PATCH 75/87] add TTNumber to state for switch trmination wfs

---
 gso/workflows/switch/terminate_switch.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/gso/workflows/switch/terminate_switch.py b/gso/workflows/switch/terminate_switch.py
index 01f149b1d..11acaabd0 100644
--- a/gso/workflows/switch/terminate_switch.py
+++ b/gso/workflows/switch/terminate_switch.py
@@ -30,8 +30,9 @@ def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
         tt_number: TTNumber
 
-    yield TerminateForm
-    return {"subscription": switch}
+    user_input = yield TerminateForm
+
+    return {"subscription": switch} | user_input.model_dump()
 
 
 @step("Remove switch from Netbox")
-- 
GitLab


From 99d02c614353018a701dc89d0e9dcdc8bc9fd7cc Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 3 Apr 2025 09:51:32 +0200
Subject: [PATCH 76/87] add TTNumber to state for lan switch trmination wfs

---
 .../terminate_lan_switch_interconnect.py                     | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/gso/workflows/lan_switch_interconnect/terminate_lan_switch_interconnect.py b/gso/workflows/lan_switch_interconnect/terminate_lan_switch_interconnect.py
index b878b7d6a..b35211f4f 100644
--- a/gso/workflows/lan_switch_interconnect/terminate_lan_switch_interconnect.py
+++ b/gso/workflows/lan_switch_interconnect/terminate_lan_switch_interconnect.py
@@ -29,8 +29,9 @@ def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
         tt_number: TTNumber
 
-    yield TerminateForm
-    return {"subscription": lan_switch_interconnect}
+    user_input = yield TerminateForm
+
+    return {"subscription": lan_switch_interconnect} | user_input.model_dump()
 
 
 @step("Release IPAM resources")
-- 
GitLab


From 0603201aa15fbe30b812a709e84612d7228b91a3 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 3 Apr 2025 09:56:15 +0200
Subject: [PATCH 77/87] revert PRODUCT_TYPE in sharepoint checklist for a L3
 core service

---
 gso/workflows/l3_core_service/base_create_l3_core_service.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/gso/workflows/l3_core_service/base_create_l3_core_service.py b/gso/workflows/l3_core_service/base_create_l3_core_service.py
index 18a8c9d84..dd8459285 100644
--- a/gso/workflows/l3_core_service/base_create_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_l3_core_service.py
@@ -347,6 +347,7 @@ def create_new_sharepoint_checklist(
             "Title": f"{subscription.description}",
             "TT_NUMBER": tt_number,
             "ACTIVITY_TYPE": "Creation",
+            "PRODUCT_TYPE": subscription.product.name,
             "LOCATION": f"{new_ep.edge_port_name} {new_ep.edge_port_description} on {new_ep.node.router_fqdn}",
             "GAP_PROCESS_URL": f"{load_oss_params().GENERAL.public_hostname}/workflows/{process_id}",
         },
-- 
GitLab


From f8b6b3b4f49db3c6b9e8d7c36b1f312c34e5699e Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 3 Apr 2025 10:04:09 +0200
Subject: [PATCH 78/87] add a message to L3 lenght assertion

---
 gso/workflows/l3_core_service/shared.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/gso/workflows/l3_core_service/shared.py b/gso/workflows/l3_core_service/shared.py
index 263dbb67c..00c3ea8ba 100644
--- a/gso/workflows/l3_core_service/shared.py
+++ b/gso/workflows/l3_core_service/shared.py
@@ -15,7 +15,9 @@ L3_PRODUCT_NAMES = [
     ProductName.COPERNICUS,
 ]
 L3_CORE_SERVICE_PRODUCT_TYPES = [GeantIP.__name__, IAS.__name__, LHCOne.__name__, Copernicus.__name__]
-assert len(L3_PRODUCT_NAMES) == len(L3_CORE_SERVICE_PRODUCT_TYPES)  # noqa: S101
+assert len(L3_PRODUCT_NAMES) == len(  # noqa: S101
+    L3_CORE_SERVICE_PRODUCT_TYPES
+), "The number of L3 product names does not match the number of L3 core service product types."
 
 L3ProductNameType = Literal[ProductName.IAS, ProductName.LHCONE, ProductName.COPERNICUS, ProductName.GEANT_IP]
 
-- 
GitLab


From 46256ad644cee61bbf8a6c4d7e82a9dd492a8001 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 3 Apr 2025 10:08:15 +0200
Subject: [PATCH 79/87] remove None from typehints for ias_flavour in imported
 ias

---
 gso/workflows/l3_core_service/ias/create_imported_ias.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/gso/workflows/l3_core_service/ias/create_imported_ias.py b/gso/workflows/l3_core_service/ias/create_imported_ias.py
index 639fdf36a..a393d155b 100644
--- a/gso/workflows/l3_core_service/ias/create_imported_ias.py
+++ b/gso/workflows/l3_core_service/ias/create_imported_ias.py
@@ -27,8 +27,8 @@ def initial_input_form_generator() -> FormGenerator:
     initial_user_input = yield from initial_generator
 
     # Additional IAS step
-    class IASExtraForm(FormPage):  # TODO: Think about the order of this form when user is filling it
-        ias_flavor: IASFlavor | None = IASFlavor.IAS_PS_OPT_OUT
+    class IASExtraForm(FormPage):
+        ias_flavor: IASFlavor = IASFlavor.IAS_PS_OPT_OUT
 
     ias_extra = yield IASExtraForm
     return initial_user_input | ias_extra.model_dump()
-- 
GitLab


From 671771d2d5f6217a57cb5e90ca32d79f06f5aaad Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 3 Apr 2025 10:41:54 +0200
Subject: [PATCH 80/87] make L3CoreServiceBlockInactive required in all L3
 services

---
 gso/products/product_blocks/copernicus.py     | 2 +-
 gso/products/product_blocks/geant_ip.py       | 2 +-
 gso/products/product_blocks/ias.py            | 2 +-
 gso/products/product_blocks/lhcone.py         | 2 +-
 gso/products/product_types/copernicus.py      | 4 ++--
 gso/products/product_types/geant_ip.py        | 4 ++--
 gso/products/product_types/ias.py             | 4 ++--
 gso/products/product_types/l3_core_service.py | 2 +-
 gso/products/product_types/lhcone.py          | 4 ++--
 9 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/gso/products/product_blocks/copernicus.py b/gso/products/product_blocks/copernicus.py
index e98263a48..d62d39000 100644
--- a/gso/products/product_blocks/copernicus.py
+++ b/gso/products/product_blocks/copernicus.py
@@ -15,7 +15,7 @@ class CopernicusBlockInactive(
 ):
     """An inactive Copernicus product block. See `CopernicusBlock`."""
 
-    l3_core: L3CoreServiceBlockInactive | None = None
+    l3_core: L3CoreServiceBlockInactive
 
 
 class CopernicusBlockProvisioning(CopernicusBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
diff --git a/gso/products/product_blocks/geant_ip.py b/gso/products/product_blocks/geant_ip.py
index 2050794d1..742566298 100644
--- a/gso/products/product_blocks/geant_ip.py
+++ b/gso/products/product_blocks/geant_ip.py
@@ -15,7 +15,7 @@ class GeantIPBlockInactive(
 ):
     """An inactive GeantIP product block. See `GeantIPBlock`."""
 
-    l3_core: L3CoreServiceBlockInactive | None = None
+    l3_core: L3CoreServiceBlockInactive
 
 
 class GeantIPBlockProvisioning(GeantIPBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
diff --git a/gso/products/product_blocks/ias.py b/gso/products/product_blocks/ias.py
index fdf26d324..f57d76fcb 100644
--- a/gso/products/product_blocks/ias.py
+++ b/gso/products/product_blocks/ias.py
@@ -22,7 +22,7 @@ class IASFlavor(strEnum):
 class IASBlockInactive(ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="IASBlock"):
     """An inactive IAS product block. See `IASBlock`."""
 
-    l3_core: L3CoreServiceBlockInactive | None = None
+    l3_core: L3CoreServiceBlockInactive
     ias_flavor: IASFlavor = IASFlavor.IAS_PS_OPT_OUT
 
 
diff --git a/gso/products/product_blocks/lhcone.py b/gso/products/product_blocks/lhcone.py
index 48292a5aa..016a2a8f4 100644
--- a/gso/products/product_blocks/lhcone.py
+++ b/gso/products/product_blocks/lhcone.py
@@ -15,7 +15,7 @@ class LHCOneBlockInactive(
 ):
     """An inactive LHCOne product block. See `LHCOneBlock`."""
 
-    l3_core: L3CoreServiceBlockInactive | None = None
+    l3_core: L3CoreServiceBlockInactive
 
 
 class LHCOneBlockProvisioning(LHCOneBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
diff --git a/gso/products/product_types/copernicus.py b/gso/products/product_types/copernicus.py
index cb6c2e687..570938b7a 100644
--- a/gso/products/product_types/copernicus.py
+++ b/gso/products/product_types/copernicus.py
@@ -23,7 +23,7 @@ class CopernicusInactive(BaseL3SubscriptionModel, is_base=True):
     copernicus: CopernicusBlockInactive
 
     @property
-    def l3_core(self) -> L3CoreServiceBlockInactive | None:
+    def l3_core(self) -> L3CoreServiceBlockInactive:
         """Getter: Retrieve the l3_core from the copernicus attribute."""
         return self.copernicus.l3_core
 
@@ -56,7 +56,7 @@ class ImportedCopernicusInactive(BaseL3SubscriptionModel, is_base=True):
     copernicus: CopernicusBlockInactive
 
     @property
-    def l3_core(self) -> L3CoreServiceBlockInactive | None:
+    def l3_core(self) -> L3CoreServiceBlockInactive:
         """Getter: Retrieve the l3_core from the copernicus attribute."""
         return self.copernicus.l3_core
 
diff --git a/gso/products/product_types/geant_ip.py b/gso/products/product_types/geant_ip.py
index d5e07325d..2710f1afe 100644
--- a/gso/products/product_types/geant_ip.py
+++ b/gso/products/product_types/geant_ip.py
@@ -25,7 +25,7 @@ class GeantIPInactive(BaseL3SubscriptionModel, is_base=True):
     geant_ip: GeantIPBlockInactive
 
     @property
-    def l3_core(self) -> L3CoreServiceBlockInactive | None:
+    def l3_core(self) -> L3CoreServiceBlockInactive:
         """Getter: Retrieve the l3_core from the geant_ip attribute."""
         return self.geant_ip.l3_core
 
@@ -58,7 +58,7 @@ class ImportedGeantIPInactive(BaseL3SubscriptionModel, is_base=True):
     geant_ip: GeantIPBlockInactive
 
     @property
-    def l3_core(self) -> L3CoreServiceBlockInactive | None:
+    def l3_core(self) -> L3CoreServiceBlockInactive:
         """Getter: Retrieve the l3_core from the geant_ip attribute."""
         return self.geant_ip.l3_core
 
diff --git a/gso/products/product_types/ias.py b/gso/products/product_types/ias.py
index 6eb6193fe..693ad644e 100644
--- a/gso/products/product_types/ias.py
+++ b/gso/products/product_types/ias.py
@@ -25,7 +25,7 @@ class IASInactive(BaseL3SubscriptionModel, is_base=True):
     ias: IASBlockInactive
 
     @property
-    def l3_core(self) -> L3CoreServiceBlockInactive | None:
+    def l3_core(self) -> L3CoreServiceBlockInactive:
         """Getter: Retrieve the l3_core from the 'ias' attribute."""
         return self.ias.l3_core
 
@@ -58,7 +58,7 @@ class ImportedIASInactive(BaseL3SubscriptionModel, is_base=True):
     ias: IASBlockInactive
 
     @property
-    def l3_core(self) -> L3CoreServiceBlockInactive | None:
+    def l3_core(self) -> L3CoreServiceBlockInactive:
         """Getter: Retrieve the l3_core from the 'ias' attribute."""
         return self.ias.l3_core
 
diff --git a/gso/products/product_types/l3_core_service.py b/gso/products/product_types/l3_core_service.py
index b7b6937b9..5026060ec 100644
--- a/gso/products/product_types/l3_core_service.py
+++ b/gso/products/product_types/l3_core_service.py
@@ -16,7 +16,7 @@ class BaseL3SubscriptionModel(SubscriptionModel, ABC):
 
     @property
     @abstractmethod
-    def l3_core(self) -> L3CoreServiceBlockInactive | None:
+    def l3_core(self) -> L3CoreServiceBlockInactive:
         """Getter: Should be implemented by subclasses."""
 
     @l3_core.setter
diff --git a/gso/products/product_types/lhcone.py b/gso/products/product_types/lhcone.py
index ad48086a2..6e130a2ed 100644
--- a/gso/products/product_types/lhcone.py
+++ b/gso/products/product_types/lhcone.py
@@ -25,7 +25,7 @@ class LHCOneInactive(BaseL3SubscriptionModel, is_base=True):
     lhcone: LHCOneBlockInactive
 
     @property
-    def l3_core(self) -> L3CoreServiceBlockInactive | None:
+    def l3_core(self) -> L3CoreServiceBlockInactive:
         """Getter: Retrieve the l3_core from the lhcone attribute."""
         return self.lhcone.l3_core
 
@@ -58,7 +58,7 @@ class ImportedLHCOneInactive(BaseL3SubscriptionModel, is_base=True):
     lhcone: LHCOneBlockInactive
 
     @property
-    def l3_core(self) -> L3CoreServiceBlockInactive | None:
+    def l3_core(self) -> L3CoreServiceBlockInactive:
         """Getter: Retrieve the l3_core from the lhcone attribute."""
         return self.lhcone.l3_core
 
-- 
GitLab


From f6a196a8438b03d1f46d47b9a1d4856fe30aec27 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 3 Apr 2025 09:09:35 +0000
Subject: [PATCH 81/87] Apply 1 suggestion(s) to 1 file(s)

Co-authored-by: Karel van Klink <karel.vanklink@geant.org>
---
 gso/workflows/l3_core_service/ias/create_ias.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gso/workflows/l3_core_service/ias/create_ias.py b/gso/workflows/l3_core_service/ias/create_ias.py
index 8243ed758..20a4c9005 100644
--- a/gso/workflows/l3_core_service/ias/create_ias.py
+++ b/gso/workflows/l3_core_service/ias/create_ias.py
@@ -35,7 +35,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
 
     # Additional IAS step
     class IASExtraForm(FormPage):
-        ias_flavor: IASFlavor | None = IASFlavor.IAS_PS_OPT_OUT
+        ias_flavor: IASFlavor | str = IASFlavor.IAS_PS_OPT_OUT  # TODO: remove type hint workaround
 
     ias_extra = yield IASExtraForm
     return initial_user_input | ias_extra.model_dump()
-- 
GitLab


From 28e7eceacd1ca5046631f261bb0562be10580d4b Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 3 Apr 2025 11:15:02 +0200
Subject: [PATCH 82/87] fix IASFlavor type hint everywhere

---
 gso/cli/imports.py                        | 2 +-
 test/fixtures/l3_core_service_fixtures.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/gso/cli/imports.py b/gso/cli/imports.py
index cb0158842..6a82ddecc 100644
--- a/gso/cli/imports.py
+++ b/gso/cli/imports.py
@@ -318,7 +318,7 @@ class L3CoreServiceImportModel(BaseModel):
 class IASImportModel(L3CoreServiceImportModel):
     """Import IAS model."""
 
-    ias_flavor: IASFlavor | None = IASFlavor.IAS_PS_OPT_OUT
+    ias_flavor: IASFlavor = IASFlavor.IAS_PS_OPT_OUT
 
 
 class LanSwitchInterconnectRouterSideImportModel(BaseModel):
diff --git a/test/fixtures/l3_core_service_fixtures.py b/test/fixtures/l3_core_service_fixtures.py
index f41248bfa..04e9c175f 100644
--- a/test/fixtures/l3_core_service_fixtures.py
+++ b/test/fixtures/l3_core_service_fixtures.py
@@ -222,7 +222,7 @@ def ias_subscription_factory(faker, partner_factory, access_port_factory, save_l
         ap_list: list[AccessPort] | None = None,
         start_date="2023-05-24T00:00:00+00:00",
         status: SubscriptionLifecycle | None = None,
-        ias_flavor: str | None = IASFlavor.IAS_PS_OPT_OUT,
+        ias_flavor: IASFlavor | None = IASFlavor.IAS_PS_OPT_OUT,
         *,
         is_imported: bool | None = False,
     ) -> SubscriptionModel:
-- 
GitLab


From 1da44ed05f16421fa8aa8a945a30509a99ec2802 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Thu, 3 Apr 2025 11:28:28 +0200
Subject: [PATCH 83/87] make vale happy

---
 gso/products/product_types/ias.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/gso/products/product_types/ias.py b/gso/products/product_types/ias.py
index 693ad644e..b70ba1130 100644
--- a/gso/products/product_types/ias.py
+++ b/gso/products/product_types/ias.py
@@ -26,12 +26,12 @@ class IASInactive(BaseL3SubscriptionModel, is_base=True):
 
     @property
     def l3_core(self) -> L3CoreServiceBlockInactive:
-        """Getter: Retrieve the l3_core from the 'ias' attribute."""
+        """Getter: Retrieve the l3_core from the ``ias`` attribute."""
         return self.ias.l3_core
 
     @l3_core.setter
     def l3_core(self, value: L3CoreServiceBlockInactive) -> None:
-        """Setter: Set the l3_core on the 'ias' attribute."""
+        """Setter: Set the l3_core on the ``ias`` attribute."""
         self.ias.l3_core = value
 
     @property
@@ -59,12 +59,12 @@ class ImportedIASInactive(BaseL3SubscriptionModel, is_base=True):
 
     @property
     def l3_core(self) -> L3CoreServiceBlockInactive:
-        """Getter: Retrieve the l3_core from the 'ias' attribute."""
+        """Getter: Retrieve the l3_core from the ``ias`` attribute."""
         return self.ias.l3_core
 
     @l3_core.setter
     def l3_core(self, value: L3CoreServiceBlockInactive) -> None:
-        """Setter: Set the l3_core on the 'ias' attribute."""
+        """Setter: Set the l3_core on the ``ias`` attribute."""
         self.ias.l3_core = value
 
     @property
-- 
GitLab


From a2e86313c2be44713504d4083492cb53b7aa4110 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Tue, 8 Apr 2025 09:25:19 +0200
Subject: [PATCH 84/87] rebase with develop and add missing logic

---
 .../geant_ip/validate_prefix_list.py               | 10 +++++++++-
 .../l3_core_service/validate_prefix_list.py        |  3 +--
 .../l3_core_service/test_validate_prefix_list.py   | 14 +++++---------
 3 files changed, 15 insertions(+), 12 deletions(-)

diff --git a/gso/workflows/l3_core_service/geant_ip/validate_prefix_list.py b/gso/workflows/l3_core_service/geant_ip/validate_prefix_list.py
index c532d56d1..5f259b576 100644
--- a/gso/workflows/l3_core_service/geant_ip/validate_prefix_list.py
+++ b/gso/workflows/l3_core_service/geant_ip/validate_prefix_list.py
@@ -21,6 +21,7 @@ from gso.workflows.l3_core_service.base_validate_prefix_list import (
 )
 def validate_geant_ip_prefix_list() -> StepList:
     """Validate prefix-lists for an existing GÉANT IP subscription."""
+    fqdn_list_is_empty = conditional(lambda state: state["ap_fqdn_list"] == [])
     prefix_list_has_drifted = conditional(lambda state: bool(state["prefix_list_drift"]))
 
     redeploy_prefix_list_steps = (
@@ -37,4 +38,11 @@ def validate_geant_ip_prefix_list() -> StepList:
         >> prefix_list_has_drifted(redeploy_prefix_list_steps)
     )
 
-    return begin >> store_process_subscription(Target.SYSTEM) >> build_fqdn_list >> prefix_list_validation_steps >> done
+    return (
+        begin
+        >> store_process_subscription(Target.SYSTEM)
+        >> build_fqdn_list
+        >> fqdn_list_is_empty(done)
+        >> prefix_list_validation_steps
+        >> done
+    )
diff --git a/gso/workflows/l3_core_service/validate_prefix_list.py b/gso/workflows/l3_core_service/validate_prefix_list.py
index f9e580b1c..9c9b6cd43 100644
--- a/gso/workflows/l3_core_service/validate_prefix_list.py
+++ b/gso/workflows/l3_core_service/validate_prefix_list.py
@@ -1,4 +1,3 @@
-
 """Prefix Validation workflow for L3 Core Service subscription objects."""
 
 from typing import Any
@@ -139,4 +138,4 @@ def validate_prefix_list() -> StepList:
         >> fqdn_list_is_empty(done)
         >> prefix_list_should_be_validated(prefix_list_validation_steps)
         >> done
-    )
\ No newline at end of file
+    )
diff --git a/test/workflows/l3_core_service/test_validate_prefix_list.py b/test/workflows/l3_core_service/test_validate_prefix_list.py
index e3a6bd8bb..a2baecb68 100644
--- a/test/workflows/l3_core_service/test_validate_prefix_list.py
+++ b/test/workflows/l3_core_service/test_validate_prefix_list.py
@@ -3,7 +3,7 @@ from unittest.mock import patch
 import pytest
 
 from gso.products.product_types.geant_ip import GeantIP
-from gso.utils.shared_enums import Vendor
+from gso.utils.shared_enums import APType, Vendor
 from test import USER_CONFIRM_EMPTY_FORM
 from test.workflows import (
     assert_complete,
@@ -95,7 +95,7 @@ def test_validate_prefix_list_without_diff(mock_lso_interaction, geant_ip_subscr
 
 @pytest.mark.workflow()
 def test_validate_prefix_skip_on_juniper(
-    l3_core_service_subscription_factory,
+    geant_ip_subscription_factory,
     access_port_factory,
     service_binding_port_factory,
     edge_port_subscription_factory,
@@ -129,19 +129,15 @@ def test_validate_prefix_skip_on_juniper(
             ),
         ),
     ]
-    subscription_id = str(
-        l3_core_service_subscription_factory(
-            l3_core_service_type=L3CoreServiceType.GEANT_IP, ap_list=ap_list
-        ).subscription_id
-    )
+    subscription_id = str(geant_ip_subscription_factory(ap_list=ap_list).subscription_id)
     initial_l3_core_service_data = [{"subscription_id": subscription_id}]
     # Run the workflow and extract results
     #  Assert workflow completion since it is skipped if it is on a Juniper
-    result, _, _ = run_workflow("validate_prefix_list", initial_l3_core_service_data)
+    result, _, _ = run_workflow("validate_geant_ip_prefix_list", initial_l3_core_service_data)
     assert_complete(result)
     # Extract the state and validate subscription attributes
     state = extract_state(result)
     subscription_id = state["subscription_id"]
-    subscription = L3CoreService.from_subscription(subscription_id)
+    subscription = GeantIP.from_subscription(subscription_id)
     assert subscription.status == "active"
     assert subscription.insync is True
-- 
GitLab


From 01747cfb45fe5b693e84a656c3236bcb788d6c01 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Tue, 8 Apr 2025 09:33:14 +0200
Subject: [PATCH 85/87] remove validate_prefix_list.py left from rebase

---
 .../l3_core_service/validate_prefix_list.py   | 141 ------------------
 1 file changed, 141 deletions(-)
 delete mode 100644 gso/workflows/l3_core_service/validate_prefix_list.py

diff --git a/gso/workflows/l3_core_service/validate_prefix_list.py b/gso/workflows/l3_core_service/validate_prefix_list.py
deleted file mode 100644
index 9c9b6cd43..000000000
--- a/gso/workflows/l3_core_service/validate_prefix_list.py
+++ /dev/null
@@ -1,141 +0,0 @@
-"""Prefix Validation workflow for L3 Core Service subscription objects."""
-
-from typing import Any
-
-from orchestrator.config.assignee import Assignee
-from orchestrator.forms import SubmitFormPage
-from orchestrator.targets import Target
-from orchestrator.workflow import StepList, begin, conditional, done, inputstep, step, workflow
-from orchestrator.workflows.steps import resync, store_process_subscription, unsync
-from orchestrator.workflows.utils import wrap_modify_initial_input_form
-from pydantic import Field
-from pydantic_forms.types import FormGenerator, State, UUIDstr
-from pydantic_forms.validators import Label
-
-from gso.products.product_types.l3_core_service import L3CoreService, L3CoreServiceType
-from gso.services.lso_client import LSOState, anonymous_lso_interaction, lso_interaction
-from gso.services.partners import get_partner_by_id
-from gso.utils.shared_enums import Vendor
-
-
-@step("Prepare list of all Access Ports")
-def build_fqdn_list(subscription_id: UUIDstr) -> State:
-    """Build the list of all FQDNs in the access ports of L3 Core Service subscription, excluding Juniper devices."""
-    subscription = L3CoreService.from_subscription(subscription_id)
-    ap_fqdn_list = [
-        ap.sbp.edge_port.node.router_fqdn
-        for ap in subscription.l3_core_service.ap_list
-        if ap.sbp.edge_port.node.vendor != Vendor.JUNIPER
-    ]
-    return {"ap_fqdn_list": ap_fqdn_list, "subscription": subscription}
-
-
-@step("[DRY RUN] Validate Prefix-Lists")
-def validate_prefix_lists_dry(subscription: dict[str, Any], process_id: UUIDstr, ap_fqdn_list: list[str]) -> LSOState:
-    """Workflow step for running a playbook that validates prefix-lists in dry run mode."""
-    extra_vars = {
-        "subscription": subscription,
-        "partner_name": get_partner_by_id(subscription["customer_id"]).name,
-        "dry_run": True,
-        "verb": "deploy",
-        "object": "prefix_list",
-        "is_verification_workflow": "true",
-        "commit_comment": f"GSO_PROCESS_ID: {process_id} - Validate prefix-lists for {subscription["description"]}",
-    }
-
-    return {
-        "playbook_name": "gap_ansible/playbooks/validate_prefix_list.yaml",
-        "inventory": {"all": {"hosts": dict.fromkeys(ap_fqdn_list)}},
-        "extra_vars": extra_vars,
-    }
-
-
-@step("Evaluate validation of Prefix-Lists")
-def _evaluate_result_has_diff(callback_result: dict) -> State:
-    return {"callback_result": callback_result, "prefix_list_drift": bool(callback_result["return_code"] != 0)}
-
-
-@inputstep("Await operator confirmation", assignee=Assignee.SYSTEM)
-def await_operator() -> FormGenerator:
-    """Show a form for the operator to start redeploying the prefix list that has drifted."""
-
-    class AwaitOperatorForm(SubmitFormPage):
-        info_label_a: Label = Field("A drift has been detected for this prefix list!", exclude=True)
-        info_label_b: Label = Field("Please continue this workflow to redeploy the drifted prefix list.", exclude=True)
-
-    yield AwaitOperatorForm
-
-    return {}
-
-
-@step("[DRY RUN] Deploy Prefix-Lists")
-def deploy_prefix_lists_dry(subscription: dict[str, Any], process_id: UUIDstr, ap_fqdn_list: list[str]) -> LSOState:
-    """Workflow step for running a playbook that deploys prefix-lists in dry run mode."""
-    extra_vars = {
-        "subscription": subscription,
-        "partner_name": get_partner_by_id(subscription["customer_id"]).name,
-        "dry_run": True,
-        "verb": "deploy",
-        "object": "prefix_list",
-        "is_verification_workflow": "false",
-        "commit_comment": f"GSO_PROCESS_ID: {process_id} - Deploy prefix-lists for {subscription["description"]}",
-    }
-
-    return {
-        "playbook_name": "gap_ansible/playbooks/deploy_prefix_list.yaml",
-        "inventory": {"all": {"hosts": dict.fromkeys(ap_fqdn_list)}},
-        "extra_vars": extra_vars,
-    }
-
-
-@step("[REAL] Deploy Prefix-Lists")
-def deploy_prefix_lists_real(subscription: dict[str, Any], process_id: UUIDstr, ap_fqdn_list: list[str]) -> LSOState:
-    """Workflow step for running a playbook that deploys prefix-lists."""
-    extra_vars = {
-        "subscription": subscription,
-        "partner_name": get_partner_by_id(subscription["customer_id"]).name,
-        "dry_run": False,
-        "verb": "deploy",
-        "object": "prefix_list",
-        "is_verification_workflow": "false",
-        "commit_comment": f"GSO_PROCESS_ID: {process_id} - Deploy prefix-lists for {subscription["description"]}",
-    }
-
-    return {
-        "playbook_name": "gap_ansible/playbooks/deploy_prefix_list.yaml",
-        "inventory": {"all": {"hosts": dict.fromkeys(ap_fqdn_list)}},
-        "extra_vars": extra_vars,
-    }
-
-
-@workflow("Validate Prefix-List", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None)))
-def validate_prefix_list() -> StepList:
-    """Validate prefix-lists for an existing L3 Core Service subscription."""
-    prefix_list_should_be_validated = conditional(
-        lambda state: state["subscription"]["l3_core_service_type"] == L3CoreServiceType.GEANT_IP
-    )
-    fqdn_list_is_empty = conditional(lambda state: state["ap_fqdn_list"] == [])
-    prefix_list_has_drifted = conditional(lambda state: bool(state["prefix_list_drift"]))
-
-    redeploy_prefix_list_steps = (
-        begin
-        >> unsync
-        >> await_operator
-        >> lso_interaction(deploy_prefix_lists_dry)
-        >> lso_interaction(deploy_prefix_lists_real)
-        >> resync
-    )
-    prefix_list_validation_steps = (
-        begin
-        >> anonymous_lso_interaction(validate_prefix_lists_dry, _evaluate_result_has_diff)
-        >> prefix_list_has_drifted(redeploy_prefix_list_steps)
-    )
-
-    return (
-        begin
-        >> store_process_subscription(Target.SYSTEM)
-        >> build_fqdn_list
-        >> fqdn_list_is_empty(done)
-        >> prefix_list_should_be_validated(prefix_list_validation_steps)
-        >> done
-    )
-- 
GitLab


From 308097754223772ad01f23645c716f510e5b4e30 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Tue, 8 Apr 2025 10:12:10 +0200
Subject: [PATCH 86/87] Removed extra comments.

---
 gso/workflows/l3_core_service/base_create_l3_core_service.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gso/workflows/l3_core_service/base_create_l3_core_service.py b/gso/workflows/l3_core_service/base_create_l3_core_service.py
index dd8459285..6d8c451ea 100644
--- a/gso/workflows/l3_core_service/base_create_l3_core_service.py
+++ b/gso/workflows/l3_core_service/base_create_l3_core_service.py
@@ -190,7 +190,7 @@ def initialize_subscription(
         v6_bfd_settings=BFDSettings.new(subscription_id=uuid4(), **(binding_port_input.pop("v6_bfd_settings"))),
         **binding_port_input,
         bgp_session_list=sbp_bgp_session_list,
-        sbp_type=SBPType.L3,  # Adjust based on your requirement
+        sbp_type=SBPType.L3,
         edge_port=edge_port_subscription.edge_port,
         gs_id=sbp_gs_id,
     )
-- 
GitLab


From c0afb52e436fc3ece0e39aa1d22b3ed3de1931a5 Mon Sep 17 00:00:00 2001
From: Mohammad Torkashvand <mohammad.torkashvand@geant.org>
Date: Tue, 8 Apr 2025 11:30:17 +0200
Subject: [PATCH 87/87] resolve migration down_revision conflicts

---
 ..._re_model_ias.py => 2025-04-08_e1afa3790f32_re_model_ias.py} | 2 +-
 ... => 2025-04-09_9fbb3c4411ea_add_new_l3_core_services_wfs.py} | 0
 ...py => 2025-04-10_c38adde1a18e_update_wf_in_process_table.py} | 0
 3 files changed, 1 insertion(+), 1 deletion(-)
 rename gso/migrations/versions/{2025-03-25_e1afa3790f32_re_model_ias.py => 2025-04-08_e1afa3790f32_re_model_ias.py} (99%)
 rename gso/migrations/versions/{2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py => 2025-04-09_9fbb3c4411ea_add_new_l3_core_services_wfs.py} (100%)
 rename gso/migrations/versions/{2025-03-26_c38adde1a18e_update_wf_in_process_table.py => 2025-04-10_c38adde1a18e_update_wf_in_process_table.py} (100%)

diff --git a/gso/migrations/versions/2025-03-25_e1afa3790f32_re_model_ias.py b/gso/migrations/versions/2025-04-08_e1afa3790f32_re_model_ias.py
similarity index 99%
rename from gso/migrations/versions/2025-03-25_e1afa3790f32_re_model_ias.py
rename to gso/migrations/versions/2025-04-08_e1afa3790f32_re_model_ias.py
index b25ef7920..d2e5b6d42 100644
--- a/gso/migrations/versions/2025-03-25_e1afa3790f32_re_model_ias.py
+++ b/gso/migrations/versions/2025-04-08_e1afa3790f32_re_model_ias.py
@@ -17,7 +17,7 @@ from alembic import op
 
 # revision identifiers, used by Alembic
 revision = "e1afa3790f32"
-down_revision = "b14f71db2b58"
+down_revision = "3541c7e57284"
 branch_labels = None
 depends_on = None
 
diff --git a/gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py b/gso/migrations/versions/2025-04-09_9fbb3c4411ea_add_new_l3_core_services_wfs.py
similarity index 100%
rename from gso/migrations/versions/2025-03-26_9fbb3c4411ea_add_new_l3_core_services_wfs.py
rename to gso/migrations/versions/2025-04-09_9fbb3c4411ea_add_new_l3_core_services_wfs.py
diff --git a/gso/migrations/versions/2025-03-26_c38adde1a18e_update_wf_in_process_table.py b/gso/migrations/versions/2025-04-10_c38adde1a18e_update_wf_in_process_table.py
similarity index 100%
rename from gso/migrations/versions/2025-03-26_c38adde1a18e_update_wf_in_process_table.py
rename to gso/migrations/versions/2025-04-10_c38adde1a18e_update_wf_in_process_table.py
-- 
GitLab