diff --git a/gso/cli/imports.py b/gso/cli/imports.py index f1553d222d34c974584c45ef15cff56dfa55c371..cd2e5998dd39ea965f1c476357c1738afeaae372 100644 --- a/gso/cli/imports.py +++ b/gso/cli/imports.py @@ -12,7 +12,7 @@ import typer import yaml from orchestrator.db import db from orchestrator.services.processes import start_process -from orchestrator.types import SubscriptionLifecycle +from orchestrator.types import SubscriptionLifecycle, UUIDstr from pydantic import BaseModel, ValidationError, field_validator, model_validator from sqlalchemy.exc import SQLAlchemyError @@ -23,6 +23,7 @@ from gso.products.product_blocks.edge_port import EdgePortType, EncapsulationTyp from gso.products.product_blocks.iptrunk import IptrunkType from gso.products.product_blocks.router import RouterRole from gso.products.product_blocks.service_binding_port import VLAN_ID +from gso.products.product_blocks.switch import SwitchModel from gso.products.product_types.nren_l3_core_service import NRENL3CoreServiceType from gso.services.partners import ( PartnerEmail, @@ -39,7 +40,16 @@ from gso.services.subscriptions import ( from gso.utils.shared_enums import SBPType, Vendor from gso.utils.types.base_site import BaseSiteValidatorModel from gso.utils.types.interfaces import LAGMember, LAGMemberList, PhysicalPortCapacity -from gso.utils.types.ip_address import IPAddress, IPv4AddressType, IPV4Netmask, IPv6AddressType, IPV6Netmask, PortNumber +from gso.utils.types.ip_address import ( + AddressSpace, + IPAddress, + IPv4AddressType, + IPV4Netmask, + IPv4NetworkType, + IPv6AddressType, + IPV6Netmask, + PortNumber, +) app: typer.Typer = typer.Typer() @@ -69,6 +79,16 @@ class RouterImportModel(BaseModel): router_lo_iso_address: str +class SwitchImportModel(BaseModel): + """Required fields for importing an existing :class:`gso.product.product_types.switch`.""" + + fqdn: str + ts_port: PortNumber + site: UUIDstr + switch_vendor: Vendor + switch_model: SwitchModel + + class SuperPopSwitchImportModel(BaseModel): """Required fields for importing an existing :class:`gso.product.product_types.super_pop_switch`.""" @@ -278,16 +298,47 @@ class NRENL3CoreServiceImportModel(BaseModel): return value +class LanSwitchInterconnectRouterSideImportModel(BaseModel): + """Import LAN Switch Interconnect Router side model.""" + + node: UUIDstr + ae_iface: str + ae_members: LAGMemberList[LAGMember] + ipv4_address: IPv4AddressType + + +class LanSwitchInterconnectSwitchSideImportModel(BaseModel): + """Import LAN Switch Interconnect Switch side model.""" + + switch: UUIDstr + ae_iface: str + ae_members: LAGMemberList[LAGMember] + ipv4_address: IPv4AddressType + + +class LanSwitchInterconnectImportModel(BaseModel): + """Import LAN Switch Interconnect model.""" + + lan_switch_interconnect_description: str + lan_switch_interconnect_ip_network: IPv4NetworkType | None + address_space: AddressSpace + minimum_links: int + router_side: LanSwitchInterconnectRouterSideImportModel + switch_side: LanSwitchInterconnectSwitchSideImportModel + + T = TypeVar( "T", SiteImportModel, RouterImportModel, + SwitchImportModel, IptrunkImportModel, SuperPopSwitchImportModel, OfficeRouterImportModel, OpenGearImportModel, EdgePortImportModel, NRENL3CoreServiceImportModel, + LanSwitchInterconnectImportModel, ) common_filepath_option = typer.Option( @@ -380,6 +431,12 @@ def import_routers(filepath: str = common_filepath_option) -> None: _generic_import_product(Path(filepath), ProductType.IMPORTED_ROUTER, "router", "hostname", RouterImportModel) +@app.command() +def import_switches(filepath: str = common_filepath_option) -> None: + """Import switches into GSO.""" + _generic_import_product(Path(filepath), ProductType.IMPORTED_SWITCH, "switch", "fqdn", SwitchImportModel) + + @app.command() def import_super_pop_switches(filepath: str = common_filepath_option) -> None: """Import Super PoP Switches into GSO.""" @@ -581,3 +638,15 @@ def import_nren_l3_core_service(filepath: str = common_filepath_option) -> None: typer.echo("Successfully created imported NREN L3 Core Services:") for item in successfully_imported_data: typer.echo(f"- {item}") + + +@app.command() +def import_lan_switch_interconnect(filepath: str = common_filepath_option) -> None: + """Import :term:`LAN` Switch Interconnect services into :term:`GSO`.""" + _generic_import_product( + Path(filepath), + ProductType.IMPORTED_LAN_SWITCH_INTERCONNECT, + "lan_switch_interconnect", + "lan_switch_interconnect_description", + LanSwitchInterconnectImportModel, + ) diff --git a/gso/migrations/versions/2024-08-29_0e7e7d749617_add_switch_workflows.py b/gso/migrations/versions/2024-08-29_0e7e7d749617_add_switch_workflows.py index 0d6b41cff953fed51a2dd505b4b1416d71673d2d..8807ad742a497397f293389a53a587fd0b79eb3a 100644 --- a/gso/migrations/versions/2024-08-29_0e7e7d749617_add_switch_workflows.py +++ b/gso/migrations/versions/2024-08-29_0e7e7d749617_add_switch_workflows.py @@ -1,7 +1,7 @@ """Add switch workflows. Revision ID: 0e7e7d749617 -Revises: 7412c5b7ebe4 +Revises: 94d9de9246fe Create Date: 2024-08-29 15:45:57.581710 """ @@ -10,7 +10,7 @@ from alembic import op # revision identifiers, used by Alembic. revision = '0e7e7d749617' -down_revision = '7412c5b7ebe4' +down_revision = '94d9de9246fe' branch_labels = None depends_on = None @@ -41,6 +41,54 @@ new_workflows = [ "target": "SYSTEM", "description": "Validate switch subscription", "product_type": "Switch" + }, + { + "name": "create_imported_switch", + "target": "CREATE", + "description": "Create Imported Switch", + "product_type": "ImportedSwitch" + }, + { + "name": "import_switch", + "target": "MODIFY", + "description": "Import Switch", + "product_type": "ImportedSwitch" + }, + { + "name": "create_lan_switch_interconnect", + "target": "CREATE", + "description": "Create LAN Switch Interconnect", + "product_type": "LanSwitchInterconnect" + }, + { + "name": "terminate_lan_switch_interconnect", + "target": "TERMINATE", + "description": "Terminate LAN Switch Interconnect", + "product_type": "LanSwitchInterconnect" + }, + { + "name": "validate_lan_switch_interconnect", + "target": "SYSTEM", + "description": "Validate LAN Switch Interconnect", + "product_type": "LanSwitchInterconnect" + }, + { + "name": "validate_lan_switch_interconnect", + "target": "SYSTEM", + "description": "Validate LAN Switch Interconnect", + "product_type": "LanSwitchInterconnect" + }, + { + "name": "create_imported_lan_switch_interconnect", + "target": "CREATE", + "description": "Create Imported LAN Switch Interconnect", + "product_type": "ImportedLanSwitchInterconnect" + }, + { + "name": "import_lan_switch_interconnect", + "target": "MODIFY", + "description": "Import LAN Switch Interconnect", + "product_type": "ImportedLanSwitchInterconnect" } ] diff --git a/gso/migrations/versions/2024-10-28_2bbe91a01715_update_switch_pop_vlan_and_switch_.py b/gso/migrations/versions/2024-10-28_2bbe91a01715_update_switch_pop_vlan_and_switch_.py deleted file mode 100644 index a42c3ba99ddb18348316b61f98da89ed2ab7a6f3..0000000000000000000000000000000000000000 --- a/gso/migrations/versions/2024-10-28_2bbe91a01715_update_switch_pop_vlan_and_switch_.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Update Switch, PoP VLAN, and Switch interconnect product models. - -Revision ID: 2bbe91a01715 -Revises: 0e7e7d749617 -Create Date: 2024-10-28 15:18:54.138252 - -""" -import sqlalchemy as sa -from alembic import op - -# revision identifiers, used by Alembic. -revision = '2bbe91a01715' -down_revision = '0e7e7d749617' -branch_labels = None -depends_on = None - - -def upgrade() -> None: - conn = op.get_bind() - conn.execute(sa.text(""" -UPDATE resource_types SET resource_type='ts_port' WHERE resource_types.resource_type = 'switch_ts_port' - """)) - conn.execute(sa.text(""" -UPDATE resource_types SET resource_type='fqdn' WHERE resource_types.resource_type = 'switch_hostname' - """)) - conn.execute(sa.text(""" -INSERT INTO resource_types (resource_type, description) VALUES ('lan_switch_interconnect_ip_network', 'IP resources for a LAN Switch Interconnect') 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 ('lan_switch_interconnect_ip_network'))) - """)) - - -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 ('lan_switch_interconnect_ip_network')) - """)) - 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 ('lan_switch_interconnect_ip_network')) - """)) - 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 ('lan_switch_interconnect_ip_network')) - """)) - conn.execute(sa.text(""" -DELETE FROM resource_types WHERE resource_types.resource_type IN ('lan_switch_interconnect_ip_network') - """)) - conn.execute(sa.text(""" -UPDATE resource_types SET resource_type='switch_ts_port' WHERE resource_types.resource_type = 'ts_port' - """)) - conn.execute(sa.text(""" -UPDATE resource_types SET resource_type='switch_hostname' WHERE resource_types.resource_type = 'fqdn' - """)) diff --git a/gso/migrations/versions/2024-10-29_94d9de9246fe_add_imported_switches_update_domain_.py b/gso/migrations/versions/2024-10-29_94d9de9246fe_add_imported_switches_update_domain_.py new file mode 100644 index 0000000000000000000000000000000000000000..9a835eab838625f4bb2d5f383f3678c225cb678d --- /dev/null +++ b/gso/migrations/versions/2024-10-29_94d9de9246fe_add_imported_switches_update_domain_.py @@ -0,0 +1,110 @@ +"""Add Imported Switches, update domain models. + +Revision ID: 94d9de9246fe +Revises: 7412c5b7ebe4 +Create Date: 2024-10-29 15:45:28.294699 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '94d9de9246fe' +down_revision = '7412c5b7ebe4' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + conn = op.get_bind() + conn.execute(sa.text(""" +UPDATE resource_types SET resource_type='ts_port' WHERE resource_types.resource_type = 'switch_ts_port' + """)) + conn.execute(sa.text(""" +UPDATE resource_types SET resource_type='fqdn' WHERE resource_types.resource_type = 'switch_hostname' + """)) + conn.execute(sa.text(""" +INSERT INTO products (name, description, product_type, tag, status) VALUES ('Imported Switch', 'An existing Switch that is imported into the subscription database', 'ImportedSwitch', 'IMP_SWITCH', 'active') RETURNING products.product_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('lan_switch_interconnect_ip_network', 'IP resources for a LAN Switch interconnect') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO product_product_blocks (product_id, product_block_id) VALUES ((SELECT products.product_id FROM products WHERE products.name IN ('Imported Switch')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('SwitchBlock'))) + """)) + 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 ('lan_switch_interconnect_ip_network'))) + """)) + conn.execute(sa.text(""" +INSERT INTO products (name, description, product_type, tag, status) VALUES ('Imported LAN Switch Interconnect', 'An existing LAN Switch Interconnect that is imported into the subscription database', 'ImportedLanSwitchInterconnect', 'IMP_LSI', 'active') RETURNING products.product_id + """)) + conn.execute(sa.text(""" +INSERT INTO product_product_blocks (product_id, product_block_id) VALUES ((SELECT products.product_id FROM products WHERE products.name IN ('Imported LAN Switch Interconnect')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('LanSwitchInterconnectBlock'))) + """)) + 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 ('LanSwitchInterconnectSwitchSideBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv4_address'))), ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('LanSwitchInterconnectRouterSideBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv4_address'))) + """)) + + +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 ('LanSwitchInterconnectSwitchSideBlock', 'LanSwitchInterconnectRouterSideBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv4_address')) + """)) + 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 ('LanSwitchInterconnectSwitchSideBlock', 'LanSwitchInterconnectRouterSideBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv4_address')) + """)) + conn.execute(sa.text(""" + DELETE FROM product_product_blocks WHERE product_product_blocks.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Imported LAN Switch Interconnect')) AND product_product_blocks.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('LanSwitchInterconnectBlock')) + """)) + conn.execute(sa.text(""" + DELETE FROM processes WHERE processes.pid IN (SELECT processes_subscriptions.pid FROM processes_subscriptions WHERE processes_subscriptions.subscription_id IN (SELECT subscriptions.subscription_id FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Imported LAN Switch Interconnect')))) + """)) + conn.execute(sa.text(""" + DELETE FROM processes_subscriptions WHERE processes_subscriptions.subscription_id IN (SELECT subscriptions.subscription_id FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Imported LAN Switch Interconnect'))) + """)) + conn.execute(sa.text(""" + DELETE FROM subscription_instances WHERE subscription_instances.subscription_id IN (SELECT subscriptions.subscription_id FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Imported LAN Switch Interconnect'))) + """)) + conn.execute(sa.text(""" + DELETE FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Imported LAN Switch Interconnect')) + """)) + conn.execute(sa.text(""" + DELETE FROM products WHERE products.name IN ('Imported LAN Switch Interconnect') + """)) + 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 ('lan_switch_interconnect_ip_network')) + """)) + 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 ('lan_switch_interconnect_ip_network')) + """)) + 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 ('lan_switch_interconnect_ip_network')) + """)) + conn.execute(sa.text(""" +DELETE FROM resource_types WHERE resource_types.resource_type IN ('lan_switch_interconnect_ip_network') + """)) + conn.execute(sa.text(""" +DELETE FROM product_product_blocks WHERE product_product_blocks.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Imported Switch')) AND product_product_blocks.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('SwitchBlock')) + """)) + conn.execute(sa.text(""" +DELETE FROM processes WHERE processes.pid IN (SELECT processes_subscriptions.pid FROM processes_subscriptions WHERE processes_subscriptions.subscription_id IN (SELECT subscriptions.subscription_id FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Imported Switch')))) + """)) + conn.execute(sa.text(""" +DELETE FROM processes_subscriptions WHERE processes_subscriptions.subscription_id IN (SELECT subscriptions.subscription_id FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Imported Switch'))) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instances WHERE subscription_instances.subscription_id IN (SELECT subscriptions.subscription_id FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Imported Switch'))) + """)) + conn.execute(sa.text(""" +DELETE FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Imported Switch')) + """)) + conn.execute(sa.text(""" +DELETE FROM products WHERE products.name IN ('Imported Switch') + """)) + conn.execute(sa.text(""" +UPDATE resource_types SET resource_type='switch_ts_port' WHERE resource_types.resource_type = 'ts_port' + """)) + conn.execute(sa.text(""" +UPDATE resource_types SET resource_type='switch_hostname' WHERE resource_types.resource_type = 'fqdn' + """)) diff --git a/gso/products/__init__.py b/gso/products/__init__.py index ad3c2d2a7be2d9c863541d38d950100b0acb59a8..457a86ed3601e66ffb728f5ac00fdc3937907612 100644 --- a/gso/products/__init__.py +++ b/gso/products/__init__.py @@ -10,7 +10,7 @@ from pydantic_forms.types import strEnum from gso.products.product_types.edge_port import EdgePort, ImportedEdgePort from gso.products.product_types.iptrunk import ImportedIptrunk, Iptrunk -from gso.products.product_types.lan_switch_interconnect import LanSwitchInterconnect +from gso.products.product_types.lan_switch_interconnect import ImportedLanSwitchInterconnect, LanSwitchInterconnect from gso.products.product_types.nren_l3_core_service import ImportedNRENL3CoreService, NRENL3CoreService from gso.products.product_types.office_router import ImportedOfficeRouter, OfficeRouter from gso.products.product_types.opengear import ImportedOpengear, Opengear @@ -18,7 +18,7 @@ from gso.products.product_types.pop_vlan import PopVlan from gso.products.product_types.router import ImportedRouter, Router from gso.products.product_types.site import ImportedSite, Site from gso.products.product_types.super_pop_switch import ImportedSuperPopSwitch, SuperPopSwitch -from gso.products.product_types.switch import Switch +from gso.products.product_types.switch import ImportedSwitch, Switch class ProductName(strEnum): @@ -30,7 +30,9 @@ class ProductName(strEnum): SUPER_POP_SWITCH = "Super PoP switch" OFFICE_ROUTER = "Office router" SWITCH = "Switch" + IMPORTED_SWITCH = "Imported Switch" LAN_SWITCH_INTERCONNECT = "LAN Switch Interconnect" + IMPORTED_LAN_SWITCH_INTERCONNECT = "Imported LAN Switch Interconnect" POP_VLAN = "Pop VLAN" IMPORTED_IP_TRUNK = "Imported IP trunk" IMPORTED_ROUTER = "Imported router" @@ -56,7 +58,9 @@ class ProductType(strEnum): SUPER_POP_SWITCH = SuperPopSwitch.__name__ OFFICE_ROUTER = OfficeRouter.__name__ SWITCH = Switch.__name__ + IMPORTED_SWITCH = ImportedSwitch.__name__ LAN_SWITCH_INTERCONNECT = LanSwitchInterconnect.__name__ + IMPORTED_LAN_SWITCH_INTERCONNECT = ImportedLanSwitchInterconnect.__name__ POP_VLAN = PopVlan.__name__ IMPORTED_IP_TRUNK = ImportedIptrunk.__name__ IMPORTED_ROUTER = ImportedRouter.__name__ @@ -81,7 +85,9 @@ SUBSCRIPTION_MODEL_REGISTRY.update( ProductName.SUPER_POP_SWITCH.value: SuperPopSwitch, ProductName.OFFICE_ROUTER.value: OfficeRouter, ProductName.SWITCH.value: Switch, + ProductName.IMPORTED_SWITCH.value: ImportedSwitch, ProductName.LAN_SWITCH_INTERCONNECT.value: LanSwitchInterconnect, + ProductName.IMPORTED_LAN_SWITCH_INTERCONNECT.value: ImportedLanSwitchInterconnect, ProductName.POP_VLAN.value: PopVlan, ProductName.IMPORTED_IP_TRUNK.value: ImportedIptrunk, ProductName.IMPORTED_ROUTER.value: ImportedRouter, diff --git a/gso/products/product_blocks/lan_switch_interconnect.py b/gso/products/product_blocks/lan_switch_interconnect.py index 39e31c9f5cb376a377d148a8f581236c3ee8c016..945efc5a14ffb6f8e58b60aa375b91165f19616b 100644 --- a/gso/products/product_blocks/lan_switch_interconnect.py +++ b/gso/products/product_blocks/lan_switch_interconnect.py @@ -1,19 +1,12 @@ """LAN Switch Interconnect product block that has all parameters of a subscription throughout its lifecycle.""" from orchestrator.domain.base import ProductBlockModel -from orchestrator.types import SubscriptionLifecycle, strEnum +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.ip_address import IPv4NetworkType - - -class LanSwitchInterconnectAddressSpace(strEnum): - """Types of :term:`LAN` Switch Interconnect. Can be private or public.""" - - PRIVATE = "Private" - PUBLIC = "Public" +from gso.utils.types.ip_address import AddressSpace, IPv4AddressType, IPv4NetworkType class LanSwitchInterconnectInterfaceBlockInactive( @@ -55,6 +48,7 @@ class LanSwitchInterconnectRouterSideBlockInactive( node: RouterBlockInactive ae_iface: str | None = None ae_members: LAGMemberList[LanSwitchInterconnectInterfaceBlockInactive] + ipv4_address: IPv4AddressType | None = None class LanSwitchInterconnectRouterSideBlockProvisioning( @@ -65,6 +59,7 @@ class LanSwitchInterconnectRouterSideBlockProvisioning( node: RouterBlockProvisioning ae_iface: str | None = None ae_members: LAGMemberList[LanSwitchInterconnectInterfaceBlockProvisioning] # type: ignore[assignment] + ipv4_address: IPv4AddressType | None class LanSwitchInterconnectRouterSideBlock( @@ -75,6 +70,7 @@ class LanSwitchInterconnectRouterSideBlock( node: RouterBlock ae_iface: str ae_members: LAGMemberList[LanSwitchInterconnectInterfaceBlock] # type: ignore[assignment] + ipv4_address: IPv4AddressType | None class LanSwitchInterconnectSwitchSideBlockInactive( @@ -87,6 +83,7 @@ class LanSwitchInterconnectSwitchSideBlockInactive( switch: SwitchBlockInactive ae_iface: str | None = None ae_members: LAGMemberList[LanSwitchInterconnectInterfaceBlockInactive] + ipv4_address: IPv4AddressType | None = None class LanSwitchInterconnectSwitchSideBlockProvisioning( @@ -97,6 +94,7 @@ class LanSwitchInterconnectSwitchSideBlockProvisioning( switch: SwitchBlockProvisioning ae_iface: str | None = None ae_members: LAGMemberList[LanSwitchInterconnectInterfaceBlockProvisioning] # type: ignore[assignment] + ipv4_address: IPv4AddressType | None class LanSwitchInterconnectSwitchSideBlock( @@ -107,6 +105,7 @@ class LanSwitchInterconnectSwitchSideBlock( switch: SwitchBlock ae_iface: str ae_members: LAGMemberList[LanSwitchInterconnectInterfaceBlock] # type: ignore[assignment] + ipv4_address: IPv4AddressType | None class LanSwitchInterconnectBlockInactive( @@ -118,7 +117,7 @@ class LanSwitchInterconnectBlockInactive( lan_switch_interconnect_description: str | None = None lan_switch_interconnect_ip_network: IPv4NetworkType | None = None - address_space: LanSwitchInterconnectAddressSpace | None = None + address_space: AddressSpace | None = None minimum_links: int | None = None router_side: LanSwitchInterconnectRouterSideBlockInactive switch_side: LanSwitchInterconnectSwitchSideBlockInactive @@ -131,7 +130,7 @@ class LanSwitchInterconnectBlockProvisioning( lan_switch_interconnect_description: str | None = None lan_switch_interconnect_ip_network: IPv4NetworkType | None - address_space: LanSwitchInterconnectAddressSpace | None = None + address_space: AddressSpace | None = None minimum_links: int | None = None router_side: LanSwitchInterconnectRouterSideBlockProvisioning switch_side: LanSwitchInterconnectSwitchSideBlockProvisioning @@ -145,7 +144,7 @@ class LanSwitchInterconnectBlock(LanSwitchInterconnectBlockProvisioning, lifecyc #: The :term:`IP` resources for this :term:`VLAN` Switch Interconnect. lan_switch_interconnect_ip_network: IPv4NetworkType | None #: The address space of the :term:`VLAN` Switch Interconnect. It can be private or public. - address_space: LanSwitchInterconnectAddressSpace + address_space: AddressSpace #: The minimum amount of links the :term:`LAN` Switch Interconnect should consist of. minimum_links: int #: The router side of the :term:`LAN` Switch Interconnect. diff --git a/gso/products/product_types/lan_switch_interconnect.py b/gso/products/product_types/lan_switch_interconnect.py index 32433c4694b0f097bcdc80df742ca9695a9dee53..0f83fe3714ac68bc605b77c498d56b51412d7873 100644 --- a/gso/products/product_types/lan_switch_interconnect.py +++ b/gso/products/product_types/lan_switch_interconnect.py @@ -26,3 +26,17 @@ class LanSwitchInterconnect(LanSwitchInterconnectProvisioning, lifecycle=[Subscr """An LAN Switch Interconnect that is active.""" lan_switch_interconnect: LanSwitchInterconnectBlock + + +class ImportedLanSwitchInterconnectInactive(SubscriptionModel, is_base=True): + """An imported, inactive :term:`LAN` Switch Interconnect.""" + + lan_switch_interconnect: LanSwitchInterconnectBlockInactive + + +class ImportedLanSwitchInterconnect( + ImportedLanSwitchInterconnectInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE] +): + """An imported :term:`LAN` Switch Interconnect.""" + + lan_switch_interconnect: LanSwitchInterconnectBlock diff --git a/gso/products/product_types/switch.py b/gso/products/product_types/switch.py index ef2eb05eac333628ba4ae5bef2424283adec3758..10cc9bb202754866a6b8646abdcd10d31f074f85 100644 --- a/gso/products/product_types/switch.py +++ b/gso/products/product_types/switch.py @@ -22,3 +22,17 @@ class Switch(SwitchProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): """A switch that is currently active.""" switch: SwitchBlock + + +class ImportedSwitchInactive(SubscriptionModel, is_base=True): + """An imported, inactive switch.""" + + switch: SwitchBlockInactive + + +class ImportedSwitch( + ImportedSwitchInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE] +): + """An imported switch that is currently active.""" + + switch: SwitchBlock diff --git a/gso/translations/en-GB.json b/gso/translations/en-GB.json index ef3a1bdeae293d10fa74fbb80a550860c3792841..1858d04ece8f39a1bd1f83de8994e6c71633ba9a 100644 --- a/gso/translations/en-GB.json +++ b/gso/translations/en-GB.json @@ -53,6 +53,7 @@ "create_switch": "Create Switch", "create_edge_port": "Create Edge Port", "create_nren_l3_core_service": "Create NREN L3 Core Service", + "create_lan_switch_interconnect": "Create LAN Switch Interconnect", "deploy_twamp": "Deploy TWAMP", "migrate_iptrunk": "Migrate IP Trunk", "migrate_nren_l3_core_service": "Migrate NREN L3 Core Service", @@ -68,6 +69,7 @@ "terminate_site": "Terminate Site", "terminate_switch": "Terminate Switch", "terminate_edge_port": "Terminate Edge Port", + "terminate_lan_switch_interconnect": "Terminate LAN Switch Interconnect", "redeploy_base_config": "Redeploy base config", "update_ibgp_mesh": "Update iBGP mesh", "create_imported_site": "NOT FOR HUMANS -- Import existing site", @@ -78,6 +80,8 @@ "create_imported_opengear": "NOT FOR HUMANS -- Import existing OpenGear", "create_imported_edge_port": "NOT FOR HUMANS -- Import existing Edge Port", "create_imported_nren_l3_core_service": "NOT FOR HUMANS -- Import existing NREN L3 Core Service", + "create_imported_switch": "NOT FOR HUMANS -- Import existing Switch", + "create_imported_lan_switch_interconnect": "NOT FOR HUMANS -- Import existing LAN Switch Interconnect", "import_site": "NOT FOR HUMANS -- Finalize import into a Site product", "import_router": "NOT FOR HUMANS -- Finalize import into a Router product", "import_iptrunk": "NOT FOR HUMANS -- Finalize import into an IP trunk product", @@ -86,10 +90,13 @@ "import_opengear": "NOT FOR HUMANS -- Finalize import into an OpenGear", "import_edge_port": "NOT FOR HUMANS -- Finalize import into an Edge Port", "import_nren_l3_core_service": "NOT FOR HUMANS -- Finalize import into a NREN L3 Core Service", + "import_switch": "NOT FOR HUMANS -- Finalize import into a Switch", + "import_lan_switch_interconnect": "NOT FOR HUMANS -- Finalize import into a LAN Switch Interconnect", "validate_iptrunk": "Validate IP Trunk configuration", "validate_router": "Validate Router configuration", "validate_switch": "Validate Switch configuration", "validate_edge_port": "Validate Edge Port", + "validate_lan_switch_interconnect": "Validate LAN Switch Interconnect", "task_validate_geant_products": "Validation task for GEANT products", "task_send_email_notifications": "Send email notifications for failed tasks", "task_create_partners": "Create partner task", diff --git a/gso/utils/types/ip_address.py b/gso/utils/types/ip_address.py index 6186a1f83d6a495aa591e1e96a6a2030794de7fd..14e00bb248a91caf30574f8e0e8d6774045a9a47 100644 --- a/gso/utils/types/ip_address.py +++ b/gso/utils/types/ip_address.py @@ -4,6 +4,7 @@ import ipaddress from typing import Annotated, Any from pydantic import AfterValidator, Field, PlainSerializer +from pydantic_forms.types import strEnum from typing_extensions import Doc @@ -52,3 +53,10 @@ PortNumber = Annotated[ "and can therefore not be selected for permanent allocation." ), ] + + +class AddressSpace(strEnum): + """Types of address space. Can be private or public.""" + + PRIVATE = "PRIVATE" + PUBLIC = "PRIVATE" diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py index 0ab6a9978fa45a8ac64ed434d20b05a127391944..0c36e0669929d1c3b544513d6e344d7a3f92efd6 100644 --- a/gso/workflows/__init__.py +++ b/gso/workflows/__init__.py @@ -20,6 +20,8 @@ WF_USABLE_MAP.update({ "terminate_site": ALL_ALIVE_STATES, "terminate_router": ALL_ALIVE_STATES, "terminate_iptrunk": ALL_ALIVE_STATES, + "terminate_switch": ALL_ALIVE_STATES, + "terminate_lan_switch_interconnect": ALL_ALIVE_STATES, "promote_p_to_pe": [SubscriptionLifecycle.ACTIVE], "validate_router": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE], "validate_iptrunk": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE], @@ -55,11 +57,19 @@ LazyWorkflowInstance("gso.workflows.switch.create_switch", "create_switch") LazyWorkflowInstance("gso.workflows.switch.activate_switch", "activate_switch") LazyWorkflowInstance("gso.workflows.switch.terminate_switch", "terminate_switch") LazyWorkflowInstance("gso.workflows.switch.validate_switch", "validate_switch") +LazyWorkflowInstance("gso.workflows.switch.create_imported_switch", "create_imported_switch") +LazyWorkflowInstance("gso.workflows.switch.import_switch", "import_switch") # LAN Switch Interconnect workflows LazyWorkflowInstance( "gso.workflows.lan_switch_interconnect.create_lan_switch_interconnect", "create_lan_switch_interconnect" ) +LazyWorkflowInstance( + "gso.workflows.lan_switch_interconnect.terminate_lan_switch_interconnect", "terminate_lan_switch_interconnect" +) +LazyWorkflowInstance( + "gso.workflows.lan_switch_interconnect.validate_lan_switch_interconnect", "validate_lan_switch_interconnect" +) # Site workflows LazyWorkflowInstance("gso.workflows.site.create_site", "create_site") 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 new file mode 100644 index 0000000000000000000000000000000000000000..282b0e2631ace830ded715e979a91c0fcb6af1b4 --- /dev/null +++ b/gso/workflows/lan_switch_interconnect/create_imported_lan_switch_interconnect.py @@ -0,0 +1,85 @@ +"""Import an existing :term:`LAN` Switch Interconnect into the subscription database.""" + +from uuid import uuid4 + +from orchestrator import step, workflow +from orchestrator.forms import FormPage +from orchestrator.targets import Target +from orchestrator.types import FormGenerator, State, SubscriptionLifecycle +from orchestrator.workflow import StepList, begin, done +from orchestrator.workflows.steps import resync, set_status, store_process_subscription +from products.product_types.lan_switch_interconnect import ImportedLanSwitchInterconnectInactive + +from gso.cli.imports import LanSwitchInterconnectRouterSideImportModel, LanSwitchInterconnectSwitchSideImportModel +from gso.products import ProductName +from gso.products.product_blocks.lan_switch_interconnect import ( + LanSwitchInterconnectRouterSideBlockInactive, + LanSwitchInterconnectSwitchSideBlockInactive, +) +from gso.services.partners import get_partner_by_name +from gso.services.subscriptions import get_product_id_by_name +from gso.utils.types.ip_address import AddressSpace, IPv4NetworkType + + +def _initial_input_form_generator() -> FormGenerator: + class ImportLanSwitchInterconnect(FormPage): + lan_switch_interconnect_description: str + lan_switch_interconnect_ip_network: IPv4NetworkType | None + address_space: AddressSpace + minimum_links: int + router_side: LanSwitchInterconnectRouterSideImportModel + switch_side: LanSwitchInterconnectSwitchSideImportModel + + user_input = yield ImportLanSwitchInterconnect + return user_input.model_dump() | {"partner": "GEANT"} + + +@step("Create subscription") +def create_subscription(partner: str) -> State: + """Create a new subscription object.""" + partner_id = get_partner_by_name(partner)["partner_id"] + product_id = get_product_id_by_name(ProductName.IMPORTED_LAN_SWITCH_INTERCONNECT) + subscription = ImportedLanSwitchInterconnectInactive.from_product_id(product_id, partner_id) + + return {"subscription": subscription, "subscription_id": subscription.subscription_id} + + +@step("Initialize subscription") +def initialize_subscription( + subscription: ImportedLanSwitchInterconnectInactive, + lan_switch_interconnect_description: str, + lan_switch_interconnect_ip_network: IPv4NetworkType | None, + address_space: AddressSpace, + minimum_links: int, + lan_switch_interconnect_router_side: dict, + lan_switch_interconnect_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.lan_switch_interconnect_ip_network = lan_switch_interconnect_ip_network + subscription.lan_switch_interconnect.address_space = address_space + subscription.lan_switch_interconnect.minimum_links = minimum_links + subscription.lan_switch_interconnect.router_side = LanSwitchInterconnectRouterSideBlockInactive.new( + uuid4(), **lan_switch_interconnect_router_side + ) + subscription.lan_switch_interconnect.switch_side = LanSwitchInterconnectSwitchSideBlockInactive.new( + uuid4(), **lan_switch_interconnect_switch_side + ) + + return {"subscription": subscription} + + +@workflow( + "Create Imported LAN Switch Interconnect", initial_input_form=_initial_input_form_generator, target=Target.CREATE +) +def create_imported_lan_switch_interconnect() -> StepList: + """Create an imported :term:`LAN` Switch Interconnect without provisioning it.""" + return ( + begin + >> create_subscription + >> store_process_subscription(Target.CREATE) + >> initialize_subscription + >> set_status(SubscriptionLifecycle.ACTIVE) + >> resync + >> done + ) 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 44bc2d4705be009f26e4971e4432c11f9433f0d4..44f4f811523f7dd3998a3e6e6e0474e2284ee635 100644 --- a/gso/workflows/lan_switch_interconnect/create_lan_switch_interconnect.py +++ b/gso/workflows/lan_switch_interconnect/create_lan_switch_interconnect.py @@ -14,7 +14,6 @@ from pydantic import AfterValidator, ConfigDict from pydantic_forms.validators import Divider, ReadOnlyField from gso.products.product_blocks.lan_switch_interconnect import ( - LanSwitchInterconnectAddressSpace, LanSwitchInterconnectInterfaceBlockInactive, ) from gso.products.product_types.lan_switch_interconnect import LanSwitchInterconnectInactive @@ -36,6 +35,7 @@ from gso.utils.types.interfaces import ( PhysicalPortCapacity, validate_interface_names_are_unique, ) +from gso.utils.types.ip_address import AddressSpace from gso.utils.types.tt_number import TTNumber @@ -47,7 +47,7 @@ def _initial_input_form(product_name: str) -> FormGenerator: partner: ReadOnlyField("GEANT", default_type=str) # type: ignore[valid-type] router_side: active_router_selector() # type: ignore[valid-type] switch_side: active_switch_selector() # type: ignore[valid-type] - address_space: LanSwitchInterconnectAddressSpace + address_space: AddressSpace description: str minimum_link_count: int divider: Divider @@ -116,7 +116,7 @@ def create_subscription(product: UUIDstr, partner: str) -> State: def initialize_subscription( subscription: LanSwitchInterconnectInactive, description: str, - address_space: LanSwitchInterconnectAddressSpace, + address_space: AddressSpace, minimum_link_count: int, router_side: UUIDstr, router_side_iface: JuniperPhyInterface, @@ -146,7 +146,7 @@ def initialize_subscription( @workflow( - "Create LAN switch interconnect", + "Create LAN Switch Interconnect", initial_input_form=wrap_create_initial_input_form(_initial_input_form), target=Target.CREATE, ) diff --git a/gso/workflows/lan_switch_interconnect/import_lan_switch_interconnect.py b/gso/workflows/lan_switch_interconnect/import_lan_switch_interconnect.py new file mode 100644 index 0000000000000000000000000000000000000000..e0f247aa28d0578ec4e5898e32f316b22c449286 --- /dev/null +++ b/gso/workflows/lan_switch_interconnect/import_lan_switch_interconnect.py @@ -0,0 +1,37 @@ +"""Import an existing :term:`LAN` Switch Interconnect into the service database.""" + +from orchestrator import step, workflow +from orchestrator.targets import Target +from orchestrator.types import State, UUIDstr +from orchestrator.workflow import StepList, begin, done +from orchestrator.workflows.steps import resync, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form + +from gso.products import ProductName +from gso.products.product_types.lan_switch_interconnect import ImportedLanSwitchInterconnect, LanSwitchInterconnect +from gso.services.subscriptions import get_product_id_by_name + + +@step("Create new switch subscription") +def import_lan_switch_interconnect_subscription(subscription_id: UUIDstr) -> State: + """Take an imported subscription, and turn it into a full subscription.""" + old_subscription = ImportedLanSwitchInterconnect.from_subscription(subscription_id) + new_subscription_id = get_product_id_by_name(ProductName.LAN_SWITCH_INTERCONNECT) + new_subscription = LanSwitchInterconnect.from_other_product(old_subscription, new_subscription_id) # type: ignore[arg-type] + + return {"subscription": new_subscription} + + +@workflow( + "Import LAN Switch Interconnect", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None) +) +def import_lan_switch_interconnect() -> StepList: + """Modify an ImportedLanSwitchInterconnect subscription into subscription to complete the import.""" + return ( + begin + >> store_process_subscription(Target.MODIFY) + >> unsync + >> import_lan_switch_interconnect_subscription + >> resync + >> done + ) diff --git a/gso/workflows/lan_switch_interconnect/terminate_lan_switch_interconnect.py b/gso/workflows/lan_switch_interconnect/terminate_lan_switch_interconnect.py new file mode 100644 index 0000000000000000000000000000000000000000..9df06c9a08b046e6c8891b84ff88252b2fc0605a --- /dev/null +++ b/gso/workflows/lan_switch_interconnect/terminate_lan_switch_interconnect.py @@ -0,0 +1,48 @@ +"""Workflow for terminating a LAN Switch interconnect.""" + +from orchestrator import begin, workflow +from orchestrator.forms import FormPage +from orchestrator.targets import Target +from orchestrator.types import SubscriptionLifecycle, UUIDstr +from orchestrator.workflow import StepList, done +from orchestrator.workflows.steps import resync, set_status, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form +from pydantic_forms.types import FormGenerator +from pydantic_forms.validators import Label + +from gso.products.product_types.lan_switch_interconnect import LanSwitchInterconnect +from gso.utils.types.tt_number import TTNumber + + +def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator: + lan_switch_interconnect = LanSwitchInterconnect.from_subscription(subscription_id) + + class TerminateForm(FormPage): + if lan_switch_interconnect.status == SubscriptionLifecycle.INITIAL: + info_label: Label = ( + "This will immediately mark the subscription as terminated, preventing any other workflows from " + "interacting with this product subscription." + ) + info_label_2: Label = "ONLY EXECUTE THIS WORKFLOW WHEN YOU ARE ABSOLUTELY SURE WHAT YOU ARE DOING." + + tt_number: TTNumber + + yield TerminateForm + return {"subscription": lan_switch_interconnect} + + +@workflow( + "Terminate LAN Switch Interconnect", + initial_input_form=wrap_modify_initial_input_form(_input_form_generator), + target=Target.TERMINATE, +) +def terminate_lan_switch_interconnect() -> StepList: + """Terminate a :term:`LAN` Switch Interconnect.""" + return ( + begin + >> store_process_subscription(Target.TERMINATE) + >> unsync + >> set_status(SubscriptionLifecycle.TERMINATED) + >> resync + >> done + ) diff --git a/gso/workflows/lan_switch_interconnect/validate_lan_switch_interconnect.py b/gso/workflows/lan_switch_interconnect/validate_lan_switch_interconnect.py new file mode 100644 index 0000000000000000000000000000000000000000..f2a623dedb2a75716ec989b0c29795b4217541a2 --- /dev/null +++ b/gso/workflows/lan_switch_interconnect/validate_lan_switch_interconnect.py @@ -0,0 +1,40 @@ +"""Validation workflow for :term:`LAN` Switch Interconnect subscription objects.""" + +from typing import Any + +from orchestrator.targets import Target +from orchestrator.workflow import StepList, begin, done, step, workflow +from orchestrator.workflows.steps import resync, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form + +from gso.services.lso_client import LSOState, anonymous_lso_interaction + + +@step("Check config for drift") +def validate_config(subscription: dict[str, Any]) -> LSOState: + """Workflow step for running a playbook that checks whether config has drifted.""" + return { + "playbook_name": "lan_switch_interconnect.yaml", + "inventory": {"all": {"hosts": {None: None}}}, + "extra_vars": { + "subscription_json": subscription, + "verb": "deploy", + "dry_run": "true", + "is_verification_workflow": "true", + }, + } + + +@workflow( + "Validate LAN Switch Interconnect", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None)) +) +def validate_lan_switch_interconnect() -> StepList: + """Validate an existing :term:`LAN` Switch Interconnect.""" + return ( + begin + >> store_process_subscription(Target.SYSTEM) + >> unsync + >> anonymous_lso_interaction(validate_config) + >> resync + >> done + ) diff --git a/gso/workflows/router/create_imported_router.py b/gso/workflows/router/create_imported_router.py index 12a71c79b51e0166d976a175d9833870300af1f5..cb729eb276c6a4734dd31c98ddd92bd1240b61cd 100644 --- a/gso/workflows/router/create_imported_router.py +++ b/gso/workflows/router/create_imported_router.py @@ -25,10 +25,7 @@ def create_subscription(partner: str) -> State: product_id = get_product_id_by_name(ProductName.IMPORTED_ROUTER) subscription = ImportedRouterInactive.from_product_id(product_id, partner_id) - return { - "subscription": subscription, - "subscription_id": subscription.subscription_id, - } + return {"subscription": subscription, "subscription_id": subscription.subscription_id} def initial_input_form_generator() -> FormGenerator: diff --git a/gso/workflows/switch/create_imported_switch.py b/gso/workflows/switch/create_imported_switch.py new file mode 100644 index 0000000000000000000000000000000000000000..558e53b6e25fbac1b3c37df95b5dbdd45af5c517 --- /dev/null +++ b/gso/workflows/switch/create_imported_switch.py @@ -0,0 +1,72 @@ +"""A creation workflow that adds an existing switch to the service database.""" + +from orchestrator import step, workflow +from orchestrator.forms import FormPage +from orchestrator.targets import Target +from orchestrator.types import FormGenerator, State, SubscriptionLifecycle, UUIDstr +from orchestrator.workflow import StepList, begin, done +from orchestrator.workflows.steps import resync, set_status, store_process_subscription + +from gso.products import ProductName +from gso.products.product_blocks.switch import SwitchModel +from gso.products.product_types.site import Site +from gso.products.product_types.switch import ImportedSwitchInactive +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 Vendor +from gso.utils.types.ip_address import PortNumber + + +def _initial_input_form_generator() -> FormGenerator: + class ImportSwitch(FormPage): + fqdn: str + ts_port: PortNumber + site: UUIDstr + switch_vendor: Vendor + switch_model: SwitchModel + + user_input = yield ImportSwitch + return user_input.model_dump() | {"partner": "GEANT"} + + +@step("Create subscription") +def create_subscription(partner: str) -> State: + """Create a new subscription object.""" + partner_id = get_partner_by_name(partner)["partner_id"] + product_id = get_product_id_by_name(ProductName.IMPORTED_SWITCH) + subscription = ImportedSwitchInactive.from_product_id(product_id, partner_id) + + return {"subscription": subscription, "subscription_id": subscription.subscription_id} + + +@step("Initialize subscription") +def initialize_subscription( + subscription: ImportedSwitchInactive, + fqdn: str, + ts_port: PortNumber, + site: UUIDstr, + switch_vendor: Vendor, + switch_model: SwitchModel, +) -> State: + """Initialize the switch subscription using input data.""" + subscription.switch.fqdn = fqdn + subscription.switch.ts_port = ts_port + subscription.switch.site = Site.from_subscription(site).site + subscription.switch.switch_vendor = switch_vendor + subscription.switch.switch_model = switch_model + + return {"subscription": subscription} + + +@workflow("Create Imported Switch", initial_input_form=_initial_input_form_generator, target=Target.CREATE) +def create_imported_switch() -> StepList: + """Create an imported switch without provisioning it.""" + return ( + begin + >> create_subscription + >> store_process_subscription(Target.CREATE) + >> initialize_subscription + >> set_status(SubscriptionLifecycle.ACTIVE) + >> resync + >> done + ) diff --git a/gso/workflows/switch/import_switch.py b/gso/workflows/switch/import_switch.py new file mode 100644 index 0000000000000000000000000000000000000000..8ecf2d43930b1e40f84d1866f2db0a7cf8115306 --- /dev/null +++ b/gso/workflows/switch/import_switch.py @@ -0,0 +1,28 @@ +"""Import an existing Switch into the service database.""" + +from orchestrator import step, workflow +from orchestrator.targets import Target +from orchestrator.types import State, UUIDstr +from orchestrator.workflow import StepList, begin, done +from orchestrator.workflows.steps import resync, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form + +from gso.products import ProductName +from gso.products.product_types.switch import ImportedSwitch, Switch +from gso.services.subscriptions import get_product_id_by_name + + +@step("Create new switch subscription") +def import_switch_subscription(subscription_id: UUIDstr) -> State: + """Take an ImportedSwitch subscription, and turn it into a switch subscription.""" + old_switch = ImportedSwitch.from_subscription(subscription_id) + new_subscription_id = get_product_id_by_name(ProductName.SWITCH) + new_subscription = Switch.from_other_product(old_switch, new_subscription_id) # type: ignore[arg-type] + + return {"subscription": new_subscription} + + +@workflow("Import Switch", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None)) +def import_switch() -> StepList: + """Modify an ImportedSwitch subscription into a Switch subscription to complete the import.""" + return begin >> store_process_subscription(Target.MODIFY) >> unsync >> import_switch_subscription >> resync >> done diff --git a/gso/workflows/switch/terminate_switch.py b/gso/workflows/switch/terminate_switch.py index 6c1a5d98de0f030656490bb6a4a071bd9ec76e37..80917c6376a66f7037e2a1cb9a9971dfc6177b17 100644 --- a/gso/workflows/switch/terminate_switch.py +++ b/gso/workflows/switch/terminate_switch.py @@ -12,7 +12,7 @@ from pydantic_forms.types import FormGenerator, UUIDstr from pydantic_forms.validators import Label from gso.products.product_types.switch import Switch -from gso.utils.types import TTNumber +from gso.utils.types.tt_number import TTNumber def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator: diff --git a/gso/workflows/switch/validate_switch.py b/gso/workflows/switch/validate_switch.py index 55d9cf1c67c7b74a15c341949df8314929c9e3c2..9faebc251ed707d34507677d3eef4dc88756c1fd 100644 --- a/gso/workflows/switch/validate_switch.py +++ b/gso/workflows/switch/validate_switch.py @@ -8,30 +8,29 @@ from orchestrator.workflows.steps import resync, store_process_subscription, uns from orchestrator.workflows.utils import wrap_modify_initial_input_form from gso.products.product_types.switch import Switch -from gso.services.lso_client import anonymous_lso_interaction, execute_playbook +from gso.services.lso_client import LSOState, anonymous_lso_interaction from gso.services.netbox_client import NetboxClient @step("Validate switch in Netbox") def check_netbox_device(subscription: Switch) -> None: """Fetch the device in Netbox. Will raise an exception when it is not found.""" - NetboxClient().get_device_by_name(subscription.switch.switch_fqdn) + NetboxClient().get_device_by_name(subscription.switch.fqdn) @step("Check base config for drift") -def verify_base_config(subscription: dict[str, Any], callback_route: str) -> None: +def verify_base_config(subscription: dict[str, Any]) -> LSOState: """Workflow step for running a playbook that checks whether base config has drifted.""" - execute_playbook( - playbook_name="switch_base_config.yaml", - callback_route=callback_route, - inventory=subscription["switch"]["switch_fqdn"], - extra_vars={ + return { + "playbook_name": "switch_base_config.yaml", + "inventory": {"all": {"hosts": {subscription["switch"]["fqdn"]: None}}}, + "extra_vars": { "subscription_json": subscription, "verb": "deploy", "dry_run": "true", "is_verification_workflow": "true", }, - ) + } @workflow( diff --git a/test/cli/test_imports.py b/test/cli/test_imports.py index 33ab32dd2acc6b1746322a1694528fa2323ee48d..62cf1a8d01ed5e0b41dc5f2eb518728f72b546b5 100644 --- a/test/cli/test_imports.py +++ b/test/cli/test_imports.py @@ -7,23 +7,27 @@ import pytest from gso.cli.imports import ( import_edge_port, import_iptrunks, + import_lan_switch_interconnect, import_nren_l3_core_service, import_office_routers, import_opengear, import_routers, import_sites, import_super_pop_switches, + import_switches, ) from gso.products.product_blocks.bgp_session import IPFamily from gso.products.product_blocks.edge_port import EdgePortType, EncapsulationType from gso.products.product_blocks.iptrunk import IptrunkType from gso.products.product_blocks.router import RouterRole from gso.products.product_blocks.site import SiteTier +from gso.products.product_blocks.switch import SwitchModel from gso.products.product_types.router import Router from gso.products.product_types.site import Site from gso.utils.helpers import iso_from_ipv4 from gso.utils.shared_enums import Vendor from gso.utils.types.interfaces import PhysicalPortCapacity +from gso.utils.types.ip_address import AddressSpace ############## @@ -200,6 +204,59 @@ def opengear_data(temp_file, faker, site_subscription_factory): return _opengear_data +@pytest.fixture() +def switch_data(temp_file, faker, site_subscription_factory): + def _switch_data(**kwargs): + switch_data = { + "fqdn": faker.domain_name(levels=4), + "ts_port": faker.port_number(is_user=True), + "site": site_subscription_factory(), + "switch_vendor": Vendor.JUNIPER, + "switch_model": SwitchModel.EX3400, + } + switch_data.update(**kwargs) + + temp_file.write_text(json.dumps([switch_data])) + return {"path": str(temp_file), "data": switch_data} + + return _switch_data + + +@pytest.fixture() +def lan_switch_interconnect_data(temp_file, faker, switch_subscription_factory, router_subscription_factory): + def _lan_switch_interconnect_data(**kwargs): + lan_switch_interconnect_data = { + "lan_switch_interconnect_description": faker.sentence(), + "lan_switch_interconnect_ip_network": str(faker.ipv4_network()), + "address_space": AddressSpace.PUBLIC, + "minimum_links": 1, + "router_side": { + "node": router_subscription_factory(), + "ae_iface": faker.nokia_lag_interface_name(), + "ae_members": [ + {"interface_name": faker.network_interface(), "interface_description": faker.sentence()} + for _ in range(2) + ], + "ipv4_address": faker.ipv4(), + }, + "switch_side": { + "switch": switch_subscription_factory(), + "ae_iface": faker.juniper_ae_interface_name(), + "ae_members": [ + {"interface_name": faker.network_interface(), "interface_description": faker.sentence()} + for _ in range(2) + ], + "ipv4_address": faker.ipv4(), + }, + } + lan_switch_interconnect_data.update(**kwargs) + + temp_file.write_text(json.dumps([lan_switch_interconnect_data])) + return {"path": str(temp_file), "data": lan_switch_interconnect_data} + + return _lan_switch_interconnect_data + + @pytest.fixture() def edge_port_data(temp_file, faker, router_subscription_factory, partner_factory): def _edge_port_data(**kwargs): @@ -425,6 +482,20 @@ router_lo_ipv6_address assert mock_start_process.call_count == 0 +@patch("gso.cli.imports.time.sleep") +@patch("gso.cli.imports.start_process") +def test_import_switch_success(mock_start_process, mock_sleep, switch_data): + import_switches(switch_data()["path"]) + assert mock_start_process.call_count == 1 + + +@patch("gso.cli.imports.time.sleep") +@patch("gso.cli.imports.start_process") +def test_import_lan_switch_interconnect(mock_start_process, mock_sleep, lan_switch_interconnect_data): + import_lan_switch_interconnect(lan_switch_interconnect_data()["path"]) + assert mock_start_process.call_count == 1 + + @patch("gso.cli.imports.time.sleep") @patch("gso.cli.imports.start_process") def test_import_iptrunk_successful(mock_start_process, mock_sleep, iptrunk_data): diff --git a/test/fixtures/__init__.py b/test/fixtures/__init__.py index b140d22205a176541ed8868aefc6318ad722ae99..3f183fb2e8778f758d7bba96a459d1df29ac717b 100644 --- a/test/fixtures/__init__.py +++ b/test/fixtures/__init__.py @@ -1,5 +1,6 @@ from test.fixtures.edge_port_fixtures import edge_port_subscription_factory from test.fixtures.iptrunk_fixtures import iptrunk_side_subscription_factory, iptrunk_subscription_factory +from test.fixtures.lan_switch_interconnect_fixtures import lan_switch_interconnect_subscription_factory from test.fixtures.nren_l3_core_service_fixtures import ( bgp_session_subscription_factory, nren_access_port_factory, @@ -18,6 +19,7 @@ __all__ = [ "edge_port_subscription_factory", "iptrunk_side_subscription_factory", "iptrunk_subscription_factory", + "lan_switch_interconnect_subscription_factory", "nren_access_port_factory", "nren_l3_core_service_subscription_factory", "office_router_subscription_factory", diff --git a/test/fixtures/lan_switch_interconnect_fixtures.py b/test/fixtures/lan_switch_interconnect_fixtures.py new file mode 100644 index 0000000000000000000000000000000000000000..3f78deb4a9ddcf82e1db84088e08eedf685cf49d --- /dev/null +++ b/test/fixtures/lan_switch_interconnect_fixtures.py @@ -0,0 +1,88 @@ +from uuid import uuid4 + +import pytest +from orchestrator.db import db +from orchestrator.domain import SubscriptionModel +from orchestrator.types import SubscriptionLifecycle, UUIDstr + +from gso.products import ProductName +from gso.products.product_blocks.lan_switch_interconnect import ( + LanSwitchInterconnectRouterSideBlockInactive, + LanSwitchInterconnectSwitchSideBlockInactive, +) +from gso.products.product_types.lan_switch_interconnect import ( + ImportedLanSwitchInterconnectInactive, + LanSwitchInterconnectInactive, +) +from gso.services.subscriptions import get_product_id_by_name +from gso.utils.types.ip_address import AddressSpace, IPv4AddressType, IPv4NetworkType + + +@pytest.fixture() +def lan_switch_interconnect_subscription_factory( + faker, geant_partner, router_subscription_factory, switch_subscription_factory +): + def _create_subscription( + description: str | None = None, + partner: dict | None = None, + status: SubscriptionLifecycle | None = None, + start_date: str = "2024-10-30T02:12:22+33:33", + lan_switch_interconnect_description: str | None = None, + lan_switch_interconnect_ip_network: IPv4NetworkType | None = None, + address_space: AddressSpace | None = None, + minimum_links: int | 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, + router_side_ipv4_address: IPv4AddressType | None = None, + switch_side_switch: UUIDstr | None = None, + switch_side_ae_iface: str | None = None, + switch_side_ae_members: list[dict[str, str]] | None = None, + switch_side_ipv4_address: IPv4AddressType | None = None, + *, + is_imported: bool = True, + ) -> UUIDstr: + if partner is None: + partner = geant_partner + + if is_imported: + product_id = get_product_id_by_name(ProductName.LAN_SWITCH_INTERCONNECT) + subscription = LanSwitchInterconnectInactive.from_product_id(product_id, partner["patrner_id"]) + else: + product_id = get_product_id_by_name(ProductName.IMPORTED_LAN_SWITCH_INTERCONNECT) + subscription = ImportedLanSwitchInterconnectInactive.from_product_id(product_id, partner["partner_id"]) + + subscription.lan_switch_interconnect.lan_switch_interconnect_description = ( + lan_switch_interconnect_description or faker.sentence() + ) + subscription.lan_switch_interconnect.lan_switch_interconnect_ip_network = ( + lan_switch_interconnect_ip_network or faker.ipv4_network() + ) + subscription.lan_switch_interconnect.address_space = address_space or AddressSpace.PRIVATE + subscription.lan_switch_interconnect.minimum_links = minimum_links or 1 + subscription.lan_switch_interconnect.router_side = LanSwitchInterconnectRouterSideBlockInactive.new( + uuid4(), + node=router_side_node or router_subscription_factory(), + ae_iface=router_side_ae_iface or faker.network_interface(), + ae_members=router_side_ae_members or faker.link_members_nokia()[:2], + ipv4_address=router_side_ipv4_address or faker.ipv4(), + ) + subscription.lan_switch_interconnect.switch_side = LanSwitchInterconnectSwitchSideBlockInactive.new( + uuid4(), + switch=switch_side_switch or switch_subscription_factory(), + ae_iface=switch_side_ae_iface or faker.network_interface(), + ae_members=switch_side_ae_members or faker.link_members_juniper()[:2], + ipv4_address=switch_side_ipv4_address or faker.ipv4(), + ) + + subscription = SubscriptionModel.from_other_lifecycle(subscription, SubscriptionLifecycle.ACTIVE) + subscription.description = description or "Generated LAN Switch Interconnect" + subscription.start_date = start_date + subscription.status = status or SubscriptionLifecycle.ACTIVE + + subscription.save() + db.session.commit() + + return str(subscription.subscription_id) + + return _create_subscription diff --git a/test/fixtures/switch_fixtures.py b/test/fixtures/switch_fixtures.py index 2e9bd0cea86cfe3b762b6a735396784e281456c5..fa7f34d60fd020398455312aed84402afcd7a0c4 100644 --- a/test/fixtures/switch_fixtures.py +++ b/test/fixtures/switch_fixtures.py @@ -7,7 +7,7 @@ from pydantic_forms.types import UUIDstr from gso.products import ProductName from gso.products.product_blocks.switch import SwitchModel from gso.products.product_types.site import Site -from gso.products.product_types.switch import SwitchInactive +from gso.products.product_types.switch import ImportedSwitchInactive, SwitchInactive from gso.services.subscriptions import get_product_id_by_name from gso.utils.shared_enums import Vendor from gso.utils.types.ip_address import PortNumber @@ -35,7 +35,6 @@ def switch_subscription_factory(faker, geant_partner, site_subscription_factory) switch_subscription = SwitchInactive.from_product_id(product_id, partner["partner_id"]) else: product_id = get_product_id_by_name(ProductName.IMPORTED_SWITCH) - raise NotImplementedError switch_subscription = ImportedSwitchInactive.from_product_id(product_id, partner["partner_id"]) switch_subscription.switch.fqdn = fqdn or faker.domain_name(levels=4) 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 a4a0f5d12738a1b3b8495357e3c8ee1af0242781..7c28828fab86c7710a817daa7a01ef17e11fbf03 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 @@ -4,9 +4,9 @@ import pytest from orchestrator.types import SubscriptionLifecycle from gso.products import ProductName -from gso.products.product_blocks.lan_switch_interconnect import LanSwitchInterconnectAddressSpace from gso.products.product_types.lan_switch_interconnect import LanSwitchInterconnect from gso.services.subscriptions import get_product_id_by_name +from gso.utils.types.ip_address import AddressSpace from test.services.conftest import MockedNetboxClient from test.workflows import assert_complete, extract_state, run_workflow @@ -28,7 +28,7 @@ def _netbox_client_mock(): @pytest.fixture() def input_form_data(faker, router_subscription_factory, switch_subscription_factory): - def _generate_form_data(address_space: LanSwitchInterconnectAddressSpace): + def _generate_form_data(address_space: AddressSpace): return [ { "product": get_product_id_by_name(ProductName.LAN_SWITCH_INTERCONNECT), @@ -55,9 +55,7 @@ def input_form_data(faker, router_subscription_factory, switch_subscription_fact return _generate_form_data -@pytest.mark.parametrize( - "address_space", [LanSwitchInterconnectAddressSpace.PRIVATE, LanSwitchInterconnectAddressSpace.PUBLIC] -) +@pytest.mark.parametrize("address_space", [AddressSpace.PRIVATE, AddressSpace.PUBLIC]) @pytest.mark.workflow() def test_create_lan_switch_interconnect_success( address_space, diff --git a/test/workflows/lan_switch_interconnect/test_validate_lan_switch_interconnect.py b/test/workflows/lan_switch_interconnect/test_validate_lan_switch_interconnect.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391