diff --git a/Changelog.md b/Changelog.md index 371dcf4f9723110ce3b7f9d8da4bfb293a815e4b..a5131efa7c62f51f243fb36b935dd50ee8028323 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,9 @@ # Changelog +## [2.23] - 2024-11-05 +- Added new workflows and updated the products of Swich and LAN Swith Interconnect +- Upgraded orchestrator-core to 2.8.0 + ## [2.22] - 2024-10-31 - Added EdgePort, IAS and GEANT IP products and required workflows - Refactored pydantic models for maintainability diff --git a/docs/source/module/workflows/lan_switch_interconnect/create_imported_lan_switch_interconnect.rst b/docs/source/module/workflows/lan_switch_interconnect/create_imported_lan_switch_interconnect.rst new file mode 100644 index 0000000000000000000000000000000000000000..6900a2f8f6ee4ccb34ded8dc28d94a7f86091759 --- /dev/null +++ b/docs/source/module/workflows/lan_switch_interconnect/create_imported_lan_switch_interconnect.rst @@ -0,0 +1,6 @@ +``gso.workflows.lan_switch_interconnect.create_imported_lan_switch_interconnect`` +================================================================================= + +.. automodule:: gso.workflows.lan_switch_interconnect.create_imported_lan_switch_interconnect + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/lan_switch_interconnect/create_lan_switch_interconnect.rst b/docs/source/module/workflows/lan_switch_interconnect/create_lan_switch_interconnect.rst new file mode 100644 index 0000000000000000000000000000000000000000..9e1e9ce47de6bc5de2b1fc71eb1df168479db1f7 --- /dev/null +++ b/docs/source/module/workflows/lan_switch_interconnect/create_lan_switch_interconnect.rst @@ -0,0 +1,6 @@ +``gso.workflows.lan_switch_interconnect.create_lan_switch_interconnect`` +======================================================================== + +.. automodule:: gso.workflows.lan_switch_interconnect.create_lan_switch_interconnect + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/lan_switch_interconnect/import_lan_switch_interconnect.rst b/docs/source/module/workflows/lan_switch_interconnect/import_lan_switch_interconnect.rst new file mode 100644 index 0000000000000000000000000000000000000000..230944df9445c9a94b7a4d26a56eded75acc5dee --- /dev/null +++ b/docs/source/module/workflows/lan_switch_interconnect/import_lan_switch_interconnect.rst @@ -0,0 +1,6 @@ +``gso.workflows.lan_switch_interconnect.import_lan_switch_interconnect`` +======================================================================== + +.. automodule:: gso.workflows.lan_switch_interconnect.import_lan_switch_interconnect + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/lan_switch_interconnect/index.rst b/docs/source/module/workflows/lan_switch_interconnect/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..f8a8b4f8e7d185c300dd77ac382accdf8db462eb --- /dev/null +++ b/docs/source/module/workflows/lan_switch_interconnect/index.rst @@ -0,0 +1,19 @@ +``gso.workflows.lan_switch_interconnect`` +========================================= + +.. automodule:: gso.workflows.lan_switch_interconnect + :members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + :maxdepth: 2 + :titlesonly: + + create_imported_lan_switch_interconnect + create_lan_switch_interconnect + import_lan_switch_interconnect + terminate_lan_switch_interconnect + validate_lan_switch_interconnect diff --git a/docs/source/module/workflows/lan_switch_interconnect/terminate_lan_switch_interconnect.rst b/docs/source/module/workflows/lan_switch_interconnect/terminate_lan_switch_interconnect.rst new file mode 100644 index 0000000000000000000000000000000000000000..7575a60fe18911455a00e79081bd62ddfe97383e --- /dev/null +++ b/docs/source/module/workflows/lan_switch_interconnect/terminate_lan_switch_interconnect.rst @@ -0,0 +1,6 @@ +``gso.workflows.lan_switch_interconnect.terminate_lan_switch_interconnect`` +=========================================================================== + +.. automodule:: gso.workflows.lan_switch_interconnect.terminate_lan_switch_interconnect + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/lan_switch_interconnect/validate_lan_switch_interconnect.rst b/docs/source/module/workflows/lan_switch_interconnect/validate_lan_switch_interconnect.rst new file mode 100644 index 0000000000000000000000000000000000000000..17530cf9e81819770c478b64b06c1284a91cb4b3 --- /dev/null +++ b/docs/source/module/workflows/lan_switch_interconnect/validate_lan_switch_interconnect.rst @@ -0,0 +1,6 @@ +``gso.workflows.lan_switch_interconnect.validate_lan_switch_interconnect`` +========================================================================== + +.. automodule:: gso.workflows.lan_switch_interconnect.validate_lan_switch_interconnect + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/switch/activate_switch.rst b/docs/source/module/workflows/switch/activate_switch.rst new file mode 100644 index 0000000000000000000000000000000000000000..dc850fa583c452c1e47bd9a5c45dd0633e2cec78 --- /dev/null +++ b/docs/source/module/workflows/switch/activate_switch.rst @@ -0,0 +1,6 @@ +``gso.workflows.switch.activate_switch`` +======================================== + +.. automodule:: gso.workflows.switch.activate_switch + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/switch/create_imported_switch.rst b/docs/source/module/workflows/switch/create_imported_switch.rst new file mode 100644 index 0000000000000000000000000000000000000000..2dcd76641d8489becc4dd9e209e1035343b413fd --- /dev/null +++ b/docs/source/module/workflows/switch/create_imported_switch.rst @@ -0,0 +1,6 @@ +``gso.workflows.switch.create_imported_switch`` +=============================================== + +.. automodule:: gso.workflows.switch.create_imported_switch + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/switch/create_switch.rst b/docs/source/module/workflows/switch/create_switch.rst new file mode 100644 index 0000000000000000000000000000000000000000..c2c8d19563bb59c00d4053e0f3f6e0454777158f --- /dev/null +++ b/docs/source/module/workflows/switch/create_switch.rst @@ -0,0 +1,6 @@ +``gso.workflows.switch.create_switch`` +====================================== + +.. automodule:: gso.workflows.switch.create_switch + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/switch/import_switch.rst b/docs/source/module/workflows/switch/import_switch.rst new file mode 100644 index 0000000000000000000000000000000000000000..82f140766a082f4d31a825c973b00d74790c2fbf --- /dev/null +++ b/docs/source/module/workflows/switch/import_switch.rst @@ -0,0 +1,6 @@ +``gso.workflows.switch.import_switch`` +====================================== + +.. automodule:: gso.workflows.switch.import_switch + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/switch/index.rst b/docs/source/module/workflows/switch/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..160939de0474626ddb2c47778f959d3d0f5ce2e7 --- /dev/null +++ b/docs/source/module/workflows/switch/index.rst @@ -0,0 +1,20 @@ +``gso.workflows.switch`` +======================== + +.. automodule:: gso.workflows.switch + :members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + :maxdepth: 2 + :titlesonly: + + activate_switch + create_imported_switch + create_switch + import_switch + terminate_switch + validate_switch diff --git a/docs/source/module/workflows/switch/terminate_switch.rst b/docs/source/module/workflows/switch/terminate_switch.rst new file mode 100644 index 0000000000000000000000000000000000000000..15d3336f7f37c265c1ad21c0ae5c96f656853c37 --- /dev/null +++ b/docs/source/module/workflows/switch/terminate_switch.rst @@ -0,0 +1,6 @@ +``gso.workflows.switch.terminate_switch`` +========================================= + +.. automodule:: gso.workflows.switch.terminate_switch + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/switch/validate_switch.rst b/docs/source/module/workflows/switch/validate_switch.rst new file mode 100644 index 0000000000000000000000000000000000000000..40bbbf30d8a2ccea4924f7d9512bd162ef720b28 --- /dev/null +++ b/docs/source/module/workflows/switch/validate_switch.rst @@ -0,0 +1,6 @@ +``gso.workflows.switch.validate_switch`` +======================================== + +.. automodule:: gso.workflows.switch.validate_switch + :members: + :show-inheritance: 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 new file mode 100644 index 0000000000000000000000000000000000000000..8807ad742a497397f293389a53a587fd0b79eb3a --- /dev/null +++ b/gso/migrations/versions/2024-08-29_0e7e7d749617_add_switch_workflows.py @@ -0,0 +1,105 @@ +"""Add switch workflows. + +Revision ID: 0e7e7d749617 +Revises: 94d9de9246fe +Create Date: 2024-08-29 15:45:57.581710 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '0e7e7d749617' +down_revision = '94d9de9246fe' +branch_labels = None +depends_on = None + + +from orchestrator.migrations.helpers import create_workflow, delete_workflow + +new_workflows = [ + { + "name": "create_switch", + "target": "CREATE", + "description": "Create Switch", + "product_type": "Switch" + }, + { + "name": "activate_switch", + "target": "MODIFY", + "description": "Activate switch", + "product_type": "Switch" + }, + { + "name": "terminate_switch", + "target": "TERMINATE", + "description": "Terminate switch", + "product_type": "Switch" + }, + { + "name": "validate_switch", + "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" + } +] + + +def upgrade() -> None: + conn = op.get_bind() + for workflow in new_workflows: + create_workflow(conn, workflow) + + +def downgrade() -> None: + conn = op.get_bind() + for workflow in new_workflows: + delete_workflow(conn, workflow["name"]) 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 e2626de5c680bb983454ed32b523e61f1b78ad6d..945efc5a14ffb6f8e58b60aa375b91165f19616b 100644 --- a/gso/products/product_blocks/lan_switch_interconnect.py +++ b/gso/products/product_blocks/lan_switch_interconnect.py @@ -1,18 +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 - - -class LanSwitchInterconnectAddressSpace(strEnum): - """Types of LAN Switch Interconnect. Can be private or public.""" - - PRIVATE = "Private" - PUBLIC = "Public" +from gso.utils.types.ip_address import AddressSpace, IPv4AddressType, IPv4NetworkType class LanSwitchInterconnectInterfaceBlockInactive( @@ -20,7 +14,7 @@ class LanSwitchInterconnectInterfaceBlockInactive( lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="LanSwitchInterconnectInterfaceBlock", ): - """An inactive LAN Switch Interconnect interface.""" + """An inactive :term:`LAN` Switch Interconnect interface.""" interface_name: str | None = None interface_description: str | None = None @@ -29,7 +23,7 @@ class LanSwitchInterconnectInterfaceBlockInactive( class LanSwitchInterconnectInterfaceBlockProvisioning( LanSwitchInterconnectInterfaceBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING] ): - """A LAN Switch Interconnect interface that is being provisioned.""" + """A :term:`LAN` Switch Interconnect interface that is being provisioned.""" interface_name: str interface_description: str @@ -49,31 +43,34 @@ class LanSwitchInterconnectRouterSideBlockInactive( lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="LanSwitchInterconnectRouterSideBlock", ): - """An inactive LAN Switch Interconnect router side.""" + """An inactive :term:`LAN` Switch Interconnect router side.""" node: RouterBlockInactive ae_iface: str | None = None ae_members: LAGMemberList[LanSwitchInterconnectInterfaceBlockInactive] + ipv4_address: IPv4AddressType | None = None class LanSwitchInterconnectRouterSideBlockProvisioning( LanSwitchInterconnectRouterSideBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING] ): - """An LAN Switch Interconnect router side that is being provisioned.""" + """A :term:`LAN` Switch Interconnect router side that is being provisioned.""" node: RouterBlockProvisioning ae_iface: str | None = None ae_members: LAGMemberList[LanSwitchInterconnectInterfaceBlockProvisioning] # type: ignore[assignment] + ipv4_address: IPv4AddressType | None class LanSwitchInterconnectRouterSideBlock( LanSwitchInterconnectRouterSideBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE] ): - """An active LAN Switch Interconnect router side.""" + """An active :term:`LAN` Switch Interconnect router side.""" node: RouterBlock ae_iface: str ae_members: LAGMemberList[LanSwitchInterconnectInterfaceBlock] # type: ignore[assignment] + ipv4_address: IPv4AddressType | None class LanSwitchInterconnectSwitchSideBlockInactive( @@ -81,31 +78,34 @@ class LanSwitchInterconnectSwitchSideBlockInactive( lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="LanSwitchInterconnectSwitchSideBlock", ): - """An inactive LAN Switch Interconnect switch side.""" + """An inactive :term:`LAN` Switch Interconnect switch side.""" - node: SwitchBlockInactive + switch: SwitchBlockInactive ae_iface: str | None = None ae_members: LAGMemberList[LanSwitchInterconnectInterfaceBlockInactive] + ipv4_address: IPv4AddressType | None = None class LanSwitchInterconnectSwitchSideBlockProvisioning( LanSwitchInterconnectSwitchSideBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING] ): - """An LAN Switch Interconnect switch side that is being provisioned.""" + """A :term:`LAN` Switch Interconnect switch side that is being provisioned.""" - node: SwitchBlockProvisioning + switch: SwitchBlockProvisioning ae_iface: str | None = None ae_members: LAGMemberList[LanSwitchInterconnectInterfaceBlockProvisioning] # type: ignore[assignment] + ipv4_address: IPv4AddressType | None class LanSwitchInterconnectSwitchSideBlock( LanSwitchInterconnectSwitchSideBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE] ): - """An active LAN Switch Interconnect switch side.""" + """An active :term:`LAN` Switch Interconnect switch side.""" - node: SwitchBlock + switch: SwitchBlock ae_iface: str ae_members: LAGMemberList[LanSwitchInterconnectInterfaceBlock] # type: ignore[assignment] + ipv4_address: IPv4AddressType | None class LanSwitchInterconnectBlockInactive( @@ -113,10 +113,11 @@ class LanSwitchInterconnectBlockInactive( lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="LanSwitchInterconnectBlock", ): - """A LAN Switch Interconnect that's currently inactive, see :class:`LanSwitchInterconnectBlock`.""" + """A :term:`LAN` Switch Interconnect that's currently inactive, see :class:`LanSwitchInterconnectBlock`.""" lan_switch_interconnect_description: str | None = None - address_space: LanSwitchInterconnectAddressSpace | None = None + lan_switch_interconnect_ip_network: IPv4NetworkType | None = None + address_space: AddressSpace | None = None minimum_links: int | None = None router_side: LanSwitchInterconnectRouterSideBlockInactive switch_side: LanSwitchInterconnectSwitchSideBlockInactive @@ -125,25 +126,28 @@ class LanSwitchInterconnectBlockInactive( class LanSwitchInterconnectBlockProvisioning( LanSwitchInterconnectBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING] ): - """A LAN Switch Interconnect that's currently being provisioned, see :class:`LanSwitchInterconnectBlock`.""" + """A :term:`LAN` Switch Interconnect that's currently being provisioned, see :class:`LanSwitchInterconnectBlock`.""" lan_switch_interconnect_description: str | None = None - address_space: LanSwitchInterconnectAddressSpace | None = None + lan_switch_interconnect_ip_network: IPv4NetworkType | None + address_space: AddressSpace | None = None minimum_links: int | None = None router_side: LanSwitchInterconnectRouterSideBlockProvisioning switch_side: LanSwitchInterconnectSwitchSideBlockProvisioning class LanSwitchInterconnectBlock(LanSwitchInterconnectBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): - """A LAN Switch Interconnect that's currently deployed in the network.""" + """A :term:`LAN` Switch Interconnect that's currently deployed in the network.""" - #: A human-readable description of this LAN Switch Interconnect. + #: A human-readable description of this :term:`LAN` Switch Interconnect. lan_switch_interconnect_description: str - #: The address space of the VLAN Switch Interconnect. It can be private or public. - address_space: LanSwitchInterconnectAddressSpace - #: The minimum amount of links the LAN Switch Interconnect should consist of. + #: 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: AddressSpace + #: The minimum amount of links the :term:`LAN` Switch Interconnect should consist of. minimum_links: int - #: The router side of the LAN Switch Interconnect. + #: The router side of the :term:`LAN` Switch Interconnect. router_side: LanSwitchInterconnectRouterSideBlock - #: The switch side of the LAN Switch Interconnect. + #: The switch side of the :term:`LAN` Switch Interconnect. switch_side: LanSwitchInterconnectSwitchSideBlock diff --git a/gso/products/product_blocks/pop_vlan.py b/gso/products/product_blocks/pop_vlan.py index 191e7255951f24f9514a592989546774d17f2185..7e60aa2e2c762bd3d24977b3ddceae8a1b0f89f4 100644 --- a/gso/products/product_blocks/pop_vlan.py +++ b/gso/products/product_blocks/pop_vlan.py @@ -1,4 +1,4 @@ -"""Pop VLAN product block that has all parameters of a subscription throughout its lifecycle.""" +"""Pop :term:`VLAN` product block that has all parameters of a subscription throughout its lifecycle.""" from ipaddress import IPv4Network, IPv6Network from typing import Annotated, TypeVar @@ -32,7 +32,7 @@ PortList = Annotated[list[T], AfterValidator(validate_unique_list), Doc("A list class PopVlanPortBlockInactive( ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="PopVlanPortBlock" ): - """An inactive Pop VLAN port.""" + """An inactive Pop :term:`VLAN` port.""" port_name: str | None = None port_description: str | None = None @@ -40,15 +40,15 @@ class PopVlanPortBlockInactive( class PopVlanPortBlockProvisioning(PopVlanPortBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): - """A Pop VLAN port that is being provisioned.""" + """A Pop :term:`VLAN` port that is being provisioned.""" - port_name: str | None = None - port_description: str | None = None - tagged: bool | None = None + port_name: str | None + port_description: str | None + tagged: bool | None class PopVlanPortBlock(PopVlanPortBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): - """An active Pop VLAN port.""" + """An active Pop :term:`VLAN` port.""" port_name: str port_description: str @@ -60,10 +60,10 @@ class PopVlanBlockInactive( lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="PopVlanBlock", ): - """A Pop VLAN that's currently inactive, see :class:`PopVlanBlock`.""" + """A Pop :term:`VLAN` that's currently inactive, see :class:`PopVlanBlock`.""" vlan_id: int - pop_vlan_description: str | None + pop_vlan_description: str | None = None lan_switch_interconnect: LanSwitchInterconnectBlockInactive ports: PortList[PopVlanPortBlockProvisioning] layer_preference: LayerPreference @@ -72,31 +72,31 @@ class PopVlanBlockInactive( class PopVlanBlockProvisioning(PopVlanBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): - """A Pop VLAN that's currently being provisioned, see :class:`PopVlanBlock`.""" + """A Pop :term:`VLAN` that's currently being provisioned, see :class:`PopVlanBlock`.""" vlan_id: int - pop_vlan_description: str | None = None + pop_vlan_description: str | None lan_switch_interconnect: LanSwitchInterconnectBlockProvisioning ports: PortList[PopVlanPortBlockProvisioning] layer_preference: LayerPreference - ipv4_network: IPv4Network | None = None - ipv6_network: IPv6Network | None = None + ipv4_network: IPv4Network | None + ipv6_network: IPv6Network | None class PopVlanBlock(PopVlanBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): - """A Pop VLAN that's currently deployed in the network.""" + """A Pop :term:`VLAN` that's currently deployed in the network.""" - #: The VLAN ID of the Pop VLAN. + #: The :term:`VLAN` ID of the Pop :term:`VLAN`. vlan_id: int - #: The description of the Pop VLAN. + #: The description of the Pop :term:`VLAN`. pop_vlan_description: str - #: The LAN Switch Interconnect that this Pop VLAN is connected to. + #: The :term:`LAN` Switch Interconnect that this Pop :term:`VLAN` is connected to. lan_switch_interconnect: LanSwitchInterconnectBlock - #: The ports of the Pop VLAN. + #: The ports of the Pop :term:`VLAN`. ports: PortList[PopVlanPortBlock] # type: ignore[assignment] - #: The level of the layer preference for the Pop VLAN (L2 or L3). + #: The level of the layer preference for the Pop :term:`VLAN` (L2 or L3). layer_preference: LayerPreference - #: IPv4 network for the Pop VLAN if layer preference is L3. - ipv4_network: IPv4Network | None = None - #: IPv6 network for the Pop VLAN if layer preference is L3. - ipv6_network: IPv6Network | None = None + #: IPv4 network for the Pop :term:`VLAN` if layer preference is L3. + ipv4_network: IPv4Network | None + #: IPv6 network for the Pop :term:`VLAN` if layer preference is L3. + ipv6_network: IPv6Network | None diff --git a/gso/products/product_blocks/switch.py b/gso/products/product_blocks/switch.py index bdf2b4657f8d8674a5c4e19c192f455ef1e31524..790807ad058c6dece6eb1cab7dd055ae9bac96fd 100644 --- a/gso/products/product_blocks/switch.py +++ b/gso/products/product_blocks/switch.py @@ -26,9 +26,9 @@ class SwitchBlockInactive( ): """A switch that's being currently inactive. See :class:`SwitchBlock`.""" - switch_hostname: str | None = None - switch_ts_port: PortNumber | None = None - switch_site: SiteBlockInactive | None = None + fqdn: str | None = None + ts_port: PortNumber | None = None + site: SiteBlockInactive | None = None switch_vendor: Vendor | None = None switch_model: SwitchModel | None = None @@ -36,23 +36,23 @@ class SwitchBlockInactive( class SwitchBlockProvisioning(SwitchBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): """A switch that's being provisioned. See :class:`SwitchBlock`.""" - switch_hostname: str - switch_ts_port: PortNumber - switch_site: SiteBlockProvisioning + fqdn: str + ts_port: PortNumber + site: SiteBlockProvisioning switch_vendor: Vendor - switch_model: SwitchModel | None = None + switch_model: SwitchModel class SwitchBlock(SwitchBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): """A switch that's currently deployed in the network.""" - #: The hostname of the switch. - switch_hostname: str + #: The :term:`FQDN` of the switch. + fqdn: str #: The port of the terminal server that this switch is connected to. Used to offer out of band access. - switch_ts_port: PortNumber + ts_port: PortNumber #: The :class:`Site` that this switch resides in. Both physically and computationally. - switch_site: SiteBlock + site: SiteBlock #: The vendor of the switch. switch_vendor: Vendor #: The model of the switch. - switch_model: SwitchModel | None = None + switch_model: SwitchModel 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..3b91167fe978a863a133abe75c68b275a1b028fb 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 = "PUBLIC" diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py index 5a6b9d082339add5f4890d54b3654228a989d7de..362f96ab161f8d15847185ebb8355a6df1a321e8 100644 --- a/gso/workflows/__init__.py +++ b/gso/workflows/__init__.py @@ -17,9 +17,12 @@ WF_USABLE_MAP.update({ "deploy_twamp": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE], "modify_trunk_interface": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE], "activate_iptrunk": [SubscriptionLifecycle.PROVISIONING], + "activate_switch": [SubscriptionLifecycle.PROVISIONING], "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], @@ -50,6 +53,32 @@ LazyWorkflowInstance("gso.workflows.router.validate_router", "validate_router") LazyWorkflowInstance("gso.workflows.router.promote_p_to_pe", "promote_p_to_pe") LazyWorkflowInstance("gso.workflows.router.modify_kentik_license", "modify_router_kentik_license") +# Switch workflows +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" +) +LazyWorkflowInstance( + "gso.workflows.lan_switch_interconnect.create_imported_lan_switch_interconnect", + "create_imported_lan_switch_interconnect", +) +LazyWorkflowInstance( + "gso.workflows.lan_switch_interconnect.import_lan_switch_interconnect", "import_lan_switch_interconnect" +) + # Site workflows LazyWorkflowInstance("gso.workflows.site.create_site", "create_site") LazyWorkflowInstance("gso.workflows.site.modify_site", "modify_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..1439c2fe7572c7fe89cf0e2c9fb4fcc4164b5de2 --- /dev/null +++ b/gso/workflows/lan_switch_interconnect/create_imported_lan_switch_interconnect.py @@ -0,0 +1,100 @@ +"""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 gso.cli.imports import LanSwitchInterconnectRouterSideImportModel, LanSwitchInterconnectSwitchSideImportModel +from gso.products import ProductName +from gso.products.product_blocks.lan_switch_interconnect import ( + LanSwitchInterconnectInterfaceBlockInactive, + LanSwitchInterconnectRouterSideBlockInactive, + LanSwitchInterconnectSwitchSideBlockInactive, +) +from gso.products.product_types.lan_switch_interconnect import ImportedLanSwitchInterconnectInactive +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.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, + 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.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 + + router_block = Router.from_subscription(router_side.pop("node")).router + router_side_interfaces = [ + LanSwitchInterconnectInterfaceBlockInactive.new(uuid4(), **ae_member) + for ae_member in router_side.pop("ae_members") + ] + subscription.lan_switch_interconnect.router_side = LanSwitchInterconnectRouterSideBlockInactive.new( + uuid4(), **router_side, node=router_block, ae_members=router_side_interfaces + ) + + switch_block = Switch.from_subscription(switch_side.pop("switch")).switch + switch_side_interfaces = [ + LanSwitchInterconnectInterfaceBlockInactive.new(uuid4(), **ae_member) + for ae_member in switch_side.pop("ae_members") + ] + subscription.lan_switch_interconnect.switch_side = LanSwitchInterconnectSwitchSideBlockInactive.new( + uuid4(), **switch_side, switch=switch_block, ae_members=switch_side_interfaces + ) + + 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 973cf1d88495b4343e390c258d16c3c9f78f65c9..eeec84e17d08e53cf3bf9fcf3531f38314772c11 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,7 +35,9 @@ 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 +from gso.workflows.shared import create_summary_form def _initial_input_form(product_name: str) -> FormGenerator: @@ -44,16 +45,17 @@ def _initial_input_form(product_name: str) -> FormGenerator: model_config = ConfigDict(title=product_name) tt_number: TTNumber + 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 vlan_id: ReadOnlyField(111, default_type=int) # type: ignore[valid-type] - user_input = yield CreateLANSwitchInterconnectForm - router = Router.from_subscription(user_input.router_side) + initial_input = yield CreateLANSwitchInterconnectForm + router = Router.from_subscription(initial_input.router_side) if router.router.vendor == Vendor.NOKIA: @@ -66,19 +68,19 @@ def _initial_input_form(product_name: str) -> FormGenerator: router_side_ae_member_list = Annotated[ list[NokiaLAGMemberA], AfterValidator(validate_interface_names_are_unique), - Len(min_length=user_input.minimum_link_count), + Len(min_length=initial_input.minimum_link_count), ] else: router_side_ae_member_list = Annotated[ # type: ignore[assignment, misc] list[JuniperLAGMember], AfterValidator(validate_interface_names_are_unique), - Len(min_length=user_input.minimum_link_count), + Len(min_length=initial_input.minimum_link_count), ] class InterconnectRouterSideForm(FormPage): model_config = ConfigDict(title="Please enter interface names and descriptions for the router side.") - router_side_iface: available_lags_choices(user_input.router_side) or JuniperAEInterface # type: ignore[valid-type] + router_side_iface: available_lags_choices(initial_input.router_side) or JuniperAEInterface or str # type: ignore[valid-type] router_side_ae_members: router_side_ae_member_list router_side_input = yield InterconnectRouterSideForm @@ -99,8 +101,24 @@ def _initial_input_form(product_name: str) -> FormGenerator: switch_side_ae_members: switch_side_ae_member_list switch_side_input = yield InterconnectSwitchSideForm + user_input = initial_input.model_dump() | router_side_input.model_dump() | switch_side_input.model_dump() + summary_fields = [ + "tt_number", + "partner", + "router_side", + "switch_side", + "address_space", + "description", + "minimum_link_count", + "vlan_id", + "router_side_iface", + "router_side_ae_members", + "switch_side_iface", + "switch_side_ae_members", + ] + yield from create_summary_form(user_input, product_name, summary_fields) - return user_input.model_dump() | router_side_input.model_dump() | switch_side_input.model_dump() + return user_input @step("Create subscription") @@ -108,14 +126,14 @@ def create_subscription(product: UUIDstr, partner: str) -> State: """Create a new subscription object in the database.""" subscription = LanSwitchInterconnectInactive.from_product_id(product, get_partner_by_name(partner)["partner_id"]) - return {"subscription": subscription} + return {"subscription": subscription, "subscription_id": subscription.subscription_id} @step("Initialize subscription") def initialize_subscription( subscription: LanSwitchInterconnectInactive, description: str, - address_space: LanSwitchInterconnectAddressSpace, + address_space: AddressSpace, minimum_link_count: int, router_side: UUIDstr, router_side_iface: JuniperPhyInterface, @@ -134,7 +152,7 @@ def initialize_subscription( subscription.lan_switch_interconnect.router_side.ae_members.append( LanSwitchInterconnectInterfaceBlockInactive.new(subscription_id=uuid4(), **member) ) - subscription.lan_switch_interconnect.switch_side.node = Switch.from_subscription(switch_side).switch + subscription.lan_switch_interconnect.switch_side.switch = Switch.from_subscription(switch_side).switch subscription.lan_switch_interconnect.switch_side.ae_iface = switch_side_iface for member in switch_side_ae_members: subscription.lan_switch_interconnect.switch_side.ae_members.append( @@ -145,7 +163,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/router/modify_kentik_license.py b/gso/workflows/router/modify_kentik_license.py index bd97b9c04605ed475fb7800a6cf965b5c3c895a1..e1a87021483bbf05017be50129064c7d4ed78894 100644 --- a/gso/workflows/router/modify_kentik_license.py +++ b/gso/workflows/router/modify_kentik_license.py @@ -33,7 +33,7 @@ def _initial_input_form(subscription_id: UUIDstr) -> FormGenerator: ) class ModifyKentikLicenseForm(FormPage): - new_plan_id: available_kentik_plans # type: ignore[valid-type] + new_plan_id: available_kentik_plans or str # type: ignore[valid-type] @model_validator(mode="before") def router_must_be_nokia_p(cls, data: Any) -> Any: diff --git a/gso/workflows/switch/__init__.py b/gso/workflows/switch/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..acc604b8258770eaa9d31d3568811e5b7f81940e --- /dev/null +++ b/gso/workflows/switch/__init__.py @@ -0,0 +1 @@ +"""Workflows for switches.""" diff --git a/gso/workflows/switch/activate_switch.py b/gso/workflows/switch/activate_switch.py new file mode 100644 index 0000000000000000000000000000000000000000..1d0d7113764c4a2732526fd7c2123d85fe39675a --- /dev/null +++ b/gso/workflows/switch/activate_switch.py @@ -0,0 +1,50 @@ +"""Workflow for activating a switch, making it available to other subscriptions.""" + +from orchestrator.config.assignee import Assignee +from orchestrator.forms import FormPage +from orchestrator.forms.validators import Label +from orchestrator.targets import Target +from orchestrator.types import FormGenerator, SubscriptionLifecycle, UUIDstr +from orchestrator.workflow import StepList, begin, done, inputstep, workflow +from orchestrator.workflows.steps import resync, set_status, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form + +from gso.products.product_types.switch import Switch + + +def _initial_input_form(subscription_id: UUIDstr) -> FormGenerator: + switch = Switch.from_subscription(subscription_id) + + class ActivateSwitchForm(FormPage): + info_label: Label = "Start approval process for switch activation." + + yield ActivateSwitchForm + return {"subscription": switch} + + +@inputstep("Verify checklist completion", assignee=Assignee.SYSTEM) +def verify_complete_checklist() -> FormGenerator: + """Ask the operator to provide a link to the completed checklist in SharePoint.""" + + class VerifyCompleteForm(FormPage): + info_label: Label = "Please enter URL to the completed checklist in SharePoint. Then continue this workflow." + checklist_url: str = "" + + user_input = yield VerifyCompleteForm + return {"checklist_url": user_input.model_dump()["checklist_url"]} + + +@workflow( + "Activate switch", initial_input_form=(wrap_modify_initial_input_form(_initial_input_form)), target=Target.MODIFY +) +def activate_switch() -> StepList: + """Take a switch and move it from a `PROVISIONING` to an `ACTIVE` state.""" + return ( + begin + >> store_process_subscription(Target.MODIFY) + >> unsync + >> verify_complete_checklist + >> set_status(SubscriptionLifecycle.ACTIVE) + >> resync + >> done + ) 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/create_switch.py b/gso/workflows/switch/create_switch.py new file mode 100644 index 0000000000000000000000000000000000000000..0074c8e4cc870a3afaffd8ac704759841dcf9a52 --- /dev/null +++ b/gso/workflows/switch/create_switch.py @@ -0,0 +1,219 @@ +"""A creation workflow for adding a new switch to the subscription database.""" + +from typing import Self + +from orchestrator.config.assignee import Assignee +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, inputstep, 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 ConfigDict, model_validator +from pydantic_forms.validators import Label, ReadOnlyField + +from gso.products.product_blocks.switch import SwitchModel +from gso.products.product_types.site import Site +from gso.products.product_types.switch import SwitchInactive, SwitchProvisioning +from gso.services import infoblox +from gso.services.lso_client import LSOState, lso_interaction +from gso.services.partners import get_partner_by_name +from gso.services.sharepoint import SharePointClient +from gso.settings import load_oss_params +from gso.utils.helpers import active_site_selector, generate_fqdn +from gso.utils.shared_enums import Vendor +from gso.utils.types.ip_address import PortNumber +from gso.utils.types.tt_number import TTNumber +from gso.utils.workflow_steps import prompt_sharepoint_checklist_url +from gso.workflows.shared import create_summary_form + + +def _initial_input_form_generator(product_name: str) -> FormGenerator: + """Input form for creating a new Switch.""" + + class CreateSwitchForm(FormPage): + model_config = ConfigDict(title=product_name) + + tt_number: TTNumber + partner: ReadOnlyField("GEANT", default_type=str) # type: ignore[valid-type] + switch_site: active_site_selector() or str # type: ignore[valid-type] + hostname: str + ts_port: PortNumber + vendor: ReadOnlyField(Vendor.JUNIPER, default_type=Vendor) # type: ignore[valid-type] + model: ReadOnlyField(SwitchModel.EX3400, default_type=SwitchModel) # type: ignore[valid-type] + + @model_validator(mode="after") + def hostname_must_be_available(self) -> Self: + if not self.switch_site: + msg = "Please select a site before setting the hostname." + raise ValueError(msg) + + selected_site = Site.from_subscription(self.switch_site).site + input_fqdn = generate_fqdn(self.hostname, selected_site.site_name, selected_site.site_country_code) + if not infoblox.hostname_available(input_fqdn): + msg = f'FQDN "{input_fqdn}" is not available.' + raise ValueError(msg) + + return self + + input_form = yield CreateSwitchForm + user_input = input_form.model_dump() + summary_fields = ["tt_number", "partner", "switch_site", "hostname", "ts_port", "vendor", "model"] + yield from create_summary_form(user_input, product_name, summary_fields) + + return user_input + + +@step("Create subscription") +def create_subscription(product: UUIDstr, partner: str) -> State: + """Create a new subscription object.""" + subscription = SwitchInactive.from_product_id(product, get_partner_by_name(partner)["partner_id"]) + + return {"subscription": subscription, "subscription_id": subscription.subscription_id} + + +@step("Initialize subscription") +def initialize_subscription( + subscription: SwitchInactive, + switch_site: str, + ts_port: PortNumber, + vendor: Vendor, + model: SwitchModel, + hostname: str, +) -> State: + """Initialize the subscription with user input.""" + subscription.switch.site = Site.from_subscription(switch_site).site + subscription.switch.fqdn = generate_fqdn( + hostname, subscription.switch.site.site_name, subscription.switch.site.site_country_code + ) + subscription.switch.ts_port = ts_port + subscription.switch.switch_vendor = vendor + subscription.switch.switch_model = model + subscription.description = f"Switch {subscription.switch.fqdn}" + + return {"subscription": subscription} + + +@step("[DRY RUN] Deploy base config") +def deploy_base_config_dry(subscription: dict, tt_number: str, process_id: UUIDstr) -> LSOState: + """Perform a dry run of provisioning base config on a switch.""" + extra_vars = { + "subscription_json": subscription, + "dry_run": True, + "verb": "deploy", + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Deploy base config", + } + + return { + "playbook_name": "switch_base_config.yaml", + "extra_vars": extra_vars, + "inventory": {"all": {"hosts": {subscription["switch"]["fqdn"]: None}}}, + } + + +@step("[FOR REAL] Deploy base config") +def deploy_base_config_real(subscription: dict, tt_number: str, process_id: UUIDstr) -> LSOState: + """Provision base config on a switch.""" + extra_vars = { + "subscription_json": subscription, + "dry_run": False, + "verb": "deploy", + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Deploy base config", + } + + return { + "playbook_name": "switch_base_config.yaml", + "extra_vars": extra_vars, + "inventory": {"all": {"hosts": {subscription["switch"]["fqdn"]: None}}}, + } + + +@inputstep("Prompt for console login", assignee=Assignee.SYSTEM) +def prompt_console_login() -> FormGenerator: + """Wait for confirmation from an operator that console login is possible.""" + + class ConsoleLoginPage(FormPage): + model_config = ConfigDict(title="Please confirm before continuing") + + info_label: Label = "Please confirm you are able to log in to the switch using out of band connectivity." + + yield ConsoleLoginPage + return {} + + +@inputstep("Prompt IMS insertion", assignee=Assignee.SYSTEM) +def prompt_insert_in_ims() -> FormGenerator: + """Wait for confirmation from an operator that the switch has been inserted in IMS.""" + + class IMSPrompt(FormPage): + model_config = ConfigDict(title="Update IMS mediation server") + + info_label_1: Label = "Insert the switch into IMS." + info_label_2: Label = "Once this is done, press submit to continue the workflow." + + yield IMSPrompt + return {} + + +@step("Create Netbox device") +def create_netbox_device() -> State: + """Add the switch as a new device in Netbox.""" + return {"netbox_device": "Not implemented."} + + +@step("Run post-deployment checks") +def run_post_deploy_checks(subscription: dict) -> LSOState: + """Workflow step for running checks after installing base config.""" + return { + "playbook_name": "switch_base_config_checks.yaml", + "extra_vars": {"subscription_json": subscription}, + "inventory": {"all": {"hosts": {subscription["switch"]["fqdn"]: None}}}, + } + + +@step("Create a new SharePoint checklist") +def create_new_sharepoint_checklist(subscription: SwitchProvisioning, tt_number: str, process_id: UUIDstr) -> State: + """Create a new checklist in SharePoint for approving this router.""" + new_list_item_url = SharePointClient().add_list_item( + list_name="switch", + fields={ + "Title": subscription.switch.fqdn, + "TT_NUMBER": tt_number, + "GAP_PROCESS_URL": f"{load_oss_params().GENERAL.public_hostname}/workflows/{process_id}", + }, + ) + + return {"checklist_url": new_list_item_url} + + +@workflow( + "Create Switch", + initial_input_form=wrap_create_initial_input_form(_initial_input_form_generator), + target=Target.CREATE, +) +def create_switch() -> StepList: + """Create a new Switch. + + * Create a subscription object in the service database + * Deploy base configuration on the switch + * Add the switch to Netbox + * Run a check playbook after deploying base configuration + * Create a new checklist in SharePoint + """ + return ( + begin + >> create_subscription + >> store_process_subscription(Target.CREATE) + >> initialize_subscription + >> lso_interaction(deploy_base_config_dry) + >> lso_interaction(deploy_base_config_real) + >> prompt_console_login + >> prompt_insert_in_ims + >> create_netbox_device + >> lso_interaction(run_post_deploy_checks) + >> set_status(SubscriptionLifecycle.PROVISIONING) + >> create_new_sharepoint_checklist + >> prompt_sharepoint_checklist_url + >> 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 new file mode 100644 index 0000000000000000000000000000000000000000..da74b82cfa231f28b6c812cfcd852222bf343d73 --- /dev/null +++ b/gso/workflows/switch/terminate_switch.py @@ -0,0 +1,69 @@ +"""Workflow for terminating a switch.""" + +from orchestrator import begin, done, workflow +from orchestrator.forms import FormPage +from orchestrator.targets import Target +from orchestrator.types import SubscriptionLifecycle, UUIDstr +from orchestrator.workflow import StepList, step +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.switch import Switch +from gso.services.infoblox import delete_host_by_fqdn +from gso.services.netbox_client import NetboxClient +from gso.utils.types.tt_number import TTNumber + + +def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator: + """Input form to confirm that this switch indeed must be terminated.""" + switch = Switch.from_subscription(subscription_id) + + class TerminateForm(FormPage): + if switch.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": switch} + + +@step("Remove switch from Netbox") +def remove_device_from_netbox(subscription: Switch) -> None: + """Remove the switch from Netbox.""" + NetboxClient().delete_device(subscription.switch.fqdn) + + +@step("Remove switch from IPAM") +def remove_device_from_ipam(subscription: Switch) -> None: + """Remove the switch from :term:`IPAM`.""" + delete_host_by_fqdn(subscription.switch.fqdn) + + +@workflow( + "Terminate switch", + initial_input_form=wrap_modify_initial_input_form(_input_form_generator), + target=Target.TERMINATE, +) +def terminate_switch() -> StepList: + """Terminate a switch subscription. + + * Remove the switch from Netbox + * Mark the service as terminated in the service database + """ + return ( + begin + >> store_process_subscription(Target.TERMINATE) + >> unsync + >> remove_device_from_netbox + >> remove_device_from_ipam + >> set_status(SubscriptionLifecycle.TERMINATED) + >> resync + >> done + ) diff --git a/gso/workflows/switch/validate_switch.py b/gso/workflows/switch/validate_switch.py new file mode 100644 index 0000000000000000000000000000000000000000..17711d17188a293302d87acb9db957813b4c3739 --- /dev/null +++ b/gso/workflows/switch/validate_switch.py @@ -0,0 +1,49 @@ +"""Validation workflow for switch 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.products.product_types.switch import Switch +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 if it is not found.""" + NetboxClient().get_device_by_name(subscription.switch.fqdn) + + +@step("Check base config for drift") +def verify_base_config(subscription: dict[str, Any]) -> LSOState: + """Workflow step for running a playbook that checks whether base config has drifted.""" + 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( + "Validate switch subscription", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None)) +) +def validate_switch() -> StepList: + """Validate an existing, active switch subscription.""" + return ( + begin + >> store_process_subscription(Target.SYSTEM) + >> unsync + >> check_netbox_device + >> anonymous_lso_interaction(verify_base_config) + >> resync + >> done + ) diff --git a/requirements.txt b/requirements.txt index 0ccb4d649c6ee9a30d3a633ab451747097e0d933..2488de134a518a88436df822e7a57e5519ee8935 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -orchestrator-core==2.7.6 +orchestrator-core==2.8.0 requests==2.31.0 infoblox-client~=0.6.0 pycountry==23.12.11 diff --git a/setup.py b/setup.py index 3ec7a018f534de6d21b39ba0c786eba2e04042ca..bf6eb267a561df5a72e8c986af8d7981bb40a616 100644 --- a/setup.py +++ b/setup.py @@ -4,14 +4,14 @@ from setuptools import find_packages, setup setup( name="geant-service-orchestrator", - version="2.22", + version="2.23", author="GÉANT Orchestration and Automation Team", author_email="goat@geant.org", description="GÉANT Service Orchestrator", url="https://gitlab.software.geant.org/goat/gap/geant-service-orchestrator", packages=find_packages(), install_requires=[ - "orchestrator-core==2.7.6", + "orchestrator-core==2.8.0", "requests==2.31.0", "infoblox-client~=0.6.0", "pycountry==23.12.11", 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/conftest.py b/test/conftest.py index 981768655dfde2eaf4700853488e2a2c6bedb049..8c8d246c5bd97c16ef7f8db09752c2dca0ffd804 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -40,6 +40,7 @@ from test.fixtures import ( # noqa: F401 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, @@ -48,6 +49,7 @@ from test.fixtures import ( # noqa: F401 service_binding_port_factory, site_subscription_factory, super_pop_switch_subscription_factory, + switch_subscription_factory, ) logging.getLogger("faker.factory").setLevel(logging.WARNING) @@ -108,7 +110,15 @@ class FakerProvider(BaseProvider): return self.generator.random_int(min=1, max=128) def network_interface(self) -> str: - return self.generator.numerify("ge-@#/@#/@#") + interface = self.generator.random_choices(elements=("ge", "et", "xe"))[0] + number = self.generator.numerify("-%/%/%") + return f"{interface}{number}" + + def juniper_ae_interface_name(self) -> str: + return self.generator.numerify("ae@#") + + def nokia_lag_interface_name(self) -> str: + return self.generator.numerify("lag-@#") def link_members_juniper(self) -> LAGMemberList[LAGMember]: iface_amount = self.generator.random_int(min=2, max=5) diff --git a/test/fixtures/__init__.py b/test/fixtures/__init__.py index 0b69c5fb506e854a84e6e0e908826fcc250ec08e..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, @@ -11,12 +12,14 @@ from test.fixtures.opengear_fixtures import opengear_subscription_factory from test.fixtures.router_fixtures import router_subscription_factory from test.fixtures.site_fixtures import site_subscription_factory from test.fixtures.super_pop_switch_fixtures import super_pop_switch_subscription_factory +from test.fixtures.switch_fixtures import switch_subscription_factory __all__ = [ "bgp_session_subscription_factory", "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", @@ -25,4 +28,5 @@ __all__ = [ "service_binding_port_factory", "site_subscription_factory", "super_pop_switch_subscription_factory", + "switch_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..fe0cb87f304b7eb04991bedcb5ad893d743b1483 --- /dev/null +++ b/test/fixtures/lan_switch_interconnect_fixtures.py @@ -0,0 +1,106 @@ +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 ( + LanSwitchInterconnectInterfaceBlockInactive, + LanSwitchInterconnectRouterSideBlockInactive, + LanSwitchInterconnectSwitchSideBlockInactive, +) +from gso.products.product_types.lan_switch_interconnect import ( + ImportedLanSwitchInterconnectInactive, + LanSwitchInterconnectInactive, +) +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.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 | None = "2024-01-01T10:20:30+01:02", + 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 | None = 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["partner_id"]) + else: + product_id = get_product_id_by_name(ProductName.IMPORTED_LAN_SWITCH_INTERCONNECT) + subscription = ImportedLanSwitchInterconnectInactive.from_product_id(product_id, partner["partner_id"]) + + router_side_ae_members = router_side_ae_members or [ + LanSwitchInterconnectInterfaceBlockInactive.new( + uuid4(), interface_name=faker.network_interface(), interface_description=faker.sentence() + ) + for _ in range(2) + ] + switch_side_ae_members = switch_side_ae_members or [ + LanSwitchInterconnectInterfaceBlockInactive.new( + uuid4(), interface_name=faker.network_interface(), interface_description=faker.sentence() + ) + for _ in range(2) + ] + + 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.from_subscription(router_subscription_factory()).router, + ae_iface=router_side_ae_iface or faker.network_interface(), + ae_members=router_side_ae_members, + ipv4_address=router_side_ipv4_address or faker.ipv4(), + ) + subscription.lan_switch_interconnect.switch_side = LanSwitchInterconnectSwitchSideBlockInactive.new( + uuid4(), + switch=switch_side_switch or Switch.from_subscription(switch_subscription_factory()).switch, + ae_iface=switch_side_ae_iface or faker.network_interface(), + ae_members=switch_side_ae_members, + ipv4_address=switch_side_ipv4_address or faker.ipv4(), + ) + + subscription = SubscriptionModel.from_other_lifecycle(subscription, SubscriptionLifecycle.ACTIVE) + subscription.insync = True + subscription.description = description or faker.sentence() + subscription.start_date = start_date + + if status: + subscription.status = status + + 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 new file mode 100644 index 0000000000000000000000000000000000000000..fa7f34d60fd020398455312aed84402afcd7a0c4 --- /dev/null +++ b/test/fixtures/switch_fixtures.py @@ -0,0 +1,59 @@ +import pytest +from orchestrator.db import db +from orchestrator.domain import SubscriptionModel +from orchestrator.types import SubscriptionLifecycle +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 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 + + +@pytest.fixture() +def switch_subscription_factory(faker, geant_partner, site_subscription_factory): + def subscription_create( + partner: dict | None = None, + description: str | None = None, + start_date: str | None = "2024-01-01T10:20:30+01:02", + fqdn: str | None = None, + ts_port: PortNumber | None = None, + site: UUIDstr | None = None, + switch_vendor: Vendor | None = None, + switch_model: SwitchModel | None = None, + status: SubscriptionLifecycle | 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.SWITCH) + switch_subscription = SwitchInactive.from_product_id(product_id, partner["partner_id"]) + else: + product_id = get_product_id_by_name(ProductName.IMPORTED_SWITCH) + switch_subscription = ImportedSwitchInactive.from_product_id(product_id, partner["partner_id"]) + + switch_subscription.switch.fqdn = fqdn or faker.domain_name(levels=4) + switch_subscription.switch.ts_port = ts_port or faker.port_number(is_user=True) + switch_subscription.switch.site = site or Site.from_subscription(site_subscription_factory()).site + switch_subscription.switch.switch_vendor = switch_vendor or Vendor.JUNIPER + switch_subscription.switch.switch_model = switch_model or SwitchModel.EX3400 + + switch_subscription = SubscriptionModel.from_other_lifecycle(switch_subscription, SubscriptionLifecycle.ACTIVE) + switch_subscription.insync = True + switch_subscription.description = description or faker.sentence() + switch_subscription.start_date = start_date + + if status: + switch_subscription.status = status + + switch_subscription.save() + db.session.commit() + + return str(switch_subscription.subscription_id) + + return subscription_create diff --git a/test/services/conftest.py b/test/services/conftest.py index 3467d545d2c571e947c75022c8a574197394e723..ee0c59e1ea76e0e12a1ee69365cbe9e71109d0d6 100644 --- a/test/services/conftest.py +++ b/test/services/conftest.py @@ -64,6 +64,9 @@ class MockedNetboxClient: def delete_interface(): return None + def get_available_lags_in_range(self): + return self.get_available_lags() + class MockedSharePointClient: class BaseMockObject: @@ -104,15 +107,18 @@ class MockedKentikClient: @staticmethod def get_plans() -> list[dict[str, Any]]: - return [{"id": 0, "plan_name": "kentik-plan-1"}, {"id": 1, "plan_name": "kentik-plan-2"}] + return [ + {"id": 0, "name": "kentik-plan-1", "active": True, "devices": [], "max_devices": 9999}, + {"id": 1, "name": "kentik-plan-2", "active": True, "devices": [], "max_devices": 9999}, + ] @staticmethod def get_plan(plan_id: int) -> dict[str, Any]: - return {"id": plan_id, "plan_name": "kentik-mocked-plan"} + return {"id": plan_id, "name": "kentik-mocked-plan", "active": True, "devices": [], "max_devices": 9999} @staticmethod def get_plan_by_name(plan_name: str) -> dict[str, Any]: - return {"id": 0, "plan_name": plan_name} + return {"id": 0, "name": plan_name, "active": True, "devices": [], "max_devices": 9999} @staticmethod def create_device(device: NewKentikDevice) -> dict[str, Any]: diff --git a/test/workflows/edge_port/test_create_edge_port.py b/test/workflows/edge_port/test_create_edge_port.py index ad804c91f5143148098b7cc4fbfc427d99783d29..84c87b21cd08b0e9b2a0fb73d6f3e5f4a9b6171a 100644 --- a/test/workflows/edge_port/test_create_edge_port.py +++ b/test/workflows/edge_port/test_create_edge_port.py @@ -81,7 +81,6 @@ def input_form_wizard_data(request, router_subscription_factory, partner_factory @patch("gso.services.lso_client._send_request") def test_successful_edge_port_creation( mock_execute_playbook, - responses, input_form_wizard_data, faker, _netbox_client_mock, # noqa: PT019 diff --git a/test/workflows/iptrunk/test_create_iptrunk.py b/test/workflows/iptrunk/test_create_iptrunk.py index 7a38a234a7e0ba98dea56081b07e0bcc8be21c7e..cac63729ec0507232f904074c48bb7c8b5f203a1 100644 --- a/test/workflows/iptrunk/test_create_iptrunk.py +++ b/test/workflows/iptrunk/test_create_iptrunk.py @@ -119,7 +119,6 @@ def test_successful_iptrunk_creation_with_standard_lso_result( mock_allocate_v4_network, mock_allocate_v6_network, mock_execute_playbook, - responses, input_form_wizard_data, faker, _netbox_client_mock, # noqa: PT019 @@ -176,7 +175,6 @@ def test_iptrunk_creation_fails_when_lso_return_code_is_one( mock_allocate_v4_network, mock_allocate_v6_network, mock_execute_playbook, - responses, input_form_wizard_data, faker, _netbox_client_mock, # noqa: PT019 @@ -217,7 +215,6 @@ def test_successful_iptrunk_creation_with_juniper_interface_names( mock_allocate_v4_network, mock_allocate_v6_network, mock_execute_playbook, - responses, input_form_wizard_data, faker, data_config_filename: PathLike, diff --git a/test/workflows/lan_switch_interconnect/__init__.py b/test/workflows/lan_switch_interconnect/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 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 new file mode 100644 index 0000000000000000000000000000000000000000..69349ae02c7d58dbbdc670aad70b855edb5f7a11 --- /dev/null +++ b/test/workflows/lan_switch_interconnect/test_create_imported_lan_switch_interconnect.py @@ -0,0 +1,44 @@ +import pytest +from orchestrator.types import SubscriptionLifecycle + +from gso.products import ProductName +from gso.products.product_types.lan_switch_interconnect import ImportedLanSwitchInterconnect +from gso.utils.types.ip_address import AddressSpace +from test.workflows import ( + assert_complete, + extract_state, + run_workflow, +) + + +@pytest.fixture() +def workflow_input_data(faker, router_subscription_factory, switch_subscription_factory): + return { + "lan_switch_interconnect_description": faker.sentence(), + "lan_switch_interconnect_ip_network": 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": faker.link_members_nokia(), + "ipv4_address": faker.ipv4(), + }, + "switch_side": { + "switch": switch_subscription_factory(), + "ae_iface": faker.juniper_ae_interface_name(), + "ae_members": faker.link_members_juniper(), + "ipv4_address": faker.ipv4(), + }, + } + + +@pytest.mark.workflow() +def test_create_imported_lan_switch_interconnect_success(workflow_input_data): + 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 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 new file mode 100644 index 0000000000000000000000000000000000000000..2c6657d260a88b8871dcc843a8fb1e7a636685f4 --- /dev/null +++ b/test/workflows/lan_switch_interconnect/test_create_lan_switch_interconnect.py @@ -0,0 +1,74 @@ +from unittest.mock import patch + +import pytest +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.ip_address import AddressSpace +from test.services.conftest import MockedNetboxClient +from test.workflows import assert_complete, extract_state, run_workflow + + +@pytest.fixture() +def _netbox_client_mock(): + # Mock NetboxClient methods + with ( + patch("gso.services.netbox_client.NetboxClient.get_device_by_name") as mock_get_device_by_name, + patch("gso.services.netbox_client.NetboxClient.get_available_interfaces") as mock_get_available_interfaces, + patch("gso.services.netbox_client.NetboxClient.get_available_lags_in_range") as mock_available_lags_in_range, + ): + mock_get_device_by_name.return_value = MockedNetboxClient().get_device_by_name() + mock_get_available_interfaces.return_value = MockedNetboxClient().get_available_interfaces() + mock_available_lags_in_range.return_value = MockedNetboxClient().get_available_lags_in_range() + + yield + + +@pytest.fixture() +def input_form_data(faker, router_subscription_factory, switch_subscription_factory): + def _generate_form_data(address_space: AddressSpace): + 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(), + "address_space": address_space, + "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], + }, + {}, + ] + + return _generate_form_data + + +@pytest.mark.parametrize("address_space", [AddressSpace.PRIVATE, AddressSpace.PUBLIC]) +@pytest.mark.workflow() +def test_create_lan_switch_interconnect_success( + address_space, + input_form_data, + _netbox_client_mock, # noqa: PT019 +): + initial_data = input_form_data(address_space) + + result, _, _ = run_workflow("create_lan_switch_interconnect", initial_data) + + assert_complete(result) + state = extract_state(result) + subscription_id = state["subscription_id"] + subscription = LanSwitchInterconnect.from_subscription(subscription_id) + assert subscription.status == SubscriptionLifecycle.ACTIVE diff --git a/test/workflows/lan_switch_interconnect/test_import_lan_switch_interconnect.py b/test/workflows/lan_switch_interconnect/test_import_lan_switch_interconnect.py new file mode 100644 index 0000000000000000000000000000000000000000..5808299f5de5f01fd001a39819f091cb716bca8b --- /dev/null +++ b/test/workflows/lan_switch_interconnect/test_import_lan_switch_interconnect.py @@ -0,0 +1,20 @@ +import pytest +from orchestrator.types import SubscriptionLifecycle + +from gso.products import ProductName +from gso.products.product_types.lan_switch_interconnect import LanSwitchInterconnect +from test.workflows import assert_complete, run_workflow + + +@pytest.mark.workflow() +def test_import_lan_switch_interconnect_success(lan_switch_interconnect_subscription_factory): + imported_lan_switch_interconnect = lan_switch_interconnect_subscription_factory(is_imported=False) + result, _, _ = run_workflow( + "import_lan_switch_interconnect", [{"subscription_id": imported_lan_switch_interconnect}] + ) + subscription = LanSwitchInterconnect.from_subscription(imported_lan_switch_interconnect) + + assert_complete(result) + assert subscription.product.name == ProductName.LAN_SWITCH_INTERCONNECT + assert subscription.status == SubscriptionLifecycle.ACTIVE + assert subscription.insync is True diff --git a/test/workflows/lan_switch_interconnect/test_terminate_lan_switch_interconnect.py b/test/workflows/lan_switch_interconnect/test_terminate_lan_switch_interconnect.py new file mode 100644 index 0000000000000000000000000000000000000000..6d5ae5383dd4559662f293535a3b3253d7123033 --- /dev/null +++ b/test/workflows/lan_switch_interconnect/test_terminate_lan_switch_interconnect.py @@ -0,0 +1,17 @@ +import pytest + +from gso.products.product_types.lan_switch_interconnect import LanSwitchInterconnect +from test.workflows import assert_complete, extract_state, run_workflow + + +@pytest.mark.workflow() +def test_terminate_lan_switch_interconnect(lan_switch_interconnect_subscription_factory, faker): + subscription_id = lan_switch_interconnect_subscription_factory() + initial_lan_switch_interconnect_data = [{"subscription_id": subscription_id}, {"tt_number": faker.tt_number()}] + result, _, _ = run_workflow("terminate_lan_switch_interconnect", initial_lan_switch_interconnect_data) + assert_complete(result) + + state = extract_state(result) + subscription_id = state["subscription_id"] + subscription = LanSwitchInterconnect.from_subscription(subscription_id) + assert subscription.status == "terminated" 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 diff --git a/test/workflows/nren_l3_core_service/test_create_nren_l3_core_service.py b/test/workflows/nren_l3_core_service/test_create_nren_l3_core_service.py index 1287bd01c9288d4f577512f8a832f565ed38a01a..d8eac84e705567a8ef1e5fb0add47457607751f3 100644 --- a/test/workflows/nren_l3_core_service/test_create_nren_l3_core_service.py +++ b/test/workflows/nren_l3_core_service/test_create_nren_l3_core_service.py @@ -34,7 +34,6 @@ def base_bgp_peer_input(faker): def test_create_nren_l3_core_service_success( mock_lso_client, l3_core_type, - responses, faker, partner_factory, edge_port_subscription_factory, diff --git a/test/workflows/router/test_modify_connection_strategy.py b/test/workflows/router/test_modify_connection_strategy.py index fd3a01357a9fbb7558a38a0797d6ff55d23eeeb9..20f8f31a1780b4222705d17102e887b550b4771a 100644 --- a/test/workflows/router/test_modify_connection_strategy.py +++ b/test/workflows/router/test_modify_connection_strategy.py @@ -6,7 +6,7 @@ from test.workflows import assert_complete, run_workflow @pytest.mark.workflow() -def test_modify_connection_strategy(responses, router_subscription_factory): +def test_modify_connection_strategy(router_subscription_factory): subscription_id = router_subscription_factory(router_access_via_ts=True) subscription = Router.from_subscription(subscription_id) assert subscription.router.router_access_via_ts is True diff --git a/test/workflows/router/test_modify_router_kentik_license.py b/test/workflows/router/test_modify_router_kentik_license.py new file mode 100644 index 0000000000000000000000000000000000000000..9d5352c515b25cc921622d7e1e58a1726fe499a1 --- /dev/null +++ b/test/workflows/router/test_modify_router_kentik_license.py @@ -0,0 +1,31 @@ +from unittest.mock import patch + +import pytest + +from gso.products.product_blocks.router import RouterRole +from gso.products.product_types.router import Router +from test.services.conftest import MockedKentikClient +from test.workflows import assert_complete, extract_state, run_workflow + + +@pytest.mark.workflow() +@patch("gso.services.kentik_client.KentikClient") +def test_modify_router_kentik_license_success( + mock_kentik_client, + router_subscription_factory, + faker, +): + # Set up mock return values + product_id = router_subscription_factory(router_role=RouterRole.PE) + mock_kentik_client.return_value = MockedKentikClient + + # Run workflow + initial_input_data = [{"subscription_id": product_id}, {"new_plan_id": "0"}] + result, _, _ = run_workflow("modify_router_kentik_license", initial_input_data) + + assert_complete(result) + + state = extract_state(result) + subscription_id = state["subscription_id"] + subscription = Router.from_subscription(subscription_id) + assert subscription.status == "active" diff --git a/test/workflows/site/test_create_site.py b/test/workflows/site/test_create_site.py index 112e078f39c78a1545ada8d14be18dbd61da7858..b744c2e049036c01df4c869ca43fd6ac68d43f71 100644 --- a/test/workflows/site/test_create_site.py +++ b/test/workflows/site/test_create_site.py @@ -9,7 +9,7 @@ from test.workflows import assert_complete, extract_state, run_workflow @pytest.mark.workflow() -def test_create_site(responses, faker): +def test_create_site(faker): product_id = get_product_id_by_name(ProductName.SITE) initial_site_data = [ {"product": product_id}, @@ -41,7 +41,7 @@ def test_create_site(responses, faker): @pytest.mark.workflow() -def test_site_name_is_incorrect(responses, faker): +def test_site_name_is_incorrect(faker): """Test validate site name on site creation. The site name is a string with 3 upper case letter and one digit. diff --git a/test/workflows/site/test_modify_site.py b/test/workflows/site/test_modify_site.py index fb45a9576e6917e66af9370dab3a69435bdf20db..ff82fc7df2c5b759a57760dfac0a1480aa953eeb 100644 --- a/test/workflows/site/test_modify_site.py +++ b/test/workflows/site/test_modify_site.py @@ -6,7 +6,7 @@ from test.workflows import assert_complete, extract_state, run_workflow @pytest.mark.workflow() -def test_modify_site(responses, site_subscription_factory, faker): +def test_modify_site(site_subscription_factory, faker): subscription_id = site_subscription_factory() initial_site_data = [ {"subscription_id": subscription_id}, diff --git a/test/workflows/site/test_terminate_site.py b/test/workflows/site/test_terminate_site.py index 26cad9e28c90524bfc586bd6f628a9278923f726..93c1cd2141fe9f95cc21a168c447831d5447153f 100644 --- a/test/workflows/site/test_terminate_site.py +++ b/test/workflows/site/test_terminate_site.py @@ -5,7 +5,7 @@ from test.workflows import assert_complete, extract_state, run_workflow @pytest.mark.workflow() -def test_terminate_site(responses, site_subscription_factory): +def test_terminate_site(site_subscription_factory): subscription_id = site_subscription_factory() initial_site_data = [{"subscription_id": subscription_id}, {}] result, _, _ = run_workflow("terminate_site", initial_site_data) diff --git a/test/workflows/switch/__init__.py b/test/workflows/switch/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/test/workflows/switch/test_activate_switch.py b/test/workflows/switch/test_activate_switch.py new file mode 100644 index 0000000000000000000000000000000000000000..52ffa94c4f16d6ae4b7d1c792eb64915583cb76f --- /dev/null +++ b/test/workflows/switch/test_activate_switch.py @@ -0,0 +1,36 @@ +import pytest + +from gso.products.product_types.switch import Switch +from test.workflows import ( + assert_complete, + assert_suspended, + extract_state, + resume_workflow, + run_workflow, +) + + +@pytest.mark.workflow() +def test_activate_switch_success( + switch_subscription_factory, + faker, +): + # Set up mock return values + product_id = switch_subscription_factory(status="provisioning") + # Sanity check + assert Switch.from_subscription(product_id).status == "provisioning" + + # Run workflow + initial_input_data = [{"subscription_id": product_id}, {}] + result, process_stat, step_log = run_workflow("activate_switch", initial_input_data) + + assert_suspended(result) + result, step_log = resume_workflow(process_stat, step_log, input_data=[{"checklist_url": "http://localhost"}]) + + assert_complete(result) + + state = extract_state(result) + subscription_id = state["subscription_id"] + subscription = Switch.from_subscription(subscription_id) + + assert subscription.status == "active" diff --git a/test/workflows/switch/test_create_imported_switch.py b/test/workflows/switch/test_create_imported_switch.py new file mode 100644 index 0000000000000000000000000000000000000000..76ad61c7a80615d4b88ae94add85c5ddabf3bcd8 --- /dev/null +++ b/test/workflows/switch/test_create_imported_switch.py @@ -0,0 +1,34 @@ +import pytest +from orchestrator.types import SubscriptionLifecycle + +from gso.products import ProductName +from gso.products.product_blocks.switch import SwitchModel +from gso.products.product_types.switch import ImportedSwitch +from gso.utils.shared_enums import Vendor +from test.workflows import ( + assert_complete, + extract_state, + run_workflow, +) + + +@pytest.fixture() +def workflow_input_data(faker, site_subscription_factory): + return { + "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, + } + + +@pytest.mark.workflow() +def test_create_imported_switch_success(workflow_input_data): + result, _, _ = run_workflow("create_imported_switch", [workflow_input_data]) + state = extract_state(result) + subscription = ImportedSwitch.from_subscription(state["subscription_id"]) + + assert_complete(result) + assert subscription.product.name == ProductName.IMPORTED_SWITCH + assert subscription.status == SubscriptionLifecycle.ACTIVE diff --git a/test/workflows/switch/test_create_switch.py b/test/workflows/switch/test_create_switch.py new file mode 100644 index 0000000000000000000000000000000000000000..c11edf80bc4f3059957c2e697be3d536ca7fdbd6 --- /dev/null +++ b/test/workflows/switch/test_create_switch.py @@ -0,0 +1,64 @@ +from unittest.mock import patch + +import pytest + +from gso.products import ProductName +from gso.products.product_types.switch import Switch +from gso.services.subscriptions import get_product_id_by_name +from test import USER_CONFIRM_EMPTY_FORM +from test.services.conftest import MockedSharePointClient +from test.workflows import ( + assert_complete, + assert_lso_interaction_success, + assert_suspended, + extract_state, + resume_workflow, + run_workflow, +) + + +@pytest.mark.workflow() +@patch("gso.services.lso_client._send_request") +@patch("gso.services.infoblox.hostname_available") +@patch("gso.services.sharepoint.SharePointClient") +def test_create_switch_success( + mock_sharepoint_client, mock_hostname_available, mock_execute_playbook, faker, site_subscription_factory +): + product_id = get_product_id_by_name(ProductName.SWITCH) + initial_form_input = [ + {"product": product_id}, + { + "tt_number": faker.tt_number(), + "switch_site": site_subscription_factory(), + "hostname": faker.domain_word(), + "ts_port": faker.port_number(is_user=True), + }, + {}, + ] + mock_hostname_available.return_value = True + mock_sharepoint_client.return_value = MockedSharePointClient + result, process_stat, step_log = run_workflow("create_switch", initial_form_input) + + # Two LSO interactions + for _ in range(2): + result, step_log = assert_lso_interaction_success(result, process_stat, step_log) + + # Two user prompts + for _ in range(2): + assert_suspended(result) + result, step_log = resume_workflow(process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) + + # One LSO interaction + result, step_log = assert_lso_interaction_success(result, process_stat, step_log) + + # Sharepoint list created + assert_suspended(result) + result, step_log = resume_workflow(process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) + + assert_complete(result) + + state = extract_state(result) + subscription_id = state["subscription_id"] + subscription = Switch.from_subscription(subscription_id) + assert subscription.status == "provisioning" + assert mock_execute_playbook.call_count == 3 diff --git a/test/workflows/switch/test_import_switch.py b/test/workflows/switch/test_import_switch.py new file mode 100644 index 0000000000000000000000000000000000000000..9bdd578d66553b9e5f06f09717aae2b35e633460 --- /dev/null +++ b/test/workflows/switch/test_import_switch.py @@ -0,0 +1,18 @@ +import pytest +from orchestrator.types import SubscriptionLifecycle + +from gso.products import ProductName +from gso.products.product_types.switch import Switch +from test.workflows import assert_complete, run_workflow + + +@pytest.mark.workflow() +def test_import_switch_success(switch_subscription_factory): + imported_switch = switch_subscription_factory(is_imported=False) + result, _, _ = run_workflow("import_switch", [{"subscription_id": imported_switch}]) + subscription = Switch.from_subscription(imported_switch) + + assert_complete(result) + assert subscription.product.name == ProductName.SWITCH + assert subscription.status == SubscriptionLifecycle.ACTIVE + assert subscription.insync is True diff --git a/test/workflows/switch/test_terminate_switch.py b/test/workflows/switch/test_terminate_switch.py new file mode 100644 index 0000000000000000000000000000000000000000..4614789c98bf09b40a746f845def31098e93c16d --- /dev/null +++ b/test/workflows/switch/test_terminate_switch.py @@ -0,0 +1,23 @@ +from unittest.mock import patch + +import pytest + +from gso.products.product_types.switch import Switch +from test.workflows import assert_complete, extract_state, run_workflow + + +@pytest.mark.workflow() +@patch("gso.services.netbox_client.NetboxClient.delete_device") +@patch("gso.services.infoblox.delete_host_by_fqdn") +def test_terminate_switch(mock_delete_host_by_fqdn, mock_delete_device, switch_subscription_factory, faker): + subscription_id = switch_subscription_factory() + initial_switch_data = [{"subscription_id": subscription_id}, {"tt_number": faker.tt_number()}] + result, _, _ = run_workflow("terminate_switch", initial_switch_data) + assert_complete(result) + + state = extract_state(result) + subscription_id = state["subscription_id"] + subscription = Switch.from_subscription(subscription_id) + assert subscription.status == "terminated" + assert mock_delete_device.call_count == 1 + assert mock_delete_host_by_fqdn.call_count == 1 diff --git a/test/workflows/switch/test_validate_switch.py b/test/workflows/switch/test_validate_switch.py new file mode 100644 index 0000000000000000000000000000000000000000..f4e449b0ca84b439193cc55825e43c0bce1cbfb1 --- /dev/null +++ b/test/workflows/switch/test_validate_switch.py @@ -0,0 +1,38 @@ +from unittest.mock import patch + +import pytest + +from gso.products.product_types.switch import Switch +from test.workflows import ( + assert_complete, + assert_lso_success, + extract_state, + run_workflow, +) + + +@pytest.mark.workflow() +@patch("gso.services.lso_client._send_request") +@patch("gso.services.netbox_client.NetboxClient.get_device_by_name") +def test_validate_switch_success( + mock_get_device_by_name, + mock_execute_playbook, + switch_subscription_factory, + faker, + data_config_filename, + geant_partner, +): + # Run workflow + subscription_id = switch_subscription_factory() + initial_switch_data = [{"subscription_id": subscription_id}] + result, process_stat, step_log = run_workflow("validate_switch", initial_switch_data) + + result, step_log = assert_lso_success(result, process_stat, step_log) + + assert_complete(result) + state = extract_state(result) + subscription = Switch.from_subscription(state["subscription_id"]) + + assert subscription.status == "active" + assert mock_execute_playbook.call_count == 1 + assert mock_get_device_by_name.call_count == 1 diff --git a/test/workflows/tasks/test_task_validate_products.py b/test/workflows/tasks/test_task_validate_products.py index b12b3a4a6b0190f7d150faf54d9c61d466f4f8de..66853d257d838b423cd7595e2a8ee2b015d94ecf 100644 --- a/test/workflows/tasks/test_task_validate_products.py +++ b/test/workflows/tasks/test_task_validate_products.py @@ -4,7 +4,7 @@ from test.workflows import assert_complete, extract_state, run_workflow @pytest.mark.workflow() -def test_task_validate_geant_products(responses): +def test_task_validate_geant_products(): result, _, _ = run_workflow("task_validate_geant_products", [{}]) assert_complete(result) state = extract_state(result)