From cb1438e6386e35e4806b5ed75f6a8da6e8e76a2c Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Thu, 10 Oct 2024 13:45:22 +0200
Subject: [PATCH] Add IPV4 and IPV6 netmask to Service Binding Port.

---
 ...5d917_add_ipv4_ipv6_netmask_to_service_.py | 99 +++++++++++++++++++
 .../product_blocks/service_binding_port.py    | 10 +-
 gso/settings.py                               | 14 +--
 gso/utils/types/ip_address.py                 |  2 +
 gso/workflows/geant_ip/create_geant_ip.py     |  4 +-
 .../geant_ip/create_imported_geant_ip.py      |  4 +-
 gso/workflows/geant_ip/modify_geant_ip.py     |  4 +-
 test/conftest.py                              |  6 ++
 test/fixtures/geant_ip_fixtures.py            |  4 +
 .../geant_ip/test_create_geant_ip.py          |  2 +
 .../geant_ip/test_create_imported_geant_ip.py |  2 +
 11 files changed, 137 insertions(+), 14 deletions(-)
 create mode 100644 gso/migrations/versions/2024-10-10_df108295d917_add_ipv4_ipv6_netmask_to_service_.py

diff --git a/gso/migrations/versions/2024-10-10_df108295d917_add_ipv4_ipv6_netmask_to_service_.py b/gso/migrations/versions/2024-10-10_df108295d917_add_ipv4_ipv6_netmask_to_service_.py
new file mode 100644
index 00000000..98a1bb47
--- /dev/null
+++ b/gso/migrations/versions/2024-10-10_df108295d917_add_ipv4_ipv6_netmask_to_service_.py
@@ -0,0 +1,99 @@
+"""Add IPV4/IPV6 netmask to Service Binding Port model .
+
+Revision ID: df108295d917
+Revises: bf05800fe9fc
+Create Date: 2024-10-10 11:39:43.051211
+
+"""
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = 'df108295d917'
+down_revision = 'bf05800fe9fc'
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    conn = op.get_bind()
+    conn.execute(sa.text("""
+INSERT INTO resource_types (resource_type, description) VALUES ('ipv4_mask', 'IPV4 subnet mask') RETURNING resource_types.resource_type_id
+    """))
+    conn.execute(sa.text("""
+INSERT INTO resource_types (resource_type, description) VALUES ('ipv6_mask', 'IPV6 subnet mask') RETURNING resource_types.resource_type_id
+    """))
+    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 ('ServiceBindingPort')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv4_mask')))
+    """))
+    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 ('ServiceBindingPort')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv6_mask')))
+    """))
+    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 = 'ServiceBindingPort'
+                    )
+                )
+
+                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'
+                FROM resource_types
+                CROSS JOIN subscription_instance_ids
+                WHERE resource_types.resource_type = 'ipv4_mask'
+        
+    """))
+    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 = 'ServiceBindingPort'
+                    )
+                )
+
+                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'
+                FROM resource_types
+                CROSS JOIN subscription_instance_ids
+                WHERE resource_types.resource_type = 'ipv6_mask'
+        
+    """))
+
+
+def downgrade() -> None:
+    conn = op.get_bind()
+    conn.execute(sa.text("""
+DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv4_mask'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv4_mask'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv6_mask'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv6_mask'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM subscription_instance_values WHERE subscription_instance_values.resource_type_id IN (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv4_mask', 'ipv6_mask'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM resource_types WHERE resource_types.resource_type IN ('ipv4_mask', 'ipv6_mask')
+    """))
diff --git a/gso/products/product_blocks/service_binding_port.py b/gso/products/product_blocks/service_binding_port.py
index 1aaf0862..f88a2e2c 100644
--- a/gso/products/product_blocks/service_binding_port.py
+++ b/gso/products/product_blocks/service_binding_port.py
@@ -12,7 +12,7 @@ from pydantic import Field
 from gso.products.product_blocks.bgp_session import BGPSession, BGPSessionInactive, BGPSessionProvisioning
 from gso.products.product_blocks.edge_port import EdgePortBlock, EdgePortBlockInactive, EdgePortBlockProvisioning
 from gso.utils.shared_enums import SBPType
-from gso.utils.types.ip_address import IPv4AddressType, IPv6AddressType
+from gso.utils.types.ip_address import IPv4AddressType, IPV4Netmask, IPv6AddressType, IPV6Netmask
 
 VLAN_ID = Annotated[int, Field(gt=0, lt=4096)]
 
@@ -26,7 +26,9 @@ class ServiceBindingPortInactive(
     vlan_id: VLAN_ID | None = None
     sbp_type: SBPType | None = None
     ipv4_address: IPv4AddressType | None = None
+    ipv4_mask: IPV4Netmask | None = None
     ipv6_address: IPv6AddressType | None = None
+    ipv6_mask: IPV6Netmask | None = None
     custom_firewall_filters: bool | None = None
     geant_sid: str | None = None
     sbp_bgp_session_list: list[BGPSessionInactive] = Field(default_factory=list)
@@ -40,7 +42,9 @@ class ServiceBindingPortProvisioning(ServiceBindingPortInactive, lifecycle=[Subs
     vlan_id: VLAN_ID | None = None
     sbp_type: SBPType
     ipv4_address: IPv4AddressType | None = None
+    ipv4_mask: IPV4Netmask | None = None
     ipv6_address: IPv6AddressType | None = None
+    ipv6_mask: IPV6Netmask | None = None
     custom_firewall_filters: bool
     geant_sid: str
     sbp_bgp_session_list: list[BGPSessionProvisioning]  # type: ignore[assignment]
@@ -58,8 +62,12 @@ class ServiceBindingPort(ServiceBindingPortProvisioning, lifecycle=[Subscription
     sbp_type: SBPType
     #: If layer 3, IPv4 resources.
     ipv4_address: IPv4AddressType | None = None
+    #: IPV4 subnet mask.
+    ipv4_mask: IPV4Netmask | None = None
     #: If layer 3, IPv6 resources.
     ipv6_address: IPv6AddressType | None = None
+    #: IPV6 subnet mask.
+    ipv6_mask: IPV6Netmask | None = None
     #: Any custom firewall filters that the partner may require.
     custom_firewall_filters: bool
     #: The GÉANT service ID of this binding port.
diff --git a/gso/settings.py b/gso/settings.py
index 0979dcb3..3596eb1c 100644
--- a/gso/settings.py
+++ b/gso/settings.py
@@ -9,15 +9,13 @@ import json
 import logging
 import os
 from pathlib import Path
-from typing import Annotated
 
 from orchestrator.types import UUIDstr
-from pydantic import EmailStr, Field
+from pydantic import EmailStr
 from pydantic_forms.types import strEnum
 from pydantic_settings import BaseSettings
-from typing_extensions import Doc
 
-from gso.utils.types.ip_address import PortNumber
+from gso.utils.types.ip_address import IPV4Netmask, IPV6Netmask, PortNumber
 
 logger = logging.getLogger(__name__)
 
@@ -62,16 +60,12 @@ class InfoBloxParams(BaseSettings):
     password: str
 
 
-V4Netmask = Annotated[int, Field(ge=0, le=32), Doc("A valid netmask for an IPv4 network or address.")]
-V6Netmask = Annotated[int, Field(ge=0, le=128), Doc("A valid netmask for an IPv6 network or address.")]
-
-
 class V4NetworkParams(BaseSettings):
     """A set of parameters that describe an IPv4 network in InfoBlox."""
 
     containers: list[ipaddress.IPv4Network]
     networks: list[ipaddress.IPv4Network]
-    mask: V4Netmask
+    mask: IPV4Netmask
 
 
 class V6NetworkParams(BaseSettings):
@@ -79,7 +73,7 @@ class V6NetworkParams(BaseSettings):
 
     containers: list[ipaddress.IPv6Network]
     networks: list[ipaddress.IPv6Network]
-    mask: V6Netmask
+    mask: IPV6Netmask
 
 
 class ServiceNetworkParams(BaseSettings):
diff --git a/gso/utils/types/ip_address.py b/gso/utils/types/ip_address.py
index 820377b2..6186a1f8 100644
--- a/gso/utils/types/ip_address.py
+++ b/gso/utils/types/ip_address.py
@@ -39,6 +39,8 @@ IPv6AddressType = Annotated[ipaddress.IPv6Address, PlainSerializer(_str, return_
 IPv6NetworkType = Annotated[ipaddress.IPv6Network, PlainSerializer(_str, return_type=str, when_used="always")]
 IPAddress = Annotated[str, AfterValidator(validate_ipv4_or_ipv6)]
 IPNetwork = Annotated[str, AfterValidator(validate_ipv4_or_ipv6_network)]
+IPV4Netmask = Annotated[int, Field(ge=0, le=32), Doc("A valid netmask for an IPv4 network or address.")]
+IPV6Netmask = Annotated[int, Field(ge=0, le=128), Doc("A valid netmask for an IPv6 network or address.")]
 PortNumber = Annotated[
     int,
     Field(
diff --git a/gso/workflows/geant_ip/create_geant_ip.py b/gso/workflows/geant_ip/create_geant_ip.py
index 7c95268e..85b114a4 100644
--- a/gso/workflows/geant_ip/create_geant_ip.py
+++ b/gso/workflows/geant_ip/create_geant_ip.py
@@ -24,7 +24,7 @@ from gso.utils.helpers import (
     partner_choice,
 )
 from gso.utils.shared_enums import APType, SBPType
-from gso.utils.types.ip_address import IPv4AddressType, IPv6AddressType
+from gso.utils.types.ip_address import IPv4AddressType, IPV4Netmask, IPv6AddressType, IPV6Netmask
 from gso.utils.types.tt_number import TTNumber
 
 
@@ -106,7 +106,9 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
             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: Divider = Field(None, exclude=True)
             v4_bgp_peer: IPv4BGPPeer
diff --git a/gso/workflows/geant_ip/create_imported_geant_ip.py b/gso/workflows/geant_ip/create_imported_geant_ip.py
index ef620e73..3c5b7a03 100644
--- a/gso/workflows/geant_ip/create_imported_geant_ip.py
+++ b/gso/workflows/geant_ip/create_imported_geant_ip.py
@@ -20,7 +20,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.utils.shared_enums import SBPType
-from gso.utils.types.ip_address import IPAddress, IPv4AddressType, IPv6AddressType
+from gso.utils.types.ip_address import IPAddress, IPv4AddressType, IPV4Netmask, IPv6AddressType, IPV6Netmask
 
 
 def initial_input_form_generator() -> FormGenerator:
@@ -49,7 +49,9 @@ def initial_input_form_generator() -> FormGenerator:
         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]
diff --git a/gso/workflows/geant_ip/modify_geant_ip.py b/gso/workflows/geant_ip/modify_geant_ip.py
index d568147b..fadf6aed 100644
--- a/gso/workflows/geant_ip/modify_geant_ip.py
+++ b/gso/workflows/geant_ip/modify_geant_ip.py
@@ -21,7 +21,7 @@ from gso.products.product_types.edge_port import EdgePort
 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, SBPType
-from gso.utils.types.ip_address import IPv4AddressType, IPv6AddressType
+from gso.utils.types.ip_address import IPv4AddressType, IPV4Netmask, IPv6AddressType, IPV6Netmask
 
 
 def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
@@ -137,7 +137,9 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
             # it's a layer 3 service. The ignore statements are there to put our type checker at ease.
             vlan_id: VLAN_ID = current_sbp.vlan_id  # type: ignore[assignment]
             ipv4_address: IPv4AddressType = current_sbp.ipv4_address  # type: ignore[assignment]
+            ipv4_mask: IPV4Netmask = current_sbp.ipv4_mask  # type: ignore[assignment]
             ipv6_address: IPv6AddressType = current_sbp.ipv6_address  # type: ignore[assignment]
+            ipv6_mask: IPV6Netmask = current_sbp.ipv6_mask  # type: ignore[assignment]
             custom_firewall_filters: bool = current_sbp.custom_firewall_filters
             divider: Divider = Field(None, exclude=True)
             v4_bgp_peer: IPv4BGPPeer = IPv4BGPPeer(
diff --git a/test/conftest.py b/test/conftest.py
index 1083a001..e8d0a283 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -101,6 +101,12 @@ class FakerProvider(BaseProvider):
 
         return site_name
 
+    def ipv4_netmask(self) -> int:
+        return self.generator.random_int(min=1, max=32)
+
+    def ipv6_netmask(self) -> int:
+        return self.generator.random_int(min=1, max=128)
+
     def network_interface(self) -> str:
         return self.generator.numerify("ge-@#/@#/@#")
 
diff --git a/test/fixtures/geant_ip_fixtures.py b/test/fixtures/geant_ip_fixtures.py
index 5e35a7c8..49b501e9 100644
--- a/test/fixtures/geant_ip_fixtures.py
+++ b/test/fixtures/geant_ip_fixtures.py
@@ -59,7 +59,9 @@ def service_binding_port_factory(faker, bgp_session_subscription_factory, edge_p
         geant_sid: str | None = None,
         sbp_type: SBPType = SBPType.L3,
         ipv4_address: str | None = None,
+        ipv4_mask: int | None = None,
         ipv6_address: str | None = None,
+        ipv6_mask: int | None = None,
         vlan_id: int | None = None,
         edge_port: EdgePort | None = None,
         *,
@@ -72,7 +74,9 @@ def service_binding_port_factory(faker, bgp_session_subscription_factory, edge_p
             vlan_id=vlan_id or faker.pyint(min_value=1, max_value=4096),
             sbp_type=sbp_type,
             ipv4_address=ipv4_address or faker.ipv4(),
+            ipv4_mask=ipv4_mask or faker.ipv4_netmask(),
             ipv6_address=ipv6_address or faker.ipv6(),
+            ipv6_mask=ipv6_mask or faker.ipv6_netmask(),
             custom_firewall_filters=custom_firewall_filters,
             geant_sid=geant_sid or faker.geant_sid(),
             sbp_bgp_session_list=sbp_bgp_session_list
diff --git a/test/workflows/geant_ip/test_create_geant_ip.py b/test/workflows/geant_ip/test_create_geant_ip.py
index a93698ed..1b5f68d8 100644
--- a/test/workflows/geant_ip/test_create_geant_ip.py
+++ b/test/workflows/geant_ip/test_create_geant_ip.py
@@ -52,7 +52,9 @@ def test_create_geant_ip_success(
             "is_tagged": faker.boolean(),
             "vlan_id": faker.vlan_id(),
             "ipv4_address": faker.ipv4(),
+            "ipv4_mask": faker.ipv4_netmask(),
             "ipv6_address": faker.ipv6(),
+            "ipv6_mask": faker.ipv6_netmask(),
             "custom_firewall_filters": faker.boolean(),
             "v4_bgp_peer": base_bgp_peer_input() | {"add_v4_multicast": faker.boolean(), "peer_address": faker.ipv4()},
             "v6_bgp_peer": base_bgp_peer_input() | {"add_v6_multicast": faker.boolean(), "peer_address": faker.ipv6()},
diff --git a/test/workflows/geant_ip/test_create_imported_geant_ip.py b/test/workflows/geant_ip/test_create_imported_geant_ip.py
index ffd975f2..87487bc7 100644
--- a/test/workflows/geant_ip/test_create_imported_geant_ip.py
+++ b/test/workflows/geant_ip/test_create_imported_geant_ip.py
@@ -20,7 +20,9 @@ def imported_geant_ip_creation_input_form_data(edge_port_subscription_factory, p
                 "is_tagged": faker.boolean(),
                 "vlan_id": faker.vlan_id(),
                 "ipv4_address": faker.ipv4(),
+                "ipv4_mask": faker.ipv4_netmask(),
                 "ipv6_address": faker.ipv6(),
+                "ipv6_mask": faker.ipv6_netmask(),
                 "custom_firewall_filters": faker.boolean(),
                 "bgp_peers": [
                     {
-- 
GitLab