diff --git a/gso/api/v1/imports.py b/gso/api/v1/imports.py index 684d7b7eb43bd878b5d3919a768f7a70ab33172c..8920765a83efa12cc50f2bf56ae9465c9e746c6e 100644 --- a/gso/api/v1/imports.py +++ b/gso/api/v1/imports.py @@ -11,11 +11,12 @@ from pydantic import BaseModel, root_validator, validator from gso.auth.security import opa_security_default from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity -from gso.products.product_blocks.router import RouterRole, RouterVendor +from gso.products.product_blocks.router import RouterRole from gso.products.product_blocks.site import SiteTier from gso.services import subscriptions from gso.services.crm import CustomerNotFoundError, get_customer_by_name from gso.utils.helpers import BaseSiteValidatorModel, LAGMember +from gso.utils.shared_choices import PortNumber, Vendor router = APIRouter(prefix="/imports", tags=["Imports"], dependencies=[Depends(opa_security_default)]) @@ -50,7 +51,7 @@ class RouterImportModel(BaseModel): router_site: str hostname: str ts_port: int - router_vendor: RouterVendor + router_vendor: Vendor router_role: RouterRole router_lo_ipv4_address: ipaddress.IPv4Address router_lo_ipv6_address: ipaddress.IPv6Address @@ -136,6 +137,27 @@ class IptrunkImportModel(BaseModel): return values +class SuperPopSwitchImportModel(BaseModel): + """Required fields for importing an existing :class:`gso.product.product_types.super_pop_switch`.""" + + customer: str + super_pop_switch_site: str + hostname: str + super_pop_switch_ts_port: PortNumber + super_pop_switch_mgmt_ipv4_address: ipaddress.IPv4Address + + +class OfficeRouterImportModel(BaseModel): + """Required fields for importing an existing :class:`gso.product.product_types.office_router`.""" + + customer: str + office_router_site: str + office_router_fqdn: str + office_router_ts_port: PortNumber + office_router_lo_ipv4_address: ipaddress.IPv4Address + office_router_lo_ipv6_address: ipaddress.IPv6Address + + def _start_process(process_name: str, data: dict) -> UUID: """Start a process and handle common exceptions.""" pid: UUID = processes.start_process(process_name, [data]) @@ -184,7 +206,7 @@ def import_router(router_data: RouterImportModel) -> dict[str, Any]: :raises HTTPException: If there's an error in the process. """ pid = _start_process("import_router", router_data.dict()) - return {"detail": "Router added successfully", "pid": pid} + return {"detail": "Router has been added successfully", "pid": pid} @router.post("/iptrunks", status_code=status.HTTP_201_CREATED, response_model=ImportResponseModel) @@ -200,4 +222,36 @@ def import_iptrunk(iptrunk_data: IptrunkImportModel) -> dict[str, Any]: :raises HTTPException: If there's an error in the process. """ pid = _start_process("import_iptrunk", iptrunk_data.dict()) - return {"detail": "Iptrunk added successfully", "pid": pid} + return {"detail": "Iptrunk has been added successfully", "pid": pid} + + +@router.post("/super-pop-switches", status_code=status.HTTP_201_CREATED, response_model=ImportResponseModel) +def import_super_pop_switch(super_pop_switch_data: SuperPopSwitchImportModel) -> dict[str, Any]: + """Import a Super PoP switch by running the import_super_pop_switch workflow. + + :param super_pop_switch_data: The Super PoP switch information to be imported. + :type super_pop_switch_data: SuperPopSwitchImportModel + + :return: A dictionary containing the process id of the started process and detail message. + :rtype: dict[str, Any] + + :raises HTTPException: If there's an error in the process. + """ + pid = _start_process("import_super_pop_switch", super_pop_switch_data.dict()) + return {"detail": "Super PoP switch has been added successfully", "pid": pid} + + +@router.post("/office-routers", status_code=status.HTTP_201_CREATED, response_model=ImportResponseModel) +def import_office_router(office_router_data: OfficeRouterImportModel) -> dict[str, Any]: + """Import a office router by running the import_office_router workflow. + + :param office_router_data: The office router information to be imported. + :type office_router_data: OfficeRouterImportModel + + :return: A dictionary containing the process id of the started process and detail message. + :rtype: dict[str, Any] + + :raises HTTPException: If there's an error in the process. + """ + pid = _start_process("import_office_router", office_router_data.dict()) + return {"detail": "Office router has been added successfully", "pid": pid} diff --git a/gso/cli/imports.py b/gso/cli/imports.py index 6aa55c5dfc6bb5701d4cf81f5db6d6a1c3244d12..1a20c14be3840b76a6085f8756af943ef27c9b3d 100644 --- a/gso/cli/imports.py +++ b/gso/cli/imports.py @@ -11,17 +11,23 @@ from pydantic import ValidationError from gso.api.v1.imports import ( IptrunkImportModel, + OfficeRouterImportModel, RouterImportModel, SiteImportModel, + SuperPopSwitchImportModel, import_iptrunk, + import_office_router, import_router, import_site, + import_super_pop_switch, ) from gso.services.subscriptions import get_active_subscriptions_by_field_and_value app: typer.Typer = typer.Typer() -T = TypeVar("T", SiteImportModel, RouterImportModel, IptrunkImportModel) +T = TypeVar( + "T", SiteImportModel, RouterImportModel, IptrunkImportModel, SuperPopSwitchImportModel, OfficeRouterImportModel +) common_filepath_option = typer.Option( default="data.json", @@ -89,6 +95,20 @@ def import_routers(filepath: str = common_filepath_option) -> None: generic_import_data(filepath, RouterImportModel, import_router, "hostname") +@app.command() +def import_super_pop_switches(filepath: str = common_filepath_option) -> None: + """Import Super PoP Switches into GSO.""" + # Use the import_data function to handle common import logic + generic_import_data(filepath, SuperPopSwitchImportModel, import_super_pop_switch, "hostname") + + +@app.command() +def import_office_routers(filepath: str = common_filepath_option) -> None: + """Import office routers into GSO.""" + # Use the import_data function to handle common import logic + generic_import_data(filepath, OfficeRouterImportModel, import_office_router, "office_router_fqdn") + + def get_router_subscription_id(node_name: str) -> str | None: """Get the subscription id for a router by its node name.""" subscriptions = get_active_subscriptions_by_field_and_value( diff --git a/gso/migrations/versions/2024-02-14_1c3c90ea1d8c_add_super_pop_switch_product.py b/gso/migrations/versions/2024-02-14_1c3c90ea1d8c_add_super_pop_switch_product.py new file mode 100644 index 0000000000000000000000000000000000000000..e4ad3711bdc02a03b2e021d591ae5e6b8235133f --- /dev/null +++ b/gso/migrations/versions/2024-02-14_1c3c90ea1d8c_add_super_pop_switch_product.py @@ -0,0 +1,113 @@ +"""Add Super PoP switch product.. + +Revision ID: 1c3c90ea1d8c +Revises: bacd55c26106 +Create Date: 2024-02-14 11:00:11.858023 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '1c3c90ea1d8c' +down_revision = 'bacd55c26106' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + conn = op.get_bind() + conn.execute(sa.text(""" +INSERT INTO products (name, description, product_type, tag, status) VALUES ('Super PoP switch', 'Super PoP switch', 'SuperPopSwitch', 'Super_POP_SWITCH', 'active') RETURNING products.product_id + """)) + conn.execute(sa.text(""" +INSERT INTO product_blocks (name, description, tag, status) VALUES ('SuperPopSwitchBlock', 'Super PoP switch block', 'SUPER_POP_SWITCH_BLK', 'active') RETURNING product_blocks.product_block_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('super_pop_switch_ts_port', 'The port of the terminal server that this Super PoP switch is connected to. Used to offer out of band access.') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('super_pop_switch_fqdn', 'Super PoP switch FQDN.') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('super_pop_switch_mgmt_ipv4_address', 'The IPv4 management address of the Super PoP switch.') 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 ('Super PoP switch')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('SuperPopSwitchBlock'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_relations (in_use_by_id, depends_on_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('SuperPopSwitchBlock')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('SiteBlock'))) + """)) + 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 ('SuperPopSwitchBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('super_pop_switch_fqdn'))) + """)) + 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 ('SuperPopSwitchBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('super_pop_switch_ts_port'))) + """)) + 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 ('SuperPopSwitchBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('super_pop_switch_mgmt_ipv4_address'))) + """)) + 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 ('SuperPopSwitchBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('vendor'))) + """)) + + +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 ('SuperPopSwitchBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('super_pop_switch_fqdn')) + """)) + 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 ('SuperPopSwitchBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('super_pop_switch_fqdn')) + """)) + 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 ('SuperPopSwitchBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('super_pop_switch_ts_port')) + """)) + 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 ('SuperPopSwitchBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('super_pop_switch_ts_port')) + """)) + 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 ('SuperPopSwitchBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('super_pop_switch_mgmt_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 ('SuperPopSwitchBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('super_pop_switch_mgmt_ipv4_address')) + """)) + 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 ('SuperPopSwitchBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('vendor')) + """)) + 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 ('SuperPopSwitchBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('vendor')) + """)) + 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 ('super_pop_switch_ts_port', 'super_pop_switch_fqdn', 'super_pop_switch_mgmt_ipv4_address')) + """)) + conn.execute(sa.text(""" +DELETE FROM resource_types WHERE resource_types.resource_type IN ('super_pop_switch_ts_port', 'super_pop_switch_fqdn', 'super_pop_switch_mgmt_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 ('Super PoP switch')) AND product_product_blocks.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('SuperPopSwitchBlock')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_relations WHERE product_block_relations.in_use_by_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('SuperPopSwitchBlock')) AND product_block_relations.depends_on_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('SiteBlock')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instances WHERE subscription_instances.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('SuperPopSwitchBlock')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_blocks WHERE product_blocks.name IN ('SuperPopSwitchBlock') + """)) + 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 ('Super PoP 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 ('Super PoP 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 ('Super PoP switch'))) + """)) + conn.execute(sa.text(""" +DELETE FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Super PoP switch')) + """)) + conn.execute(sa.text(""" +DELETE FROM products WHERE products.name IN ('Super PoP switch') + """)) diff --git a/gso/migrations/versions/2024-02-20_6e4952687205_add_office_router_product.py b/gso/migrations/versions/2024-02-20_6e4952687205_add_office_router_product.py new file mode 100644 index 0000000000000000000000000000000000000000..631facbc24057107a3ae7274e379e397fe308fd5 --- /dev/null +++ b/gso/migrations/versions/2024-02-20_6e4952687205_add_office_router_product.py @@ -0,0 +1,125 @@ +"""Add office router product.. + +Revision ID: 6e4952687205 +Revises: 1c3c90ea1d8c +Create Date: 2024-02-20 11:22:30.804258 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '6e4952687205' +down_revision = '1c3c90ea1d8c' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + conn = op.get_bind() + conn.execute(sa.text(""" +INSERT INTO products (name, description, product_type, tag, status) VALUES ('Office router', 'Office router product', 'OfficeRouter', 'OFFICE_ROUTER', 'active') RETURNING products.product_id + """)) + conn.execute(sa.text(""" +INSERT INTO product_blocks (name, description, tag, status) VALUES ('OfficeRouterBlock', 'Office router product block', 'OFFICE_ROUTER_BLOCK', 'active') RETURNING product_blocks.product_block_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('office_router_fqdn', 'Office router FQDN') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('office_router_ts_port', 'The port of the terminal server that this office router is connected to') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('office_router_lo_ipv4_address', 'The IPv4 loopback address of the office router.') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('office_router_lo_ipv6_address', 'The IPv6 loopback address of the office router.') 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 ('Office router')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('OfficeRouterBlock'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_relations (in_use_by_id, depends_on_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('OfficeRouterBlock')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('SiteBlock'))) + """)) + 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 ('OfficeRouterBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('office_router_fqdn'))) + """)) + 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 ('OfficeRouterBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('office_router_ts_port'))) + """)) + 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 ('OfficeRouterBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('office_router_lo_ipv4_address'))) + """)) + 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 ('OfficeRouterBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('office_router_lo_ipv6_address'))) + """)) + 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 ('OfficeRouterBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('vendor'))) + """)) + + +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 ('OfficeRouterBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('office_router_fqdn')) + """)) + 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 ('OfficeRouterBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('office_router_fqdn')) + """)) + 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 ('OfficeRouterBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('office_router_ts_port')) + """)) + 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 ('OfficeRouterBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('office_router_ts_port')) + """)) + 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 ('OfficeRouterBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('office_router_lo_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 ('OfficeRouterBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('office_router_lo_ipv4_address')) + """)) + 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 ('OfficeRouterBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('office_router_lo_ipv6_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 ('OfficeRouterBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('office_router_lo_ipv6_address')) + """)) + 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 ('OfficeRouterBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('vendor')) + """)) + 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 ('OfficeRouterBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('vendor')) + """)) + 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 ('office_router_fqdn', 'office_router_ts_port', 'office_router_lo_ipv4_address', 'office_router_lo_ipv6_address')) + """)) + conn.execute(sa.text(""" +DELETE FROM resource_types WHERE resource_types.resource_type IN ('office_router_fqdn', 'office_router_ts_port', 'office_router_lo_ipv4_address', 'office_router_lo_ipv6_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 ('Office router')) AND product_product_blocks.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('OfficeRouterBlock')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_relations WHERE product_block_relations.in_use_by_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('OfficeRouterBlock')) AND product_block_relations.depends_on_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('SiteBlock')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instances WHERE subscription_instances.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('OfficeRouterBlock')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_blocks WHERE product_blocks.name IN ('OfficeRouterBlock') + """)) + 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 ('Office router')))) + """)) + 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 ('Office router'))) + """)) + 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 ('Office router'))) + """)) + conn.execute(sa.text(""" +DELETE FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Office router')) + """)) + conn.execute(sa.text(""" +DELETE FROM products WHERE products.name IN ('Office router') + """)) diff --git a/gso/products/__init__.py b/gso/products/__init__.py index bd721395366659c40907dd13a07fc28ec2fa7d76..2fd25d9fe556299706f618213d06a9ae9f04763e 100644 --- a/gso/products/__init__.py +++ b/gso/products/__init__.py @@ -9,8 +9,10 @@ from orchestrator.domain import SUBSCRIPTION_MODEL_REGISTRY from pydantic_forms.types import strEnum from gso.products.product_types.iptrunk import Iptrunk +from gso.products.product_types.office_router import OfficeRouter from gso.products.product_types.router import Router from gso.products.product_types.site import Site +from gso.products.product_types.super_pop_switch import SuperPopSwitch class ProductType(strEnum): @@ -19,6 +21,8 @@ class ProductType(strEnum): IP_TRUNK = "IP trunk" ROUTER = "Router" SITE = "Site" + SUPER_POP_SWITCH = "Super PoP switch" + OFFICE_ROUTER = "Office router" SUBSCRIPTION_MODEL_REGISTRY.update( @@ -26,5 +30,7 @@ SUBSCRIPTION_MODEL_REGISTRY.update( "IP trunk": Iptrunk, "Router": Router, "Site": Site, + "Super PoP switch": SuperPopSwitch, + "Office router": OfficeRouter, }, ) diff --git a/gso/products/product_blocks/office_router.py b/gso/products/product_blocks/office_router.py new file mode 100644 index 0000000000000000000000000000000000000000..83471d8f66f903f8d7c4f2b7129e197cd650c2a7 --- /dev/null +++ b/gso/products/product_blocks/office_router.py @@ -0,0 +1,56 @@ +"""Product block for :class:`office router` products.""" + +import ipaddress + +from orchestrator.domain.base import ProductBlockModel +from orchestrator.types import SubscriptionLifecycle + +from gso.products.product_blocks.site import ( + SiteBlock, + SiteBlockInactive, + SiteBlockProvisioning, +) +from gso.utils.shared_choices import PortNumber, Vendor + + +class OfficeRouterBlockInactive( + ProductBlockModel, + lifecycle=[SubscriptionLifecycle.INITIAL], + product_block_name="OfficeRouterBlock", +): + """An office router that's being currently inactive. See :class:`OfficeRouterBlock`.""" + + office_router_fqdn: str | None = None + office_router_ts_port: PortNumber | None = None + office_router_lo_ipv4_address: ipaddress.IPv4Address | None = None + office_router_lo_ipv6_address: ipaddress.IPv6Address | None = None + office_router_site: SiteBlockInactive | None + vendor: Vendor | None = None + + +class OfficeRouterBlockProvisioning(OfficeRouterBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """An office router that's being provisioned. See :class:`RouterBlock`.""" + + office_router_fqdn: str | None = None + office_router_ts_port: PortNumber | None = None + office_router_lo_ipv4_address: ipaddress.IPv4Address | None = None + office_router_lo_ipv6_address: ipaddress.IPv6Address | None = None + office_router_site: SiteBlockProvisioning | None + vendor: Vendor | None = None + + +class OfficeRouterBlock(OfficeRouterBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """An office router that's currently deployed in the network.""" + + #: Office router FQDN. + office_router_fqdn: str + #: The port of the terminal server that this office router is connected to. Used to offer out of band access. + office_router_ts_port: PortNumber + #: The IPv4 loopback address of the office router. + office_router_lo_ipv4_address: ipaddress.IPv4Address + #: The IPv6 loopback address of the office router. + office_router_lo_ipv6_address: ipaddress.IPv6Address + #: The :class:`Site` that this office router resides in. Both physically and computationally. + office_router_site: SiteBlock + #: The vendor of an office router. Defaults to Juniper. + vendor: Vendor = Vendor.JUNIPER diff --git a/gso/products/product_blocks/router.py b/gso/products/product_blocks/router.py index 5d1a307d85825759b734a0c06b665d1e7eb06866..1905b8652f3ad18c51a0433502ee11f7d10eef3f 100644 --- a/gso/products/product_blocks/router.py +++ b/gso/products/product_blocks/router.py @@ -4,14 +4,13 @@ import ipaddress from orchestrator.domain.base import ProductBlockModel from orchestrator.types import SubscriptionLifecycle, strEnum -from pydantic import ConstrainedInt -from gso import settings from gso.products.product_blocks.site import ( SiteBlock, SiteBlockInactive, SiteBlockProvisioning, ) +from gso.utils.shared_choices import PortNumber, Vendor class RouterRole(strEnum): @@ -22,23 +21,6 @@ class RouterRole(strEnum): AMT = "amt" -class RouterVendor(strEnum): - """Enumerator for the different product vendors that are supported.""" - - JUNIPER = "juniper" - NOKIA = "nokia" - - -class PortNumber(ConstrainedInt): - """Constrained integer for valid port numbers. - - The range from 49152 to 65535 is marked as ephemeral, and can therefore not be selected for permanent allocation. - """ - - gt = 0 - le = 49151 - - class RouterBlockInactive( ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], @@ -54,13 +36,7 @@ class RouterBlockInactive( router_lo_iso_address: str | None = None router_role: RouterRole | None = None router_site: SiteBlockInactive | None - vendor: RouterVendor | None = None - - -def generate_fqdn(hostname: str, site_name: str, country_code: str) -> str: - """Generate an :term:`FQDN` from a hostname, site name, and a country code.""" - oss = settings.load_oss_params() - return f"{hostname}.{site_name.lower()}.{country_code.lower()}{oss.IPAM.LO.domain_name}" + vendor: Vendor | None = None class RouterBlockProvisioning(RouterBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): @@ -74,8 +50,7 @@ class RouterBlockProvisioning(RouterBlockInactive, lifecycle=[SubscriptionLifecy router_lo_iso_address: str router_role: RouterRole router_site: SiteBlockProvisioning - vendor: RouterVendor - + vendor: Vendor class RouterBlock(RouterBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): """A router that's currently deployed in the network.""" @@ -97,4 +72,4 @@ class RouterBlock(RouterBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTI #: The :class:`Site` that this router resides in. Both physically and computationally. router_site: SiteBlock #: The vendor of a router. - vendor: RouterVendor + vendor: Vendor diff --git a/gso/products/product_blocks/super_pop_switch.py b/gso/products/product_blocks/super_pop_switch.py new file mode 100644 index 0000000000000000000000000000000000000000..2e2d5b7c1aa548ce5bbbf9d586370edfdb6153ee --- /dev/null +++ b/gso/products/product_blocks/super_pop_switch.py @@ -0,0 +1,52 @@ +"""Product block for :class:`Super PoP Switch` products.""" + +import ipaddress + +from orchestrator.domain.base import ProductBlockModel +from orchestrator.types import SubscriptionLifecycle + +from gso.products.product_blocks.site import ( + SiteBlock, + SiteBlockInactive, + SiteBlockProvisioning, +) +from gso.utils.shared_choices import PortNumber, Vendor + + +class SuperPopSwitchBlockInactive( + ProductBlockModel, + lifecycle=[SubscriptionLifecycle.INITIAL], + product_block_name="SuperPopSwitchBlock", +): + """A Super PoP switch that's being currently inactive. See :class:`SuperPopSwitchBlock`.""" + + super_pop_switch_fqdn: str | None = None + super_pop_switch_ts_port: PortNumber | None = None + super_pop_switch_mgmt_ipv4_address: ipaddress.IPv4Address | None = None + super_pop_switch_site: SiteBlockInactive | None + vendor: Vendor | None = None + + +class SuperPopSwitchBlockProvisioning(SuperPopSwitchBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """A Super PoP switch that's being provisioned. See :class:`SuperPopSwitchBlock`.""" + + super_pop_switch_fqdn: str | None = None + super_pop_switch_ts_port: PortNumber | None = None + super_pop_switch_mgmt_ipv4_address: ipaddress.IPv4Address | None = None + super_pop_switch_site: SiteBlockProvisioning | None + vendor: Vendor | None = None + + +class SuperPopSwitchBlock(SuperPopSwitchBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """A Super PoP switch that's currently deployed in the network.""" + + #: Super PoP switch FQDN. + super_pop_switch_fqdn: str + #: The port of the terminal server that this Super PoP switch is connected to. Used to offer out of band access. + super_pop_switch_ts_port: PortNumber + #: The IPv4 management address of the Super PoP switch. + super_pop_switch_mgmt_ipv4_address: ipaddress.IPv4Address + #: The :class:`Site` that this Super PoP switch resides in. Both physically and computationally. + super_pop_switch_site: SiteBlock + #: The vendor of a Super PoP switch. Defaults to Juniper. + vendor: Vendor = Vendor.JUNIPER diff --git a/gso/products/product_types/office_router.py b/gso/products/product_types/office_router.py new file mode 100644 index 0000000000000000000000000000000000000000..6fff33e041edc35e1f333f66a01ac3d6f78fc286 --- /dev/null +++ b/gso/products/product_types/office_router.py @@ -0,0 +1,28 @@ +"""An office router product type.""" + +from orchestrator.domain.base import SubscriptionModel +from orchestrator.types import SubscriptionLifecycle + +from gso.products.product_blocks.office_router import ( + OfficeRouterBlock, + OfficeRouterBlockInactive, + OfficeRouterBlockProvisioning, +) + + +class OfficeRouterInactive(SubscriptionModel, is_base=True): + """An inactive office router.""" + + office_router: OfficeRouterBlockInactive + + +class OfficeRouterProvisioning(OfficeRouterInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """An office router that is being provisioned.""" + + office_router: OfficeRouterBlockProvisioning + + +class OfficeRouter(OfficeRouterProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """An office router that is currently active.""" + + office_router: OfficeRouterBlock diff --git a/gso/products/product_types/super_pop_switch.py b/gso/products/product_types/super_pop_switch.py new file mode 100644 index 0000000000000000000000000000000000000000..e12b1a3d12793d372c47b5022a85307a7bf730ce --- /dev/null +++ b/gso/products/product_types/super_pop_switch.py @@ -0,0 +1,28 @@ +"""A Super PoP switch product type.""" + +from orchestrator.domain.base import SubscriptionModel +from orchestrator.types import SubscriptionLifecycle + +from gso.products.product_blocks.super_pop_switch import ( + SuperPopSwitchBlock, + SuperPopSwitchBlockInactive, + SuperPopSwitchBlockProvisioning, +) + + +class SuperPopSwitchInactive(SubscriptionModel, is_base=True): + """An inactive Super PoP switch.""" + + super_pop_switch: SuperPopSwitchBlockInactive + + +class SuperPopSwitchProvisioning(SuperPopSwitchInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """A Super PoP switch that is being provisioned.""" + + super_pop_switch: SuperPopSwitchBlockProvisioning + + +class SuperPopSwitch(SuperPopSwitchProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """A Super PoP switch that is currently active.""" + + super_pop_switch: SuperPopSwitchBlock diff --git a/gso/services/subscriptions.py b/gso/services/subscriptions.py index e77b8d7ba60bfa869ac2a4b4f6638c8eaa55e8ab..e89d9c25ad1cc52f68ae6ac3aea75a0542b92559 100644 --- a/gso/services/subscriptions.py +++ b/gso/services/subscriptions.py @@ -20,6 +20,7 @@ from orchestrator.types import SubscriptionLifecycle from pydantic_forms.types import UUIDstr from gso.products import ProductType +from gso.products.product_types.site import Site SubscriptionType = dict[str, Any] @@ -177,3 +178,17 @@ def count_incomplete_validate_products() -> int: def get_insync_subscriptions() -> list[SubscriptionTable]: """Retrieve all subscriptions that are currently in sync.""" return SubscriptionTable.query.join(ProductTable).filter(SubscriptionTable.insync.is_(True)).all() + + +def get_site_by_name(site_name: str) -> Site: + """Get a site by its name. + + :param site_name: The name of the site. + :type site_name: str + """ + subscription = get_active_subscriptions_by_field_and_value("site_name", site_name) + if not subscription: + msg = f"Site with name {site_name} not found." + raise ValueError(msg) + + return Site.from_subscription(subscription[0].subscription_id) diff --git a/gso/utils/helpers.py b/gso/utils/helpers.py index c43a80737f56e37e3288e78d89a0ab38dced94e4..74050ef545b768042115093cb19263dbb7831dd7 100644 --- a/gso/utils/helpers.py +++ b/gso/utils/helpers.py @@ -12,12 +12,13 @@ from pydantic import BaseModel, validator from pydantic.fields import ModelField from pydantic_forms.validators import Choice +from gso import settings from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock -from gso.products.product_blocks.router import RouterVendor from gso.products.product_blocks.site import SiteTier from gso.products.product_types.router import Router from gso.services.netbox_client import NetboxClient from gso.services.subscriptions import get_active_subscriptions_by_field_and_value +from gso.utils.shared_choices import Vendor class LAGMember(BaseModel): @@ -44,7 +45,7 @@ def available_interfaces_choices(router_id: UUID, speed: str) -> Choice | None: For Nokia routers, return a list of available interfaces. For Juniper routers, return a string. """ - if get_router_vendor(router_id) != RouterVendor.NOKIA: + if get_router_vendor(router_id) != Vendor.NOKIA: return None interfaces = { interface["name"]: f"{interface['name']} {interface['description']}" @@ -54,16 +55,16 @@ def available_interfaces_choices(router_id: UUID, speed: str) -> Choice | None: def available_interfaces_choices_including_current_members( - router_id: UUID, - speed: str, - interfaces: list[IptrunkInterfaceBlock], + router_id: UUID, + speed: str, + interfaces: list[IptrunkInterfaceBlock], ) -> Choice | None: """Return a list of available interfaces for a given router and speed including the current members. For Nokia routers, return a list of available interfaces. For Juniper routers, return a string. """ - if get_router_vendor(router_id) != RouterVendor.NOKIA: + if get_router_vendor(router_id) != Vendor.NOKIA: return None available_interfaces = list(NetboxClient().get_available_interfaces(router_id, speed)) @@ -88,20 +89,20 @@ def available_lags_choices(router_id: UUID) -> Choice | None: For Nokia routers, return a list of available lags. For Juniper routers, return ``None``. """ - if get_router_vendor(router_id) != RouterVendor.NOKIA: + if get_router_vendor(router_id) != Vendor.NOKIA: return None side_a_ae_iface_list = NetboxClient().get_available_lags(router_id) return Choice("ae iface", zip(side_a_ae_iface_list, side_a_ae_iface_list, strict=True)) # type: ignore[arg-type] -def get_router_vendor(router_id: UUID) -> RouterVendor: +def get_router_vendor(router_id: UUID) -> Vendor: """Retrieve the vendor of a router. :param router_id: The :term:`UUID` of the router. :type router_id: :class:`uuid.UUID` :return: The vendor of the router. - :rtype: RouterVendor: + :rtype: Vendor: """ return Router.from_subscription(router_id).router.vendor @@ -130,7 +131,7 @@ def validate_router_in_netbox(subscription_id: UUIDstr) -> UUIDstr: :rtype: :class:`UUIDstr` """ router_type = Router.from_subscription(subscription_id) - if router_type.router.vendor == RouterVendor.NOKIA: + if router_type.router.vendor == Vendor.NOKIA: device = NetboxClient().get_device_by_name(router_type.router.router_fqdn) if not device: msg = "The selected router does not exist in Netbox." @@ -259,7 +260,7 @@ def validate_interface_name_list(interface_name_list: list, vendor: str) -> list exception. """ # For Nokia nothing to do - if vendor == RouterVendor.NOKIA: + if vendor == Vendor.NOKIA: return interface_name_list pattern = re.compile(r"^(ge|et|xe)-[0-9]/[0-9]/[0-9]$") for interface in interface_name_list: @@ -290,3 +291,9 @@ def validate_tt_number(tt_number: str) -> str: raise ValueError(err_msg) return tt_number + + +def generate_fqdn(hostname: str, site_name: str, country_code: str) -> str: + """Generate an :term:`FQDN` from a hostname, site name, and a country code.""" + oss = settings.load_oss_params() + return f"{hostname}.{site_name.lower()}.{country_code.lower()}{oss.IPAM.LO.domain_name}" diff --git a/gso/utils/shared_choices.py b/gso/utils/shared_choices.py new file mode 100644 index 0000000000000000000000000000000000000000..4861e9e134ecf113d74772a08282e7928829d19b --- /dev/null +++ b/gso/utils/shared_choices.py @@ -0,0 +1,21 @@ +"""Shared choices for the different models.""" + +from pydantic import ConstrainedInt +from pydantic_forms.types import strEnum + + +class Vendor(strEnum): + """Enumerator for the different product vendors that are supported.""" + + JUNIPER = "juniper" + NOKIA = "nokia" + + +class PortNumber(ConstrainedInt): + """Constrained integer for valid port numbers. + + The range from 49152 to 65535 is marked as ephemeral, and can therefore not be selected for permanent allocation. + """ + + gt = 0 + le = 49151 diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py index 3566d7fc4fcf997d286223edf0dc097fa2aa789e..d25088730e93388aadef15057e80d3eca8a93ce6 100644 --- a/gso/workflows/__init__.py +++ b/gso/workflows/__init__.py @@ -32,3 +32,5 @@ LazyWorkflowInstance("gso.workflows.site.terminate_site", "terminate_site") LazyWorkflowInstance("gso.workflows.tasks.import_site", "import_site") LazyWorkflowInstance("gso.workflows.tasks.import_router", "import_router") LazyWorkflowInstance("gso.workflows.tasks.import_iptrunk", "import_iptrunk") +LazyWorkflowInstance("gso.workflows.tasks.import_super_pop_switch", "import_super_pop_switch") +LazyWorkflowInstance("gso.workflows.tasks.import_office_router", "import_office_router") diff --git a/gso/workflows/iptrunk/create_iptrunk.py b/gso/workflows/iptrunk/create_iptrunk.py index 904803ca9d0c59e3dd974d9e134cd906f86b84cd..ea2aef09b013a4ed3148818584488b8d56b3d2bf 100644 --- a/gso/workflows/iptrunk/create_iptrunk.py +++ b/gso/workflows/iptrunk/create_iptrunk.py @@ -21,7 +21,7 @@ from gso.products.product_blocks.iptrunk import ( IptrunkType, PhyPortCapacity, ) -from gso.products.product_blocks.router import RouterVendor + from gso.products.product_types.iptrunk import IptrunkInactive from gso.products.product_types.router import Router from gso.services import infoblox, subscriptions @@ -38,6 +38,7 @@ from gso.utils.helpers import ( validate_router_in_netbox, validate_tt_number, ) +from gso.utils.shared_choices import Vendor def initial_input_form_generator(product_name: str) -> FormGenerator: @@ -85,7 +86,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: class JuniperAeMembers(UniqueConstrainedList[LAGMember]): min_items = initial_user_input.iptrunk_minimum_links - if get_router_vendor(router_a) == RouterVendor.NOKIA: + if get_router_vendor(router_a) == Vendor.NOKIA: class NokiaLAGMemberA(LAGMember): interface_name: available_interfaces_choices( # type: ignore[valid-type] @@ -133,7 +134,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: user_input_router_side_b = yield SelectRouterSideB router_b = user_input_router_side_b.side_b_node_id.name - if get_router_vendor(router_b) == RouterVendor.NOKIA: + if get_router_vendor(router_b) == Vendor.NOKIA: class NokiaLAGMemberB(LAGMember): interface_name: available_interfaces_choices( # type: ignore[valid-type] @@ -403,7 +404,7 @@ def reserve_interfaces_in_netbox(subscription: IptrunkInactive) -> State: """Create the :term:`LAG` interfaces in NetBox and attach the lag interfaces to the physical interfaces.""" nbclient = NetboxClient() for trunk_side in subscription.iptrunk.iptrunk_sides: - if get_router_vendor(trunk_side.iptrunk_side_node.owner_subscription_id) == RouterVendor.NOKIA: + if get_router_vendor(trunk_side.iptrunk_side_node.owner_subscription_id) == Vendor.NOKIA: # Create :term:`LAG` interfaces lag_interface: Interfaces = nbclient.create_interface( iface_name=trunk_side.iptrunk_side_ae_iface, @@ -472,8 +473,8 @@ def create_iptrunk() -> StepList: * Allocate the interfaces in Netbox * Set the subscription to active in the database """ - side_a_is_nokia = conditional(lambda state: get_router_vendor(state["side_a_node_id"]) == RouterVendor.NOKIA) - side_b_is_nokia = conditional(lambda state: get_router_vendor(state["side_b_node_id"]) == RouterVendor.NOKIA) + side_a_is_nokia = conditional(lambda state: get_router_vendor(state["side_a_node_id"]) == Vendor.NOKIA) + side_b_is_nokia = conditional(lambda state: get_router_vendor(state["side_b_node_id"]) == Vendor.NOKIA) return ( init diff --git a/gso/workflows/iptrunk/migrate_iptrunk.py b/gso/workflows/iptrunk/migrate_iptrunk.py index 71729149bf86db868b0bfb477565120ff8e4d98d..edcc87dff51864dab5ae3e66d9e6c8bb18e82cce 100644 --- a/gso/workflows/iptrunk/migrate_iptrunk.py +++ b/gso/workflows/iptrunk/migrate_iptrunk.py @@ -25,7 +25,6 @@ from pydantic_forms.core import ReadOnlyField from pynetbox.models.dcim import Interfaces from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock -from gso.products.product_blocks.router import RouterVendor from gso.products.product_types.iptrunk import Iptrunk from gso.products.product_types.router import Router from gso.services import infoblox @@ -41,6 +40,7 @@ from gso.utils.helpers import ( validate_interface_name_list, validate_tt_number, ) +from gso.utils.shared_choices import Vendor from gso.utils.workflow_steps import set_isis_to_90000 @@ -109,7 +109,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: new_router = new_side_iptrunk_router_input.new_node side_a_ae_iface = available_lags_choices(new_router) or str - new_side_is_nokia = get_router_vendor(new_router) == RouterVendor.NOKIA + new_side_is_nokia = get_router_vendor(new_router) == Vendor.NOKIA if new_side_is_nokia: class NokiaLAGMember(LAGMember): @@ -155,7 +155,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: @validator("new_lag_interface", allow_reuse=True, pre=True, always=True) def lag_interface_proper_name(cls, new_lag_interface: str) -> str: - if get_router_vendor(new_router) == RouterVendor.JUNIPER: + if get_router_vendor(new_router) == Vendor.JUNIPER: juniper_lag_re = re.compile("^ae\\d{1,2}$") if not juniper_lag_re.match(new_lag_interface): msg = "Invalid LAG name, please try again." @@ -643,10 +643,10 @@ def migrate_iptrunk() -> StepList: * Update the subscription model in the database * Update the reserved interfaces in Netbox """ - new_side_is_nokia = conditional(lambda state: get_router_vendor(state["new_node"]) == RouterVendor.NOKIA) + new_side_is_nokia = conditional(lambda state: get_router_vendor(state["new_node"]) == Vendor.NOKIA) old_side_is_nokia = conditional( lambda state: get_router_vendor(state["old_side_data"]["iptrunk_side_node"]["owner_subscription_id"]) - == RouterVendor.NOKIA + == Vendor.NOKIA ) should_restore_isis_metric = conditional(lambda state: state["restore_isis_metric"]) diff --git a/gso/workflows/iptrunk/modify_trunk_interface.py b/gso/workflows/iptrunk/modify_trunk_interface.py index 1dcda4fd5ceb2f3b2a4114533ff313a76823ac53..0e0aa5254bceb4c70bf55835a9427b5070aa66b1 100644 --- a/gso/workflows/iptrunk/modify_trunk_interface.py +++ b/gso/workflows/iptrunk/modify_trunk_interface.py @@ -21,7 +21,6 @@ from gso.products.product_blocks.iptrunk import ( IptrunkType, PhyPortCapacity, ) -from gso.products.product_blocks.router import RouterVendor from gso.products.product_types.iptrunk import Iptrunk from gso.services.netbox_client import NetboxClient from gso.services.provisioning_proxy import execute_playbook, pp_interaction @@ -34,6 +33,7 @@ from gso.utils.helpers import ( validate_iptrunk_unique_interface, validate_tt_number, ) +from gso.utils.shared_choices import Vendor def initialize_ae_members(subscription: Iptrunk, initial_user_input: dict, side_index: int) -> type[LAGMember]: @@ -41,7 +41,7 @@ def initialize_ae_members(subscription: Iptrunk, initial_user_input: dict, side_ router = subscription.iptrunk.iptrunk_sides[side_index].iptrunk_side_node router_vendor = get_router_vendor(router.owner_subscription_id) iptrunk_minimum_link = initial_user_input["iptrunk_minimum_links"] - if router_vendor == RouterVendor.NOKIA: + if router_vendor == Vendor.NOKIA: iptrunk_speed = initial_user_input["iptrunk_speed"] class NokiaLAGMember(LAGMember): @@ -386,13 +386,13 @@ def modify_trunk_interface() -> StepList: lambda state: get_router_vendor( state["subscription"]["iptrunk"]["iptrunk_sides"][0]["iptrunk_side_node"]["owner_subscription_id"] ) - == RouterVendor.NOKIA + == Vendor.NOKIA ) side_b_is_nokia = conditional( lambda state: get_router_vendor( state["subscription"]["iptrunk"]["iptrunk_sides"][1]["iptrunk_side_node"]["owner_subscription_id"] ) - == RouterVendor.NOKIA + == Vendor.NOKIA ) return ( init diff --git a/gso/workflows/iptrunk/terminate_iptrunk.py b/gso/workflows/iptrunk/terminate_iptrunk.py index 33ee86fde6129a05278d8985a3959e2ef945c067..aa5a91eaae3dd0526dbce2effdafb44492ae9ce2 100644 --- a/gso/workflows/iptrunk/terminate_iptrunk.py +++ b/gso/workflows/iptrunk/terminate_iptrunk.py @@ -19,12 +19,12 @@ from orchestrator.workflows.utils import wrap_modify_initial_input_form from pydantic import validator from gso.products.product_blocks.iptrunk import IptrunkSideBlock -from gso.products.product_blocks.router import RouterVendor from gso.products.product_types.iptrunk import Iptrunk from gso.services import infoblox from gso.services.netbox_client import NetboxClient from gso.services.provisioning_proxy import execute_playbook, pp_interaction from gso.utils.helpers import get_router_vendor, validate_tt_number +from gso.utils.shared_choices import Vendor from gso.utils.workflow_steps import set_isis_to_90000 @@ -159,14 +159,14 @@ def terminate_iptrunk() -> StepList: and get_router_vendor( state["subscription"]["iptrunk"]["iptrunk_sides"][0]["iptrunk_side_node"]["owner_subscription_id"] ) - == RouterVendor.NOKIA + == Vendor.NOKIA ) side_b_is_nokia = conditional( lambda state: state["clean_up_netbox"] and get_router_vendor( state["subscription"]["iptrunk"]["iptrunk_sides"][1]["iptrunk_side_node"]["owner_subscription_id"] ) - == RouterVendor.NOKIA + == Vendor.NOKIA ) config_steps = ( diff --git a/gso/workflows/router/create_router.py b/gso/workflows/router/create_router.py index 29a60dbb87d73f68f695710d3b746a8e8fa4c198..bdf829b7b9ad1857b05d36e5e95d3fe72cbb26ba 100644 --- a/gso/workflows/router/create_router.py +++ b/gso/workflows/router/create_router.py @@ -13,19 +13,15 @@ from orchestrator.workflows.utils import wrap_create_initial_input_form from pydantic import validator from pydantic_forms.core import ReadOnlyField -from gso.products.product_blocks.router import ( - PortNumber, - RouterRole, - RouterVendor, - generate_fqdn, -) +from gso.products.product_blocks.router import RouterRole from gso.products.product_types.router import RouterInactive from gso.products.product_types.site import Site from gso.services import infoblox, subscriptions from gso.services.crm import get_customer_by_name from gso.services.netbox_client import NetboxClient from gso.services.provisioning_proxy import pp_interaction -from gso.utils.helpers import iso_from_ipv4 +from gso.utils.helpers import generate_fqdn, iso_from_ipv4 +from gso.utils.shared_choices import PortNumber, Vendor from gso.utils.workflow_steps import deploy_base_config_dry, deploy_base_config_real, run_checks_after_base_config @@ -47,7 +43,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: tt_number: str customer: str = ReadOnlyField("GÉANT") - vendor: RouterVendor + vendor: Vendor router_site: _site_selector() # type: ignore[valid-type] hostname: str ts_port: PortNumber @@ -91,7 +87,7 @@ def initialize_subscription( ts_port: PortNumber, router_site: str, router_role: RouterRole, - vendor: RouterVendor, + vendor: Vendor, ) -> State: """Initialise the subscription object in the service database.""" subscription.router.router_ts_port = ts_port @@ -221,7 +217,7 @@ def create_router() -> StepList: * Validate :term:`IPAM` resources * Create a new device in Netbox """ - router_is_nokia = conditional(lambda state: state["vendor"] == RouterVendor.NOKIA) + router_is_nokia = conditional(lambda state: state["vendor"] == Vendor.NOKIA) return ( init diff --git a/gso/workflows/router/terminate_router.py b/gso/workflows/router/terminate_router.py index 1a83943a76852689ed931da4f22ea7f172022cfd..1c5052b55f7f6d07179e43135e51a5cd1106cb4b 100644 --- a/gso/workflows/router/terminate_router.py +++ b/gso/workflows/router/terminate_router.py @@ -18,11 +18,11 @@ from orchestrator.workflows.steps import ( ) from orchestrator.workflows.utils import wrap_modify_initial_input_form -from gso.products.product_blocks.router import RouterVendor from gso.products.product_types.router import Router from gso.services import infoblox from gso.services.netbox_client import NetboxClient from gso.services.provisioning_proxy import execute_playbook, pp_interaction +from gso.utils.shared_choices import Vendor logger = logging.getLogger(__name__) @@ -41,7 +41,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: clean_up_ipam: bool = True user_input = yield TerminateForm - return user_input.dict() | {"router_is_nokia": router.router.vendor == RouterVendor.NOKIA} + return user_input.dict() | {"router_is_nokia": router.router.vendor == Vendor.NOKIA} @step("Deprovision loopback IPs from IPAM") diff --git a/gso/workflows/tasks/import_office_router.py b/gso/workflows/tasks/import_office_router.py new file mode 100644 index 0000000000000000000000000000000000000000..ce7b3664c9cdf93735d519d92ead73c9fdcd9530 --- /dev/null +++ b/gso/workflows/tasks/import_office_router.py @@ -0,0 +1,94 @@ +"""A creation workflow that adds existing office routers to the coreDB.""" + +import ipaddress + +from orchestrator import workflow +from orchestrator.forms import FormPage +from orchestrator.targets import Target +from orchestrator.types import FormGenerator, State, SubscriptionLifecycle +from orchestrator.workflow import StepList, done, init, step +from orchestrator.workflows.steps import resync, set_status, store_process_subscription + +from gso.products import ProductType +from gso.products.product_types import office_router +from gso.products.product_types.office_router import OfficeRouterInactive +from gso.services import subscriptions +from gso.services.crm import get_customer_by_name +from gso.services.subscriptions import get_site_by_name +from gso.utils.shared_choices import PortNumber, Vendor + + +@step("Create subscription") +def create_subscription(customer: str) -> State: + """Create a new subscription object.""" + customer_id = get_customer_by_name(customer)["id"] + product_id = subscriptions.get_product_id_by_name(ProductType.OFFICE_ROUTER) + subscription = OfficeRouterInactive.from_product_id(product_id, customer_id) + + return { + "subscription": subscription, + "subscription_id": subscription.subscription_id, + } + + +def initial_input_form_generator() -> FormGenerator: + """Generate a form that is filled in using information passed through the :term:`API` endpoint.""" + + class ImportOfficeRouter(FormPage): + class Config: + title = "Import an office router" + + customer: str + office_router_site: str + office_router_fqdn: str + office_router_ts_port: PortNumber + office_router_lo_ipv4_address: ipaddress.IPv4Address + office_router_lo_ipv6_address: ipaddress.IPv6Address + + user_input = yield ImportOfficeRouter + + return user_input.dict() + + +@step("Initialize subscription") +def initialize_subscription( + subscription: OfficeRouterInactive, + office_router_fqdn: str, + office_router_ts_port: PortNumber, + office_router_site: str, + office_router_lo_ipv4_address: ipaddress.IPv4Address | None = None, + office_router_lo_ipv6_address: ipaddress.IPv6Address | None = None, +) -> State: + """Initialise the office router subscription using input data.""" + subscription.office_router.office_router_ts_port = office_router_ts_port + site_obj = get_site_by_name(office_router_site).site + subscription.office_router.office_router_site = site_obj + subscription.office_router.office_router_fqdn = office_router_fqdn + subscription.description = f"Office router {office_router_fqdn}" + subscription.office_router.office_router_lo_ipv4_address = office_router_lo_ipv4_address + subscription.office_router.office_router_lo_ipv6_address = office_router_lo_ipv6_address + subscription.office_router.vendor = Vendor.JUNIPER + + subscription = office_router.OfficeRouterProvisioning.from_other_lifecycle( + subscription, SubscriptionLifecycle.PROVISIONING + ) + + return {"subscription": subscription} + + +@workflow( + "Import office router", + initial_input_form=initial_input_form_generator, + target=Target.CREATE, +) +def import_office_router() -> StepList: + """Import an office router without provisioning it.""" + return ( + init + >> create_subscription + >> store_process_subscription(Target.CREATE) + >> initialize_subscription + >> set_status(SubscriptionLifecycle.ACTIVE) + >> resync + >> done + ) diff --git a/gso/workflows/tasks/import_router.py b/gso/workflows/tasks/import_router.py index 878b538a40b0cf326380863d7008ef44b8acaab3..e7e332ff6fc9522434d875760376534cdb3732e9 100644 --- a/gso/workflows/tasks/import_router.py +++ b/gso/workflows/tasks/import_router.py @@ -11,26 +11,14 @@ from orchestrator.workflows.steps import resync, set_status, store_process_subsc from gso.products import ProductType from gso.products.product_blocks import router as router_pb -from gso.products.product_blocks.router import PortNumber, RouterRole, RouterVendor, generate_fqdn +from gso.products.product_blocks.router import RouterRole from gso.products.product_types import router from gso.products.product_types.router import RouterInactive -from gso.products.product_types.site import Site from gso.services import subscriptions from gso.services.crm import get_customer_by_name - - -def _get_site_by_name(site_name: str) -> Site: - """Get a site by its name. - - :param site_name: The name of the site. - :type site_name: str - """ - subscription = subscriptions.get_active_subscriptions_by_field_and_value("site_name", site_name) - if not subscription: - msg = f"Site with name {site_name} not found." - raise ValueError(msg) - - return Site.from_subscription(subscription[0].subscription_id) +from gso.services.subscriptions import get_site_by_name +from gso.utils.helpers import generate_fqdn +from gso.utils.shared_choices import PortNumber, Vendor @step("Create subscription") @@ -57,7 +45,7 @@ def initial_input_form_generator() -> FormGenerator: router_site: str hostname: str ts_port: int - router_vendor: RouterVendor + router_vendor: Vendor router_role: RouterRole router_lo_ipv4_address: ipaddress.IPv4Address router_lo_ipv6_address: ipaddress.IPv6Address @@ -75,14 +63,14 @@ def initialize_subscription( ts_port: PortNumber, router_site: str, router_role: router_pb.RouterRole, - router_vendor: RouterVendor, + router_vendor: Vendor, router_lo_ipv4_address: ipaddress.IPv4Address | None = None, router_lo_ipv6_address: ipaddress.IPv6Address | None = None, router_lo_iso_address: str | None = None, ) -> State: """Initialise the router subscription using input data.""" subscription.router.router_ts_port = ts_port - router_site_obj = _get_site_by_name(router_site).site + router_site_obj = get_site_by_name(router_site).site subscription.router.router_site = router_site_obj fqdn = generate_fqdn(hostname, router_site_obj.site_name, router_site_obj.site_country_code) subscription.router.router_fqdn = fqdn diff --git a/gso/workflows/tasks/import_super_pop_switch.py b/gso/workflows/tasks/import_super_pop_switch.py new file mode 100644 index 0000000000000000000000000000000000000000..e13896f20f31e2f499e62425eb6c26c745137673 --- /dev/null +++ b/gso/workflows/tasks/import_super_pop_switch.py @@ -0,0 +1,93 @@ +"""A creation workflow that adds existing Super PoP switches to the coreDB.""" + +import ipaddress + +from orchestrator import workflow +from orchestrator.forms import FormPage +from orchestrator.targets import Target +from orchestrator.types import FormGenerator, State, SubscriptionLifecycle +from orchestrator.workflow import StepList, done, init, step +from orchestrator.workflows.steps import resync, set_status, store_process_subscription + +from gso.products import ProductType +from gso.products.product_types import super_pop_switch +from gso.products.product_types.super_pop_switch import SuperPopSwitchInactive +from gso.services import subscriptions +from gso.services.crm import get_customer_by_name +from gso.services.subscriptions import get_site_by_name +from gso.utils.helpers import generate_fqdn +from gso.utils.shared_choices import PortNumber, Vendor + + +@step("Create subscription") +def create_subscription(customer: str) -> State: + """Create a new subscription object.""" + customer_id = get_customer_by_name(customer)["id"] + product_id = subscriptions.get_product_id_by_name(ProductType.SUPER_POP_SWITCH) + subscription = SuperPopSwitchInactive.from_product_id(product_id, customer_id) + + return { + "subscription": subscription, + "subscription_id": subscription.subscription_id, + } + + +def initial_input_form_generator() -> FormGenerator: + """Generate a form that is filled in using information passed through the :term:`API` endpoint.""" + + class ImportSuperPopSwitch(FormPage): + class Config: + title = "Import a Super PoP switch" + + customer: str + super_pop_switch_site: str + hostname: str + super_pop_switch_ts_port: PortNumber + super_pop_switch_mgmt_ipv4_address: ipaddress.IPv4Address + + user_input = yield ImportSuperPopSwitch + + return user_input.dict() + + +@step("Initialize subscription") +def initialize_subscription( + subscription: SuperPopSwitchInactive, + hostname: str, + super_pop_switch_ts_port: PortNumber, + super_pop_switch_site: str, + super_pop_switch_mgmt_ipv4_address: ipaddress.IPv4Address | None = None, +) -> State: + """Initialise the Super PoP switch subscription using input data.""" + subscription.super_pop_switch.super_pop_switch_ts_port = super_pop_switch_ts_port + site_obj = get_site_by_name(super_pop_switch_site).site + subscription.super_pop_switch.super_pop_switch_site = site_obj + fqdn = generate_fqdn(hostname, site_obj.site_name, site_obj.site_country_code) + subscription.super_pop_switch.super_pop_switch_fqdn = fqdn + subscription.description = f"Super PoP switch {fqdn}" + subscription.super_pop_switch.super_pop_switch_mgmt_ipv4_address = super_pop_switch_mgmt_ipv4_address + subscription.super_pop_switch.vendor = Vendor.JUNIPER + + subscription = super_pop_switch.SuperPopSwitchProvisioning.from_other_lifecycle( + subscription, SubscriptionLifecycle.PROVISIONING + ) + + return {"subscription": subscription} + + +@workflow( + "Import Super PoP switch", + initial_input_form=initial_input_form_generator, + target=Target.CREATE, +) +def import_super_pop_switch() -> StepList: + """Import a Super PoP switch without provisioning it.""" + return ( + init + >> create_subscription + >> store_process_subscription(Target.CREATE) + >> initialize_subscription + >> set_status(SubscriptionLifecycle.ACTIVE) + >> resync + >> done + ) diff --git a/test/api/test_imports.py b/test/api/test_imports.py index d823bc367d39c3072faf1c307d4c8d796151de22..0737860921c930bb3928efcf859ed67c6a0ac676 100644 --- a/test/api/test_imports.py +++ b/test/api/test_imports.py @@ -6,13 +6,16 @@ from orchestrator.db import SubscriptionTable from orchestrator.services import subscriptions from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity -from gso.products.product_blocks.router import RouterRole, RouterVendor +from gso.products.product_blocks.router import RouterRole from gso.products.product_blocks.site import SiteTier from gso.utils.helpers import iso_from_ipv4 +from gso.utils.shared_choices import Vendor SITE_IMPORT_ENDPOINT = "/api/v1/imports/sites" ROUTER_IMPORT_ENDPOINT = "/api/v1/imports/routers" IPTRUNK_IMPORT_API_URL = "/api/v1/imports/iptrunks" +SUPER_POP_SWITCH_IMPORT_API_URL = "/api/v1/imports/super-pop-switches" +OFFICE_ROUTER_IMPORT_API_URL = "/api/v1/imports/office-routers" @pytest.fixture() @@ -114,7 +117,7 @@ def router_data(faker, site_data): return { "hostname": "127.0.0.1", "router_role": RouterRole.PE, - "router_vendor": RouterVendor.JUNIPER, + "router_vendor": Vendor.JUNIPER, "router_site": site_data["site_name"], "ts_port": 1234, "customer": "GÉANT", @@ -124,6 +127,30 @@ def router_data(faker, site_data): } +@pytest.fixture() +def super_pop_switch_data(faker, site_data): + mock_ipv4 = faker.ipv4() + return { + "hostname": "127.0.0.1", + "super_pop_switch_site": site_data["site_name"], + "super_pop_switch_ts_port": 1234, + "customer": "GÉANT", + "super_pop_switch_mgmt_ipv4_address": mock_ipv4, + } + + +@pytest.fixture() +def office_router_data(faker, site_data): + return { + "office_router_fqdn": "127.0.0.1", + "office_router_site": site_data["site_name"], + "office_router_ts_port": 1234, + "customer": "GÉANT", + "office_router_lo_ipv4_address": faker.ipv4(), + "office_router_lo_ipv6_address": faker.ipv6(), + } + + def test_import_site_endpoint(test_client, site_data): assert SubscriptionTable.query.all() == [] # Post data to the endpoint @@ -337,3 +364,59 @@ def test_import_iptrunk_fails_on_side_a_and_b_members_mismatch( }, ], } + + +def test_import_super_pop_switch_endpoint(test_client, site_data, super_pop_switch_data): + response = test_client.post(SITE_IMPORT_ENDPOINT, json=site_data) + assert response.status_code == 201 + assert SubscriptionTable.query.count() == 1 + + response = test_client.post(SUPER_POP_SWITCH_IMPORT_API_URL, json=super_pop_switch_data) + assert response.status_code == 201 + assert SubscriptionTable.query.count() == 2 + + +def test_import_super_pop_switch_endpoint_with_invalid_data(test_client, site_data, super_pop_switch_data): + response = test_client.post(SITE_IMPORT_ENDPOINT, json=site_data) + assert response.status_code == 201 + assert SubscriptionTable.query.count() == 1 + + # invalid data, missing hostname and invalid mgmt_ipv4_address + super_pop_switch_data.pop("hostname") + super_pop_switch_data["super_pop_switch_mgmt_ipv4_address"] = "invalid" + response = test_client.post(SUPER_POP_SWITCH_IMPORT_API_URL, json=super_pop_switch_data) + assert response.status_code == 422 + assert SubscriptionTable.query.count() == 1 + response = response.json() + assert response["detail"][0]["loc"] == ["body", "hostname"] + assert response["detail"][0]["msg"] == "field required" + assert response["detail"][1]["loc"] == ["body", "super_pop_switch_mgmt_ipv4_address"] + assert response["detail"][1]["msg"] == "value is not a valid IPv4 address" + + +def test_import_office_router_endpoint(test_client, site_data, office_router_data): + response = test_client.post(SITE_IMPORT_ENDPOINT, json=site_data) + assert response.status_code == 201 + assert SubscriptionTable.query.count() == 1 + + response = test_client.post(OFFICE_ROUTER_IMPORT_API_URL, json=office_router_data) + assert response.status_code == 201 + assert SubscriptionTable.query.count() == 2 + + +def test_import_office_router_endpoint_with_invalid_data(test_client, site_data, office_router_data): + response = test_client.post(SITE_IMPORT_ENDPOINT, json=site_data) + assert response.status_code == 201 + assert SubscriptionTable.query.count() == 1 + + # invalid data, missing FQDN and invalid lo_ipv6_address + office_router_data.pop("office_router_fqdn") + office_router_data["office_router_lo_ipv6_address"] = "invalid" + response = test_client.post(OFFICE_ROUTER_IMPORT_API_URL, json=office_router_data) + assert response.status_code == 422 + assert SubscriptionTable.query.count() == 1 + response = response.json() + assert response["detail"][0]["loc"] == ["body", "office_router_fqdn"] + assert response["detail"][0]["msg"] == "field required" + assert response["detail"][1]["loc"] == ["body", "office_router_lo_ipv6_address"] + assert response["detail"][1]["msg"] == "value is not a valid IPv6 address" diff --git a/test/fixtures.py b/test/fixtures.py index 32bbfdca03b7218df8e6ce36b7f184f1390668c5..933fcfc14738460aea302adba8d5af2c080be08f 100644 --- a/test/fixtures.py +++ b/test/fixtures.py @@ -12,12 +12,13 @@ from gso.products.product_blocks.iptrunk import ( IptrunkType, PhyPortCapacity, ) -from gso.products.product_blocks.router import RouterRole, RouterVendor +from gso.products.product_blocks.router import RouterRole from gso.products.product_blocks.site import SiteTier from gso.products.product_types.iptrunk import IptrunkInactive from gso.products.product_types.router import Router, RouterInactive from gso.products.product_types.site import Site, SiteInactive from gso.services import subscriptions +from gso.utils.shared_choices import Vendor CUSTOMER_ID: UUIDstr = "2f47f65a-0911-e511-80d0-005056956c1a" @@ -107,7 +108,7 @@ def nokia_router_subscription_factory(site_subscription_factory, faker): router_subscription.router.router_lo_iso_address = router_lo_iso_address router_subscription.router.router_role = router_role router_subscription.router.router_site = Site.from_subscription(router_site).site - router_subscription.router.vendor = RouterVendor.NOKIA + router_subscription.router.vendor = Vendor.NOKIA router_subscription = SubscriptionModel.from_other_lifecycle(router_subscription, SubscriptionLifecycle.ACTIVE) router_subscription.description = description @@ -158,7 +159,7 @@ def juniper_router_subscription_factory(site_subscription_factory, faker): router_subscription.router.router_lo_iso_address = router_lo_iso_address router_subscription.router.router_role = router_role router_subscription.router.router_site = Site.from_subscription(router_site).site - router_subscription.router.vendor = RouterVendor.JUNIPER + router_subscription.router.vendor = Vendor.JUNIPER router_subscription = SubscriptionModel.from_other_lifecycle(router_subscription, SubscriptionLifecycle.ACTIVE) router_subscription.description = description diff --git a/test/utils/test_helpers.py b/test/utils/test_helpers.py index 4006c4ad50101e93b10ca16c802761612a244ea7..a7a09ade563cbea460f2884ae718e1545b6975c2 100644 --- a/test/utils/test_helpers.py +++ b/test/utils/test_helpers.py @@ -3,8 +3,8 @@ from unittest.mock import patch import pytest from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock -from gso.products.product_blocks.router import RouterVendor from gso.utils.helpers import available_interfaces_choices_including_current_members, validate_tt_number +from gso.utils.shared_choices import Vendor @pytest.fixture() @@ -34,20 +34,20 @@ def generate_tt_numbers(faker, request): def test_non_nokia_router_returns_none(mock_router, faker): - mock_router.from_subscription.return_value.router.vendor = RouterVendor.JUNIPER + mock_router.from_subscription.return_value.router.vendor = Vendor.JUNIPER result = available_interfaces_choices_including_current_members(faker.uuid4(), "10G", []) assert result is None def test_nokia_router_with_no_interfaces_returns_empty_choice(mock_router, mock_netbox_client, faker): - mock_router.from_subscription.return_value.router.vendor = RouterVendor.NOKIA + mock_router.from_subscription.return_value.router.vendor = Vendor.NOKIA mock_netbox_client().get_available_interfaces.return_value = iter([]) result = available_interfaces_choices_including_current_members(faker.uuid4(), "10G", []) assert len(result) == 0 def test_nokia_router_with_interfaces_returns_choice(mock_router, mock_netbox_client, faker): - mock_router.from_subscription.return_value.router.vendor = RouterVendor.NOKIA + mock_router.from_subscription.return_value.router.vendor = Vendor.NOKIA mock_netbox_client().get_available_interfaces.return_value = iter( [ {"name": "interface1", "module": {"display": "module1"}, "description": "desc1"}, diff --git a/test/workflows/iptrunk/test_create_iptrunk.py b/test/workflows/iptrunk/test_create_iptrunk.py index c069618476f87b4beafcf4fbcb89f259fc043071..d8ca8c09c719a5bf27f666074d66f6bb2a21cf0e 100644 --- a/test/workflows/iptrunk/test_create_iptrunk.py +++ b/test/workflows/iptrunk/test_create_iptrunk.py @@ -5,9 +5,9 @@ import pytest from gso.products import Iptrunk, ProductType from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity -from gso.products.product_blocks.router import RouterVendor from gso.services.subscriptions import get_product_id_by_name from gso.utils.helpers import LAGMember +from gso.utils.shared_choices import Vendor from test.services.conftest import MockedNetboxClient from test.workflows import ( assert_complete, @@ -43,11 +43,11 @@ def _netbox_client_mock(): @pytest.fixture() def input_form_wizard_data(request, juniper_router_subscription_factory, nokia_router_subscription_factory, faker): - vendor = getattr(request, "param", RouterVendor.NOKIA) + vendor = getattr(request, "param", Vendor.NOKIA) router_side_a = nokia_router_subscription_factory() # Set side b router to Juniper - if vendor == RouterVendor.JUNIPER: + if vendor == Vendor.JUNIPER: router_side_b = juniper_router_subscription_factory() side_b_members = faker.link_members_juniper() else: @@ -165,7 +165,7 @@ def test_iptrunk_creation_fails_when_lso_return_code_is_one( assert mock_execute_playbook.call_count == 2 -@pytest.mark.parametrize("input_form_wizard_data", [RouterVendor.JUNIPER], indirect=True) +@pytest.mark.parametrize("input_form_wizard_data", [Vendor.JUNIPER], indirect=True) @pytest.mark.workflow() @patch("gso.workflows.iptrunk.create_iptrunk.execute_playbook") @patch("gso.workflows.iptrunk.create_iptrunk.infoblox.allocate_v6_network") diff --git a/test/workflows/iptrunk/test_migrate_iptrunk.py b/test/workflows/iptrunk/test_migrate_iptrunk.py index c7ca62bf11a89dbde7aa9d3bf2297eef2c5651cd..634bd52774a45370594b4413d94db40ea59db6bd 100644 --- a/test/workflows/iptrunk/test_migrate_iptrunk.py +++ b/test/workflows/iptrunk/test_migrate_iptrunk.py @@ -4,8 +4,8 @@ from unittest.mock import patch import pytest from gso.products import Iptrunk -from gso.products.product_blocks.router import RouterVendor from gso.products.product_types.router import Router +from gso.utils.shared_choices import Vendor from test import USER_CONFIRM_EMPTY_FORM from test.conftest import UseJuniperSide from test.workflows import ( @@ -189,15 +189,15 @@ def test_migrate_iptrunk_success( vendor_new = Router.from_subscription(new_router).router.vendor # Only Nokia will be checked on netbox - num_nokia_lags = 1 if vendor_new == RouterVendor.NOKIA else 0 - num_nokia_reserved = 2 * (vendor_new == RouterVendor.NOKIA) - num_nokia_attached = 2 * (vendor_new == RouterVendor.NOKIA) + num_nokia_lags = 1 if vendor_new == Vendor.NOKIA else 0 + num_nokia_reserved = 2 * (vendor_new == Vendor.NOKIA) + num_nokia_attached = 2 * (vendor_new == Vendor.NOKIA) # Only interfaces lag delete for nokia node is tested - num_nokia_lag_del = 1 * (vendor_old == RouterVendor.NOKIA) + num_nokia_lag_del = 1 * (vendor_old == Vendor.NOKIA) # Only free interfaces when node was nokia - num_nokia_free = 2 * (vendor_old == RouterVendor.NOKIA) + num_nokia_free = 2 * (vendor_old == Vendor.NOKIA) # Assert all Netbox calls have been made assert mocked_create_interface.call_count == num_nokia_lags # once for creating the LAG on the newly replaced side: diff --git a/test/workflows/iptrunk/test_modify_trunk_interface.py b/test/workflows/iptrunk/test_modify_trunk_interface.py index b3b15a75e8904eab51b767fb6ef3d527169efff1..5a7d1596c4fb0c53a3ddb840bfa89f1d96bbb233 100644 --- a/test/workflows/iptrunk/test_modify_trunk_interface.py +++ b/test/workflows/iptrunk/test_modify_trunk_interface.py @@ -4,7 +4,7 @@ import pytest from gso.products import Iptrunk from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity -from gso.products.product_blocks.router import RouterVendor +from gso.utils.shared_choices import Vendor from test.conftest import UseJuniperSide from test.workflows import ( assert_complete, @@ -141,15 +141,15 @@ def test_iptrunk_modify_trunk_interface_success( # Only Nokia interfaces are checked vendor_side_a = subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.vendor vendor_side_b = subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.vendor - num_ifaces = (len(new_side_a_ae_members) if vendor_side_a == RouterVendor.NOKIA else 0) + ( - len(new_side_b_ae_members) if vendor_side_b == RouterVendor.NOKIA else 0 + num_ifaces = (len(new_side_a_ae_members) if vendor_side_a == Vendor.NOKIA else 0) + ( + len(new_side_b_ae_members) if vendor_side_b == Vendor.NOKIA else 0 ) # Define free interfaces for only nokia sides - num_free_ifaces = 2 * (vendor_side_a == RouterVendor.NOKIA) + 2 * (vendor_side_b == RouterVendor.NOKIA) + num_free_ifaces = 2 * (vendor_side_a == Vendor.NOKIA) + 2 * (vendor_side_b == Vendor.NOKIA) # lag interface for nokia sides - num_lag_ifaces = int(vendor_side_a == RouterVendor.NOKIA) + int(vendor_side_b == RouterVendor.NOKIA) + num_lag_ifaces = int(vendor_side_a == Vendor.NOKIA) + int(vendor_side_b == Vendor.NOKIA) assert mocked_reserve_interface.call_count == num_ifaces # Only nokia interfaces per side num is randomly generated assert mocked_attach_interface_to_lag.call_count == num_ifaces diff --git a/test/workflows/router/test_create_router.py b/test/workflows/router/test_create_router.py index ca012757153478de3ca4e50fa37b44daa9696595..898823b79e84bb34200225c132f4319515999cf5 100644 --- a/test/workflows/router/test_create_router.py +++ b/test/workflows/router/test_create_router.py @@ -4,10 +4,11 @@ import pytest from infoblox_client import objects from gso.products import ProductType, Site -from gso.products.product_blocks.router import RouterRole, RouterVendor +from gso.products.product_blocks.router import RouterRole from gso.products.product_types.router import Router from gso.services.subscriptions import get_product_id_by_name from test import USER_CONFIRM_EMPTY_FORM +from gso.utils.shared_choices import Vendor from test.workflows import ( assert_complete, assert_pp_interaction_failure, @@ -29,7 +30,7 @@ def router_creation_input_form_data(site_subscription_factory, faker): "hostname": faker.pystr(), "ts_port": faker.pyint(), "router_role": faker.random_choices(elements=(RouterRole.P, RouterRole.PE, RouterRole.AMT), length=1)[0], - "vendor": RouterVendor.NOKIA, + "vendor": Vendor.NOKIA, }