diff --git a/gso/migrations/versions/2024-12-09_818d4ffe65df_add_vlan_ids_to_lan_switch_interconnect.py b/gso/migrations/versions/2024-12-09_818d4ffe65df_add_vlan_ids_to_lan_switch_interconnect.py new file mode 100644 index 0000000000000000000000000000000000000000..004f37c3a90a9f72ca119ff906a9b439650ee20e --- /dev/null +++ b/gso/migrations/versions/2024-12-09_818d4ffe65df_add_vlan_ids_to_lan_switch_interconnect.py @@ -0,0 +1,53 @@ +"""Add VLAN IDs to LAN Switch Interconnect. + +Revision ID: 818d4ffe65df +Revises: fc7bd696014e +Create Date: 2024-12-09 11:11:35.239599 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '818d4ffe65df' +down_revision = 'fc7bd696014e' +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 ('switch_management_vlan_id', 'VLAN ID of the switch management network') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('dcn_management_vlan_id', 'VLAN ID of the DCN management network') 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 ('LanSwitchInterconnectBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('switch_management_vlan_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 ('LanSwitchInterconnectBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('dcn_management_vlan_id'))) + """)) + + +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 ('LanSwitchInterconnectBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('switch_management_vlan_id')) + """)) + 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 ('LanSwitchInterconnectBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('switch_management_vlan_id')) + """)) + 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 ('LanSwitchInterconnectBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('dcn_management_vlan_id')) + """)) + 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 ('LanSwitchInterconnectBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('dcn_management_vlan_id')) + """)) + 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 ('switch_management_vlan_id', 'dcn_management_vlan_id')) + """)) + conn.execute(sa.text(""" +DELETE FROM resource_types WHERE resource_types.resource_type IN ('switch_management_vlan_id', 'dcn_management_vlan_id') + """)) diff --git a/gso/products/product_blocks/lan_switch_interconnect.py b/gso/products/product_blocks/lan_switch_interconnect.py index 30c2fb93672c66a110d0dee5cdb1171113b84a0f..b62156afb2fea7bea50651173e8cb85b8380e534 100644 --- a/gso/products/product_blocks/lan_switch_interconnect.py +++ b/gso/products/product_blocks/lan_switch_interconnect.py @@ -6,6 +6,7 @@ from orchestrator.types import SubscriptionLifecycle from gso.products.product_blocks.router import RouterBlock, RouterBlockInactive, RouterBlockProvisioning from gso.products.product_blocks.switch import SwitchBlock, SwitchBlockInactive, SwitchBlockProvisioning from gso.utils.types.interfaces import LAGMemberList +from gso.utils.types.virtual_identifiers import VLAN_ID class LanSwitchInterconnectInterfaceBlockInactive( @@ -110,6 +111,8 @@ class LanSwitchInterconnectBlockInactive( lan_switch_interconnect_description: str | None = None minimum_links: int | None = None + switch_management_vlan_id: VLAN_ID | None = None + dcn_management_vlan_id: VLAN_ID | None = None router_side: LanSwitchInterconnectRouterSideBlockInactive switch_side: LanSwitchInterconnectSwitchSideBlockInactive @@ -121,6 +124,8 @@ class LanSwitchInterconnectBlockProvisioning( lan_switch_interconnect_description: str | None = None minimum_links: int | None = None + switch_management_vlan_id: VLAN_ID + dcn_management_vlan_id: VLAN_ID | None router_side: LanSwitchInterconnectRouterSideBlockProvisioning switch_side: LanSwitchInterconnectSwitchSideBlockProvisioning @@ -132,6 +137,10 @@ class LanSwitchInterconnectBlock(LanSwitchInterconnectBlockProvisioning, lifecyc lan_switch_interconnect_description: str #: The minimum amount of links the LAN Switch Interconnect should consist of. minimum_links: int + #: VLAN ID for the switch management network. + switch_management_vlan_id: VLAN_ID + #: VLAN ID for the DCN management network, if the site of this product contains optical equipment. + dcn_management_vlan_id: VLAN_ID | None #: The router side of the LAN Switch Interconnect. router_side: LanSwitchInterconnectRouterSideBlock #: The switch side of the LAN Switch Interconnect. diff --git a/gso/utils/types/virtual_identifiers.py b/gso/utils/types/virtual_identifiers.py index 093678e38ff37bee29a3199514b63214d57f6b3b..78e5546e3a983268f3d9a5cd379b88c52fd07c77 100644 --- a/gso/utils/types/virtual_identifiers.py +++ b/gso/utils/types/virtual_identifiers.py @@ -13,3 +13,6 @@ VC_ID = Annotated[ "A Virtual Circuit ID, the upper limit comes from the highest number that a service ID could be in Nokia srOS." ), ] + +DEFAULT_SWITCH_MANAGEMENT_VLAN_ID: VLAN_ID = 998 +DEFAULT_DCN_MANAGEMENT_VLAN_ID: VLAN_ID = 103 diff --git a/gso/workflows/lan_switch_interconnect/create_imported_lan_switch_interconnect.py b/gso/workflows/lan_switch_interconnect/create_imported_lan_switch_interconnect.py index 6d6632b2b10c0b1e33d49d1fd7d7e387670ed13a..f89c37ffff15c282534384d72766fd497a9e852f 100644 --- a/gso/workflows/lan_switch_interconnect/create_imported_lan_switch_interconnect.py +++ b/gso/workflows/lan_switch_interconnect/create_imported_lan_switch_interconnect.py @@ -21,12 +21,19 @@ from gso.products.product_types.router import Router from gso.products.product_types.switch import Switch from gso.services.partners import get_partner_by_name from gso.services.subscriptions import get_product_id_by_name +from gso.utils.types.virtual_identifiers import ( + DEFAULT_DCN_MANAGEMENT_VLAN_ID, + DEFAULT_SWITCH_MANAGEMENT_VLAN_ID, + VLAN_ID, +) def _initial_input_form_generator() -> FormGenerator: class ImportLanSwitchInterconnect(FormPage): lan_switch_interconnect_description: str minimum_links: int + switch_management_vlan_id: VLAN_ID | None = DEFAULT_SWITCH_MANAGEMENT_VLAN_ID + dcn_management_vlan_id: VLAN_ID | None = DEFAULT_DCN_MANAGEMENT_VLAN_ID router_side: LanSwitchInterconnectRouterSideImportModel switch_side: LanSwitchInterconnectSwitchSideImportModel @@ -49,12 +56,16 @@ def initialize_subscription( subscription: ImportedLanSwitchInterconnectInactive, lan_switch_interconnect_description: str, minimum_links: int, + switch_management_vlan_id: VLAN_ID, + dcn_management_vlan_id: VLAN_ID, router_side: dict, switch_side: dict, ) -> State: """Initialize the subscription using input data.""" subscription.lan_switch_interconnect.lan_switch_interconnect_description = lan_switch_interconnect_description subscription.lan_switch_interconnect.minimum_links = minimum_links + subscription.lan_switch_interconnect.switch_management_vlan_id = switch_management_vlan_id + subscription.lan_switch_interconnect.dcn_management_vlan_id = dcn_management_vlan_id router_block = Router.from_subscription(router_side.pop("node")).router router_side_interfaces = [ diff --git a/gso/workflows/lan_switch_interconnect/create_lan_switch_interconnect.py b/gso/workflows/lan_switch_interconnect/create_lan_switch_interconnect.py index 15e14ef674e4b9624d5184542a3b538e4a3776eb..1e8c3bc58cedd1eeb4d16bf5f0cfbc7be414c192 100644 --- a/gso/workflows/lan_switch_interconnect/create_lan_switch_interconnect.py +++ b/gso/workflows/lan_switch_interconnect/create_lan_switch_interconnect.py @@ -13,7 +13,7 @@ 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 AfterValidator, ConfigDict -from pydantic_forms.validators import Divider, ReadOnlyField +from pydantic_forms.validators import ReadOnlyField from gso.products.product_blocks.lan_switch_interconnect import ( LanSwitchInterconnectInterfaceBlockInactive, @@ -42,6 +42,7 @@ from gso.utils.types.interfaces import ( validate_interface_names_are_unique, ) from gso.utils.types.tt_number import TTNumber +from gso.utils.types.virtual_identifiers import DEFAULT_DCN_MANAGEMENT_VLAN_ID, DEFAULT_SWITCH_MANAGEMENT_VLAN_ID from gso.workflows.shared import create_summary_form @@ -55,8 +56,6 @@ def _initial_input_form(product_name: str) -> FormGenerator: switch_side: active_switch_selector() # type: ignore[valid-type] description: str minimum_link_count: int - divider: Divider - vlan_id: ReadOnlyField(111, default_type=int) # type: ignore[valid-type] initial_input = yield CreateLANSwitchInterconnectForm router = Router.from_subscription(initial_input.router_side) @@ -113,7 +112,6 @@ def _initial_input_form(product_name: str) -> FormGenerator: "switch_side", "description", "minimum_link_count", - "vlan_id", "router_side_iface", "router_side_ae_members", "switch_side_iface", @@ -147,6 +145,7 @@ def initialize_subscription( """Update the product model with all input from the operator.""" subscription.lan_switch_interconnect.lan_switch_interconnect_description = description subscription.lan_switch_interconnect.minimum_links = minimum_link_count + subscription.lan_switch_interconnect.switch_management_vlan_id = DEFAULT_SWITCH_MANAGEMENT_VLAN_ID subscription.lan_switch_interconnect.router_side.node = Router.from_subscription(router_side).router subscription.lan_switch_interconnect.router_side.ae_iface = router_side_iface for member in router_side_ae_members: @@ -159,6 +158,8 @@ def initialize_subscription( subscription.lan_switch_interconnect.switch_side.ae_members.append( LanSwitchInterconnectInterfaceBlockInactive.new(subscription_id=uuid4(), **member) ) + if subscription.lan_switch_interconnect.router_side.node.router_site.site_contains_optical_equipment: + subscription.lan_switch_interconnect.dcn_management_vlan_id = DEFAULT_DCN_MANAGEMENT_VLAN_ID return {"subscription": subscription} diff --git a/test/fixtures/lan_switch_interconnect_fixtures.py b/test/fixtures/lan_switch_interconnect_fixtures.py index 0ea52e20252045f436172633036c6eb1435c68e1..b98d800d1fa68f5b2e22b8d75c651ceb7dc9adfb 100644 --- a/test/fixtures/lan_switch_interconnect_fixtures.py +++ b/test/fixtures/lan_switch_interconnect_fixtures.py @@ -18,6 +18,11 @@ from gso.products.product_types.lan_switch_interconnect import ( from gso.products.product_types.router import Router from gso.products.product_types.switch import Switch from gso.services.subscriptions import get_product_id_by_name +from gso.utils.types.virtual_identifiers import ( + DEFAULT_DCN_MANAGEMENT_VLAN_ID, + DEFAULT_SWITCH_MANAGEMENT_VLAN_ID, + VLAN_ID, +) @pytest.fixture() @@ -31,6 +36,8 @@ def lan_switch_interconnect_subscription_factory( start_date: str | None = "2024-01-01T10:20:30+01:02", lan_switch_interconnect_description: str | None = None, minimum_links: int | None = None, + switch_management_vlan_id: VLAN_ID | None = None, + dcn_management_vlan_id: VLAN_ID | None = None, router_side_node: UUIDstr | None = None, router_side_ae_iface: str | None = None, router_side_ae_members: list[dict[str, str]] | None = None, @@ -78,6 +85,12 @@ def lan_switch_interconnect_subscription_factory( ae_iface=switch_side_ae_iface or faker.network_interface(), ae_members=switch_side_ae_members, ) + subscription.lan_switch_interconnect.dcn_management_vlan_id = ( + dcn_management_vlan_id or DEFAULT_DCN_MANAGEMENT_VLAN_ID + ) + subscription.lan_switch_interconnect.switch_management_vlan_id = ( + switch_management_vlan_id or DEFAULT_SWITCH_MANAGEMENT_VLAN_ID + ) subscription = SubscriptionModel.from_other_lifecycle(subscription, SubscriptionLifecycle.ACTIVE) subscription.insync = True diff --git a/test/workflows/lan_switch_interconnect/test_create_imported_lan_switch_interconnect.py b/test/workflows/lan_switch_interconnect/test_create_imported_lan_switch_interconnect.py index 74e6e879e3d2ea4b9de54fa4f4f764237586e56b..8c3f9c4c6d363d8c96377438ebd9474c6aafd122 100644 --- a/test/workflows/lan_switch_interconnect/test_create_imported_lan_switch_interconnect.py +++ b/test/workflows/lan_switch_interconnect/test_create_imported_lan_switch_interconnect.py @@ -3,6 +3,7 @@ from orchestrator.types import SubscriptionLifecycle from gso.products import ProductName from gso.products.product_types.lan_switch_interconnect import ImportedLanSwitchInterconnect +from gso.utils.types.virtual_identifiers import DEFAULT_DCN_MANAGEMENT_VLAN_ID, DEFAULT_SWITCH_MANAGEMENT_VLAN_ID from test.workflows import ( assert_complete, extract_state, @@ -37,3 +38,24 @@ def test_create_imported_lan_switch_interconnect_success(workflow_input_data): assert_complete(result) assert subscription.product.name == ProductName.IMPORTED_LAN_SWITCH_INTERCONNECT assert subscription.status == SubscriptionLifecycle.ACTIVE + assert subscription.lan_switch_interconnect.dcn_management_vlan_id == DEFAULT_DCN_MANAGEMENT_VLAN_ID + assert subscription.lan_switch_interconnect.switch_management_vlan_id == DEFAULT_SWITCH_MANAGEMENT_VLAN_ID + + +@pytest.mark.workflow() +def test_create_imported_lan_switch_interconnect_custom_vlan_ids(faker, workflow_input_data): + custom_switch_vlan_id = faker.vlan_id() + custom_dcn_vlan_id = faker.vlan_id() + workflow_input_data.update({ + "switch_management_vlan_id": custom_switch_vlan_id, + "dcn_management_vlan_id": custom_dcn_vlan_id, + }) + result, _, _ = run_workflow("create_imported_lan_switch_interconnect", [workflow_input_data]) + state = extract_state(result) + subscription = ImportedLanSwitchInterconnect.from_subscription(state["subscription_id"]) + + assert_complete(result) + assert subscription.product.name == ProductName.IMPORTED_LAN_SWITCH_INTERCONNECT + assert subscription.status == SubscriptionLifecycle.ACTIVE + assert subscription.lan_switch_interconnect.dcn_management_vlan_id == custom_dcn_vlan_id + assert subscription.lan_switch_interconnect.switch_management_vlan_id == custom_switch_vlan_id diff --git a/test/workflows/lan_switch_interconnect/test_create_lan_switch_interconnect.py b/test/workflows/lan_switch_interconnect/test_create_lan_switch_interconnect.py index a3511f21326cef1d336f82c73493cc88ecaa887c..a0c338041649ae37ad12835855e65640921ed61f 100644 --- a/test/workflows/lan_switch_interconnect/test_create_lan_switch_interconnect.py +++ b/test/workflows/lan_switch_interconnect/test_create_lan_switch_interconnect.py @@ -6,6 +6,7 @@ from orchestrator.types import SubscriptionLifecycle from gso.products import ProductName from gso.products.product_types.lan_switch_interconnect import LanSwitchInterconnect from gso.services.subscriptions import get_product_id_by_name +from gso.utils.types.virtual_identifiers import DEFAULT_DCN_MANAGEMENT_VLAN_ID from test.services.conftest import MockedNetboxClient from test.workflows import assert_complete, extract_state, run_workflow @@ -26,43 +27,51 @@ def _netbox_client_mock(): @pytest.fixture() -def input_form_data(faker, router_subscription_factory, switch_subscription_factory): - return [ - { - "product": get_product_id_by_name(ProductName.LAN_SWITCH_INTERCONNECT), - }, - { - "tt_number": faker.tt_number(), - "router_side": router_subscription_factory(), - "switch_side": switch_subscription_factory(), - "description": faker.sentence(), - "minimum_link_count": 2, - "vlan_id": 111, # VLAN ID for new interconnections is always 111 - }, - { - "router_side_iface": "lag-1", - "router_side_ae_members": faker.link_members_nokia()[:2], - }, - { - "switch_side_iface": faker.network_interface(), - "switch_side_ae_members": faker.link_members_juniper()[:2], - }, - {}, - ] +def input_form_data(faker, router_subscription_factory, switch_subscription_factory, site_subscription_factory): + def _input_form_data(site_contains_optical_equipment): + return [ + { + "product": get_product_id_by_name(ProductName.LAN_SWITCH_INTERCONNECT), + }, + { + "tt_number": faker.tt_number(), + "router_side": router_subscription_factory( + router_site=site_subscription_factory( + site_contains_optical_equipment=site_contains_optical_equipment + ) + ), + "switch_side": switch_subscription_factory(), + "description": faker.sentence(), + "minimum_link_count": 2, + }, + { + "router_side_iface": "lag-1", + "router_side_ae_members": faker.link_members_nokia()[:2], + }, + { + "switch_side_iface": faker.network_interface(), + "switch_side_ae_members": faker.link_members_juniper()[:2], + }, + {}, + ] + + return _input_form_data @pytest.mark.workflow() -@patch("gso.services.infoblox.create_v6_network_by_ip") -@patch("gso.services.infoblox.create_v4_network_by_ip") -@patch("gso.services.infoblox.create_host_by_ip") +@pytest.mark.parametrize("site_contains_optical_equipment", [True, False]) +@patch("gso.workflows.lan_switch_interconnect.create_lan_switch_interconnect.create_v6_network_by_ip") +@patch("gso.workflows.lan_switch_interconnect.create_lan_switch_interconnect.create_v4_network_by_ip") +@patch("gso.workflows.lan_switch_interconnect.create_lan_switch_interconnect.create_host_by_ip") def test_create_lan_switch_interconnect_success( mock_create_host, mock_create_v4_network, mock_create_v6_network, + site_contains_optical_equipment, input_form_data, _netbox_client_mock, # noqa: PT019 ): - result, _, _ = run_workflow("create_lan_switch_interconnect", input_form_data) + result, _, _ = run_workflow("create_lan_switch_interconnect", input_form_data(site_contains_optical_equipment)) assert_complete(result) state = extract_state(result) @@ -72,3 +81,6 @@ def test_create_lan_switch_interconnect_success( assert mock_create_v4_network.call_count == 1 assert mock_create_v6_network.call_count == 1 assert mock_create_host.call_count == 4 + assert subscription.lan_switch_interconnect.dcn_management_vlan_id == ( + DEFAULT_DCN_MANAGEMENT_VLAN_ID if site_contains_optical_equipment else None + )