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 0000000000000000000000000000000000000000..98a1bb47618d51e73b557b0156d71dcd1173df5e --- /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 1aaf0862ec8b9a8b636aa0f3a3da196d850498c7..f88a2e2ccfb07260b84f59b4d641cbafdde884a9 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 0979dcb3a700da9c26fc6f35ebd40356b471ee4c..3596eb1cfcd6855d38c077f10f2babb1bd10ade5 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 820377b2435067534e0b24a676e7aff1e3786395..6186a1f83d6a495aa591e1e96a6a2030794de7fd 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 7c95268e37d85f7181d5951635bcf9ab668d07a9..85b114a46df5befd085a87d79aad0639077fd4a7 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 ef620e732a377ceb0e8fe1a6e4969dd2dac50e5c..3c5b7a0323cd91862fbafe4cdc17045cc7352607 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 d568147be1ce54f1206632be5b519a7a87a49f7b..fadf6aeda90cd57df3b015980557fd620e19e17d 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 1083a001572bb3b12617f20cd2fae1b2d7fcd45f..e8d0a283340af798bc2ac81a0f12eb8a5060bebf 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 5e35a7c83cf75f2d7aed0153ca8c6d84774e85bb..49b501e9de3c9b143bb9628ef7e71583df2f01dc 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 a93698ed4327cd0f810ee03fbdabc0473233dc8b..1b5f68d860d88252c9d6daf51bedce8143747912 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 ffd975f2e05725615d53b9d84c0535f22c3e634e..87487bc750828b2f0aabe66efc0b9b7d1f4f10c7 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": [ {