From c3e80d385511c15090938f990a09a6a38019cbbd Mon Sep 17 00:00:00 2001
From: Karel van Klink <karel.vanklink@geant.org>
Date: Fri, 27 Sep 2024 15:26:16 +0200
Subject: [PATCH] =?UTF-8?q?Add=20modification=20workflow=20for=20G=C3=89AN?=
 =?UTF-8?q?T=20IP?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 ..._g\303\251ant_ip_modification_workflow.py" | 39 ++++++++++
 gso/translations/en-GB.json                   |  1 +
 gso/workflows/__init__.py                     |  1 +
 gso/workflows/geant_ip/create_geant_ip.py     | 25 +++----
 gso/workflows/geant_ip/modify_geant_ip.py     | 73 +++++++++++++++++++
 5 files changed, 123 insertions(+), 16 deletions(-)
 create mode 100644 "gso/migrations/versions/2024-09-26_289e5334848f_add_g\303\251ant_ip_modification_workflow.py"
 create mode 100644 gso/workflows/geant_ip/modify_geant_ip.py

diff --git "a/gso/migrations/versions/2024-09-26_289e5334848f_add_g\303\251ant_ip_modification_workflow.py" "b/gso/migrations/versions/2024-09-26_289e5334848f_add_g\303\251ant_ip_modification_workflow.py"
new file mode 100644
index 00000000..0e64bb46
--- /dev/null
+++ "b/gso/migrations/versions/2024-09-26_289e5334848f_add_g\303\251ant_ip_modification_workflow.py"
@@ -0,0 +1,39 @@
+"""Add GÉANT IP modification workflow.
+
+Revision ID: 289e5334848f
+Revises: f4239c9361b4
+Create Date: 2024-09-26 15:27:51.471877
+
+"""
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = '289e5334848f'
+down_revision = 'f4239c9361b4'
+branch_labels = None
+depends_on = None
+
+
+from orchestrator.migrations.helpers import create_workflow, delete_workflow
+
+new_workflows = [
+    {
+        "name": "modify_geant_ip",
+        "target": "MODIFY",
+        "description": "Modify G\u00c9ANT IP",
+        "product_type": "GeantIP"
+    }
+]
+
+
+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/translations/en-GB.json b/gso/translations/en-GB.json
index 900b1af0..22e58fe6 100644
--- a/gso/translations/en-GB.json
+++ b/gso/translations/en-GB.json
@@ -54,6 +54,7 @@
         "modify_connection_strategy": "Modify connection strategy",
         "modify_router_kentik_license": "Modify device license in Kentik",
         "modify_edge_port": "Modify Edge Port",
+        "modify_geant_ip": "Modify GÉANT IP",
         "terminate_iptrunk": "Terminate IP Trunk",
         "terminate_router": "Terminate Router",
         "terminate_site": "Terminate Site",
diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py
index 29d180c1..4bf546c4 100644
--- a/gso/workflows/__init__.py
+++ b/gso/workflows/__init__.py
@@ -89,3 +89,4 @@ LazyWorkflowInstance("gso.workflows.edge_port.import_edge_port", "import_edge_po
 
 #  GÉANT IP workflows
 LazyWorkflowInstance("gso.workflows.geant_ip.create_geant_ip", "create_geant_ip")
+LazyWorkflowInstance("gso.workflows.geant_ip.modify_geant_ip", "modify_geant_ip")
diff --git a/gso/workflows/geant_ip/create_geant_ip.py b/gso/workflows/geant_ip/create_geant_ip.py
index a55ebd2e..a194abcb 100644
--- a/gso/workflows/geant_ip/create_geant_ip.py
+++ b/gso/workflows/geant_ip/create_geant_ip.py
@@ -33,7 +33,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
     """Gather input from the operator to build a new subscription object."""
 
     class CreateGeantIPForm(FormPage):
-        model_config = ConfigDict(title=f"{product_name} - Select partner")
+        model_config = ConfigDict(title="GÉANT IP - Select partner")
 
         tt_number: TTNumber
         partner: partner_choice()  # type: ignore[valid-type]
@@ -53,7 +53,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
         return edge_ports
 
     class EdgePortSelectionForm(FormPage):
-        model_config = ConfigDict(title=f"{product_name} - Select Edge Ports")
+        model_config = ConfigDict(title="GÉANT IP - Select Edge Ports")
         info_label: Label = Field(
             "Please select the Edge Ports where this GÉANT IP service will terminate", exclude=True
         )
@@ -62,19 +62,15 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
 
     selected_edge_ports = yield EdgePortSelectionForm
     ep_list = selected_edge_ports.edge_ports
-    total_ep_count = len(ep_list)
-    current_ep_index = 0
 
     class BaseBGPPeer(BaseModel):
         bfd_enabled: bool = False
         bfd_interval: int | None = None
         bfd_multiplier: int | None = None
-        rtbh_enabled: bool = False
         has_custom_policies: bool = False
         authentication_key: str
         multipath_enabled: bool = False
         send_default_route: bool = False
-        is_multi_hop: bool = False
         is_passive: bool = False
 
     class IPv4BGPPeer(BaseBGPPeer):
@@ -95,18 +91,16 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
         def families(self) -> list[IPFamily]:
             return [IPFamily.V6UNICAST, IPFamily.V6MULTICAST] if self.add_v6_multicast else [IPFamily.V6UNICAST]
 
+    bgp_peer_defaults = {"rtbh_enabled": True, "is_multi_hop": True}
     binding_port_inputs = []
-    while current_ep_index < total_ep_count:
-        current_edge_port = EdgePort.from_subscription(ep_list[current_ep_index].edge_port)
+    for ep_index, edge_port in enumerate(ep_list):
 
         class BindingPortsInputForm(FormPage):
-            model_config = ConfigDict(
-                title=f"{product_name} - Configure Service Binding Ports ({current_ep_index + 1}/{total_ep_count})"
-            )
+            model_config = ConfigDict(title=f"GÉANT IP - Configure Edge Ports ({ep_index + 1}/{len(ep_list)})")
             info_label: Label = Field("Please configure the Service Binding Ports for each Edge Port.", exclude=True)
             current_ep_label: Label = Field(
-                f"Currently configuring on {current_edge_port.description} "
-                f"(Access Port type: {ep_list[current_ep_index].ap_type})",
+                f"Currently configuring on {EdgePort.from_subscription(edge_port.edge_port).description} "
+                f"(Access Port type: {edge_port.ap_type})",
                 exclude=True,
             )
 
@@ -125,11 +119,10 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
 
         binding_port_input["sbp_type"] = SBPType.L3
         binding_port_input["bgp_peers"] = [
-            binding_port_input_form.v4_bgp_peer.model_dump(),
-            binding_port_input_form.v6_bgp_peer.model_dump(),
+            binding_port_input_form.v4_bgp_peer.model_dump() | bgp_peer_defaults,
+            binding_port_input_form.v6_bgp_peer.model_dump() | bgp_peer_defaults,
         ]
         binding_port_inputs.append(binding_port_input)
-        current_ep_index += 1
 
     return (
         initial_user_input.model_dump()
diff --git a/gso/workflows/geant_ip/modify_geant_ip.py b/gso/workflows/geant_ip/modify_geant_ip.py
new file mode 100644
index 00000000..d70db663
--- /dev/null
+++ b/gso/workflows/geant_ip/modify_geant_ip.py
@@ -0,0 +1,73 @@
+"""A modification workflow for a GÉANT IP subscription."""
+
+from orchestrator import begin, done, step, workflow
+from orchestrator.forms import FormPage
+from orchestrator.targets import Target
+from orchestrator.types import FormGenerator, UUIDstr
+from orchestrator.workflows.steps import State, resync, store_process_subscription, unsync
+from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from pydantic import BaseModel, ConfigDict
+
+from gso.products.product_types.geant_ip import GeantIP
+from gso.utils.helpers import active_edge_port_selector
+from gso.utils.shared_enums import APType
+
+
+def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+    subscription = GeantIP.from_subscription(subscription_id)
+
+    class AccessPortSelection(BaseModel):
+        geant_ip_ep: active_edge_port_selector(partner_id=subscription.customer_id)  # type: ignore[valid-type]
+        nren_ap_type: APType
+
+    def validate_edge_ports_are_unique(edge_ports: list[AccessPortSelection]) -> list[AccessPortSelection]:
+        """Verify if interfaces are unique."""
+        port_names = [port.geant_ip_ep for port in edge_ports]
+        if len(port_names) != len(set(port_names)):
+            msg = "Edge Ports must be unique."
+            raise ValueError(msg)
+        return edge_ports
+
+    class ModifyGeantIPAccessPortsForm(FormPage):
+        model_config = ConfigDict(title="Modify GÉANT IP")
+        access_ports: list[AccessPortSelection] = [
+            AccessPortSelection(
+                geant_ip_ep=str(access_port.geant_ip_ep.owner_subscription_id), nren_ap_type=access_port.nren_ap_type
+            )
+            for access_port in subscription.geant_ip.geant_ip_ap_list
+        ]
+
+    access_port_input = yield ModifyGeantIPAccessPortsForm
+    ap_list = access_port_input.access_ports
+    total_ap_count = len(ap_list)
+
+    access_port_inputs = []
+    for access_port_index, access_port in enumerate(ap_list):
+        access_port_input.append(access_port)
+
+    return access_port_input.model_dump()
+
+
+@step("Update subscription model")
+def modify_geant_ip_subscription(subscription: GeantIP) -> State:
+    """Update the subscription model."""
+    return {"subscription": subscription}
+
+
+@workflow(
+    "Modify GÉANT IP",
+    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
+    target=Target.MODIFY,
+)
+def modify_geant_ip():
+    """Modify a GÉANT IP subscription."""
+    return (
+        begin >> store_process_subscription(Target.MODIFY) >> unsync >> modify_geant_ip_subscription >> resync >> done
+    )
+
+
+# we can change the list of edge ports, and reflect this in new SBPs
+# we can change BFD
+# we can change firewall filters and policies
+# for BGP peers:
+# is_passive chan change
-- 
GitLab