diff --git a/gso/services/subscriptions.py b/gso/services/subscriptions.py index e6781244d04cde0c6515cd0d3237e24ee2a0cd8f..83d88751984a66a5eed4522b55077734ceda303f 100644 --- a/gso/services/subscriptions.py +++ b/gso/services/subscriptions.py @@ -355,7 +355,7 @@ def generate_unique_ga_id() -> str: """Generate a unique GA ID using the ga_id_seq database sequence. Returns: - str: A unique GA ID in the format `GA<number>`. + str: A unique GA ID in the format `GA-<number>`. Raises: ValueError: If there is an error generating the ID. diff --git a/gso/workflows/edge_port/modify_edge_port.py b/gso/workflows/edge_port/modify_edge_port.py index 943889843f273f9899bd1f98c6b2edb69a5b2137..b176eafcc9450dbbd7d8da0608bd0d13891c3f91 100644 --- a/gso/workflows/edge_port/modify_edge_port.py +++ b/gso/workflows/edge_port/modify_edge_port.py @@ -1,5 +1,6 @@ """Modify an existing edge port subscription.""" +from functools import partial from typing import Annotated, Any, Self from uuid import uuid4 @@ -10,7 +11,7 @@ from orchestrator.targets import Target from orchestrator.workflow import StepList, begin, conditional, done, step from orchestrator.workflows.steps import resync, store_process_subscription, unsync from orchestrator.workflows.utils import wrap_modify_initial_input_form -from pydantic import AfterValidator, ConfigDict, model_validator +from pydantic import AfterValidator, ConfigDict, Field, model_validator from pydantic_forms.types import FormGenerator, State, UUIDstr from pydantic_forms.validators import ReadOnlyField, validate_unique_list @@ -26,6 +27,7 @@ from gso.utils.helpers import ( ) from gso.utils.types.interfaces import LAGMember, PhysicalPortCapacity from gso.utils.types.tt_number import TTNumber +from gso.utils.types.unique_field import validate_field_is_unique def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: @@ -43,7 +45,12 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: minimum_links: int | None = subscription.edge_port.minimum_links or None mac_address: str | None = subscription.edge_port.mac_address or None ignore_if_down: bool = subscription.edge_port.ignore_if_down - ga_id: str | None = subscription.edge_port.ga_id or None + ga_id: ( + Annotated[ + str, AfterValidator(partial(validate_field_is_unique, subscription_id)), Field(pattern=r"^GA-\d{5}$") + ] + | None + ) = subscription.edge_port.ga_id or None @model_validator(mode="after") def validate_number_of_members(self) -> Self: diff --git a/gso/workflows/iptrunk/create_imported_iptrunk.py b/gso/workflows/iptrunk/create_imported_iptrunk.py index 1feec5053801b38b7e0ec75324f6e9194e37f723..722121fe6b04fb120a336b65332c21e69f6d5394 100644 --- a/gso/workflows/iptrunk/create_imported_iptrunk.py +++ b/gso/workflows/iptrunk/create_imported_iptrunk.py @@ -114,7 +114,7 @@ def initialize_subscription( subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_site.site_name, subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_site.site_name, ]) - subscription.description = f"IP trunk {side_names[0]} {side_names[1]}, gs_id:{gs_id}" + subscription.description = f"IP trunk {side_names[0]} {side_names[1]}, {gs_id}" return {"subscription": subscription} diff --git a/gso/workflows/iptrunk/create_iptrunk.py b/gso/workflows/iptrunk/create_iptrunk.py index 8f24e333ffe00af3054459acf16ad8a5099a0f83..962cc5d01ec50c76ef03f82ee0d7b5fdc6c7353c 100644 --- a/gso/workflows/iptrunk/create_iptrunk.py +++ b/gso/workflows/iptrunk/create_iptrunk.py @@ -361,7 +361,7 @@ def initialize_subscription( IptrunkInterfaceBlockInactive.new(subscription_id=uuid4(), **member), ) side_names = sorted([side_a.router_site.site_name, side_b.router_site.site_name]) - subscription.description = f"IP trunk {side_names[0]} {side_names[1]}, gs_id:{gs_id}" + subscription.description = f"IP trunk {side_names[0]} {side_names[1]}, {gs_id}" return {"subscription": subscription} diff --git a/gso/workflows/iptrunk/migrate_iptrunk.py b/gso/workflows/iptrunk/migrate_iptrunk.py index 03ee446525a74c2dca87c5bc05439cc18c14ab5a..7e7f32b45877333447d69453cad37c55da86b12e 100644 --- a/gso/workflows/iptrunk/migrate_iptrunk.py +++ b/gso/workflows/iptrunk/migrate_iptrunk.py @@ -769,7 +769,7 @@ def update_subscription_model( subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_site.site_name, subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_site.site_name, ]) - subscription.description = f"IP trunk {side_names[0]} {side_names[1]}, gs_id:{subscription.iptrunk.gs_id}" + subscription.description = f"IP trunk {side_names[0]} {side_names[1]}, {subscription.iptrunk.gs_id}" return {"subscription": subscription} diff --git a/gso/workflows/iptrunk/modify_trunk_interface.py b/gso/workflows/iptrunk/modify_trunk_interface.py index 65fa452b5b613380f2ab2ea7f525c264681b4687..ea2309e4dd7249583f1ddd4472258ecb862697bd 100644 --- a/gso/workflows/iptrunk/modify_trunk_interface.py +++ b/gso/workflows/iptrunk/modify_trunk_interface.py @@ -7,6 +7,7 @@ necessary modifications will be applied. """ import json +from functools import partial from typing import Annotated from uuid import UUID, uuid4 @@ -18,7 +19,7 @@ from orchestrator.utils.json import json_dumps 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 import ConfigDict +from pydantic import AfterValidator, ConfigDict, Field from pydantic_forms.validators import Label, ReadOnlyField from gso.products.product_blocks.iptrunk import ( @@ -39,6 +40,7 @@ from gso.utils.shared_enums import Vendor from gso.utils.types.interfaces import JuniperLAGMember, LAGMember, LAGMemberList, PhysicalPortCapacity from gso.utils.types.ip_address import IPv4AddressType, IPv6AddressType from gso.utils.types.tt_number import TTNumber +from gso.utils.types.unique_field import validate_field_is_unique from gso.workflows.iptrunk.migrate_iptrunk import check_ip_trunk_optical_levels_pre from gso.workflows.iptrunk.validate_iptrunk import check_ip_trunk_isis @@ -86,7 +88,12 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: class ModifyIptrunkForm(FormPage): tt_number: TTNumber - gs_id: str | None = subscription.iptrunk.gs_id + gs_id: ( + Annotated[ + str, AfterValidator(partial(validate_field_is_unique, subscription_id)), Field(pattern=r"^GS-\d{5}$") + ] + | None + ) = subscription.iptrunk.gs_id iptrunk_description: str | None = subscription.iptrunk.iptrunk_description iptrunk_type: IptrunkType = subscription.iptrunk.iptrunk_type warning_label: Label = ( @@ -302,7 +309,7 @@ def modify_iptrunk_subscription( subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_site.site_name, subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_site.site_name, ]) - subscription.description = f"IP trunk {side_names[0]} {side_names[1]}, gs_id:{gs_id}" + subscription.description = f"IP trunk {side_names[0]} {side_names[1]}, {gs_id}" return { "subscription": subscription, diff --git a/gso/workflows/l2_circuit/create_imported_layer_2_circuit.py b/gso/workflows/l2_circuit/create_imported_layer_2_circuit.py index 6102d86d46fe8c604ad726c20efeb26cc3bf8c31..3e5e97c992ce33c7fb0fe5300c0a1f5e0034d072 100644 --- a/gso/workflows/l2_circuit/create_imported_layer_2_circuit.py +++ b/gso/workflows/l2_circuit/create_imported_layer_2_circuit.py @@ -23,6 +23,7 @@ from gso.products.product_types.layer_2_circuit import ( 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.interfaces import BandwidthString from gso.utils.types.virtual_identifiers import VC_ID, VLAN_ID @@ -39,7 +40,7 @@ def initial_input_form_generator() -> FormGenerator: service_type: Layer2CircuitServiceType partner: str - gs_id: str + gs_id: IMPORTED_GS_ID vc_id: VC_ID layer_2_circuit_side_a: ServiceBindingPortInput layer_2_circuit_side_b: ServiceBindingPortInput 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 7862a47f56bb4f7b4686e84707142e6b0571a3d3..7280a225e4977aec5cee0245b9aac94b507bf1c2 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 @@ -21,6 +21,7 @@ from gso.products.product_types.l3_core_service import ImportedL3CoreServiceInac 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 @@ -50,7 +51,7 @@ def initial_input_form_generator() -> FormGenerator: class ServiceBindingPort(BaseModel): edge_port: UUIDstr ap_type: str - gs_id: str + gs_id: IMPORTED_GS_ID sbp_type: SBPType = SBPType.L3 is_tagged: bool = False vlan_id: VLAN_ID diff --git a/test/workflows/edge_port/test_modify_edge_port.py b/test/workflows/edge_port/test_modify_edge_port.py index 25adcd85704403110691db0ffa150da0bf168d10..0f7ed1401db0e4e4fecac754ab938b5f1b246748 100644 --- a/test/workflows/edge_port/test_modify_edge_port.py +++ b/test/workflows/edge_port/test_modify_edge_port.py @@ -1,6 +1,7 @@ from unittest.mock import patch import pytest +from pydantic_forms.exceptions import FormValidationError from gso.products.product_types.edge_port import EdgePort from gso.utils.types.interfaces import PhysicalPortCapacity @@ -37,6 +38,22 @@ def input_form_wizard_data(request, faker, edge_port_subscription_factory, partn ] +@pytest.mark.workflow() +@pytest.mark.parametrize("invalid_ga_id", ["GS-11111", "GA-1234", "GA_12345", "GA-100000"]) +def test_modify_edge_port_with_invalid_ga_id( + input_form_wizard_data, faker, invalid_ga_id, iptrunk_side_subscription_factory, iptrunk_subscription_factory +): + input_data = input_form_wizard_data + input_data[1]["ga_id"] = invalid_ga_id + iptrunk_subscription_factory( + iptrunk_sides=[iptrunk_side_subscription_factory(ga_id="GA-11111"), iptrunk_side_subscription_factory()] + ) + + # Run workflow + with pytest.raises(FormValidationError): + run_workflow("modify_edge_port", input_data) + + @pytest.mark.workflow() @patch("gso.services.lso_client._send_request") @patch("gso.services.netbox_client.NetboxClient.get_available_interfaces") diff --git a/test/workflows/iptrunk/test_create_iptrunk.py b/test/workflows/iptrunk/test_create_iptrunk.py index a45f3bf1a97e5bff1a62c07102b16abc23403bb4..d0f9cfa539b3b52600dff506931c3cd0699250ce 100644 --- a/test/workflows/iptrunk/test_create_iptrunk.py +++ b/test/workflows/iptrunk/test_create_iptrunk.py @@ -151,9 +151,7 @@ def test_successful_iptrunk_creation_with_standard_lso_result( ]) assert subscription.status == "provisioning" assert subscription.iptrunk.gs_id is not None - assert subscription.description == ( - f"IP trunk {sorted_sides[0]} {sorted_sides[1]}, gs_id:{subscription.iptrunk.gs_id}" - ) + assert subscription.description == f"IP trunk {sorted_sides[0]} {sorted_sides[1]}, {subscription.iptrunk.gs_id}" assert mock_execute_playbook.call_count == 6 # We search for 6 hosts in total, 2 in a /31 and 4 in a /126 diff --git a/test/workflows/iptrunk/test_modify_trunk_interface.py b/test/workflows/iptrunk/test_modify_trunk_interface.py index 12f49b90e4cfb524eaeb564718197aedcff80768..d8a96588a3bc6182fa498b2b50a8e849c646b483 100644 --- a/test/workflows/iptrunk/test_modify_trunk_interface.py +++ b/test/workflows/iptrunk/test_modify_trunk_interface.py @@ -1,6 +1,7 @@ from unittest.mock import patch import pytest +from pydantic_forms.exceptions import FormValidationError from gso.products.product_blocks.iptrunk import IptrunkType from gso.products.product_types.iptrunk import Iptrunk @@ -164,7 +165,7 @@ def test_iptrunk_modify_trunk_interface_success( subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_site.site_name, subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_site.site_name, ]) - assert subscription.description == f"IP trunk {side_names[0]} {side_names[1]}, gs_id:{new_sid}" + assert subscription.description == f"IP trunk {side_names[0]} {side_names[1]}, {new_sid}" assert subscription.iptrunk.gs_id == input_form_iptrunk_data[1]["gs_id"] assert subscription.iptrunk.iptrunk_description == input_form_iptrunk_data[1]["iptrunk_description"] assert subscription.iptrunk.iptrunk_type == input_form_iptrunk_data[1]["iptrunk_type"] @@ -192,3 +193,19 @@ def test_iptrunk_modify_trunk_interface_success( member.interface_description == _find_interface_by_name(new_side_b_ae_members, member.interface_name).interface_description ) + + +@pytest.mark.workflow() +@pytest.mark.parametrize("invalid_ga_id", ["GA-11111", "GS-1234", "GS_12345", "GS-100000"]) +def test_modify_iptrunk_interface_with_invalid_ga_id( + input_form_iptrunk_data, faker, invalid_ga_id, iptrunk_side_subscription_factory, iptrunk_subscription_factory +): + input_data = input_form_iptrunk_data + input_data[3]["side_a_ga_id"] = invalid_ga_id + iptrunk_subscription_factory( + iptrunk_sides=[iptrunk_side_subscription_factory(ga_id="GA-11111"), iptrunk_side_subscription_factory()] + ) + + # Run workflow + with pytest.raises(FormValidationError): + run_workflow("modify_edge_port", input_data)