diff --git a/Changelog.md b/Changelog.md index bd167f8ed8c18eef0ab1af670a9b84cae3cac900..0073e89a7b1fc69886aa7c47e5794ab58ffaa9a0 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,6 +1,13 @@ # Changelog -## [2.5] - 2024-11-21 +## [2.26] - 2024-11-27 +- Rename the `NRENL3CoreService` to `L3CoreService` +- To the existing Layer 3 core services, add LHCOne, Copernicus and IAS-GWS +- Add Layer 2 circuit services (GÉANT+ and Azure ExpressRoute) +- Add a field to BGP sessions for storing prefix limits +- Update L3 Core service to allow for BFD settings in both a BGP session and the Service Binding Port + +## [2.25] - 2024-11-21 - Updated a trunk description when it is migrated. - Made moodi steps indifferent about failure - Added a router to Kentik when creating with a PE role diff --git a/docs/source/module/workflows/l2_circuit/nren_l3_core_service/create_imported_layer_2_circuit.rst b/docs/source/module/workflows/l2_circuit/nren_l3_core_service/create_imported_layer_2_circuit.rst new file mode 100644 index 0000000000000000000000000000000000000000..c8683dd28a8653080494be20c86f8a658305d826 --- /dev/null +++ b/docs/source/module/workflows/l2_circuit/nren_l3_core_service/create_imported_layer_2_circuit.rst @@ -0,0 +1,6 @@ +``gso.workflows.l2_circuit.create_imported_layer_2_circuit`` +============================================================ + +.. automodule:: gso.workflows.l2_circuit.create_imported_layer_2_circuit + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/l2_circuit/nren_l3_core_service/create_layer_2_circuit.rst b/docs/source/module/workflows/l2_circuit/nren_l3_core_service/create_layer_2_circuit.rst new file mode 100644 index 0000000000000000000000000000000000000000..c19b27a1fef15935a4190414dae474ef45eccdb6 --- /dev/null +++ b/docs/source/module/workflows/l2_circuit/nren_l3_core_service/create_layer_2_circuit.rst @@ -0,0 +1,6 @@ +``gso.workflows.l2_circuit.create_layer_2_circuit`` +=================================================== + +.. automodule:: gso.workflows.l2_circuit.create_layer_2_circuit + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/l2_circuit/nren_l3_core_service/import_layer_2_circuit.rst b/docs/source/module/workflows/l2_circuit/nren_l3_core_service/import_layer_2_circuit.rst new file mode 100644 index 0000000000000000000000000000000000000000..10231456b09c062f5fec98b06aaa9a7d22db84ee --- /dev/null +++ b/docs/source/module/workflows/l2_circuit/nren_l3_core_service/import_layer_2_circuit.rst @@ -0,0 +1,6 @@ +``gso.workflows.l2_circuit.import_layer_2_circuit`` +=================================================== + +.. automodule:: gso.workflows.l2_circuit.import_layer_2_circuit + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/l2_circuit/nren_l3_core_service/index.rst b/docs/source/module/workflows/l2_circuit/nren_l3_core_service/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..5e3d52fb1aff1fcf798b52fe09f3d58fab38aa30 --- /dev/null +++ b/docs/source/module/workflows/l2_circuit/nren_l3_core_service/index.rst @@ -0,0 +1,20 @@ +``gso.workflows.L2_circuit +========================== + +.. automodule:: gso.workflows.L2_circuit + :members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + :maxdepth: 2 + :titlesonly: + + create_layer_2_circuit + modify_layer_2_circuit + terminate_layer_2_circuit + create_imported_layer_2_circuit + import_layer_2_circuit + diff --git a/docs/source/module/workflows/l2_circuit/nren_l3_core_service/modify_layer_2_circuit.rst b/docs/source/module/workflows/l2_circuit/nren_l3_core_service/modify_layer_2_circuit.rst new file mode 100644 index 0000000000000000000000000000000000000000..f46d037adb145b1d6efe177f365e582ea743e3c6 --- /dev/null +++ b/docs/source/module/workflows/l2_circuit/nren_l3_core_service/modify_layer_2_circuit.rst @@ -0,0 +1,6 @@ +``gso.workflows.l2_circuit.modify_layer_2_circuit`` +=================================================== + +.. automodule:: gso.workflows.l2_circuit.modify_layer_2_circuit + :members: + :show-inheritance: diff --git a/gso/cli/imports.py b/gso/cli/imports.py index cd2e5998dd39ea965f1c476357c1738afeaae372..bca9ccd4bc49daeb873bc6c696deec077fda6892 100644 --- a/gso/cli/imports.py +++ b/gso/cli/imports.py @@ -13,7 +13,7 @@ import yaml from orchestrator.db import db from orchestrator.services.processes import start_process from orchestrator.types import SubscriptionLifecycle, UUIDstr -from pydantic import BaseModel, ValidationError, field_validator, model_validator +from pydantic import BaseModel, NonNegativeInt, ValidationError, field_validator, model_validator from sqlalchemy.exc import SQLAlchemyError from gso.db.models import PartnerTable @@ -21,10 +21,11 @@ from gso.products import ProductType 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.layer_2_circuit import Layer2CircuitType 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.products.product_types.edge_port import EdgePort +from gso.products.product_types.layer_2_circuit import Layer2CircuitServiceType from gso.services.partners import ( PartnerEmail, PartnerName, @@ -39,7 +40,7 @@ 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.interfaces import BandwidthString, LAGMember, LAGMemberList, PhysicalPortCapacity from gso.utils.types.ip_address import ( AddressSpace, IPAddress, @@ -50,6 +51,7 @@ from gso.utils.types.ip_address import ( IPV6Netmask, PortNumber, ) +from gso.utils.types.virtual_identifiers import VC_ID, VLAN_ID app: typer.Typer = typer.Typer() @@ -236,17 +238,15 @@ class EdgePortImportModel(BaseModel): return self -class NRENL3CoreServiceImportModel(BaseModel): - """Import :term:`NREN` L3 Core Service model.""" +class L3CoreServiceImportModel(BaseModel): + """Import L3 Core Service model.""" class BaseBGPPeer(BaseModel): """Base BGP Peer model.""" bfd_enabled: bool = False - bfd_interval: int | None = None - bfd_multiplier: int | None = None has_custom_policies: bool = False - authentication_key: str + authentication_key: str | None multipath_enabled: bool = False send_default_route: bool = False is_passive: bool = False @@ -254,6 +254,15 @@ class NRENL3CoreServiceImportModel(BaseModel): families: list[IPFamily] is_multi_hop: bool rtbh_enabled: bool # whether Remote Triggered Blackhole is enabled + prefix_limit: NonNegativeInt | None = None + + class BFDSettingsModel(BaseModel): + """BFD Settings model.""" + + bfd_enabled: bool = False + bfd_interval_rx: int | None = None + bfd_interval_tx: int | None = None + bfd_multiplier: int | None = None class ServiceBindingPort(BaseModel): """Service Binding model.""" @@ -270,7 +279,9 @@ class NRENL3CoreServiceImportModel(BaseModel): ipv6_address: IPv6AddressType ipv6_mask: IPV6Netmask is_multi_hop: bool = True - bgp_peers: list["NRENL3CoreServiceImportModel.BaseBGPPeer"] + bgp_peers: list["L3CoreServiceImportModel.BaseBGPPeer"] + v4_bfd_settings: "L3CoreServiceImportModel.BFDSettingsModel" + v6_bfd_settings: "L3CoreServiceImportModel.BFDSettingsModel" partner: str service_binding_ports: list[ServiceBindingPort] @@ -327,6 +338,49 @@ class LanSwitchInterconnectImportModel(BaseModel): switch_side: LanSwitchInterconnectSwitchSideImportModel +class Layer2CircuitServiceImportModel(BaseModel): + """Import Layer 2 Circuit Service model.""" + + class ServiceBindingPortInput(BaseModel): + """Service Binding Port model.""" + + edge_port: UUIDstr + vlan_id: VLAN_ID + + service_type: Layer2CircuitServiceType + partner: str + geant_sid: str + vc_id: VC_ID + layer_2_circuit_side_a: ServiceBindingPortInput + layer_2_circuit_side_b: ServiceBindingPortInput + layer_2_circuit_type: Layer2CircuitType + vlan_range_lower_bound: VLAN_ID | None = None + vlan_range_upper_bound: VLAN_ID | None = None + policer_enabled: bool = False + policer_bandwidth: BandwidthString | None = None + policer_burst_rate: BandwidthString | None = None + + @field_validator("partner") + def check_if_partner_exists(cls, value: str) -> str: + """Validate that the partner exists.""" + try: + get_partner_by_name(value) + except PartnerNotFoundError as e: + msg = f"Partner {value} not found" + raise ValueError(msg) from e + + return value + + @model_validator(mode="after") + def check_if_edge_ports_exist(self) -> Self: + """Check if the edge ports exist.""" + for side in [self.layer_2_circuit_side_a, self.layer_2_circuit_side_b]: + if not EdgePort.from_subscription(side.edge_port): + msg = f"Edge Port {side.edge_port} not found" + raise ValueError(msg) + return self + + T = TypeVar( "T", SiteImportModel, @@ -337,8 +391,9 @@ T = TypeVar( OfficeRouterImportModel, OpenGearImportModel, EdgePortImportModel, - NRENL3CoreServiceImportModel, + L3CoreServiceImportModel, LanSwitchInterconnectImportModel, + Layer2CircuitServiceImportModel, ) common_filepath_option = typer.Option( @@ -601,20 +656,20 @@ def import_partners(file_path: str = typer.Argument(..., help="Path to the CSV f @app.command() -def import_nren_l3_core_service(filepath: str = common_filepath_option) -> None: - """Import :term:`NREN` L3 Core Services into :term:`GSO`.""" +def import_l3_core_service(filepath: str = common_filepath_option) -> None: + """Import L3 Core Services into :term:`GSO`.""" successfully_imported_data = [] - nren_l3_core_service_list = _read_data(Path(filepath)) + l3_core_service_list = _read_data(Path(filepath)) - for nren_l3_core_service in nren_l3_core_service_list: - partner = nren_l3_core_service["partner"] - service_type = NRENL3CoreServiceType(nren_l3_core_service["service_type"]) + for l3_core_service in l3_core_service_list: + partner = l3_core_service["partner"] + service_type = l3_core_service["service_type"] typer.echo(f"Creating imported {service_type} for {partner}") try: - initial_data = NRENL3CoreServiceImportModel(**nren_l3_core_service) - start_process("create_imported_nren_l3_core_service", [initial_data.model_dump()]) - edge_ports = [sbp["edge_port"] for sbp in nren_l3_core_service["service_binding_ports"]] + initial_data = L3CoreServiceImportModel(**l3_core_service) + start_process("create_imported_l3_core_service", [initial_data.model_dump()]) + edge_ports = [sbp["edge_port"] for sbp in l3_core_service["service_binding_ports"]] successfully_imported_data.append(edge_ports) typer.echo(f"Successfully created imported {service_type} for {partner}") except ValidationError as e: @@ -625,17 +680,22 @@ def import_nren_l3_core_service(filepath: str = common_filepath_option) -> None: # Migrate new products from imported to "full" counterpart. imported_products = get_subscriptions( - product_types=[ProductType.IMPORTED_GEANT_IP, ProductType.IMPORTED_IAS], + product_types=[ + ProductType.IMPORTED_GEANT_IP, + ProductType.IMPORTED_IAS, + ProductType.IMPORTED_LHCONE, + ProductType.IMPORTED_COPERNICUS, + ], lifecycles=[SubscriptionLifecycle.ACTIVE], includes=["subscription_id"], ) for subscription_id in imported_products: typer.echo(f"Importing {subscription_id}") - start_process("import_nren_l3_core_service", [subscription_id]) + start_process("import_l3_core_service", [subscription_id]) if successfully_imported_data: - typer.echo("Successfully created imported NREN L3 Core Services:") + typer.echo("Successfully created imported L3 Core Services:") for item in successfully_imported_data: typer.echo(f"- {item}") @@ -650,3 +710,44 @@ def import_lan_switch_interconnect(filepath: str = common_filepath_option) -> No "lan_switch_interconnect_description", LanSwitchInterconnectImportModel, ) + + +@app.command() +def import_layer_2_circuit_service(filepath: str = common_filepath_option) -> None: + """Import Layer 2 Circuit services into GSO.""" + successfully_imported_data = [] + layer_2_circuit_service_list = _read_data(Path(filepath)) + + for layer_2_circuit_service in layer_2_circuit_service_list: + partner = layer_2_circuit_service["partner"] + service_type = Layer2CircuitServiceType(layer_2_circuit_service["service_type"]) + typer.echo(f"Creating imported {service_type} for {partner}") + + try: + initial_data = Layer2CircuitServiceImportModel(**layer_2_circuit_service) + start_process("create_imported_layer_2_circuit", [initial_data.model_dump()]) + successfully_imported_data.append(initial_data.vc_id) + typer.echo( + f"Successfully created imported {service_type} with virtual circuit ID {initial_data.vc_id}" + f" for {partner}" + ) + except ValidationError as e: + typer.echo(f"Validation error: {e}") + typer.echo("Waiting for the dust to settle before importing new products...") + time.sleep(1) + + # Migrate new products from imported to "full" counterpart. + imported_products = get_subscriptions( + product_types=[ProductType.IMPORTED_EXPRESSROUTE, ProductType.IMPORTED_GEANT_PLUS], + lifecycles=[SubscriptionLifecycle.ACTIVE], + includes=["subscription_id"], + ) + + for subscription_id in imported_products: + typer.echo(f"Importing {subscription_id}") + start_process("import_layer_2_circuit", [subscription_id]) + + if successfully_imported_data: + typer.echo("Successfully created imported Layer 2 Circuit services:") + for item in successfully_imported_data: + typer.echo(f"- {item}") diff --git a/gso/migrations/versions/2024-10-29_5132c463214d_add_layer_2_cricuits_services_domain_.py b/gso/migrations/versions/2024-10-29_5132c463214d_add_layer_2_cricuits_services_domain_.py new file mode 100644 index 0000000000000000000000000000000000000000..42bc0a1eb83f6aa3b61ebada2bc64048c301d798 --- /dev/null +++ b/gso/migrations/versions/2024-10-29_5132c463214d_add_layer_2_cricuits_services_domain_.py @@ -0,0 +1,182 @@ +"""Add Layer 2 Cricuits services domain models.. + +Revision ID: 5132c463214d +Revises: 0e7e7d749617 +Create Date: 2024-10-29 13:34:39.234303 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '5132c463214d' +down_revision = '0e7e7d749617' +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 ('GÉANT Plus', 'GÉANT Plus', 'Layer2Circuit', 'G_PLUS', 'active') RETURNING products.product_id + """)) + conn.execute(sa.text(""" +INSERT INTO products (name, description, product_type, tag, status) VALUES ('Imported GÉANT Plus', 'Imported GEANT Plus', 'ImportedLayer2Circuit', 'IMP_G_PLUS', 'active') RETURNING products.product_id + """)) + conn.execute(sa.text(""" +INSERT INTO products (name, description, product_type, tag, status) VALUES ('Azure ExpressRoute', 'Azure ExpressRoute product', 'Layer2Circuit', 'ER', 'active') RETURNING products.product_id + """)) + conn.execute(sa.text(""" +INSERT INTO products (name, description, product_type, tag, status) VALUES ('Imported Azure ExpressRoute', 'Imported Azure ExpressRoute', 'ImportedLayer2Circuit', 'IMP_ER', 'active') RETURNING products.product_id + """)) + conn.execute(sa.text(""" +INSERT INTO fixed_inputs (name, value, product_id) VALUES ('layer_2_circuit_service_type', 'Azure ExpressRoute', (SELECT products.product_id FROM products WHERE products.name IN ('Azure ExpressRoute'))), ('layer_2_circuit_service_type', 'Imported Azure ExpressRoute', (SELECT products.product_id FROM products WHERE products.name IN ('Imported Azure ExpressRoute'))), ('layer_2_circuit_service_type', 'Imported GÉANT Plus', (SELECT products.product_id FROM products WHERE products.name IN ('Imported GÉANT Plus'))), ('layer_2_circuit_service_type', 'GÉANT Plus', (SELECT products.product_id FROM products WHERE products.name IN ('GÉANT Plus'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_blocks (name, description, tag, status) VALUES ('Layer2CircuitBlock', 'Layer 2 Circuit product block', 'L2_C_BLOCK', 'active') RETURNING product_blocks.product_block_id + """)) + conn.execute(sa.text(""" +INSERT INTO product_blocks (name, description, tag, status) VALUES ('Layer2CircuitSideBlock', 'Layer 2 Circuit side product block', 'L2_C_SIDE_BLOCK', 'active') RETURNING product_blocks.product_block_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('policer_enabled', 'Whether this Layer 2 Circuit is policed.') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('bandwidth', 'If policed, the bandwidth of the policer is stored.') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('vlan_range_upper_bound', 'VLAN upper bound range') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('vlan_range_lower_bound', 'VLAN lower bound range') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('virtual_circuit_id', 'Virtual Circuit ID of this Layer 2 Circuit.') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('layer_2_circuit_type', 'The type of circuit, can be tagged or untagged.') 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 ('Azure ExpressRoute')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('Layer2CircuitBlock'))), ((SELECT products.product_id FROM products WHERE products.name IN ('Imported Azure ExpressRoute')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('Layer2CircuitBlock'))), ((SELECT products.product_id FROM products WHERE products.name IN ('Imported GÉANT Plus')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('Layer2CircuitBlock'))), ((SELECT products.product_id FROM products WHERE products.name IN ('GÉANT Plus')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('Layer2CircuitBlock'))) + """)) + 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 ('Layer2CircuitBlock')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('Layer2CircuitSideBlock'))) + """)) + 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 ('Layer2CircuitSideBlock')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort'))) + """)) + 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 ('Layer2CircuitBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('virtual_circuit_id'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('Layer2CircuitBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('layer_2_circuit_type'))) + """)) + 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 ('Layer2CircuitBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('vlan_range_lower_bound'))) + """)) + 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 ('Layer2CircuitBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('vlan_range_upper_bound'))) + """)) + 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 ('Layer2CircuitBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('policer_enabled'))) + """)) + 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 ('Layer2CircuitBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bandwidth'))) + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('policer_burst_rate', 'Max burst rate of a policer') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('Layer2CircuitBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('policer_burst_rate'))) + """)) + + +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 ('Layer2CircuitBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('policer_burst_rate')) + """)) + 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 ('Layer2CircuitBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('policer_burst_rate')) + """)) + 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 ('policer_burst_rate')) + """)) + conn.execute(sa.text(""" +DELETE FROM resource_types WHERE resource_types.resource_type IN ('policer_burst_rate') + """)) + 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 ('Layer2CircuitBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('virtual_circuit_id')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('Layer2CircuitBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('virtual_circuit_id')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('Layer2CircuitBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('layer_2_circuit_type')) + """)) + 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 ('Layer2CircuitBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('layer_2_circuit_type')) + """)) + 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 ('Layer2CircuitBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('vlan_range_lower_bound')) + """)) + 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 ('Layer2CircuitBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('vlan_range_lower_bound')) + """)) + 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 ('Layer2CircuitBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('vlan_range_upper_bound')) + """)) + 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 ('Layer2CircuitBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('vlan_range_upper_bound')) + """)) + 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 ('Layer2CircuitBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('policer_enabled')) + """)) + 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 ('Layer2CircuitBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('policer_enabled')) + """)) + 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 ('Layer2CircuitBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bandwidth')) + """)) + 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 ('Layer2CircuitBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bandwidth')) + """)) + 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 ('policer_enabled', 'bandwidth', 'vlan_range_upper_bound', 'vlan_range_lower_bound', 'virtual_circuit_id', 'layer_2_circuit_type')) + """)) + conn.execute(sa.text(""" +DELETE FROM resource_types WHERE resource_types.resource_type IN ('policer_enabled', 'bandwidth', 'vlan_range_upper_bound', 'vlan_range_lower_bound', 'virtual_circuit_id', 'layer_2_circuit_type') + """)) + 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 ('Azure ExpressRoute', 'Imported Azure ExpressRoute', 'Imported GÉANT Plus', 'GÉANT Plus')) AND product_product_blocks.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('Layer2CircuitBlock')) + """)) + 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 ('Layer2CircuitBlock')) AND product_block_relations.depends_on_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('Layer2CircuitSideBlock')) + """)) + 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 ('Layer2CircuitSideBlock')) AND product_block_relations.depends_on_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')) + """)) + conn.execute(sa.text(""" +DELETE FROM fixed_inputs WHERE fixed_inputs.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Azure ExpressRoute', 'Imported Azure ExpressRoute', 'Imported GÉANT Plus', 'GÉANT Plus')) AND fixed_inputs.name = 'layer_2_circuit_service_type' + """)) + 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 ('Layer2CircuitSideBlock', 'Layer2CircuitBlock')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_blocks WHERE product_blocks.name IN ('Layer2CircuitSideBlock', 'Layer2CircuitBlock') + """)) + 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 ('Azure ExpressRoute', 'Imported Azure ExpressRoute', 'Imported GÉANT Plus', 'GÉANT Plus')))) + """)) + 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 ('Azure ExpressRoute', 'Imported Azure ExpressRoute', 'Imported GÉANT Plus', 'GÉANT Plus'))) + """)) + 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 ('Azure ExpressRoute', 'Imported Azure ExpressRoute', 'Imported GÉANT Plus', 'GÉANT Plus'))) + """)) + conn.execute(sa.text(""" +DELETE FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Azure ExpressRoute', 'Imported Azure ExpressRoute', 'Imported GÉANT Plus', 'GÉANT Plus')) + """)) + conn.execute(sa.text(""" +DELETE FROM products WHERE products.name IN ('Azure ExpressRoute', 'Imported Azure ExpressRoute', 'Imported GÉANT Plus', 'GÉANT Plus') + """)) diff --git a/gso/migrations/versions/2024-10-29_72a4f7aa499d_add_l2circuit_workflows.py b/gso/migrations/versions/2024-10-29_72a4f7aa499d_add_l2circuit_workflows.py new file mode 100644 index 0000000000000000000000000000000000000000..2b6ef3b961046aae7db1f5d50c270191185e0c95 --- /dev/null +++ b/gso/migrations/versions/2024-10-29_72a4f7aa499d_add_l2circuit_workflows.py @@ -0,0 +1,63 @@ +"""Add L2Circuit workflows.. + +Revision ID: 72a4f7aa499d +Revises: 5132c463214d +Create Date: 2024-10-29 14:04:29.807253 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '72a4f7aa499d' +down_revision = '5132c463214d' +branch_labels = None +depends_on = None + + +from orchestrator.migrations.helpers import create_workflow, delete_workflow + +new_workflows = [ + { + "name": "create_layer_2_circuit", + "target": "CREATE", + "description": "Create Layer 2 Circuit Service", + "product_type": "Layer2Circuit" + }, + { + "name": "modify_layer_2_circuit", + "target": "MODIFY", + "description": "Modify Layer 2 Circuit Service", + "product_type": "Layer2Circuit" + }, + { + "name": "terminate_layer_2_circuit", + "target": "TERMINATE", + "description": "Terminate Layer 2 Circuit Service", + "product_type": "Layer2Circuit" + }, + { + "name": "create_imported_layer_2_circuit", + "target": "CREATE", + "description": "Create imported Layer 2 Circuit", + "product_type": "ImportedLayer2Circuit" + }, + { + "name": "import_layer_2_circuit", + "target": "MODIFY", + "description": "Import Layer 2 Circuit", + "product_type": "ImportedLayer2Circuit" + }, +] + + +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-11-18_f6ceec1af371_split_bfd_settings_off_into_separate_.py b/gso/migrations/versions/2024-11-18_f6ceec1af371_split_bfd_settings_off_into_separate_.py new file mode 100644 index 0000000000000000000000000000000000000000..d1d6cfffeeaa8b7d364e799794024665616a2a88 --- /dev/null +++ b/gso/migrations/versions/2024-11-18_f6ceec1af371_split_bfd_settings_off_into_separate_.py @@ -0,0 +1,116 @@ +"""Split BFD settings off into separate product block.. + +Revision ID: f6ceec1af371 +Revises: 72a4f7aa499d +Create Date: 2024-11-18 09:40:50.214908 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'f6ceec1af371' +down_revision = '72a4f7aa499d' +branch_labels = None +depends_on = None + + +def upgrade() -> 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 ('BGPSession')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_interval')) + """)) + 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 ('BGPSession'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_interval')) + """)) + 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 ('BGPSession')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_multiplier')) + """)) + 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 ('BGPSession'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_multiplier')) + """)) + 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 ('bfd_interval')) + """)) + conn.execute(sa.text(""" +DELETE FROM resource_types WHERE resource_types.resource_type IN ('bfd_interval') + """)) + conn.execute(sa.text(""" +INSERT INTO product_blocks (name, description, tag, status) VALUES ('BFDSettings', 'A set of settings for BFD', 'BFD_SETTINGS', 'active') RETURNING product_blocks.product_block_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('bfd_interval_tx', 'the interval TX') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('bfd_interval_rx', 'the interval RX') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('ip_type', 'if it''s IPV4 or IPV6') RETURNING resource_types.resource_type_id + """)) + 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 ('ServiceBindingPort')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BFDSettings'))) + """)) + 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 ('BGPSession')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ip_type'))) + """)) + 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 ('BFDSettings')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_enabled'))) + """)) + 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 ('BFDSettings')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_interval_tx'))) + """)) + 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 ('BFDSettings')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_interval_rx'))) + """)) + 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 ('BFDSettings')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_multiplier'))) + """)) + + +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 ('BGPSession')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ip_type')) + """)) + 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 ('BGPSession'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ip_type')) + """)) + 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 ('BFDSettings')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_enabled')) + """)) + 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 ('BFDSettings'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_enabled')) + """)) + 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 ('BFDSettings')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_interval_tx')) + """)) + 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 ('BFDSettings'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_interval_tx')) + """)) + 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 ('BFDSettings')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_interval_rx')) + """)) + 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 ('BFDSettings'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_interval_rx')) + """)) + 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 ('BFDSettings')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_multiplier')) + """)) + 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 ('BFDSettings'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_multiplier')) + """)) + 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 ('bfd_interval_tx', 'bfd_interval_rx', 'ip_type')) + """)) + conn.execute(sa.text(""" +DELETE FROM resource_types WHERE resource_types.resource_type IN ('bfd_interval_tx', 'bfd_interval_rx', 'ip_type') + """)) + 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 ('ServiceBindingPort')) AND product_block_relations.depends_on_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BFDSettings')) + """)) + 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 ('BFDSettings')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_blocks WHERE product_blocks.name IN ('BFDSettings') + """)) diff --git a/gso/migrations/versions/2024-11-25_2746f861a765_rename_nrenl3core_service_to_l3core_.py b/gso/migrations/versions/2024-11-25_2746f861a765_rename_nrenl3core_service_to_l3core_.py new file mode 100644 index 0000000000000000000000000000000000000000..0655d9a44b1bae0605d3426b9c2ef3592032d20b --- /dev/null +++ b/gso/migrations/versions/2024-11-25_2746f861a765_rename_nrenl3core_service_to_l3core_.py @@ -0,0 +1,213 @@ +"""Rename NRENL3Core service to L3Core service and add LHCOne, GWS, and Copernicus. + +Revision ID: 2746f861a765 +Revises: f6ceec1af371 +Create Date: 2024-11-25 09:47:08.419689 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '2746f861a765' +down_revision = 'f6ceec1af371' +branch_labels = None +depends_on = None + +from orchestrator.migrations.helpers import create_workflow, delete_workflow + +new_workflows = [ + { + "name": "create_l3_core_service", + "target": "CREATE", + "description": "Create L3 Core Service", + "product_type": "L3CoreService" + }, + { + "name": "modify_l3_core_service", + "target": "MODIFY", + "description": "Modify L3 Core Service", + "product_type": "L3CoreService" + }, + { + "name": "create_imported_l3_core_service", + "target": "CREATE", + "description": "Create imported L3 Core Service", + "product_type": "ImportedL3CoreService" + }, + { + "name": "import_l3_core_service", + "target": "MODIFY", + "description": "Import L3 Core Service", + "product_type": "ImportedL3CoreService" + }, + { + "name": "migrate_l3_core_service", + "target": "MODIFY", + "description": "Migrate L3 Core Service", + "product_type": "L3CoreService" + } +] + +old_workflows = [ + { + "name": "create_nren_l3_core_service", + "target": "CREATE", + "description": "Create NREN L3 Core Service", + "product_type": "NRENL3CoreService" + }, + { + "name": "modify_nren_l3_core_service", + "target": "MODIFY", + "description": "Modify NREN L3 Core Service", + "product_type": "NRENL3CoreService" + }, + { + "name": "create_imported_nren_l3_core_service", + "target": "CREATE", + "description": "Create imported NREN L3 Core Service", + "product_type": "ImportedNRENL3CoreService" + }, + { + "name": "import_nren_l3_core_service", + "target": "MODIFY", + "description": "Import NREN L3 Core Service", + "product_type": "ImportedNRENL3CoreService" + }, + { + "name": "migrate_nren_l3_core_service", + "target": "MODIFY", + "description": "Migrate NREN L3 Core Service", + "product_type": "NRENL3CoreService" + } +] + + +def upgrade() -> None: + conn = op.get_bind() + for workflow in old_workflows: + delete_workflow(conn, workflow["name"]) + conn.execute(sa.text(""" +UPDATE fixed_inputs SET name='l3_core_service_type' WHERE fixed_inputs.product_id = (SELECT products.product_id FROM products WHERE products.name IN ('GÉANT IP')) AND fixed_inputs.name = 'nren_l3_core_service_type' + """)) + conn.execute(sa.text(""" +UPDATE fixed_inputs SET name='l3_core_service_type' WHERE fixed_inputs.product_id = (SELECT products.product_id FROM products WHERE products.name IN ('Imported GÉANT IP')) AND fixed_inputs.name = 'nren_l3_core_service_type' + """)) + conn.execute(sa.text(""" +UPDATE fixed_inputs SET name='l3_core_service_type' WHERE fixed_inputs.product_id = (SELECT products.product_id FROM products WHERE products.name IN ('IAS')) AND fixed_inputs.name = 'nren_l3_core_service_type' + """)) + conn.execute(sa.text(""" +UPDATE fixed_inputs SET name='l3_core_service_type' WHERE fixed_inputs.product_id = (SELECT products.product_id FROM products WHERE products.name IN ('Imported IAS')) AND fixed_inputs.name = 'nren_l3_core_service_type' + """)) + conn.execute(sa.text(""" +UPDATE products SET product_type='L3CoreService' WHERE product_type='NRENL3CoreService' + """)) + conn.execute(sa.text(""" +UPDATE products SET product_type='ImportedL3CoreService' WHERE product_type='ImportedNRENL3CoreService' + """)) + conn.execute(sa.text(""" +INSERT INTO products (name, description, product_type, tag, status) VALUES ('LHCOne', 'LHCOne', 'L3CoreService', 'LHC', 'active') RETURNING products.product_id + """)) + conn.execute(sa.text(""" +INSERT INTO products (name, description, product_type, tag, status) VALUES ('Imported LHCOne', 'Imported LHCOne', 'ImportedL3CoreService', 'IMP_LHC', 'active') RETURNING products.product_id + """)) + conn.execute(sa.text(""" +INSERT INTO products (name, description, product_type, tag, status) VALUES ('Copernicus', 'Copernicus', 'L3CoreService', 'COP', 'active') RETURNING products.product_id + """)) + conn.execute(sa.text(""" +INSERT INTO products (name, description, product_type, tag, status) VALUES ('Imported Copernicus', 'Imported Copernicus', 'ImportedL3CoreService', 'IMP_COP', 'active') RETURNING products.product_id + """)) + conn.execute(sa.text(""" +INSERT INTO products (name, description, product_type, tag, status) VALUES ('GWS', 'GWS', 'L3CoreService', 'GWS', 'active') RETURNING products.product_id + """)) + conn.execute(sa.text(""" +INSERT INTO products (name, description, product_type, tag, status) VALUES ('Imported GWS', 'Imported GWS', 'ImportedL3CoreService', 'IMP_GWS', 'active') RETURNING products.product_id + """)) + conn.execute(sa.text(""" +INSERT INTO fixed_inputs (name, value, product_id) VALUES ('l3_core_service_type', 'LHCONE', (SELECT products.product_id FROM products WHERE products.name IN ('LHCOne'))), ('l3_core_service_type', 'IMPORTED COPERNICUS', (SELECT products.product_id FROM products WHERE products.name IN ('Imported Copernicus'))), ('l3_core_service_type', 'COPERNICUS', (SELECT products.product_id FROM products WHERE products.name IN ('Copernicus'))), ('l3_core_service_type', 'IMPORTED LHCONE', (SELECT products.product_id FROM products WHERE products.name IN ('Imported LHCOne'))), ('l3_core_service_type', 'IMPORTED GWS', (SELECT products.product_id FROM products WHERE products.name IN ('Imported GWS'))), ('l3_core_service_type', 'GWS', (SELECT products.product_id FROM products WHERE products.name IN ('GWS'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_blocks (name, description, tag, status) VALUES ('L3CoreServiceBlock', 'A Core network service on Layer 3', 'L3CORE', 'active') RETURNING product_blocks.product_block_id + """)) + conn.execute(sa.text(""" +INSERT INTO product_blocks (name, description, tag, status) VALUES ('AccessPort', 'An Access Port where a Layer 3 service terminates', 'AP', 'active') RETURNING product_blocks.product_block_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 IAS')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('L3CoreServiceBlock'))), ((SELECT products.product_id FROM products WHERE products.name IN ('GÉANT IP')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('L3CoreServiceBlock'))), ((SELECT products.product_id FROM products WHERE products.name IN ('LHCOne')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('L3CoreServiceBlock'))), ((SELECT products.product_id FROM products WHERE products.name IN ('Imported LHCOne')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('L3CoreServiceBlock'))), ((SELECT products.product_id FROM products WHERE products.name IN ('IAS')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('L3CoreServiceBlock'))), ((SELECT products.product_id FROM products WHERE products.name IN ('Imported GÉANT IP')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('L3CoreServiceBlock'))), ((SELECT products.product_id FROM products WHERE products.name IN ('Copernicus')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('L3CoreServiceBlock'))), ((SELECT products.product_id FROM products WHERE products.name IN ('Imported Copernicus')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('L3CoreServiceBlock'))),((SELECT products.product_id FROM products WHERE products.name IN ('GWS')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('L3CoreServiceBlock'))), ((SELECT products.product_id FROM products WHERE products.name IN ('Imported GWS')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('L3CoreServiceBlock'))) + """)) + 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 ('AccessPort')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort'))) + """)) + 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 ('L3CoreServiceBlock')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('AccessPort'))) + """)) + 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 ('AccessPort')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ap_type'))) + """)) + 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"]) + 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 ('AccessPort')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ap_type')) + """)) + 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 ('AccessPort'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ap_type')) + """)) + 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 IAS', 'GÉANT IP', 'LHCOne', 'Imported LHCOne', 'IAS', 'Imported GÉANT IP', 'Copernicus', 'Imported Copernicus', 'GWS', 'Imported GWS')) AND product_product_blocks.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('L3CoreServiceBlock')) + """)) + 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 ('AccessPort')) AND product_block_relations.depends_on_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')) + """)) + 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 ('L3CoreServiceBlock')) AND product_block_relations.depends_on_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('AccessPort')) + """)) + conn.execute(sa.text(""" +DELETE FROM fixed_inputs WHERE fixed_inputs.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('LHCOne', 'Imported Copernicus', 'Copernicus', 'Imported LHCOne', 'GWS', 'Imported GWS')) AND fixed_inputs.name = 'l3_core_service_type' + """)) + 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 ('AccessPort', 'L3CoreServiceBlock')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_blocks WHERE product_blocks.name IN ('AccessPort', 'L3CoreServiceBlock') + """)) + 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 ('LHCOne', 'Imported Copernicus', 'Copernicus', 'Imported LHCOne', 'GWS', 'Imported GWS')))) + """)) + 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 ('LHCOne', 'Imported Copernicus', 'Copernicus', 'Imported LHCOne', 'GWS', 'Imported GWS'))) + """)) + 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 ('LHCOne', 'Imported Copernicus', 'Copernicus', 'Imported LHCOne', 'GWS', 'Imported GWS'))) + """)) + conn.execute(sa.text(""" +DELETE FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('LHCOne', 'Imported Copernicus', 'Copernicus', 'Imported LHCOne', 'GWS', 'Imported GWS')) + """)) + conn.execute(sa.text(""" +DELETE FROM products WHERE products.name IN ('LHCOne', 'Imported Copernicus', 'Copernicus', 'Imported LHCOne', 'GWS', 'Imported GWS') + """)) + conn.execute(sa.text(""" +UPDATE products SET product_type='NRENL3CoreService' WHERE product_type='L3CoreService' + """)) + conn.execute(sa.text(""" +UPDATE products SET product_type='ImportedNRENL3CoreService' WHERE product_type='ImportedL3CoreService' + """)) + conn.execute(sa.text(""" +UPDATE fixed_inputs SET name='nren_l3_core_service_type' WHERE fixed_inputs.product_id = (SELECT products.product_id FROM products WHERE products.name IN ('GÉANT IP')) AND fixed_inputs.name = 'l3_core_service_type' + """)) + conn.execute(sa.text(""" +UPDATE fixed_inputs SET name='nren_l3_core_service_type' WHERE fixed_inputs.product_id = (SELECT products.product_id FROM products WHERE products.name IN ('Imported GÉANT IP')) AND fixed_inputs.name = 'l3_core_service_type' + """)) + conn.execute(sa.text(""" +UPDATE fixed_inputs SET name='nren_l3_core_service_type' WHERE fixed_inputs.product_id = (SELECT products.product_id FROM products WHERE products.name IN ('IAS')) AND fixed_inputs.name = 'l3_core_service_type' + """)) + conn.execute(sa.text(""" +UPDATE fixed_inputs SET name='nren_l3_core_service_type' WHERE fixed_inputs.product_id = (SELECT products.product_id FROM products WHERE products.name IN ('Imported IAS')) AND fixed_inputs.name = 'l3_core_service_type' + """)) + for workflow in old_workflows: + create_workflow(conn, workflow) diff --git a/gso/migrations/versions/2024-11-27_543afff041f9_add_prefix_limit_to_bgpsession.py b/gso/migrations/versions/2024-11-27_543afff041f9_add_prefix_limit_to_bgpsession.py new file mode 100644 index 0000000000000000000000000000000000000000..a239b880512a0f68dc2f52c399533abf8b9da470 --- /dev/null +++ b/gso/migrations/versions/2024-11-27_543afff041f9_add_prefix_limit_to_bgpsession.py @@ -0,0 +1,41 @@ +"""Add prefix limit to BGPSession. + +Revision ID: 543afff041f9 +Revises: 2746f861a765 +Create Date: 2024-11-27 10:34:29.855749 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '543afff041f9' +down_revision = '2746f861a765' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + conn = op.get_bind() + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('prefix_limit', 'Prefix limit for a BGP session') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('prefix_limit'))) + """)) + + +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 ('BGPSession')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('prefix_limit')) + """)) + 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 ('BGPSession'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('prefix_limit')) + """)) + 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 ('prefix_limit')) + """)) + conn.execute(sa.text(""" +DELETE FROM resource_types WHERE resource_types.resource_type IN ('prefix_limit') + """)) diff --git a/gso/products/__init__.py b/gso/products/__init__.py index 457a86ed3601e66ffb728f5ac00fdc3937907612..4b472c9f8a4f186bca00aea8bf70507981c0b7da 100644 --- a/gso/products/__init__.py +++ b/gso/products/__init__.py @@ -10,8 +10,9 @@ 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.l3_core_service import ImportedL3CoreService, L3CoreService 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.layer_2_circuit import ImportedLayer2Circuit, Layer2Circuit, Layer2CircuitServiceType from gso.products.product_types.office_router import ImportedOfficeRouter, OfficeRouter from gso.products.product_types.opengear import ImportedOpengear, Opengear from gso.products.product_types.pop_vlan import PopVlan @@ -47,6 +48,16 @@ class ProductName(strEnum): IMPORTED_GEANT_IP = "Imported GÉANT IP" IAS = "IAS" IMPORTED_IAS = "Imported IAS" + GWS = "GWS" + IMPORTED_GWS = "Imported GWS" + LHCONE = "LHCOne" + IMPORTED_LHCONE = "Imported LHCOne" + COPERNICUS = "Copernicus" + IMPORTED_COPERNICUS = "Imported Copernicus" + GEANT_PLUS = Layer2CircuitServiceType.GEANT_PLUS + IMPORTED_GEANT_PLUS = Layer2CircuitServiceType.IMPORTED_GEANT_PLUS + EXPRESSROUTE = Layer2CircuitServiceType.EXPRESSROUTE + IMPORTED_EXPRESSROUTE = Layer2CircuitServiceType.IMPORTED_EXPRESSROUTE class ProductType(strEnum): @@ -71,10 +82,20 @@ class ProductType(strEnum): IMPORTED_OPENGEAR = Opengear.__name__ EDGE_PORT = EdgePort.__name__ IMPORTED_EDGE_PORT = ImportedEdgePort.__name__ - GEANT_IP = NRENL3CoreService.__name__ - IMPORTED_GEANT_IP = ImportedNRENL3CoreService.__name__ - IAS = NRENL3CoreService.__name__ - IMPORTED_IAS = ImportedNRENL3CoreService.__name__ + GEANT_IP = L3CoreService.__name__ + IMPORTED_GEANT_IP = ImportedL3CoreService.__name__ + IAS = L3CoreService.__name__ + IMPORTED_IAS = ImportedL3CoreService.__name__ + GWS = L3CoreService.__name__ + IMPORTED_GWS = ImportedL3CoreService.__name__ + LHCONE = L3CoreService.__name__ + IMPORTED_LHCONE = ImportedL3CoreService.__name__ + COPERNICUS = L3CoreService.__name__ + IMPORTED_COPERNICUS = ImportedL3CoreService.__name__ + GEANT_PLUS = Layer2Circuit.__name__ + IMPORTED_GEANT_PLUS = ImportedLayer2Circuit.__name__ + EXPRESSROUTE = Layer2Circuit.__name__ + IMPORTED_EXPRESSROUTE = ImportedLayer2Circuit.__name__ SUBSCRIPTION_MODEL_REGISTRY.update( @@ -98,9 +119,21 @@ SUBSCRIPTION_MODEL_REGISTRY.update( ProductName.IMPORTED_OPENGEAR.value: ImportedOpengear, ProductName.EDGE_PORT.value: EdgePort, ProductName.IMPORTED_EDGE_PORT.value: ImportedEdgePort, - ProductName.GEANT_IP.value: NRENL3CoreService, - ProductName.IMPORTED_GEANT_IP.value: ImportedNRENL3CoreService, - ProductName.IAS.value: NRENL3CoreService, - ProductName.IMPORTED_IAS.value: ImportedNRENL3CoreService, + ProductName.GEANT_IP.value: L3CoreService, + ProductName.IMPORTED_GEANT_IP.value: ImportedL3CoreService, + ProductName.IAS.value: L3CoreService, + ProductName.IMPORTED_IAS.value: ImportedL3CoreService, + ProductName.GWS.value: L3CoreService, + ProductName.IMPORTED_GWS.value: ImportedL3CoreService, + ProductName.LHCONE.value: L3CoreService, + ProductName.IMPORTED_LHCONE.value: ImportedL3CoreService, + ProductName.COPERNICUS.value: L3CoreService, + ProductName.IMPORTED_COPERNICUS.value: ImportedL3CoreService, + ProductName.GEANT_PLUS.value: Layer2Circuit, + ProductName.IMPORTED_GEANT_PLUS.value: ImportedLayer2Circuit, + ProductName.EXPRESSROUTE.value: Layer2Circuit, + ProductName.IMPORTED_EXPRESSROUTE.value: ImportedLayer2Circuit, }, ) + +__all__ = ["ProductName", "ProductType"] diff --git a/gso/products/product_blocks/bgp_session.py b/gso/products/product_blocks/bgp_session.py index 65c5f0a0908e6c6750dac9abae25d23290d1367c..2a57eb69b1ea5d200f2ac96ad6f8169f203093ef 100644 --- a/gso/products/product_blocks/bgp_session.py +++ b/gso/products/product_blocks/bgp_session.py @@ -3,7 +3,7 @@ import strawberry from orchestrator.domain.base import ProductBlockModel from orchestrator.types import SubscriptionLifecycle -from pydantic import Field +from pydantic import Field, NonNegativeInt from pydantic_forms.types import strEnum from gso.utils.types.ip_address import IPAddress @@ -13,19 +13,24 @@ from gso.utils.types.ip_address import IPAddress class IPFamily(strEnum): """Possible IP families of a :term:`BGP` peering.""" - V4UNICAST = "v4unicast" - V6UNICAST = "v6unicast" - V4MULTICAST = "v4multicast" - V6MULTICAST = "v6multicast" + V4UNICAST = "ipv4" + V6UNICAST = "ipv6" + V4MULTICAST = "mcast-ipv4" + V6MULTICAST = "mcast-ipv6" + + +@strawberry.enum +class IPTypes(strEnum): + """Possible IP types of a :term:`BGP` peering.""" + + IPV4 = "ipv4" + IPV6 = "ipv6" class BGPSessionInactive(ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="BGPSession"): """A :term:`BGP` session that is currently inactive. See :class:`BGPSession`.""" peer_address: IPAddress | None = None - bfd_enabled: bool | None = None - bfd_interval: int | None = None - bfd_multiplier: int | None = None families: list[IPFamily] = Field(default_factory=list) has_custom_policies: bool | None = None authentication_key: str | None = None @@ -34,23 +39,26 @@ class BGPSessionInactive(ProductBlockModel, lifecycle=[SubscriptionLifecycle.INI is_multi_hop: bool = False is_passive: bool = False rtbh_enabled: bool = False + bfd_enabled: bool = False + ip_type: IPTypes | None = None + prefix_limit: NonNegativeInt | None = None class BGPSessionProvisioning(BGPSessionInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): """A :term:`BGP` session that is currently being provisioned. See :class:`BGPSession`.""" peer_address: IPAddress - bfd_enabled: bool - bfd_interval: int | None = None - bfd_multiplier: int | None = None families: list[IPFamily] has_custom_policies: bool - authentication_key: str + authentication_key: str | None multipath_enabled: bool send_default_route: bool is_multi_hop: bool is_passive: bool rtbh_enabled: bool + bfd_enabled: bool + ip_type: IPTypes + prefix_limit: NonNegativeInt | None class BGPSession(BGPSessionProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): @@ -58,18 +66,12 @@ class BGPSession(BGPSessionProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE #: The peering address of the session. peer_address: IPAddress - #: Whether :term:`BFD` is enabled. - bfd_enabled: bool - #: The :term:`BFD` interval, if enabled. - bfd_interval: int | None = None - #: The :term:`BFD` multiplier, if enabled. - bfd_multiplier: int | None = None #: The list of IP families enabled for this session. families: list[IPFamily] #: Whether any custom policies exist for this session. has_custom_policies: bool #: The authentication key of the :term:`BGP` session. - authentication_key: str + authentication_key: str | None #: Whether multi-path is enabled. multipath_enabled: bool #: Whether we send a last resort route. @@ -80,3 +82,9 @@ class BGPSession(BGPSessionProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE is_passive: bool #: Whether Remote Triggered Blackhole is enabled rtbh_enabled: bool + #: Settings for :term:`BFD`. + bfd_enabled: bool + #: The IP type of the session. + ip_type: IPTypes + #: A prefix limit, if required + prefix_limit: NonNegativeInt | None diff --git a/gso/products/product_blocks/l3_core_service.py b/gso/products/product_blocks/l3_core_service.py new file mode 100644 index 0000000000000000000000000000000000000000..add0724bee631819c1049b37be3b8132e91e8b8b --- /dev/null +++ b/gso/products/product_blocks/l3_core_service.py @@ -0,0 +1,56 @@ +"""Product blocks for Layer 3 Core Service products.""" + +from orchestrator.domain.base import ProductBlockModel +from orchestrator.types import SubscriptionLifecycle +from pydantic import Field + +from gso.products.product_blocks.service_binding_port import ( + ServiceBindingPort, + ServiceBindingPortInactive, + ServiceBindingPortProvisioning, +) +from gso.utils.shared_enums import APType + + +class AccessPortInactive(ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="AccessPort"): + """An access port for an R&E service that is inactive.""" + + ap_type: APType | None = None + sbp: ServiceBindingPortInactive + + +class AccessPortProvisioning(AccessPortInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """An access port for an R&E service that is being provisioned.""" + + ap_type: APType + sbp: ServiceBindingPortProvisioning + + +class AccessPort(AccessPortProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """An access port for an R&E service.""" + + #: The type of Access Port + ap_type: APType + #: The corresponding :term:`SBP` of this Access Port. + sbp: ServiceBindingPort + + +class L3CoreServiceBlockInactive( + ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="L3CoreServiceBlock" +): + """An inactive L3 Core service subscription. See :class:`L3CoreServiceBlock`.""" + + ap_list: list[AccessPortInactive] = Field(default_factory=list) + + +class L3CoreServiceBlockProvisioning(L3CoreServiceBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """A provisioning L3 Core Service subscription. See :class:`L3CoreServiceBlock`.""" + + ap_list: list[AccessPortProvisioning] # type: ignore[assignment] + + +class L3CoreServiceBlock(L3CoreServiceBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """An active L3 Core Service subscription block.""" + + #: The list of Access Points where this service is present. + ap_list: list[AccessPort] # type: ignore[assignment] diff --git a/gso/products/product_blocks/layer_2_circuit.py b/gso/products/product_blocks/layer_2_circuit.py new file mode 100644 index 0000000000000000000000000000000000000000..261caa48669fd604c4182a5bb878a39b24647301 --- /dev/null +++ b/gso/products/product_blocks/layer_2_circuit.py @@ -0,0 +1,115 @@ +"""Layer 2 Circuit product block.""" + +from collections.abc import Sequence +from typing import Annotated, TypeVar + +from annotated_types import Len +from orchestrator.domain.base import ProductBlockModel +from orchestrator.types import SubscriptionLifecycle +from pydantic import AfterValidator +from pydantic_forms.types import strEnum +from pydantic_forms.validators import validate_unique_list +from typing_extensions import Doc + +from gso.products.product_blocks.service_binding_port import ( + ServiceBindingPort, + ServiceBindingPortInactive, + ServiceBindingPortProvisioning, +) +from gso.utils.types.interfaces import BandwidthString +from gso.utils.types.virtual_identifiers import VC_ID, VLAN_ID + + +class Layer2CircuitSideBlockInactive( + ProductBlockModel, + lifecycle=[SubscriptionLifecycle.INITIAL], + product_block_name="Layer2CircuitSideBlock", +): + """One inactive side of a Layer 2 Circuit.""" + + sbp: ServiceBindingPortInactive + + +class Layer2CircuitSideBlockProvisioning( + Layer2CircuitSideBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING] +): + """One provisioning side of a Layer 2 Circuit.""" + + sbp: ServiceBindingPortProvisioning + + +class Layer2CircuitSideBlock(Layer2CircuitSideBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """One side of a Layer 2 Circuit.""" + + sbp: ServiceBindingPort + + +Layer2CircuitSideBlockType = TypeVar( + "Layer2CircuitSideBlockType", + "Layer2CircuitSideBlockInactive", + "Layer2CircuitSideBlockProvisioning", + "Layer2CircuitSideBlock", +) + +Layer2CircuitSides = Annotated[ + Sequence[Layer2CircuitSideBlockType], + AfterValidator(validate_unique_list), + Len(min_length=2, max_length=2), + Doc("A list of two Layer 2 Circuit sides."), +] + + +class Layer2CircuitType(strEnum): + """The two types of Layer 2 Circuit.""" + + TAGGED = "TAGGED" + UNTAGGED = "UNTAGGED" + + +class Layer2CircuitBlockInactive( + ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="Layer2CircuitBlock" +): + """An inactive Layer 2 Circuit, see :class:`Layer2CircuitBlock`.""" + + layer_2_circuit_sides: Layer2CircuitSides[Layer2CircuitSideBlockInactive] + virtual_circuit_id: VC_ID | None = None + layer_2_circuit_type: Layer2CircuitType | None = None + vlan_range_lower_bound: VLAN_ID | None = None + vlan_range_upper_bound: VLAN_ID | None = None + policer_enabled: bool | None = None + policer_burst_rate: BandwidthString | None = None + bandwidth: BandwidthString | None = None + + +class Layer2CircuitBlockProvisioning(Layer2CircuitBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """A provisioning Layer 2 Circuit, see :class:`Layer2CircuitBlock`.""" + + layer_2_circuit_sides: Layer2CircuitSides[Layer2CircuitSideBlockProvisioning] + virtual_circuit_id: VC_ID + layer_2_circuit_type: Layer2CircuitType + vlan_range_lower_bound: VLAN_ID | None + vlan_range_upper_bound: VLAN_ID | None + policer_enabled: bool + policer_burst_rate: BandwidthString | None + bandwidth: BandwidthString | None + + +class Layer2CircuitBlock(Layer2CircuitBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """An active Layer 2 Circuit.""" + + #: The two sides that the Layer 2 Circuit is connected to. + layer_2_circuit_sides: Layer2CircuitSides[Layer2CircuitSideBlock] + #: Virtual Circuit ID of this Layer 2 Circuit. + virtual_circuit_id: VC_ID + #: The type of circuit, can be tagged or untagged. + layer_2_circuit_type: Layer2CircuitType + #: If tagged, the lower and upper bounds will set the :term:`VLAN` range. + vlan_range_lower_bound: VLAN_ID | None + #: Lower and Upper bounds are including. + vlan_range_upper_bound: VLAN_ID | None + #: Whether this Layer 2 Circuit is policed. + policer_enabled: bool + #: If policed, the burst rate of the policer + policer_burst_rate: BandwidthString | None + #: If policed, the bandwidth of the policer is stored. + bandwidth: BandwidthString | None diff --git a/gso/products/product_blocks/nren_l3_core_service.py b/gso/products/product_blocks/nren_l3_core_service.py deleted file mode 100644 index 37eb7616511dc34d887db0ad324dee8b59ec4f96..0000000000000000000000000000000000000000 --- a/gso/products/product_blocks/nren_l3_core_service.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Product blocks for :class:`NREN` Layer 3 Core Service products.""" - -from orchestrator.domain.base import ProductBlockModel -from orchestrator.types import SubscriptionLifecycle -from pydantic import Field - -from gso.products.product_blocks.service_binding_port import ( - ServiceBindingPort, - ServiceBindingPortInactive, - ServiceBindingPortProvisioning, -) -from gso.utils.shared_enums import APType - - -class NRENAccessPortInactive( - ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="NRENAccessPort" -): - """An access port for an R&E :term:`NREN` service that is inactive.""" - - ap_type: APType | None = None - sbp: ServiceBindingPortInactive - - -class NRENAccessPortProvisioning(NRENAccessPortInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): - """An access port for an R&E :term:`NREN` service that is being provisioned.""" - - ap_type: APType - sbp: ServiceBindingPortProvisioning - - -class NRENAccessPort(NRENAccessPortProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): - """An access port for an R&E :term:`NREN` service.""" - - #: The type of Access Port - ap_type: APType - #: The corresponding :term:`SBP` of this Access Port. - sbp: ServiceBindingPort - - -class NRENL3CoreServiceBlockInactive( - ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="NRENL3CoreServiceBlock" -): - """An inactive :term:`NREN` L3 Core service subscription. See :class:`NRENL3CoreServiceBlock`.""" - - nren_ap_list: list[NRENAccessPortInactive] = Field(default_factory=list) - - -class NRENL3CoreServiceBlockProvisioning( - NRENL3CoreServiceBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING] -): - """A provisioning :term:`NREN` L3 Core Service subscription. See :class:`NRENL3CoreServiceBlock`.""" - - nren_ap_list: list[NRENAccessPortProvisioning] # type: ignore[assignment] - - -class NRENL3CoreServiceBlock(NRENL3CoreServiceBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): - """An active :term:`NREN` L3 Core Service subscription block.""" - - #: The list of Access Points where this service is present. - nren_ap_list: list[NRENAccessPort] # type: ignore[assignment] diff --git a/gso/products/product_blocks/service_binding_port.py b/gso/products/product_blocks/service_binding_port.py index 540983821f64b86e5e3c11a9118ea8b867bff61d..2c9cd2ce37f5029c2393bccbccb786cc0b84a6b1 100644 --- a/gso/products/product_blocks/service_binding_port.py +++ b/gso/products/product_blocks/service_binding_port.py @@ -3,18 +3,52 @@ A service binding port is used to logically attach an edge port to a customer service using a :term:`VLAN`. """ -from typing import Annotated - from orchestrator.domain.base import ProductBlockModel from orchestrator.types import SubscriptionLifecycle from pydantic import Field -from gso.products.product_blocks.bgp_session import BGPSession, BGPSessionInactive, BGPSessionProvisioning +from gso.products.product_blocks.bgp_session import ( + BGPSession, + BGPSessionInactive, + BGPSessionProvisioning, +) from gso.products.product_blocks.edge_port import EdgePortBlock, EdgePortBlockInactive, EdgePortBlockProvisioning from gso.utils.shared_enums import SBPType from gso.utils.types.ip_address import IPv4AddressType, IPV4Netmask, IPv6AddressType, IPV6Netmask +from gso.utils.types.virtual_identifiers import VLAN_ID + + +class BFDSettingsInactive( + ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="BFDSettings" +): + """Settings for :term:`BFD`, see :class:`BFDSettings`.""" + + bfd_enabled: bool = False + bfd_interval_rx: int | None = None + bfd_interval_tx: int | None = None + bfd_multiplier: int | None = None + + +class BFDSettingsProvisioning(BFDSettingsInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """Settings for :term:`BFD`, see :class:`BFDSettings`.""" + + bfd_enabled: bool + bfd_interval_rx: int | None = None + bfd_interval_tx: int | None = None + bfd_multiplier: int | None + + +class BFDSettings(BFDSettingsProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """A set of settings for :term:`BFD`.""" -VLAN_ID = Annotated[int, Field(gt=0, lt=4096)] + #: Whether :term:`BFD` is enabled. + bfd_enabled: bool + #: The :term:`BFD` interval RX, if enabled. + bfd_interval_rx: int | None + #: The :term:`BFD` interval TX, if enabled. + bfd_interval_tx: int | None + #: The :term:`BFD` multiplier, if enabled. + bfd_multiplier: int | None class ServiceBindingPortInactive( @@ -33,6 +67,8 @@ class ServiceBindingPortInactive( geant_sid: str | None = None bgp_session_list: list[BGPSessionInactive] = Field(default_factory=list) edge_port: EdgePortBlockInactive | None = None + v4_bfd_settings: BFDSettingsInactive + v6_bfd_settings: BFDSettingsInactive class ServiceBindingPortProvisioning(ServiceBindingPortInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): @@ -49,6 +85,8 @@ class ServiceBindingPortProvisioning(ServiceBindingPortInactive, lifecycle=[Subs geant_sid: str bgp_session_list: list[BGPSessionProvisioning] # type: ignore[assignment] edge_port: EdgePortBlockProvisioning + v4_bfd_settings: BFDSettingsProvisioning + v6_bfd_settings: BFDSettingsProvisioning class ServiceBindingPort(ServiceBindingPortProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): @@ -76,3 +114,7 @@ class ServiceBindingPort(ServiceBindingPortProvisioning, lifecycle=[Subscription bgp_session_list: list[BGPSession] # type: ignore[assignment] #: The Edge Port on which this :term:`SBP` resides. edge_port: EdgePortBlock + #: :term:`BFD` settings for IPv4 + v4_bfd_settings: BFDSettings + #: :term:`BFD` settings for IPv6 + v6_bfd_settings: BFDSettings diff --git a/gso/products/product_types/l3_core_service.py b/gso/products/product_types/l3_core_service.py new file mode 100644 index 0000000000000000000000000000000000000000..8d4c96d5110965ec0fb412551f316666478ee122 --- /dev/null +++ b/gso/products/product_types/l3_core_service.py @@ -0,0 +1,66 @@ +"""L3 Core Service product type.""" + +from orchestrator.domain import SubscriptionModel +from orchestrator.types import SubscriptionLifecycle +from pydantic_forms.types import strEnum + +from gso.products.product_blocks.l3_core_service import ( + L3CoreServiceBlock, + L3CoreServiceBlockInactive, + L3CoreServiceBlockProvisioning, +) + + +class L3CoreServiceType(strEnum): + """Available types of Layer 3 Core Services. + + The core services offered include GÉANT IP for R&E access, and the Internet Access Service. + """ + + GEANT_IP = "GÉANT IP" + IMPORTED_GEANT_IP = "IMPORTED GÉANT IP" + IAS = "IAS" + IMPORTED_IAS = "IMPORTED IAS" + GWS = "GWS" + IMPORTED_GWS = "IMPORTED GWS" + LHCONE = "LHCONE" + IMPORTED_LHCONE = "IMPORTED LHCONE" + COPERNICUS = "COPERNICUS" + IMPORTED_COPERNICUS = "IMPORTED COPERNICUS" + + +class L3CoreServiceInactive(SubscriptionModel, is_base=True): + """An inactive L3 Core Service subscription.""" + + l3_core_service_type: L3CoreServiceType + l3_core_service: L3CoreServiceBlockInactive + + +class L3CoreServiceProvisioning(L3CoreServiceInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """A L3 Core Service subscription that's being provisioned.""" + + l3_core_service_type: L3CoreServiceType + l3_core_service: L3CoreServiceBlockProvisioning + + +class L3CoreService(L3CoreServiceProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """An active L3 Core Service subscription.""" + + l3_core_service_type: L3CoreServiceType + l3_core_service: L3CoreServiceBlock + + +class ImportedL3CoreServiceInactive(SubscriptionModel, is_base=True): + """An imported, inactive L3 Core Service subscription.""" + + l3_core_service_type: L3CoreServiceType + l3_core_service: L3CoreServiceBlockInactive + + +class ImportedL3CoreService( + ImportedL3CoreServiceInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE] +): + """An imported L3 Core Service subscription.""" + + l3_core_service_type: L3CoreServiceType + l3_core_service: L3CoreServiceBlock diff --git a/gso/products/product_types/layer_2_circuit.py b/gso/products/product_types/layer_2_circuit.py new file mode 100644 index 0000000000000000000000000000000000000000..9839a8853013901ee55151f0dc4210a4eb86c8ce --- /dev/null +++ b/gso/products/product_types/layer_2_circuit.py @@ -0,0 +1,57 @@ +"""Product type for a Layer 2 circuit.""" + +from orchestrator.domain.base import SubscriptionModel +from orchestrator.types import SubscriptionLifecycle +from pydantic_forms.types import strEnum + +from gso.products.product_blocks.layer_2_circuit import ( + Layer2CircuitBlock, + Layer2CircuitBlockInactive, + Layer2CircuitBlockProvisioning, +) + + +class Layer2CircuitServiceType(strEnum): + """Available types of Layer 2 Circuit services.""" + + GEANT_PLUS = "GÉANT Plus" + IMPORTED_GEANT_PLUS = "Imported GÉANT Plus" + EXPRESSROUTE = "Azure ExpressRoute" + IMPORTED_EXPRESSROUTE = "Imported Azure ExpressRoute" + + +class Layer2CircuitInactive(SubscriptionModel, is_base=True): + """An inactive Layer 2 Circuit.""" + + layer_2_circuit_service_type: Layer2CircuitServiceType + layer_2_circuit: Layer2CircuitBlockInactive + + +class Layer2CircuitProvisioning(Layer2CircuitInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """A Layer 2 Circuit that is provisioning.""" + + layer_2_circuit_service_type: Layer2CircuitServiceType + layer_2_circuit: Layer2CircuitBlockProvisioning + + +class Layer2Circuit(Layer2CircuitProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """An active Layer 2 Circuit.""" + + layer_2_circuit_service_type: Layer2CircuitServiceType + layer_2_circuit: Layer2CircuitBlock + + +class ImportedLayer2CircuitInactive(SubscriptionModel, is_base=True): + """An imported, inactive Layer 2 Circuit.""" + + layer_2_circuit_service_type: Layer2CircuitServiceType + layer_2_circuit: Layer2CircuitBlockInactive + + +class ImportedLayer2Circuit( + ImportedLayer2CircuitInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE] +): + """An imported Layer 2 Circuit.""" + + layer_2_circuit_service_type: Layer2CircuitServiceType + layer_2_circuit: Layer2CircuitBlock diff --git a/gso/products/product_types/nren_l3_core_service.py b/gso/products/product_types/nren_l3_core_service.py deleted file mode 100644 index 20aa5ab2f766588f466a5100d1d7af69cbb696b0..0000000000000000000000000000000000000000 --- a/gso/products/product_types/nren_l3_core_service.py +++ /dev/null @@ -1,60 +0,0 @@ -""":term:`NREN` L3 Core Service product type.""" - -from orchestrator.domain import SubscriptionModel -from orchestrator.types import SubscriptionLifecycle -from pydantic_forms.types import strEnum - -from gso.products.product_blocks.nren_l3_core_service import ( - NRENL3CoreServiceBlock, - NRENL3CoreServiceBlockInactive, - NRENL3CoreServiceBlockProvisioning, -) - - -class NRENL3CoreServiceType(strEnum): - """Available types of :term:`NREN` Layer 3 Core Services. - - The core services offered include GÉANT IP for R&E access, and the Internet Access Service. - """ - - GEANT_IP = "GÉANT IP" - IMPORTED_GEANT_IP = "IMPORTED GÉANT IP" - IAS = "IAS" - IMPORTED_IAS = "IMPORTED IAS" - - -class NRENL3CoreServiceInactive(SubscriptionModel, is_base=True): - """An inactive :term:`NREN` L3 Core Service subscription.""" - - nren_l3_core_service_type: NRENL3CoreServiceType - nren_l3_core_service: NRENL3CoreServiceBlockInactive - - -class NRENL3CoreServiceProvisioning(NRENL3CoreServiceInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): - """A :term:`NREN` L3 Core Service subscription that's being provisioned.""" - - nren_l3_core_service_type: NRENL3CoreServiceType - nren_l3_core_service: NRENL3CoreServiceBlockProvisioning - - -class NRENL3CoreService(NRENL3CoreServiceProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): - """An active :term:`NREN` L3 Core Service subscription.""" - - nren_l3_core_service_type: NRENL3CoreServiceType - nren_l3_core_service: NRENL3CoreServiceBlock - - -class ImportedNRENL3CoreServiceInactive(SubscriptionModel, is_base=True): - """An imported, inactive :term:`NREN` L3 Core Service subscription.""" - - nren_l3_core_service_type: NRENL3CoreServiceType - nren_l3_core_service: NRENL3CoreServiceBlockInactive - - -class ImportedNRENL3CoreService( - ImportedNRENL3CoreServiceInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE] -): - """An imported :term:`NREN` L3 Core Service subscription.""" - - nren_l3_core_service_type: NRENL3CoreServiceType - nren_l3_core_service: NRENL3CoreServiceBlock diff --git a/gso/services/lso_client.py b/gso/services/lso_client.py index cae5dd9c28d55d3b166f7250513ad7a5d7946cba..f501ed7023a52d418dedf0c88200ada8096b92f8 100644 --- a/gso/services/lso_client.py +++ b/gso/services/lso_client.py @@ -10,11 +10,11 @@ from typing import Any, Literal, TypedDict import requests from orchestrator import step from orchestrator.config.assignee import Assignee +from orchestrator.forms import FormPage from orchestrator.types import State from orchestrator.utils.errors import ProcessFailureError from orchestrator.workflow import Step, StepList, begin, callback_step, conditional, inputstep from pydantic import ConfigDict -from pydantic_forms.core import FormPage from pydantic_forms.types import FormGenerator from pydantic_forms.validators import Label, LongText, ReadOnlyField diff --git a/gso/services/subscriptions.py b/gso/services/subscriptions.py index 1458f000439fa8bcf936a08b864eb117296caf37..b2c2561d8cb4a213b172aea7ed89cc12aed8bd1c 100644 --- a/gso/services/subscriptions.py +++ b/gso/services/subscriptions.py @@ -287,3 +287,25 @@ def get_all_active_sites() -> list[dict[str, Any]]: } for subscription in get_active_site_subscriptions(includes=["subscription_id"]) ] + + +def is_virtual_circuit_id_available(virtual_circuit_id: str) -> bool: + """Check if the given virtual circuit ID is unique in the database. + + This function verifies if the specified virtual circuit ID is not already + present in the core database. + + :param virtual_circuit_id: The virtual circuit ID to check. + :type virtual_circuit_id: str + :return: True if the virtual circuit ID is unique (not found), False if it exists. + :rtype: bool + """ + exists = ( + ResourceTypeTable.query.join(SubscriptionInstanceValueTable) + .filter( + ResourceTypeTable.resource_type == "virtual_circuit_id", + SubscriptionInstanceValueTable.value == virtual_circuit_id, + ) + .scalar() + ) + return exists is None diff --git a/gso/translations/en-GB.json b/gso/translations/en-GB.json index 1858d04ece8f39a1bd1f83de8994e6c71633ba9a..a871e3394a98a57c90dccf4db79d206ed47d2027 100644 --- a/gso/translations/en-GB.json +++ b/gso/translations/en-GB.json @@ -52,18 +52,18 @@ "create_site": "Create Site", "create_switch": "Create Switch", "create_edge_port": "Create Edge Port", - "create_nren_l3_core_service": "Create NREN L3 Core Service", + "create_l3_core_service": "Create 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", + "migrate_l3_core_service": "Migrate L3 Core Service", "modify_isis_metric": "Modify the ISIS metric", "modify_site": "Modify Site", "modify_trunk_interface": "Modify IP Trunk interface", "modify_connection_strategy": "Modify connection strategy", "modify_router_kentik_license": "Modify device license in Kentik", "modify_edge_port": "Modify Edge Port", - "modify_nren_l3_core_service": "Modify NREN L3 Core Service", + "modify_l3_core_service": "Modify L3 Core Service", "terminate_iptrunk": "Terminate IP Trunk", "terminate_router": "Terminate Router", "terminate_site": "Terminate Site", @@ -79,7 +79,7 @@ "create_imported_office_router": "NOT FOR HUMANS -- Import existing office router", "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_l3_core_service": "NOT FOR HUMANS -- Import existing 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", @@ -89,7 +89,7 @@ "import_super_pop_switch": "NOT FOR HUMANS -- Finalize import into a Super PoP switch", "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_l3_core_service": "NOT FOR HUMANS -- Finalize import into an 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", @@ -103,6 +103,11 @@ "task_modify_partners": "Modify partner task", "task_delete_partners": "Delete partner task", "task_clean_old_tasks": "Remove old cleanup tasks", - "promote_p_to_pe": "Promote P to PE" + "promote_p_to_pe": "Promote P to PE", + "create_layer_2_circuit": "Create Layer 2 Circuit", + "modify_layer_2_circuit": "Modify Layer 2 Circuit", + "terminate_layer_2_circuit": "Terminate Layer 2 Circuit", + "create_imported_layer_2_circuit": "NOT FOR HUMANS -- Import existing Layer 2 Circuit", + "import_layer_2_circuit": "NOT FOR HUMANS -- Finalize import into a Layer 2 Circuit product" } } diff --git a/gso/utils/helpers.py b/gso/utils/helpers.py index c05517a23beed1b47a5aaf37786af5fe8b8d1eee..5485bf2000e8dbd14fef41243b3966b26c13d11f 100644 --- a/gso/utils/helpers.py +++ b/gso/utils/helpers.py @@ -1,9 +1,11 @@ """Helper methods that are used across :term:`GSO`.""" +import random import re from typing import TYPE_CHECKING from uuid import UUID +from orchestrator.types import SubscriptionLifecycle from pydantic_forms.types import UUIDstr from pydantic_forms.validators import Choice @@ -13,9 +15,11 @@ from gso.products.product_types.router import Router from gso.services import subscriptions from gso.services.netbox_client import NetboxClient from gso.services.partners import get_all_partners +from gso.services.subscriptions import is_virtual_circuit_id_available from gso.utils.shared_enums import Vendor from gso.utils.types.interfaces import PhysicalPortCapacity from gso.utils.types.ip_address import IPv4AddressType +from gso.utils.types.virtual_identifiers import VC_ID if TYPE_CHECKING: from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock @@ -119,10 +123,12 @@ def generate_fqdn(hostname: str, site_name: str, country_code: str) -> str: return f"{hostname}.{site_name.lower()}.{country_code.lower()}{oss.IPAM.LO.domain_name}" -def generate_inventory_for_active_routers( +def generate_inventory_for_routers( router_role: RouterRole, exclude_routers: list[str] | None = None, router_vendor: Vendor | None = None, + *, + include_provisioning_routers: bool = True, ) -> dict: """Generate an Ansible-compatible inventory for executing playbooks. @@ -131,11 +137,18 @@ def generate_inventory_for_active_routers( :param RouterRole router_role: The role of the routers to include in the inventory. :param list exclude_routers: List of routers to exclude from the inventory. :param Vendor router_vendor: The vendor of the routers to include in the inventory. + :param bool include_provisioning_routers: Include routers that are in a ``PROVISIONING`` state. :return: A dictionary representing the inventory of active routers. :rtype: dict[str, Any] """ + lifecycles = ( + [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE] + if include_provisioning_routers + else [SubscriptionLifecycle.ACTIVE] + ) all_routers = [ - Router.from_subscription(r["subscription_id"]) for r in subscriptions.get_active_router_subscriptions() + Router.from_subscription(r["subscription_id"]) + for r in subscriptions.get_router_subscriptions(lifecycles=lifecycles) ] exclude_routers = exclude_routers or [] @@ -248,3 +261,28 @@ def validate_edge_port_number_of_members_based_on_lacp(*, number_of_members: int if number_of_members > 1 and not enable_lacp: err_msg = "Number of members must be 1 if LACP is disabled." raise ValueError(err_msg) + + +def generate_unique_vc_id(max_attempts: int = 100) -> VC_ID | None: + """Generate a unique 8-digit VC_ID starting with '11'. + + This function attempts to generate an 8-digit VC_ID beginning with '11', + checking its uniqueness before returning it. A maximum attempt limit is + set to prevent infinite loops in case the ID space is saturated. + + :param max_attempts: The maximum number of attempts to generate a unique ID. + :type max_attempts: int + :return: A unique VC_ID instance if successful, None if no unique ID is found. + :rtype: Optional[VC_ID] + """ + + def create_vc_id() -> str: + """Generate an 8-digit VC_ID starting with '11'.""" + return f"11{random.randint(100000, 999999)}" # noqa: S311 + + for _ in range(max_attempts): + vc_id = create_vc_id() + if is_virtual_circuit_id_available(vc_id): + return VC_ID(vc_id) + + return None diff --git a/gso/utils/types/interfaces.py b/gso/utils/types/interfaces.py index 15c91167de822fee116600b71bf0b05dedfd87ae..2251b49e998375a49d9b30c258fda6330f50585c 100644 --- a/gso/utils/types/interfaces.py +++ b/gso/utils/types/interfaces.py @@ -100,3 +100,26 @@ class PhysicalPortCapacity(strEnum): TEN_GIGABIT_PER_SECOND = "10G" HUNDRED_GIGABIT_PER_SECOND = "100G" FOUR_HUNDRED_GIGABIT_PER_SECOND = "400G" + + +def bandwidth_string_is_valid(bandwidth_string: str) -> str: + """Expect a bandwidth definition to follow the pattern of an int followed by a single letter. + + If this string does not consist of a number followed by a single + """ + msg = f"Expected a network capacity, e.g. 40G or 200M. Got: {bandwidth_string}" + if len(bandwidth_string) < 2: # noqa: PLR2004 not a magic value + raise ValueError(msg) + + if bandwidth_string[-1:] not in "K" "M" "G" "T": + raise ValueError(msg) + + try: + int(bandwidth_string[:-1]) # Try parsing the bandwidth number + except ValueError as e: + raise ValueError(msg) from e + + return bandwidth_string + + +BandwidthString = Annotated[str, AfterValidator(bandwidth_string_is_valid)] diff --git a/gso/utils/types/virtual_identifiers.py b/gso/utils/types/virtual_identifiers.py new file mode 100644 index 0000000000000000000000000000000000000000..2208ca742f298bae64f1217737c673c0fd2131b3 --- /dev/null +++ b/gso/utils/types/virtual_identifiers.py @@ -0,0 +1,15 @@ +"""Annotated types for virtual identifiers such as :term:`VLAN` ID or Virtual Circuit ID.""" + +from typing import Annotated + +from pydantic import Field +from typing_extensions import Doc + +VLAN_ID = Annotated[int, Field(gt=0, lt=4096)] +VC_ID = Annotated[ + int, + Field(gt=0, le=2147483648), + Doc( + "A Virtual Circuit ID, the upper limit comes from the highest number that a service ID could be in Nokia srOS." + ), +] diff --git a/gso/utils/workflow_steps.py b/gso/utils/workflow_steps.py index 9b82b69e1a26a6eb1bd4ff4b7042fec065957915..72e6104a67f611bd921da0e0a159a8cafdc959a2 100644 --- a/gso/utils/workflow_steps.py +++ b/gso/utils/workflow_steps.py @@ -5,12 +5,12 @@ from typing import Any from orchestrator import inputstep, step from orchestrator.config.assignee import Assignee +from orchestrator.forms import FormPage from orchestrator.types import State, UUIDstr from orchestrator.utils.errors import ProcessFailureError from orchestrator.utils.json import json_dumps from orchestrator.workflow import StepList, conditional from pydantic import ConfigDict -from pydantic_forms.core import FormPage from pydantic_forms.types import FormGenerator from pydantic_forms.validators import Label @@ -19,7 +19,7 @@ from gso.products.product_types.iptrunk import Iptrunk from gso.services.kentik_client import KentikClient, NewKentikDevice from gso.services.lso_client import LSOState, indifferent_lso_interaction from gso.settings import load_oss_params -from gso.utils.helpers import generate_inventory_for_active_routers +from gso.utils.helpers import generate_inventory_for_routers from gso.utils.shared_enums import Vendor @@ -51,7 +51,7 @@ def _update_sdp_mesh( *, dry_run: bool, ) -> LSOState: - inventory = generate_inventory_for_active_routers( + inventory = generate_inventory_for_routers( router_role=RouterRole.PE, router_vendor=Vendor.NOKIA, exclude_routers=[subscription["router"]["router_fqdn"]] ) @@ -89,7 +89,7 @@ def _update_sdp_single_pe( "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " f"Update the SDP mesh for L2circuits(epipes) config on PE NOKIA routers", "verb": "update_sdp_mesh", - "pe_router_list": generate_inventory_for_active_routers( + "pe_router_list": generate_inventory_for_routers( router_role=RouterRole.PE, exclude_routers=[subscription["router"]["router_fqdn"]], )["all"]["hosts"], @@ -122,7 +122,7 @@ def _add_pe_mesh_to_pe( "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " f"Add list of PE routers into iGEANT/iGEANT6 groups of the PE router", "verb": "add_pe_mesh_to_pe", - "pe_router_list": generate_inventory_for_active_routers( + "pe_router_list": generate_inventory_for_routers( router_role=RouterRole.PE, exclude_routers=[subscription["router"]["router_fqdn"]] )["all"]["hosts"], } @@ -148,7 +148,7 @@ def _add_pe_to_pe_mesh( *, dry_run: bool, ) -> LSOState: - inventory = generate_inventory_for_active_routers( + inventory = generate_inventory_for_routers( router_role=RouterRole.PE, exclude_routers=[subscription["router"]["router_fqdn"]] ) extra_vars = { @@ -178,7 +178,7 @@ def _add_all_p_to_pe( "subscription": subscription, "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Add all P-routers to this new PE", "verb": "add_all_p_to_pe", - "p_router_list": generate_inventory_for_active_routers( + "p_router_list": generate_inventory_for_routers( router_role=RouterRole.P, exclude_routers=[subscription["router"]["router_fqdn"]] )["all"]["hosts"], } @@ -204,7 +204,7 @@ def _add_pe_to_all_p( *, dry_run: bool, ) -> LSOState: - inventory = generate_inventory_for_active_routers( + inventory = generate_inventory_for_routers( router_role=RouterRole.P, exclude_routers=[subscription["router"]["router_fqdn"]] ) extra_vars = { @@ -417,11 +417,11 @@ def stop_moodi() -> StepList: host = load_oss_params().MOODI.host @step("Stop Moodi") - def _stop_moodi() -> LSOState: + def _stop_moodi(subscription: dict[str, Any]) -> LSOState: return { "playbook_name": "moodi_telemetry/playbooks/stop_moodi.yaml", "inventory": {"all": {"hosts": {host: None}}}, - "extra_vars": None, + "extra_vars": {"subscription": subscription}, } return _is_moodi_enabled(indifferent_lso_interaction(_stop_moodi)) diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py index 362f96ab161f8d15847185ebb8355a6df1a321e8..c5c3e1ef99dbf2ed77dc48620a4f3b0f2c8665ca 100644 --- a/gso/workflows/__init__.py +++ b/gso/workflows/__init__.py @@ -116,11 +116,16 @@ LazyWorkflowInstance("gso.workflows.edge_port.validate_edge_port", "validate_edg LazyWorkflowInstance("gso.workflows.edge_port.create_imported_edge_port", "create_imported_edge_port") LazyWorkflowInstance("gso.workflows.edge_port.import_edge_port", "import_edge_port") -# NREN L3 Core Service workflows -LazyWorkflowInstance("gso.workflows.nren_l3_core_service.create_nren_l3_core_service", "create_nren_l3_core_service") -LazyWorkflowInstance("gso.workflows.nren_l3_core_service.modify_nren_l3_core_service", "modify_nren_l3_core_service") -LazyWorkflowInstance( - "gso.workflows.nren_l3_core_service.create_imported_nren_l3_core_service", "create_imported_nren_l3_core_service" -) -LazyWorkflowInstance("gso.workflows.nren_l3_core_service.import_nren_l3_core_service", "import_nren_l3_core_service") -LazyWorkflowInstance("gso.workflows.nren_l3_core_service.migrate_nren_l3_core_service", "migrate_nren_l3_core_service") +# L3 Core Service workflows +LazyWorkflowInstance("gso.workflows.l3_core_service.create_l3_core_service", "create_l3_core_service") +LazyWorkflowInstance("gso.workflows.l3_core_service.modify_l3_core_service", "modify_l3_core_service") +LazyWorkflowInstance("gso.workflows.l3_core_service.create_imported_l3_core_service", "create_imported_l3_core_service") +LazyWorkflowInstance("gso.workflows.l3_core_service.import_l3_core_service", "import_l3_core_service") +LazyWorkflowInstance("gso.workflows.l3_core_service.migrate_l3_core_service", "migrate_l3_core_service") + +# Layer 2 Circuit workflows +LazyWorkflowInstance("gso.workflows.l2_circuit.create_layer_2_circuit", "create_layer_2_circuit") +LazyWorkflowInstance("gso.workflows.l2_circuit.modify_layer_2_circuit", "modify_layer_2_circuit") +LazyWorkflowInstance("gso.workflows.l2_circuit.terminate_layer_2_circuit", "terminate_layer_2_circuit") +LazyWorkflowInstance("gso.workflows.l2_circuit.create_imported_layer_2_circuit", "create_imported_layer_2_circuit") +LazyWorkflowInstance("gso.workflows.l2_circuit.import_layer_2_circuit", "import_layer_2_circuit") diff --git a/gso/workflows/edge_port/create_imported_edge_port.py b/gso/workflows/edge_port/create_imported_edge_port.py index b932175f072c4b03d414777ca9896797e9fdf81f..5aa24a28dbd02652adb9f1366e3837e9d296adf1 100644 --- a/gso/workflows/edge_port/create_imported_edge_port.py +++ b/gso/workflows/edge_port/create_imported_edge_port.py @@ -15,7 +15,7 @@ from pydantic_forms.validators import validate_unique_list from gso.products import ProductName from gso.products.product_blocks.edge_port import EdgePortAEMemberBlockInactive, EdgePortType, EncapsulationType -from gso.products.product_types.edge_port import EdgePortInactive, ImportedEdgePortInactive +from gso.products.product_types.edge_port import ImportedEdgePortInactive from gso.products.product_types.router import Router from gso.services.partners import get_partner_by_name from gso.services.subscriptions import get_product_id_by_name @@ -30,17 +30,14 @@ def create_subscription(partner: str) -> State: product_id = get_product_id_by_name(ProductName.IMPORTED_EDGE_PORT) subscription = ImportedEdgePortInactive.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: """Generate a form that is filled in using information passed through the :term:`API` endpoint.""" class ImportEdgePort(FormPage): - model_config = ConfigDict(title="Import Router") + model_config = ConfigDict(title="Import Edge Port") node: active_pe_router_selector() # type: ignore[valid-type] partner: str @@ -63,7 +60,7 @@ def initial_input_form_generator() -> FormGenerator: @step("Initialize subscription") def initialize_subscription( - subscription: EdgePortInactive, + subscription: ImportedEdgePortInactive, node: UUIDstr, service_type: EdgePortType, speed: PhysicalPortCapacity, diff --git a/gso/workflows/l2_circuit/__init__.py b/gso/workflows/l2_circuit/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..53538548aafde0121979e0ede455e545ea13a397 --- /dev/null +++ b/gso/workflows/l2_circuit/__init__.py @@ -0,0 +1 @@ +"""Workflows for Layer 2 Circuits.""" diff --git a/gso/workflows/l2_circuit/create_imported_layer_2_circuit.py b/gso/workflows/l2_circuit/create_imported_layer_2_circuit.py new file mode 100644 index 0000000000000000000000000000000000000000..1a94e4ef2b59e9f986d8ed182ea95c393f367a3a --- /dev/null +++ b/gso/workflows/l2_circuit/create_imported_layer_2_circuit.py @@ -0,0 +1,147 @@ +"""A creation workflow that adds an existing Layer 2 Circuit to the database.""" + +from typing import Any, Self +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 pydantic import BaseModel, ConfigDict, model_validator +from pydantic_forms.types import UUIDstr + +from gso.products import ProductName +from gso.products.product_blocks.layer_2_circuit import Layer2CircuitSideBlockInactive, Layer2CircuitType +from gso.products.product_blocks.service_binding_port import ServiceBindingPortInactive +from gso.products.product_types.edge_port import EdgePort +from gso.products.product_types.layer_2_circuit import ( + ImportedLayer2CircuitInactive, + Layer2CircuitServiceType, +) +from gso.services.partners import get_partner_by_name +from gso.services.subscriptions import get_product_id_by_name +from gso.utils.shared_enums import SBPType +from gso.utils.types.interfaces import BandwidthString +from gso.utils.types.virtual_identifiers import VC_ID, VLAN_ID + + +def initial_input_form_generator() -> FormGenerator: + """Generate a form that can be pre-filled using an :term:`API` endpoint.""" + + class ServiceBindingPortInput(BaseModel): + edge_port: UUIDstr + vlan_id: VLAN_ID + + class ImportLayer2CircuitForm(FormPage): + model_config = ConfigDict(title="Import Layer 2 Circuit") + + service_type: Layer2CircuitServiceType + partner: str + geant_sid: str + vc_id: VC_ID + layer_2_circuit_side_a: ServiceBindingPortInput + layer_2_circuit_side_b: ServiceBindingPortInput + layer_2_circuit_type: Layer2CircuitType + vlan_range_lower_bound: VLAN_ID | None = None + vlan_range_upper_bound: VLAN_ID | None = None + policer_enabled: bool = False + policer_bandwidth: BandwidthString | None = None + policer_burst_rate: BandwidthString | None = None + + @model_validator(mode="after") + def tagged_layer_2_circuit_has_vlan_bounds(self) -> Self: + """If a Layer 2 Circuit is tagged, it must have a :term:`VLAN` range set.""" + if self.layer_2_circuit_type == Layer2CircuitType.TAGGED and ( + self.vlan_range_lower_bound is None or self.vlan_range_upper_bound is None + ): + msg = ( + f"A tagged Layer 2 Circuit must have a VLAN range set. Received lower: " + f"{self.vlan_range_lower_bound}, upper: {self.vlan_range_upper_bound}." + ) + raise ValueError(msg) + return self + + @model_validator(mode="after") + def policer_bandwidth_required_if_policer_enabled(self) -> Self: + """If the policer is enabled, the bandwidth and burst rate must be set.""" + if self.policer_enabled and (self.policer_bandwidth is None or self.policer_burst_rate is None): + msg = ( + f"If the policer is enabled, the bandwidth and burst rate must be set. Received bandwidth: " + f"{self.policer_bandwidth}, burst rate: {self.policer_burst_rate}." + ) + raise ValueError(msg) + return self + + user_input = yield ImportLayer2CircuitForm + return user_input.model_dump() + + +@step("Create subscription") +def create_subscription(partner: str, service_type: Layer2CircuitServiceType) -> State: + """Create a new subscription object.""" + partner_id = get_partner_by_name(partner)["partner_id"] + product_id = get_product_id_by_name(ProductName(service_type)) + subscription = ImportedLayer2CircuitInactive.from_product_id(product_id, partner_id) + + return {"subscription": subscription, "subscription_id": subscription.subscription_id} + + +@step("Initialize subscription") +def initialize_subscription( + subscription: ImportedLayer2CircuitInactive, + geant_sid: str, + layer_2_circuit_side_a: dict[str, Any], + layer_2_circuit_side_b: dict[str, Any], + layer_2_circuit_type: Layer2CircuitType, + vlan_range_lower_bound: VLAN_ID | None, + vlan_range_upper_bound: VLAN_ID | None, + policer_enabled: bool, # noqa: FBT001 + policer_bandwidth: BandwidthString | None, + policer_burst_rate: BandwidthString | None, + vc_id: VC_ID, +) -> State: + """Initialize the subscription object.""" + layer_2_circuit_sides = [] + for circuit_side_data in [layer_2_circuit_side_a, layer_2_circuit_side_b]: + sbp = ServiceBindingPortInactive.new( + uuid4(), + edge_port=EdgePort.from_subscription(subscription_id=circuit_side_data["edge_port"]).edge_port, + sbp_type=SBPType.L2, + vlan_id=circuit_side_data["vlan_id"], + geant_sid=geant_sid, + is_tagged=layer_2_circuit_type == Layer2CircuitType.TAGGED, + custom_firewall_filters=False, + ) + layer2_circuit_side = Layer2CircuitSideBlockInactive.new(uuid4(), sbp=sbp) + layer_2_circuit_sides.append(layer2_circuit_side) + subscription.layer_2_circuit.layer_2_circuit_sides = layer_2_circuit_sides + subscription.layer_2_circuit.virtual_circuit_id = vc_id + subscription.layer_2_circuit.layer_2_circuit_type = layer_2_circuit_type + subscription.layer_2_circuit.vlan_range_lower_bound = vlan_range_lower_bound + subscription.layer_2_circuit.vlan_range_upper_bound = vlan_range_upper_bound + subscription.layer_2_circuit.policer_enabled = policer_enabled + subscription.layer_2_circuit.bandwidth = policer_bandwidth + subscription.layer_2_circuit.policer_burst_rate = policer_burst_rate + subscription.description = f"{subscription.product.name} - {subscription.layer_2_circuit.virtual_circuit_id}" + + return {"subscription": subscription} + + +@workflow( + "Create imported Layer 2 Circuit", + initial_input_form=initial_input_form_generator, + target=Target.CREATE, +) +def create_imported_layer_2_circuit() -> StepList: + """Import a Layer 2 Circuit 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/l2_circuit/create_layer_2_circuit.py b/gso/workflows/l2_circuit/create_layer_2_circuit.py new file mode 100644 index 0000000000000000000000000000000000000000..0a15fcebc00172a30e223b7c975badbcbd46d144 --- /dev/null +++ b/gso/workflows/l2_circuit/create_layer_2_circuit.py @@ -0,0 +1,154 @@ +"""Workflow for creating a new Layer 2 Circuit.""" + +from typing import Any, Self +from uuid import uuid4 + +from orchestrator import step, workflow +from orchestrator.forms import FormPage +from orchestrator.targets import Target +from orchestrator.types import SubscriptionLifecycle +from orchestrator.workflow import StepList, begin, done +from orchestrator.workflows.steps import resync, set_status, store_process_subscription +from orchestrator.workflows.utils import wrap_create_initial_input_form +from pydantic import BaseModel, ConfigDict, Field, model_validator +from pydantic_forms.types import FormGenerator, State, UUIDstr +from pydantic_forms.validators import Divider, Label, ReadOnlyField + +from gso.products.product_blocks.layer_2_circuit import Layer2CircuitSideBlockInactive, Layer2CircuitType +from gso.products.product_blocks.service_binding_port import ServiceBindingPortInactive +from gso.products.product_types.edge_port import EdgePort +from gso.products.product_types.layer_2_circuit import Layer2Circuit, Layer2CircuitInactive +from gso.services.partners import get_partner_by_name +from gso.utils.helpers import active_edge_port_selector, generate_unique_vc_id, partner_choice +from gso.utils.shared_enums import SBPType +from gso.utils.types.interfaces import BandwidthString +from gso.utils.types.tt_number import TTNumber +from gso.utils.types.virtual_identifiers import VLAN_ID + + +def initial_input_generator(product_name: str) -> FormGenerator: + """Gather input from the operator about a new Layer 2 Circuit subscription.""" + geant_partner_id = get_partner_by_name("GEANT")["partner_id"] + + class CreateLayer2CircuitServicePage(FormPage): + model_config = ConfigDict(title=f"{product_name}") + + tt_number: TTNumber + partner: partner_choice() = geant_partner_id # type: ignore[valid-type] + divider: Divider = Field(None, exclude=True) + + layer_2_circuit_type: Layer2CircuitType + policer_enabled: bool = False + + initial_user_input = yield CreateLayer2CircuitServicePage + + class Layer2CircuitSideSelection(BaseModel): + edge_port: active_edge_port_selector( # type: ignore[valid-type] + partner_id=initial_user_input.partner if initial_user_input.partner != geant_partner_id else None + ) + vlan_id: VLAN_ID + + def _vlan_range_field(*, is_tagged: bool) -> VLAN_ID: + """Return the appropriate field type based on whether the circuit is tagged.""" + return VLAN_ID if is_tagged else ReadOnlyField(None, default_type=int) + + def _policer_field(*, policer_enabled: bool) -> BandwidthString: + """Return the appropriate field type based on whether the policer is enabled.""" + return BandwidthString if policer_enabled else ReadOnlyField(None, default_type=str) + + class Layer2CircuitServiceSidesPage(FormPage): + model_config = ConfigDict(title=f"{product_name} - Configure Edge Ports") + + vlan_range_label: Label = Field("Please set a VLAN range, bounds including.", exclude=True) + vlan_range_lower_bound: _vlan_range_field( # type: ignore[valid-type] + is_tagged=initial_user_input.layer_2_circuit_type == Layer2CircuitType.TAGGED + ) + vlan_range_upper_bound: _vlan_range_field( # type: ignore[valid-type] + is_tagged=initial_user_input.layer_2_circuit_type == Layer2CircuitType.TAGGED + ) + vlan_divider: Divider = Field(None, exclude=True) + policer_bandwidth: _policer_field(policer_enabled=initial_user_input.policer_enabled) # type: ignore[valid-type] + policer_burst_rate: _policer_field(policer_enabled=initial_user_input.policer_enabled) # type: ignore[valid-type] + geant_sid: str + layer_2_circuit_side_a: Layer2CircuitSideSelection + side_divider: Divider = Field(None, exclude=True) + layer_2_circuit_side_b: Layer2CircuitSideSelection + + @model_validator(mode="after") + def check_unique_sides(self) -> Self: + if self.layer_2_circuit_side_a.edge_port == self.layer_2_circuit_side_b.edge_port: + msg = "Both sides of the circuit cannot be connected to the same edge port" + raise ValueError(msg) + return self + + layer_2_circuit_input = yield Layer2CircuitServiceSidesPage + + return {"product_name": product_name} | initial_user_input.model_dump() | layer_2_circuit_input.model_dump() + + +@step("Create subscription") +def create_subscription(product: UUIDstr, partner: str) -> State: + """Create a new subscription object in the database.""" + subscription = Layer2CircuitInactive.from_product_id(product, partner) + + return {"subscription": subscription, "subscription_id": subscription.subscription_id} + + +@step("Initialize subscription") +def initialize_subscription( + subscription: Layer2CircuitInactive, + layer_2_circuit_side_a: dict[str, Any], + layer_2_circuit_side_b: dict[str, Any], + layer_2_circuit_type: Layer2CircuitType, + vlan_range_lower_bound: VLAN_ID | None, + vlan_range_upper_bound: VLAN_ID | None, + policer_enabled: bool, # noqa: FBT001 + policer_bandwidth: BandwidthString | None, + policer_burst_rate: BandwidthString | None, + geant_sid: str, +) -> State: + """Build a subscription object from all user input.""" + layer_2_circuit_sides = [] + for circuit_side_data in [layer_2_circuit_side_a, layer_2_circuit_side_b]: + sbp = ServiceBindingPortInactive.new( + uuid4(), + edge_port=EdgePort.from_subscription(subscription_id=circuit_side_data["edge_port"]).edge_port, + sbp_type=SBPType.L2, + vlan_id=circuit_side_data["vlan_id"], + geant_sid=geant_sid, + is_tagged=layer_2_circuit_type == Layer2CircuitType.TAGGED, + custom_firewall_filters=False, + ) + layer2_circuit_side = Layer2CircuitSideBlockInactive.new(uuid4(), sbp=sbp) + layer_2_circuit_sides.append(layer2_circuit_side) + subscription.layer_2_circuit.layer_2_circuit_sides = layer_2_circuit_sides + subscription.layer_2_circuit.virtual_circuit_id = generate_unique_vc_id() + subscription.layer_2_circuit.layer_2_circuit_type = layer_2_circuit_type + subscription.layer_2_circuit.vlan_range_lower_bound = vlan_range_lower_bound + subscription.layer_2_circuit.vlan_range_upper_bound = vlan_range_upper_bound + subscription.layer_2_circuit.policer_enabled = policer_enabled + subscription.layer_2_circuit.bandwidth = policer_bandwidth + subscription.layer_2_circuit.policer_burst_rate = policer_burst_rate + subscription.description = f"{subscription.product.name} - {subscription.layer_2_circuit.virtual_circuit_id}" + + subscription = Layer2Circuit.from_other_lifecycle(subscription, SubscriptionLifecycle.PROVISIONING) + + return {"subscription": subscription} + + +@workflow( + "Create Layer 2 Circuit Service", + initial_input_form=wrap_create_initial_input_form(initial_input_generator), + target=Target.CREATE, +) +def create_layer_2_circuit() -> StepList: + """Create a new Layer 2 Circuit service subscription.""" + return ( + begin + >> create_subscription + >> store_process_subscription(Target.CREATE) + >> initialize_subscription + >> set_status(SubscriptionLifecycle.ACTIVE) + >> resync + >> done + ) diff --git a/gso/workflows/l2_circuit/import_layer_2_circuit.py b/gso/workflows/l2_circuit/import_layer_2_circuit.py new file mode 100644 index 0000000000000000000000000000000000000000..01224e86cbdb614eb879dfc2622fdbfb2804d19b --- /dev/null +++ b/gso/workflows/l2_circuit/import_layer_2_circuit.py @@ -0,0 +1,43 @@ +"""A modification workflow for migrating an ImportedLayer2Circuit to an Layer2Circuit subscription.""" + +from orchestrator.targets import Target +from orchestrator.types import State, UUIDstr +from orchestrator.utils.errors import ProcessFailureError +from orchestrator.workflow import StepList, done, init, 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 import ProductName +from gso.products.product_types.layer_2_circuit import ImportedLayer2Circuit, Layer2Circuit, Layer2CircuitServiceType +from gso.services.subscriptions import get_product_id_by_name + + +@step("Create imported subscription") +def import_layer_2_circuit_subscription(subscription_id: UUIDstr) -> State: + """Take an imported subscription, and turn it into a layer 2 circuit subscription.""" + old_layer_2_circuit_subscription = ImportedLayer2Circuit.from_subscription(subscription_id) + if old_layer_2_circuit_subscription.layer_2_circuit_service_type == Layer2CircuitServiceType.IMPORTED_GEANT_PLUS: + new_subscription_id = get_product_id_by_name(ProductName.GEANT_PLUS) + elif ( + old_layer_2_circuit_subscription.layer_2_circuit_service_type == Layer2CircuitServiceType.IMPORTED_EXPRESSROUTE + ): + new_subscription_id = get_product_id_by_name(ProductName.EXPRESSROUTE) + else: + msg = f"This {old_layer_2_circuit_subscription.layer_2_circuit_service_type} is already imported." + raise ProcessFailureError(message=msg, details=old_layer_2_circuit_subscription) + new_subscription = Layer2Circuit.from_other_product(old_layer_2_circuit_subscription, new_subscription_id) # type: ignore[arg-type] + + return {"subscription": new_subscription} + + +@workflow("Import Layer 2 Circuit", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None)) +def import_layer_2_circuit() -> StepList: + """Modify an imported subscription into a layer 2 circuit subscription to complete the import.""" + return ( + init + >> store_process_subscription(Target.MODIFY) + >> unsync + >> import_layer_2_circuit_subscription + >> resync + >> done + ) diff --git a/gso/workflows/l2_circuit/modify_layer_2_circuit.py b/gso/workflows/l2_circuit/modify_layer_2_circuit.py new file mode 100644 index 0000000000000000000000000000000000000000..0ac2d3919763596fdb34c6f9223c2efccc5694de --- /dev/null +++ b/gso/workflows/l2_circuit/modify_layer_2_circuit.py @@ -0,0 +1,114 @@ +"""A modification workflow for a Layer 2 Circuit subscription.""" + +from orchestrator import begin, done, workflow +from orchestrator.forms import FormPage +from orchestrator.targets import Target +from orchestrator.types import FormGenerator, UUIDstr +from orchestrator.workflow import StepList, step +from orchestrator.workflows.steps import resync, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form +from pydantic import ConfigDict, Field +from pydantic_forms.validators import Divider, Label, ReadOnlyField + +from gso.products.product_blocks.layer_2_circuit import Layer2CircuitType +from gso.products.product_types.edge_port import EdgePort +from gso.products.product_types.layer_2_circuit import Layer2Circuit +from gso.services.partners import get_partner_by_id +from gso.utils.types.interfaces import BandwidthString +from gso.utils.types.tt_number import TTNumber +from gso.utils.types.virtual_identifiers import VLAN_ID + + +def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: + """Get input from the operator about the modifications to be made to a Layer 2 Circuit subscription.""" + subscription = Layer2Circuit.from_subscription(subscription_id) + product_name = subscription.product.name + + class ModifyL2CircuitForm(FormPage): + model_config = ConfigDict(title=f"Modify {product_name}") + tt_number: TTNumber + partner: ReadOnlyField(get_partner_by_id(subscription.customer_id).name, default_type=str) # type: ignore[valid-type] + divider: Divider = Field(None, exclude=True) + + layer_2_circuit_type: Layer2CircuitType = subscription.layer_2_circuit.layer_2_circuit_type + policer_enabled: bool = subscription.layer_2_circuit.policer_enabled + + layer_2_circuit_input = yield ModifyL2CircuitForm + + class ModifyLayer2CircuitServiceSidesPage(FormPage): + model_config = ConfigDict(title=f"{product_name} - Configure Edge Ports") + + vlan_range_label: Label = Field("Please set a VLAN range, bounds including.", exclude=True) + if layer_2_circuit_input.layer_2_circuit_type == Layer2CircuitType.TAGGED: + vlan_range_lower_bound: VLAN_ID = subscription.layer_2_circuit.vlan_range_lower_bound # type: ignore[assignment] + vlan_range_upper_bound: VLAN_ID = subscription.layer_2_circuit.vlan_range_upper_bound # type: ignore[assignment] + else: + vlan_range_lower_bound: ReadOnlyField(None, default_type=int) # type: ignore[no-redef, valid-type] + vlan_range_upper_bound: ReadOnlyField(None, default_type=int) # type: ignore[no-redef, valid-type] + + vlan_divider: Divider = Field(None, exclude=True) + if layer_2_circuit_input.policer_enabled: + policer_bandwidth: BandwidthString = subscription.layer_2_circuit.bandwidth # type: ignore[assignment] + policer_burst_rate: BandwidthString = subscription.layer_2_circuit.policer_burst_rate # type: ignore[assignment] + else: + policer_bandwidth: ReadOnlyField(None, default_type=str) # type: ignore[no-redef, valid-type] + policer_burst_rate: ReadOnlyField(None, default_type=str) # type: ignore[no-redef, valid-type] + policer_divider: Divider = Field(None, exclude=True) + + layer_2_circuit_side_a: ReadOnlyField( # type: ignore[valid-type] + EdgePort.from_subscription( + subscription.layer_2_circuit.layer_2_circuit_sides[0].sbp.edge_port.owner_subscription_id + ).description, + default_type=str, + ) + side_divider: Divider = Field(None, exclude=True) + layer_2_circuit_side_b: ReadOnlyField( # type: ignore[valid-type] + EdgePort.from_subscription( + subscription.layer_2_circuit.layer_2_circuit_sides[1].sbp.edge_port.owner_subscription_id + ).description, + default_type=str, + ) + + layer_2_circuit_sides = yield ModifyLayer2CircuitServiceSidesPage + + return {"product_name": product_name} | layer_2_circuit_input.model_dump() | layer_2_circuit_sides.model_dump() + + +@step("Update Layer 2 Circuit subscription") +def modify_layer_2_circuit_subscription( + subscription: Layer2Circuit, + layer_2_circuit_type: Layer2CircuitType, + vlan_range_lower_bound: VLAN_ID | None, + vlan_range_upper_bound: VLAN_ID | None, + policer_enabled: bool, # noqa: FBT001 + policer_bandwidth: BandwidthString | None, + policer_burst_rate: BandwidthString | None, +) -> dict: + """Update the Layer 2 Circuit subscription with the new values.""" + subscription.layer_2_circuit.layer_2_circuit_type = layer_2_circuit_type + subscription.layer_2_circuit.vlan_range_lower_bound = vlan_range_lower_bound + subscription.layer_2_circuit.vlan_range_upper_bound = vlan_range_upper_bound + subscription.layer_2_circuit.policer_enabled = policer_enabled + subscription.layer_2_circuit.bandwidth = policer_bandwidth + subscription.layer_2_circuit.policer_burst_rate = policer_burst_rate + for layer_2_circuit_side in subscription.layer_2_circuit.layer_2_circuit_sides: + layer_2_circuit_side.sbp.is_tagged = layer_2_circuit_type == Layer2CircuitType.TAGGED + + return {"subscription": subscription} + + +@workflow( + "Modify Layer 2 Circuit Service", + initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), + target=Target.MODIFY, +) +def modify_layer_2_circuit() -> StepList: + """Modify an existing Layer 2 Circuit service subscription.""" + return ( + begin + >> store_process_subscription(Target.MODIFY) + >> unsync + >> modify_layer_2_circuit_subscription + >> resync + >> done + ) diff --git a/gso/workflows/l2_circuit/terminate_layer_2_circuit.py b/gso/workflows/l2_circuit/terminate_layer_2_circuit.py new file mode 100644 index 0000000000000000000000000000000000000000..1652aa02ca5e7d4384716e8dd0f0f0909549fc7d --- /dev/null +++ b/gso/workflows/l2_circuit/terminate_layer_2_circuit.py @@ -0,0 +1,40 @@ +"""Workflow for terminating a Layer 2 Circuit.""" + +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 gso.products.product_types.layer_2_circuit import Layer2Circuit +from gso.utils.types.tt_number import TTNumber + + +def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator: + layer_2_circuit = Layer2Circuit.from_subscription(subscription_id) + + class TerminateForm(FormPage): + tt_number: TTNumber + + yield TerminateForm + return {"subscription": layer_2_circuit} + + +@workflow( + "Terminate Layer 2 Circuit Service", + initial_input_form=wrap_modify_initial_input_form(_input_form_generator), + target=Target.TERMINATE, +) +def terminate_layer_2_circuit() -> StepList: + """Terminate a Layer 2 Circuit subscription.""" + return ( + begin + >> store_process_subscription(Target.TERMINATE) + >> unsync + >> set_status(SubscriptionLifecycle.TERMINATED) + >> resync + >> done + ) diff --git a/gso/workflows/l3_core_service/__init__.py b/gso/workflows/l3_core_service/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6143fba1180f4f37aa0b13a9fa91ebc578c0806a --- /dev/null +++ b/gso/workflows/l3_core_service/__init__.py @@ -0,0 +1 @@ +"""Layer 3 core service workflows.""" diff --git a/gso/workflows/nren_l3_core_service/create_imported_nren_l3_core_service.py b/gso/workflows/l3_core_service/create_imported_l3_core_service.py similarity index 54% rename from gso/workflows/nren_l3_core_service/create_imported_nren_l3_core_service.py rename to gso/workflows/l3_core_service/create_imported_l3_core_service.py index f5a2e2ea00588c4ea9893f71bbb5772c26c7f0cf..0ff4c30f9664b6d5124d2fb144ca002e8027e7a0 100644 --- a/gso/workflows/nren_l3_core_service/create_imported_nren_l3_core_service.py +++ b/gso/workflows/l3_core_service/create_imported_l3_core_service.py @@ -1,4 +1,4 @@ -"""A creation workflow for adding an existing NREN L3 Core Service to the service database.""" +"""A creation workflow for adding an existing L3 Core Service to the service database.""" from uuid import uuid4 @@ -6,32 +6,38 @@ from orchestrator import workflow from orchestrator.forms import FormPage from orchestrator.targets import Target from orchestrator.types import FormGenerator, SubscriptionLifecycle +from orchestrator.utils.errors import ProcessFailureError from orchestrator.workflow import StepList, begin, done, step from orchestrator.workflows.steps import resync, set_status, store_process_subscription -from pydantic import BaseModel +from pydantic import BaseModel, NonNegativeInt from pydantic_forms.types import UUIDstr from gso.products import ProductName -from gso.products.product_blocks.bgp_session import BGPSession, IPFamily -from gso.products.product_blocks.nren_l3_core_service import NRENAccessPortInactive -from gso.products.product_blocks.service_binding_port import VLAN_ID, ServiceBindingPortInactive +from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPTypes +from gso.products.product_blocks.l3_core_service import AccessPortInactive +from gso.products.product_blocks.service_binding_port import BFDSettings, ServiceBindingPortInactive from gso.products.product_types.edge_port import EdgePort -from gso.products.product_types.nren_l3_core_service import ImportedNRENL3CoreServiceInactive, NRENL3CoreServiceType +from gso.products.product_types.l3_core_service import ImportedL3CoreServiceInactive, L3CoreServiceType from gso.services.partners import get_partner_by_name from gso.services.subscriptions import get_product_id_by_name from gso.utils.shared_enums import SBPType from gso.utils.types.ip_address import IPAddress, IPv4AddressType, IPV4Netmask, IPv6AddressType, IPV6Netmask +from gso.utils.types.virtual_identifiers import VLAN_ID def initial_input_form_generator() -> FormGenerator: """Take all information passed to this workflow by the :term:`API` endpoint that was called.""" - class BaseBGPPeer(BaseModel): + class BFDSettingsModel(BaseModel): bfd_enabled: bool = False - bfd_interval: int | None = None + bfd_interval_rx: int | None = None + bfd_interval_tx: int | None = None bfd_multiplier: int | None = None + + class BaseBGPPeer(BaseModel): + bfd_enabled: bool = False has_custom_policies: bool = False - authentication_key: str + authentication_key: str | None multipath_enabled: bool = False send_default_route: bool = False is_passive: bool = False @@ -39,6 +45,7 @@ def initial_input_form_generator() -> FormGenerator: families: list[IPFamily] is_multi_hop: bool rtbh_enabled: bool + prefix_limit: NonNegativeInt | None = None class ServiceBindingPort(BaseModel): edge_port: UUIDstr @@ -55,45 +62,67 @@ def initial_input_form_generator() -> FormGenerator: rtbh_enabled: bool = True is_multi_hop: bool = True bgp_peers: list[BaseBGPPeer] + v4_bfd_settings: BFDSettingsModel + v6_bfd_settings: BFDSettingsModel - class ImportNRENL3CoreServiceForm(FormPage): + class ImportL3CoreServiceForm(FormPage): partner: str service_binding_ports: list[ServiceBindingPort] - service_type: NRENL3CoreServiceType + service_type: L3CoreServiceType - user_input = yield ImportNRENL3CoreServiceForm + user_input = yield ImportL3CoreServiceForm return user_input.model_dump() @step("Create subscription") -def create_subscription(partner: str, service_type: NRENL3CoreServiceType) -> dict: +def create_subscription(partner: str, service_type: L3CoreServiceType) -> dict: """Create a new subscription object in the database.""" partner_id = get_partner_by_name(partner)["partner_id"] - if service_type == NRENL3CoreServiceType.GEANT_IP: - product_id = get_product_id_by_name(ProductName.IMPORTED_GEANT_IP) - elif service_type == NRENL3CoreServiceType.IAS: - product_id = get_product_id_by_name(ProductName.IMPORTED_IAS) - subscription = ImportedNRENL3CoreServiceInactive.from_product_id(product_id, partner_id) + match service_type: + case L3CoreServiceType.GEANT_IP: + product_id = get_product_id_by_name(ProductName.IMPORTED_GEANT_IP) + case L3CoreServiceType.IAS: + product_id = get_product_id_by_name(ProductName.IMPORTED_IAS) + case L3CoreServiceType.GWS: + product_id = get_product_id_by_name(ProductName.IMPORTED_GWS) + case L3CoreServiceType.LHCONE: + product_id = get_product_id_by_name(ProductName.IMPORTED_LHCONE) + case L3CoreServiceType.COPERNICUS: + product_id = get_product_id_by_name(ProductName.IMPORTED_COPERNICUS) + case _: + msg = "L3 Core service type not defined. Cannot create subscription." + raise ProcessFailureError(msg, details=service_type) + subscription = ImportedL3CoreServiceInactive.from_product_id(product_id, partner_id) return {"subscription": subscription, "subscription_id": subscription.subscription_id} @step("Initialize subscription") -def initialize_subscription(subscription: ImportedNRENL3CoreServiceInactive, service_binding_ports: list) -> dict: +def initialize_subscription(subscription: ImportedL3CoreServiceInactive, service_binding_ports: list) -> dict: """Initialize the subscription with the user input.""" for service_binding_port in service_binding_ports: edge_port_subscription = EdgePort.from_subscription(service_binding_port.pop("edge_port")) bgp_peers = service_binding_port.pop("bgp_peers") - sbp_bgp_session_list = [BGPSession.new(subscription_id=uuid4(), **session) for session in bgp_peers] - + sbp_bgp_session_list = [ + BGPSession.new( + subscription_id=uuid4(), + ip_type=IPTypes.IPV4 + if any(family in {IPFamily.V4UNICAST, IPFamily.V4MULTICAST} for family in session["families"]) + else IPTypes.IPV6, + **session, + ) + for session in bgp_peers + ] service_binding_port_subscription = ServiceBindingPortInactive.new( subscription_id=uuid4(), edge_port=edge_port_subscription.edge_port, sbp_bgp_session_list=sbp_bgp_session_list, + v4_bfd_settings=BFDSettings.new(subscription_id=uuid4(), **(service_binding_port.pop("v4_bfd_settings"))), + v6_bfd_settings=BFDSettings.new(subscription_id=uuid4(), **(service_binding_port.pop("v6_bfd_settings"))), **service_binding_port, ) - subscription.nren_l3_core_service.nren_ap_list.append( - NRENAccessPortInactive.new( + subscription.l3_core_service.ap_list.append( + AccessPortInactive.new( subscription_id=uuid4(), ap_type=service_binding_port["ap_type"], sbp=service_binding_port_subscription, @@ -106,11 +135,11 @@ def initialize_subscription(subscription: ImportedNRENL3CoreServiceInactive, ser @workflow( - "Create imported NREN L3 Core Service", + "Create imported L3 Core Service", initial_input_form=initial_input_form_generator, target=Target.CREATE, ) -def create_imported_nren_l3_core_service() -> StepList: +def create_imported_l3_core_service() -> StepList: """Import a GÉANT IP without provisioning it.""" return ( begin diff --git a/gso/workflows/nren_l3_core_service/create_nren_l3_core_service.py b/gso/workflows/l3_core_service/create_l3_core_service.py similarity index 55% rename from gso/workflows/nren_l3_core_service/create_nren_l3_core_service.py rename to gso/workflows/l3_core_service/create_l3_core_service.py index 2ac1708d525591a0aa0d9a19b68b5ef89ef9a064..f23a2fcfb2260c42dc930cd6390c05874e89242b 100644 --- a/gso/workflows/nren_l3_core_service/create_nren_l3_core_service.py +++ b/gso/workflows/l3_core_service/create_l3_core_service.py @@ -1,6 +1,6 @@ -"""Create a new NREN L3 Core Service subscription including GÉANT IP and IAS.""" +"""Create a new L3 Core Service subscription including GÉANT IP and IAS.""" -from typing import Annotated, Any +from typing import Any from uuid import uuid4 from orchestrator.forms import FormPage @@ -10,15 +10,16 @@ from orchestrator.types import FormGenerator, State, SubscriptionLifecycle, UUID from orchestrator.workflow import StepList, begin, done, step, workflow from orchestrator.workflows.steps import resync, set_status, store_process_subscription from orchestrator.workflows.utils import wrap_create_initial_input_form -from pydantic import AfterValidator, BaseModel, ConfigDict, Field, computed_field +from pydantic import BaseModel, ConfigDict, Field, NonNegativeInt, computed_field from pydantic_forms.validators import Divider -from gso.products.product_blocks.bgp_session import BGPSession, IPFamily -from gso.products.product_blocks.nren_l3_core_service import NRENAccessPortInactive -from gso.products.product_blocks.service_binding_port import VLAN_ID, ServiceBindingPortInactive +from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPTypes +from gso.products.product_blocks.l3_core_service import AccessPortInactive +from gso.products.product_blocks.service_binding_port import BFDSettings, ServiceBindingPortInactive from gso.products.product_types.edge_port import EdgePort -from gso.products.product_types.nren_l3_core_service import NRENL3CoreService, NRENL3CoreServiceInactive +from gso.products.product_types.l3_core_service import L3CoreService, L3CoreServiceInactive from gso.services.lso_client import LSOState, lso_interaction +from gso.services.partners import get_partner_by_id from gso.utils.helpers import ( active_edge_port_selector, partner_choice, @@ -26,168 +27,183 @@ from gso.utils.helpers import ( from gso.utils.shared_enums import APType, SBPType from gso.utils.types.ip_address import IPv4AddressType, IPV4Netmask, IPv6AddressType, IPV6Netmask from gso.utils.types.tt_number import TTNumber +from gso.utils.types.virtual_identifiers import VLAN_ID def initial_input_form_generator(product_name: str) -> FormGenerator: """Gather input from the operator to build a new subscription object.""" - class CreateNRENCoreServiceForm(FormPage): + class CreateL3CoreServiceForm(FormPage): model_config = ConfigDict(title=f"{product_name} - Select partner") tt_number: TTNumber partner: partner_choice() # type: ignore[valid-type] - initial_user_input = yield CreateNRENCoreServiceForm + initial_user_input = yield CreateL3CoreServiceForm class EdgePortSelection(BaseModel): edge_port: active_edge_port_selector(partner_id=initial_user_input.partner) # type: ignore[valid-type] ap_type: APType - def validate_edge_ports_are_unique(edge_ports: list[EdgePortSelection]) -> list[EdgePortSelection]: - """Verify if interfaces are unique.""" - port_names = [port.edge_port for port in edge_ports] - if len(port_names) != len(set(port_names)): - msg = "Edge Ports must be unique." - raise ValueError(msg) - return edge_ports - class EdgePortSelectionForm(FormPage): model_config = ConfigDict(title=f"{product_name} - Select Edge Ports") info_label: Label = Field( f"Please select the Edge Ports where this {product_name} service will terminate", exclude=True ) - edge_ports: Annotated[list[EdgePortSelection], AfterValidator(validate_edge_ports_are_unique)] + edge_port: EdgePortSelection - selected_edge_ports = yield EdgePortSelectionForm - ep_list = selected_edge_ports.edge_ports + selected_edge_port = yield EdgePortSelectionForm - class BaseBGPPeer(BaseModel): + class BFDSettingsForm(BaseModel): bfd_enabled: bool = False - bfd_interval: int | None = None + bfd_interval_rx: int | None = Field(default=None, examples=["BFD RX defaults"]) + bfd_interval_tx: int | None = None bfd_multiplier: int | None = None + + class IPv4BGPPeer(BaseModel): + peer_address: IPv4AddressType + authentication_key: str | None = None has_custom_policies: bool = False - authentication_key: str + bfd_enabled: bool = False multipath_enabled: bool = False - send_default_route: bool = False + prefix_limit: NonNegativeInt | None = None is_passive: bool = False - - class IPv4BGPPeer(BaseBGPPeer): - peer_address: IPv4AddressType add_v4_multicast: bool = Field(default=False, exclude=True) + send_default_route: bool = False @computed_field # type: ignore[misc] @property def families(self) -> list[IPFamily]: return [IPFamily.V4UNICAST, IPFamily.V4MULTICAST] if self.add_v4_multicast else [IPFamily.V4UNICAST] - class IPv6BGPPeer(BaseBGPPeer): + @computed_field # type: ignore[misc] + @property + def ip_type(self) -> IPTypes: + return IPTypes.IPV4 + + class IPv6BGPPeer(BaseModel): peer_address: IPv6AddressType + authentication_key: str | None = None + has_custom_policies: bool = False + bfd_enabled: bool = False + multipath_enabled: bool = False + prefix_limit: NonNegativeInt | None = None + is_passive: bool = False add_v6_multicast: bool = Field(default=False, exclude=True) + send_default_route: bool = False @computed_field # type: ignore[misc] @property def families(self) -> list[IPFamily]: return [IPFamily.V6UNICAST, IPFamily.V6MULTICAST] if self.add_v6_multicast else [IPFamily.V6UNICAST] - binding_port_inputs = [] - for ep_index, edge_port in enumerate(ep_list): - - class BindingPortsInputForm(FormPage): - model_config = ConfigDict(title=f"{product_name} - Configure Edge Ports ({ep_index + 1}/{len(ep_list)})") - info_label: Label = Field("Please configure the Service Binding Ports for each Edge Port.", exclude=True) - current_ep_label: Label = Field( - f"Currently configuring on {EdgePort.from_subscription(edge_port.edge_port).description} " - f"(Access Port type: {edge_port.ap_type})", - exclude=True, - ) - - geant_sid: str - is_tagged: bool = False - vlan_id: VLAN_ID - ipv4_address: IPv4AddressType - ipv4_mask: IPV4Netmask - ipv6_address: IPv6AddressType - ipv6_mask: IPV6Netmask - custom_firewall_filters: bool = False - divider: Divider = Field(None, exclude=True) - v4_bgp_peer: IPv4BGPPeer - v6_bgp_peer: IPv6BGPPeer - - binding_port_input_form = yield BindingPortsInputForm - binding_port_inputs.append( - binding_port_input_form.model_dump() - | { - "bgp_peers": [ - binding_port_input_form.v4_bgp_peer.model_dump(), - binding_port_input_form.v6_bgp_peer.model_dump(), - ] - } + @computed_field # type: ignore[misc] + @property + def ip_type(self) -> IPTypes: + return IPTypes.IPV6 + + class BindingPortInputForm(FormPage): + model_config = ConfigDict(title=f"{product_name} - Configure Edge Port") + info_label: Label = Field("Please configure the Service Binding Ports for the Edge Port.", exclude=True) + current_ep_label: Label = Field( + f"Currently configuring on {EdgePort.from_subscription(selected_edge_port.edge_port.edge_port).description}" + f" (Access Port type: {selected_edge_port.edge_port.ap_type})", + exclude=True, ) + geant_sid: str + is_tagged: bool = False + vlan_id: VLAN_ID + custom_firewall_filters: bool = False + divider: Divider = Field(None, exclude=True) + v4_label: Label = Field("IPV4 SBP interface params", exclude=True) + ipv4_address: IPv4AddressType + ipv4_mask: IPV4Netmask + v4_bfd_settings: BFDSettingsForm + divider2: Divider = Field(None, exclude=True) + v6_label: Label = Field("IPV6 SBP interface params", exclude=True) + ipv6_address: IPv6AddressType + ipv6_mask: IPV6Netmask + v6_bfd_settings: BFDSettingsForm + divider3: Divider = Field(None, exclude=True) + v4_bgp_peer: IPv4BGPPeer + v6_bgp_peer: IPv6BGPPeer + + binding_port_input_form = yield BindingPortInputForm + binding_port_input = binding_port_input_form.model_dump() | { + "bgp_peers": [ + binding_port_input_form.v4_bgp_peer.model_dump(), + binding_port_input_form.v6_bgp_peer.model_dump(), + ] + } + return ( initial_user_input.model_dump() - | selected_edge_ports.model_dump() - | {"binding_port_inputs": binding_port_inputs, "product_name": product_name} + | selected_edge_port.model_dump() + | {"binding_port_input": binding_port_input, "product_name": product_name} ) @step("Create subscription") def create_subscription(product: UUIDstr, partner: str) -> State: """Create a new subscription object in the database.""" - subscription = NRENL3CoreServiceInactive.from_product_id(product, partner) + subscription = L3CoreServiceInactive.from_product_id(product, partner) return {"subscription": subscription, "subscription_id": subscription.subscription_id} @step("Initialize subscription") def initialize_subscription( - subscription: NRENL3CoreServiceInactive, edge_ports: list[dict], binding_port_inputs: list[dict], product_name: str + subscription: L3CoreServiceInactive, edge_port: dict, binding_port_input: dict, product_name: str ) -> State: """Take all user inputs and use them to populate the subscription model.""" edge_port_fqdn_list = [] - for edge_port_input, sbp_input in zip(edge_ports, binding_port_inputs, strict=False): - edge_port_subscription = EdgePort.from_subscription(edge_port_input["edge_port"]) - sbp_bgp_session_list = [ - BGPSession.new(subscription_id=uuid4(), **session, rtbh_enabled=True, is_multi_hop=True) - for session in sbp_input["bgp_peers"] - ] - service_binding_port = ServiceBindingPortInactive.new( + edge_port_subscription = EdgePort.from_subscription(edge_port["edge_port"]) + sbp_bgp_session_list = [ + BGPSession.new(subscription_id=uuid4(), rtbh_enabled=True, is_multi_hop=True, **session) + for session in binding_port_input["bgp_peers"] + ] + service_binding_port = ServiceBindingPortInactive.new( + subscription_id=uuid4(), + v4_bfd_settings=BFDSettings.new(subscription_id=uuid4(), **(binding_port_input.pop("v4_bfd_settings"))), + v6_bfd_settings=BFDSettings.new(subscription_id=uuid4(), **(binding_port_input.pop("v6_bfd_settings"))), + **binding_port_input, + bgp_session_list=sbp_bgp_session_list, + sbp_type=SBPType.L3, + edge_port=edge_port_subscription.edge_port, + ) + subscription.l3_core_service.ap_list.append( + AccessPortInactive.new( subscription_id=uuid4(), - **sbp_input, - bgp_session_list=sbp_bgp_session_list, - sbp_type=SBPType.L3, - edge_port=edge_port_subscription.edge_port, + ap_type=edge_port["ap_type"], + sbp=service_binding_port, ) - subscription.nren_l3_core_service.nren_ap_list.append( - NRENAccessPortInactive.new( - subscription_id=uuid4(), - ap_type=edge_port_input["ap_type"], - sbp=service_binding_port, - ) - ) - edge_port_fqdn_list.append(edge_port_subscription.edge_port.node.router_fqdn) + ) + edge_port_fqdn_list.append(edge_port_subscription.edge_port.node.router_fqdn) subscription.description = f"{product_name} service" - - return {"subscription": subscription, "edge_port_fqdn_list": edge_port_fqdn_list} + partner_name = get_partner_by_id(subscription.customer_id).name + return {"subscription": subscription, "edge_port_fqdn_list": edge_port_fqdn_list, "partner_name": partner_name} @step("[DRY RUN] Deploy service binding port") def provision_sbp_dry( - subscription: dict[str, Any], process_id: UUIDstr, tt_number: str, edge_port_fqdn_list: list[str] + subscription: dict[str, Any], process_id: UUIDstr, tt_number: str, edge_port_fqdn_list: list[str], partner_name: str ) -> LSOState: """Perform a dry run of deploying Service Binding Ports.""" extra_vars = { "subscription": subscription, + "partner_name": partner_name, "dry_run": True, "verb": "deploy", + "object": "sbp", "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " f"Deploy config for {subscription["description"]}", } return { - "playbook_name": "manage_sbp.yaml", + "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml", "inventory": {"all": {"hosts": dict.fromkeys(edge_port_fqdn_list)}}, "extra_vars": extra_vars, } @@ -195,19 +211,21 @@ def provision_sbp_dry( @step("[FOR REAL] Deploy service binding port") def provision_sbp_real( - subscription: dict[str, Any], process_id: UUIDstr, tt_number: str, edge_port_fqdn_list: list[str] + subscription: dict[str, Any], process_id: UUIDstr, tt_number: str, edge_port_fqdn_list: list[str], partner_name: str ) -> LSOState: """Deploy Service Binding Ports.""" extra_vars = { "subscription": subscription, + "partner_name": partner_name, "dry_run": False, "verb": "deploy", + "object": "sbp", "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " f"Deploy config for {subscription["description"]}", } return { - "playbook_name": "manage_sbp.yaml", + "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml", "inventory": {"all": {"hosts": dict.fromkeys(edge_port_fqdn_list)}}, "extra_vars": extra_vars, } @@ -216,10 +234,10 @@ def provision_sbp_real( @step("Check service binding port functionality") def check_sbp_functionality(subscription: dict[str, Any], edge_port_fqdn_list: list[str]) -> LSOState: """Check functionality of deployed Service Binding Ports.""" - extra_vars = {"subscription": subscription, "verb": "check"} + extra_vars = {"subscription": subscription, "verb": "check", "object": "sbp"} return { - "playbook_name": "manage_sbp.yaml", + "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml", "inventory": {"all": {"hosts": dict.fromkeys(edge_port_fqdn_list)}}, "extra_vars": extra_vars, } @@ -227,19 +245,21 @@ def check_sbp_functionality(subscription: dict[str, Any], edge_port_fqdn_list: l @step("[DRY RUN] Deploy BGP peers") def deploy_bgp_peers_dry( - subscription: dict[str, Any], edge_port_fqdn_list: list[str], tt_number: str, process_id: UUIDstr + subscription: dict[str, Any], edge_port_fqdn_list: list[str], tt_number: str, process_id: UUIDstr, partner_name: str ) -> LSOState: """Perform a dry run of deploying :term:`BGP` peers.""" extra_vars = { "subscription": subscription, + "partner_name": partner_name, "verb": "deploy", + "object": "bgp", "dry_run": True, "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " f"Deploying BGP peers for {subscription["description"]}", } return { - "playbook_name": "manage_sbp.yaml", + "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml", "inventory": {"all": {"hosts": dict.fromkeys(edge_port_fqdn_list)}}, "extra_vars": extra_vars, } @@ -247,19 +267,21 @@ def deploy_bgp_peers_dry( @step("[FOR REAL] Deploy BGP peers") def deploy_bgp_peers_real( - subscription: dict[str, Any], edge_port_fqdn_list: list[str], tt_number: str, process_id: UUIDstr + subscription: dict[str, Any], edge_port_fqdn_list: list[str], tt_number: str, process_id: UUIDstr, partner_name: str ) -> LSOState: """Deploy :term:`BGP` peers.""" extra_vars = { "subscription": subscription, + "partner_name": partner_name, "verb": "deploy", + "object": "bgp", "dry_run": False, "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " f"Deploying BGP peers for {subscription["description"]}", } return { - "playbook_name": "manage_sbp.yaml", + "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml", "inventory": {"all": {"hosts": dict.fromkeys(edge_port_fqdn_list)}}, "extra_vars": extra_vars, } @@ -268,29 +290,29 @@ def deploy_bgp_peers_real( @step("Check BGP peers") def check_bgp_peers(subscription: dict[str, Any], edge_port_fqdn_list: list[str]) -> LSOState: """Check correct deployment of :term:`BGP` peers.""" - extra_vars = {"subscription": subscription, "verb": "check"} + extra_vars = {"subscription": subscription, "verb": "check", "object": "bgp"} return { - "playbook_name": "manage_sbp.yaml", + "playbook_name": "gap_ansible/playbooks/l3_core_service.yaml", "inventory": {"all": {"hosts": dict.fromkeys(edge_port_fqdn_list)}}, "extra_vars": extra_vars, } @step("Update Infoblox") -def update_dns_records(subscription: NRENL3CoreService) -> State: +def update_dns_records(subscription: L3CoreService) -> State: """Update :term:`DNS` records in Infoblox.""" # TODO: implement return {"subscription": subscription} @workflow( - "Create NREN L3 Core Service", + "Create L3 Core Service", initial_input_form=wrap_create_initial_input_form(initial_input_form_generator), target=Target.CREATE, ) -def create_nren_l3_core_service() -> StepList: - """Create a new :term:`NREN` L3 Core Service subscription including GÉANT IP and IAS. +def create_l3_core_service() -> StepList: + """Create a new L3 Core Service subscription including GÉANT IP and IAS. * Create subscription object in the service database * Deploy service binding ports diff --git a/gso/workflows/l3_core_service/import_l3_core_service.py b/gso/workflows/l3_core_service/import_l3_core_service.py new file mode 100644 index 0000000000000000000000000000000000000000..974c379da1073287f11c6b797335f939d8637d5b --- /dev/null +++ b/gso/workflows/l3_core_service/import_l3_core_service.py @@ -0,0 +1,52 @@ +"""A modification workflow for migrating an ImportedGeantIP to an GeantIP subscription.""" + +from orchestrator.targets import Target +from orchestrator.types import State, UUIDstr +from orchestrator.utils.errors import ProcessFailureError +from orchestrator.workflow import StepList, done, init, 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 import ProductName +from gso.products.product_types.l3_core_service import ( + ImportedL3CoreService, + L3CoreService, + L3CoreServiceType, +) +from gso.services.subscriptions import get_product_id_by_name + + +@step("Create imported subscription") +def import_l3_core_service_subscription(subscription_id: UUIDstr) -> State: + """Take an imported subscription, and turn it into an L3 Core Service subscription.""" + old_l3_core_service = ImportedL3CoreService.from_subscription(subscription_id) + match old_l3_core_service.l3_core_service_type: + case L3CoreServiceType.IMPORTED_GEANT_IP: + new_subscription_id = get_product_id_by_name(ProductName.GEANT_IP) + case L3CoreServiceType.IMPORTED_IAS: + new_subscription_id = get_product_id_by_name(ProductName.IAS) + case L3CoreServiceType.IMPORTED_GWS: + new_subscription_id = get_product_id_by_name(ProductName.GWS) + case L3CoreServiceType.IMPORTED_LHCONE: + new_subscription_id = get_product_id_by_name(ProductName.LHCONE) + case L3CoreServiceType.IMPORTED_COPERNICUS: + new_subscription_id = get_product_id_by_name(ProductName.COPERNICUS) + case _: + msg = f"This {old_l3_core_service.l3_core_service_type} is already imported, nothing to do." + raise ProcessFailureError(message=msg, details=old_l3_core_service) + new_subscription = L3CoreService.from_other_product(old_l3_core_service, new_subscription_id) # type: ignore[arg-type] + + return {"subscription": new_subscription} + + +@workflow("Import L3 Core Service", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None)) +def import_l3_core_service() -> StepList: + """Modify an imported subscription into an L3 Core Service subscription to complete the import.""" + return ( + init + >> store_process_subscription(Target.MODIFY) + >> unsync + >> import_l3_core_service_subscription + >> resync + >> done + ) diff --git a/gso/workflows/nren_l3_core_service/migrate_nren_l3_core_service.py b/gso/workflows/l3_core_service/migrate_l3_core_service.py similarity index 78% rename from gso/workflows/nren_l3_core_service/migrate_nren_l3_core_service.py rename to gso/workflows/l3_core_service/migrate_l3_core_service.py index 5d42bccdad3a97ec0f4dc52b2b1d720ab89192e5..d8902530da600dafecb5bca62afb120d48c8d424 100644 --- a/gso/workflows/nren_l3_core_service/migrate_nren_l3_core_service.py +++ b/gso/workflows/l3_core_service/migrate_l3_core_service.py @@ -4,31 +4,29 @@ from typing import Annotated from annotated_types import Len from orchestrator import workflow +from orchestrator.forms import FormPage from orchestrator.targets import Target from orchestrator.workflow import StepList, begin, done, step from orchestrator.workflows.steps import resync, store_process_subscription, unsync from orchestrator.workflows.utils import wrap_modify_initial_input_form from pydantic import AfterValidator, BaseModel, ConfigDict, Field -from pydantic_forms.core import FormPage from pydantic_forms.types import FormGenerator, State, UUIDstr from pydantic_forms.validators import Choice, Divider from gso.products.product_types.edge_port import EdgePort -from gso.products.product_types.nren_l3_core_service import NRENL3CoreService +from gso.products.product_types.l3_core_service import L3CoreService from gso.services.subscriptions import get_active_edge_port_subscriptions from gso.utils.types.tt_number import TTNumber def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: """Gather input from the operator on what new Edge Ports this L3 Core Service should be migrated to.""" - subscription = NRENL3CoreService.from_subscription(subscription_id) + subscription = L3CoreService.from_subscription(subscription_id) partner_id = subscription.customer_id - edge_port_count = len(subscription.nren_l3_core_service.nren_ap_list) + edge_port_count = len(subscription.l3_core_service.ap_list) def _new_edge_port_selector(pid: UUIDstr) -> Choice: - existing_ep_name_list = [ - ap.sbp.edge_port.owner_subscription_id for ap in subscription.nren_l3_core_service.nren_ap_list - ] + existing_ep_name_list = [ap.sbp.edge_port.owner_subscription_id for ap in subscription.l3_core_service.ap_list] edge_port_subscriptions = list( filter( lambda ep: bool(ep["customer_id"] == pid) and ep["subscription_id"] not in existing_ep_name_list, @@ -54,7 +52,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: raise ValueError(msg) return edge_ports - class NRENL3CoreServiceEdgePortSelectionForm(FormPage): + class L3CoreServiceEdgePortSelectionForm(FormPage): model_config = ConfigDict(title=f"Migrating {subscription.product.name} to a new set of Edge Ports") tt_number: TTNumber @@ -70,19 +68,19 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: })", new_edge_port="", ) - for ap in subscription.nren_l3_core_service.nren_ap_list + for ap in subscription.l3_core_service.ap_list ] - ep_user_input = yield NRENL3CoreServiceEdgePortSelectionForm + ep_user_input = yield L3CoreServiceEdgePortSelectionForm return {"subscription_id": subscription_id, "subscription": subscription} | ep_user_input.model_dump() @step("Update subscription model") -def update_subscription_model(subscription: NRENL3CoreService, edge_port_selection: list[dict]) -> State: +def update_subscription_model(subscription: L3CoreService, edge_port_selection: list[dict]) -> State: """Update the subscription model with the new list of Access Ports.""" for index, selected_port in enumerate(edge_port_selection): - subscription.nren_l3_core_service.nren_ap_list[index].sbp.edge_port = EdgePort.from_subscription( + subscription.l3_core_service.ap_list[index].sbp.edge_port = EdgePort.from_subscription( selected_port["new_edge_port"] ).edge_port @@ -90,10 +88,10 @@ def update_subscription_model(subscription: NRENL3CoreService, edge_port_selecti @workflow( - "Migrate NREN L3 Core Service", + "Migrate L3 Core Service", initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), target=Target.MODIFY, ) -def migrate_nren_l3_core_service() -> StepList: - """Migrate a :term:`NREN` L3 Core Service to a new set of Edge Ports.""" +def migrate_l3_core_service() -> StepList: + """Migrate a L3 Core Service to a new set of Edge Ports.""" return begin >> store_process_subscription(Target.MODIFY) >> unsync >> update_subscription_model >> resync >> done diff --git a/gso/workflows/nren_l3_core_service/modify_nren_l3_core_service.py b/gso/workflows/l3_core_service/modify_l3_core_service.py similarity index 74% rename from gso/workflows/nren_l3_core_service/modify_nren_l3_core_service.py rename to gso/workflows/l3_core_service/modify_l3_core_service.py index 4331de2bcabc767d2a53904669fa2e1be7b8d724..3073b6deae63a456b428119157fd0cbd3b7ea6f8 100644 --- a/gso/workflows/nren_l3_core_service/modify_nren_l3_core_service.py +++ b/gso/workflows/l3_core_service/modify_l3_core_service.py @@ -1,4 +1,4 @@ -"""A modification workflow for a :term:`NREN` L3 Core Service subscription.""" +"""A modification workflow for a L3 Core Service subscription.""" from typing import Annotated, Any from uuid import uuid4 @@ -10,23 +10,24 @@ from orchestrator.types import FormGenerator, UUIDstr from orchestrator.workflow import StepList from orchestrator.workflows.steps import resync, store_process_subscription, unsync from orchestrator.workflows.utils import wrap_modify_initial_input_form -from pydantic import AfterValidator, BaseModel, ConfigDict, Field, computed_field +from pydantic import AfterValidator, BaseModel, ConfigDict, Field, NonNegativeInt, computed_field from pydantic_forms.types import State from pydantic_forms.validators import Divider, Label -from gso.products.product_blocks.bgp_session import BGPSession, IPFamily -from gso.products.product_blocks.nren_l3_core_service import NRENAccessPort -from gso.products.product_blocks.service_binding_port import VLAN_ID, ServiceBindingPort +from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPTypes +from gso.products.product_blocks.l3_core_service import AccessPort +from gso.products.product_blocks.service_binding_port import BFDSettings, ServiceBindingPort from gso.products.product_types.edge_port import EdgePort -from gso.products.product_types.nren_l3_core_service import NRENL3CoreService +from gso.products.product_types.l3_core_service import L3CoreService from gso.utils.helpers import active_edge_port_selector from gso.utils.shared_enums import APType, SBPType from gso.utils.types.ip_address import IPv4AddressType, IPV4Netmask, IPv6AddressType, IPV6Netmask +from gso.utils.types.virtual_identifiers import VLAN_ID def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: """Get input about added, removed, and modified Access Ports.""" - subscription = NRENL3CoreService.from_subscription(subscription_id) + subscription = L3CoreService.from_subscription(subscription_id) product_name = subscription.product.name class AccessPortSelection(BaseModel): @@ -48,49 +49,67 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: edge_port=str(access_port.sbp.edge_port.owner_subscription_id), ap_type=access_port.ap_type, ) - for access_port in subscription.nren_l3_core_service.nren_ap_list + for access_port in subscription.l3_core_service.ap_list ] access_port_input = yield ModifyAccessPortsForm input_ap_list = access_port_input.access_ports input_ep_list = [str(ap.edge_port) for ap in input_ap_list] - existing_ep_list = [ - str(ap.sbp.edge_port.owner_subscription_id) for ap in subscription.nren_l3_core_service.nren_ap_list - ] + existing_ep_list = [str(ap.sbp.edge_port.owner_subscription_id) for ap in subscription.l3_core_service.ap_list] - class BaseBGPPeer(BaseModel): + class BFDInputModel(BaseModel): bfd_enabled: bool = False - bfd_interval: int | None = None + bfd_interval_rx: int | None = None + bfd_interval_tx: int | None = None bfd_multiplier: int | None = None + + class IPv4BGPPeer(BaseModel): + peer_address: IPv4AddressType + authentication_key: str | None = None has_custom_policies: bool = False - authentication_key: str + bfd_enabled: bool = False multipath_enabled: bool = False - send_default_route: bool = False + prefix_limit: NonNegativeInt | None = None is_passive: bool = False - - class IPv4BGPPeer(BaseBGPPeer): - peer_address: IPv4AddressType add_v4_multicast: bool = Field(default=False, exclude=True) + send_default_route: bool = False @computed_field # type: ignore[misc] @property def families(self) -> list[IPFamily]: return [IPFamily.V4UNICAST, IPFamily.V4MULTICAST] if self.add_v4_multicast else [IPFamily.V4UNICAST] - class IPv6BGPPeer(BaseBGPPeer): + @computed_field # type: ignore[misc] + @property + def ip_type(self) -> IPTypes: + return IPTypes.IPV4 + + class IPv6BGPPeer(BaseModel): peer_address: IPv6AddressType + authentication_key: str | None = None + has_custom_policies: bool = False + bfd_enabled: bool = False + multipath_enabled: bool = False + prefix_limit: NonNegativeInt | None = None + is_passive: bool = False add_v6_multicast: bool = Field(default=False, exclude=True) + send_default_route: bool = False @computed_field # type: ignore[misc] @property def families(self) -> list[IPFamily]: return [IPFamily.V6UNICAST, IPFamily.V6MULTICAST] if self.add_v6_multicast else [IPFamily.V6UNICAST] + @computed_field # type: ignore[misc] + @property + def ip_type(self) -> IPTypes: + return IPTypes.IPV6 + # There are three possible scenarios for Edge Ports. They can be added, removed, or their relevant SBP can be # modified. removed_ap_list = [ access_port.subscription_instance_id - for access_port in subscription.nren_l3_core_service.nren_ap_list + for access_port in subscription.l3_core_service.ap_list if str(access_port.sbp.edge_port.owner_subscription_id) not in input_ep_list ] modified_ap_list = [ @@ -105,7 +124,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: None, ), ) - for access_port in subscription.nren_l3_core_service.nren_ap_list + for access_port in subscription.l3_core_service.ap_list if str(access_port.sbp.edge_port.owner_subscription_id) in input_ep_list ] added_ap_list = [ @@ -135,7 +154,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: geant_sid: str = current_sbp.geant_sid is_tagged: bool = current_sbp.is_tagged - # The SBP model doesn't require these three fields, but in the case of GÉANT IP OR IAS this will never + # The SBP model doesn't require these five fields, but in the case of GÉANT IP OR IAS this will never # occur since it's a layer 3 service. The ignore statements are there to put our type checker at ease. vlan_id: VLAN_ID = current_sbp.vlan_id # type: ignore[assignment] ipv4_address: IPv4AddressType = current_sbp.ipv4_address # type: ignore[assignment] @@ -143,6 +162,18 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: ipv6_address: IPv6AddressType = current_sbp.ipv6_address # type: ignore[assignment] ipv6_mask: IPV6Netmask = current_sbp.ipv6_mask # type: ignore[assignment] custom_firewall_filters: bool = current_sbp.custom_firewall_filters + v4_bfd_settings: BFDInputModel = BFDInputModel( + bfd_enabled=current_sbp.v4_bfd_settings.bfd_enabled, + bfd_multiplier=current_sbp.v4_bfd_settings.bfd_multiplier, + bfd_interval_rx=current_sbp.v4_bfd_settings.bfd_interval_rx, + bfd_interval_tx=current_sbp.v4_bfd_settings.bfd_interval_tx, + ) + v6_bfd_settings: BFDInputModel = BFDInputModel( + bfd_enabled=current_sbp.v6_bfd_settings.bfd_enabled, + bfd_multiplier=current_sbp.v6_bfd_settings.bfd_multiplier, + bfd_interval_rx=current_sbp.v6_bfd_settings.bfd_interval_rx, + bfd_interval_tx=current_sbp.v6_bfd_settings.bfd_interval_tx, + ) divider: Divider = Field(None, exclude=True) v4_bgp_peer: IPv4BGPPeer = IPv4BGPPeer( **v4_peer.model_dump(exclude=set("families")), @@ -187,6 +218,8 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: ipv4_address: IPv4AddressType ipv6_address: IPv6AddressType custom_firewall_filters: bool = False + v4_bfd_settings: BFDInputModel + v6_bfd_settings: BFDInputModel divider: Divider = Field(None, exclude=True) v4_bgp_peer: IPv4BGPPeer v6_bgp_peer: IPv6BGPPeer @@ -212,11 +245,11 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: @step("Clean up removed Edge Ports") -def remove_old_sbp_blocks(subscription: NRENL3CoreService, removed_access_ports: list[UUIDstr]) -> State: +def remove_old_sbp_blocks(subscription: L3CoreService, removed_access_ports: list[UUIDstr]) -> State: """Remove old :term:`SBP` product blocks from the GÉANT IP subscription.""" - subscription.nren_l3_core_service.nren_ap_list = [ + subscription.l3_core_service.ap_list = [ ap - for ap in subscription.nren_l3_core_service.nren_ap_list + for ap in subscription.l3_core_service.ap_list if str(ap.subscription_instance_id) not in removed_access_ports ] @@ -224,9 +257,9 @@ def remove_old_sbp_blocks(subscription: NRENL3CoreService, removed_access_ports: @step("Modify existing Service Binding Ports") -def modify_existing_sbp_blocks(subscription: NRENL3CoreService, modified_sbp_list: list[dict[str, Any]]) -> State: +def modify_existing_sbp_blocks(subscription: L3CoreService, modified_sbp_list: list[dict[str, Any]]) -> State: """Update the subscription model.""" - for access_port in subscription.nren_l3_core_service.nren_ap_list: + for access_port in subscription.l3_core_service.ap_list: current_sbp = access_port.sbp modified_sbp_data = next( sbp for sbp in modified_sbp_list if sbp["current_sbp_id"] == str(current_sbp.subscription_instance_id) @@ -235,10 +268,14 @@ def modify_existing_sbp_blocks(subscription: NRENL3CoreService, modified_sbp_lis v4_peer = next(peer for peer in current_sbp.bgp_session_list if IPFamily.V4UNICAST in peer.families) for attribute in modified_sbp_data["v4_bgp_peer"]: setattr(v4_peer, attribute, modified_sbp_data["v4_bgp_peer"][attribute]) + for attribute in modified_sbp_data["v4_bfd_settings"]: + setattr(current_sbp.v4_bfd_settings, attribute, modified_sbp_data["v4_bfd_settings"][attribute]) v6_peer = next(peer for peer in current_sbp.bgp_session_list if IPFamily.V6UNICAST in peer.families) for attribute in modified_sbp_data["v6_bgp_peer"]: setattr(v6_peer, attribute, modified_sbp_data["v6_bgp_peer"][attribute]) + for attribute in modified_sbp_data["v6_bfd_settings"]: + setattr(current_sbp.v6_bfd_settings, attribute, modified_sbp_data["v6_bfd_settings"][attribute]) current_sbp.bgp_session_list = [v4_peer, v6_peer] current_sbp.vlan_id = modified_sbp_data["vlan_id"] @@ -253,23 +290,27 @@ def modify_existing_sbp_blocks(subscription: NRENL3CoreService, modified_sbp_lis @step("Instantiate new Service Binding Ports") -def create_new_sbp_blocks(subscription: NRENL3CoreService, added_service_binding_ports: list[dict[str, Any]]) -> State: - """Add new two :term:`SBP` to the :term:`NREN` L3 Core Service subscription.""" +def create_new_sbp_blocks(subscription: L3CoreService, added_service_binding_ports: list[dict[str, Any]]) -> State: + """Add new two :term:`SBP` to the L3 Core Service subscription.""" for sbp_input in added_service_binding_ports: edge_port = EdgePort.from_subscription(sbp_input["edge_port_id"]) bgp_session_list = [ BGPSession.new(subscription_id=uuid4(), **session, rtbh_enabled=True, is_multi_hop=True) for session in sbp_input["bgp_peers"] ] + v4_bfd_settings = BFDSettings.new(subscription_id=uuid4(), **sbp_input.pop("v4_bfd_settings")) + v6_bfd_settings = BFDSettings.new(subscription_id=uuid4(), **sbp_input.pop("v6_bfd_settings")) service_binding_port = ServiceBindingPort.new( subscription_id=uuid4(), **sbp_input, + v4_bfd_settings=v4_bfd_settings, + v6_bfd_settings=v6_bfd_settings, bgp_session_list=bgp_session_list, sbp_type=SBPType.L3, edge_port=edge_port.edge_port, ) - subscription.nren_l3_core_service.nren_ap_list.append( - NRENAccessPort.new( + subscription.l3_core_service.ap_list.append( + AccessPort.new( subscription_id=uuid4(), ap_type=sbp_input["ap_type"], sbp=service_binding_port, @@ -280,11 +321,11 @@ def create_new_sbp_blocks(subscription: NRENL3CoreService, added_service_binding @workflow( - "Modify NREN L3 Core Service", + "Modify L3 Core Service", initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), target=Target.MODIFY, ) -def modify_nren_l3_core_service() -> StepList: +def modify_l3_core_service() -> StepList: """Modify a NRN L3 Core Service subscription.""" access_ports_are_removed = conditional(lambda state: bool(len(state["removed_access_ports"]) > 0)) access_ports_are_modified = conditional(lambda state: bool(len(state["modified_sbp_list"]) > 0)) diff --git a/gso/workflows/nren_l3_core_service/__init__.py b/gso/workflows/nren_l3_core_service/__init__.py deleted file mode 100644 index fb652fbe787e3e976db91a9c0cdefa7c897b61f2..0000000000000000000000000000000000000000 --- a/gso/workflows/nren_l3_core_service/__init__.py +++ /dev/null @@ -1 +0,0 @@ -""":term:`NREN` layer 3 core service workflows.""" diff --git a/gso/workflows/nren_l3_core_service/import_nren_l3_core_service.py b/gso/workflows/nren_l3_core_service/import_nren_l3_core_service.py deleted file mode 100644 index 3951924612f0c885f1076193d75d208b687e4821..0000000000000000000000000000000000000000 --- a/gso/workflows/nren_l3_core_service/import_nren_l3_core_service.py +++ /dev/null @@ -1,45 +0,0 @@ -"""A modification workflow for migrating an ImportedGeantIP to an GeantIP subscription.""" - -from orchestrator.targets import Target -from orchestrator.types import State, UUIDstr -from orchestrator.utils.errors import ProcessFailureError -from orchestrator.workflow import StepList, done, init, 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 import ProductName -from gso.products.product_types.nren_l3_core_service import ( - ImportedNRENL3CoreService, - NRENL3CoreService, - NRENL3CoreServiceType, -) -from gso.services.subscriptions import get_product_id_by_name - - -@step("Create imported subscription") -def import_nren_l3_core_service_subscription(subscription_id: UUIDstr) -> State: - """Take an imported subscription, and turn it into an :term:`NREN` L3 Core Service subscription.""" - old_nren_l3_core_service = ImportedNRENL3CoreService.from_subscription(subscription_id) - if old_nren_l3_core_service.nren_l3_core_service_type == NRENL3CoreServiceType.IMPORTED_GEANT_IP: - new_subscription_id = get_product_id_by_name(ProductName.GEANT_IP) - elif old_nren_l3_core_service.nren_l3_core_service_type == NRENL3CoreServiceType.IMPORTED_IAS: - new_subscription_id = get_product_id_by_name(ProductName.IAS) - else: - msg = f"This {old_nren_l3_core_service.nren_l3_core_service_type} is already imported, nothing to do." - raise ProcessFailureError(message=msg, details=old_nren_l3_core_service) - new_subscription = NRENL3CoreService.from_other_product(old_nren_l3_core_service, new_subscription_id) # type: ignore[arg-type] - - return {"subscription": new_subscription} - - -@workflow("Import NREN L3 Core Service", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None)) -def import_nren_l3_core_service() -> StepList: - """Modify an imported subscription into an :term:`NREN` L3 Core Service subscription to complete the import.""" - return ( - init - >> store_process_subscription(Target.MODIFY) - >> unsync - >> import_nren_l3_core_service_subscription - >> resync - >> done - ) diff --git a/gso/workflows/router/promote_p_to_pe.py b/gso/workflows/router/promote_p_to_pe.py index 68518265637a9f9072006de336c7c17c0ac41b56..16d1ffcc30c7f81fd9b88b07f26f6786c1d094b0 100644 --- a/gso/workflows/router/promote_p_to_pe.py +++ b/gso/workflows/router/promote_p_to_pe.py @@ -18,7 +18,7 @@ from gso.products.product_blocks.router import RouterRole from gso.products.product_types.router import Router from gso.services.lso_client import LSOState, lso_interaction from gso.services.subscriptions import get_all_active_sites -from gso.utils.helpers import generate_inventory_for_active_routers +from gso.utils.helpers import generate_inventory_for_routers from gso.utils.shared_enums import Vendor from gso.utils.types.tt_number import TTNumber from gso.utils.workflow_steps import ( @@ -85,7 +85,7 @@ def deploy_pe_base_config_dry(subscription: dict[str, Any], tt_number: str, proc "subscription": subscription, "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - deploy PE base config", "verb": "deploy_pe_base_config", - "pe_router_list": generate_inventory_for_active_routers(RouterRole.PE)["all"]["hosts"], + "pe_router_list": generate_inventory_for_routers(RouterRole.PE)["all"]["hosts"], "geant_sites": json.loads(json_dumps(get_all_active_sites())), } @@ -104,7 +104,7 @@ def deploy_pe_base_config_real(subscription: dict[str, Any], tt_number: str, pro "subscription": subscription, "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - deploy PE base config", "verb": "deploy_pe_base_config", - "pe_router_list": generate_inventory_for_active_routers(RouterRole.PE)["all"]["hosts"], + "pe_router_list": generate_inventory_for_routers(RouterRole.PE)["all"]["hosts"], "geant_sites": json.loads(json_dumps(get_all_active_sites())), } @@ -142,7 +142,7 @@ def remove_p_from_pe_dry(subscription: dict[str, Any], tt_number: str, process_i return { "playbook_name": "gap_ansible/playbooks/update_ibgp_mesh.yaml", - "inventory": generate_inventory_for_active_routers(RouterRole.PE), + "inventory": generate_inventory_for_routers(RouterRole.PE), "extra_vars": extra_vars, } @@ -160,7 +160,7 @@ def remove_p_from_pe_real(subscription: dict[str, Any], tt_number: str, process_ return { "playbook_name": "gap_ansible/playbooks/update_ibgp_mesh.yaml", - "inventory": generate_inventory_for_active_routers(RouterRole.PE), + "inventory": generate_inventory_for_routers(RouterRole.PE), "extra_vars": extra_vars, } diff --git a/gso/workflows/router/terminate_router.py b/gso/workflows/router/terminate_router.py index 9bc48f8ce9943692f8c475b1595d33e9964645f7..56344fc2076791c1dc0934ec2b6f6e659b261a8e 100644 --- a/gso/workflows/router/terminate_router.py +++ b/gso/workflows/router/terminate_router.py @@ -28,7 +28,7 @@ from gso.services.librenms_client import LibreNMSClient from gso.services.lso_client import LSOState, lso_interaction from gso.services.netbox_client import NetboxClient from gso.settings import load_oss_params -from gso.utils.helpers import generate_inventory_for_active_routers +from gso.utils.helpers import generate_inventory_for_routers from gso.utils.shared_enums import Vendor from gso.utils.types.tt_number import TTNumber @@ -124,7 +124,7 @@ def remove_p_from_all_pe_dry(subscription: Router, tt_number: str, process_id: U return { "playbook_name": "gap_ansible/playbooks/update_ibgp_mesh.yaml", - "inventory": generate_inventory_for_active_routers(RouterRole.PE), + "inventory": generate_inventory_for_routers(RouterRole.PE), "extra_vars": extra_vars, } @@ -142,7 +142,7 @@ def remove_p_from_all_pe_real(subscription: Router, tt_number: str, process_id: return { "playbook_name": "gap_ansible/playbooks/update_ibgp_mesh.yaml", - "inventory": generate_inventory_for_active_routers(RouterRole.PE), + "inventory": generate_inventory_for_routers(RouterRole.PE), "extra_vars": extra_vars, } @@ -160,9 +160,7 @@ def remove_pe_from_all_pe_dry(subscription: Router, tt_number: str, process_id: return { "playbook_name": "gap_ansible/playbooks/update_ibgp_mesh.yaml", - "inventory": generate_inventory_for_active_routers( - RouterRole.PE, exclude_routers=[subscription.router.router_fqdn] - ), + "inventory": generate_inventory_for_routers(RouterRole.PE, exclude_routers=[subscription.router.router_fqdn]), "extra_vars": extra_vars, } @@ -180,9 +178,7 @@ def remove_pe_from_all_pe_real(subscription: Router, tt_number: str, process_id: return { "playbook_name": "gap_ansible/playbooks/update_ibgp_mesh.yaml", - "inventory": generate_inventory_for_active_routers( - RouterRole.PE, exclude_routers=[subscription.router.router_fqdn] - ), + "inventory": generate_inventory_for_routers(RouterRole.PE, exclude_routers=[subscription.router.router_fqdn]), "extra_vars": extra_vars, } @@ -200,7 +196,7 @@ def remove_pe_from_all_p_dry(subscription: Router, tt_number: str, process_id: U return { "playbook_name": "gap_ansible/playbooks/update_ibgp_mesh.yaml", - "inventory": generate_inventory_for_active_routers(RouterRole.P), + "inventory": generate_inventory_for_routers(RouterRole.P), "extra_vars": extra_vars, } @@ -218,7 +214,7 @@ def remove_pe_from_all_p_real(subscription: Router, tt_number: str, process_id: return { "playbook_name": "gap_ansible/playbooks/update_ibgp_mesh.yaml", - "inventory": generate_inventory_for_active_routers(RouterRole.P), + "inventory": generate_inventory_for_routers(RouterRole.P), "extra_vars": extra_vars, } diff --git a/gso/workflows/router/update_ibgp_mesh.py b/gso/workflows/router/update_ibgp_mesh.py index fc071ab8fc657cd2f5fb001a165d84175c0a513b..7adea41cf5815413248c68a7670dc2840bc21734 100644 --- a/gso/workflows/router/update_ibgp_mesh.py +++ b/gso/workflows/router/update_ibgp_mesh.py @@ -17,7 +17,7 @@ from gso.products.product_types.router import Router from gso.services import librenms_client from gso.services.lso_client import LSOState, lso_interaction from gso.services.subscriptions import get_trunks_that_terminate_on_router -from gso.utils.helpers import generate_inventory_for_active_routers +from gso.utils.helpers import generate_inventory_for_routers from gso.utils.shared_enums import SNMPVersion from gso.utils.types.tt_number import TTNumber from gso.utils.workflow_steps import ( @@ -79,7 +79,7 @@ def add_p_to_mesh_dry(subscription: dict[str, Any], tt_number: str, process_id: return { "playbook_name": "gap_ansible/playbooks/update_ibgp_mesh.yaml", - "inventory": generate_inventory_for_active_routers(RouterRole.PE), + "inventory": generate_inventory_for_routers(RouterRole.PE), "extra_vars": extra_vars, } @@ -96,7 +96,7 @@ def add_p_to_mesh_real(subscription: dict[str, Any], tt_number: str, process_id: return { "playbook_name": "gap_ansible/playbooks/update_ibgp_mesh.yaml", - "inventory": generate_inventory_for_active_routers(RouterRole.PE), + "inventory": generate_inventory_for_routers(RouterRole.PE), "extra_vars": extra_vars, } @@ -107,7 +107,7 @@ def add_all_pe_to_p_dry(subscription: dict[str, Any]) -> LSOState: extra_vars = { "dry_run": True, "subscription": subscription, - "pe_router_list": generate_inventory_for_active_routers(RouterRole.PE), + "pe_router_list": generate_inventory_for_routers(RouterRole.PE), "verb": "add_pe_to_p", } @@ -124,7 +124,7 @@ def add_all_pe_to_p_real(subscription: dict[str, Any], tt_number: str, process_i extra_vars = { "dry_run": False, "subscription": subscription, - "pe_router_list": generate_inventory_for_active_routers(RouterRole.PE), + "pe_router_list": generate_inventory_for_routers(RouterRole.PE), "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Update iBGP mesh", "verb": "add_pe_to_p", } diff --git a/gso/workflows/router/validate_router.py b/gso/workflows/router/validate_router.py index 2403c52f6813ad12517ae2ccf2e32b134ced6d03..125db3171ee52f34029bf6d8b7e01c26034b4212 100644 --- a/gso/workflows/router/validate_router.py +++ b/gso/workflows/router/validate_router.py @@ -16,7 +16,7 @@ from gso.services.kentik_client import KentikClient from gso.services.librenms_client import LibreNMSClient from gso.services.lso_client import LSOState, anonymous_lso_interaction from gso.services.netbox_client import NetboxClient -from gso.utils.helpers import generate_inventory_for_active_routers +from gso.utils.helpers import generate_inventory_for_routers from gso.utils.shared_enums import Vendor @@ -52,14 +52,14 @@ def check_netbox_entry_exists(subscription: Router) -> None: client.get_device_by_name(subscription.router.router_fqdn) -@step("Verify BGP configuration on P router") +@step("Verify P BGP P-ONLY neighbors") def verify_p_ibgp(subscription: dict[str, Any]) -> LSOState: - """Perform a dry run of adding the list of all PE routers to the new P router.""" + """Verify PE neighbors in P-ONLY group on a P router.""" extra_vars = { "dry_run": True, "subscription": subscription, - "pe_router_list": generate_inventory_for_active_routers(RouterRole.PE)["all"]["hosts"], - "verb": "verify_p_ibgp", + "pe_router_list": generate_inventory_for_routers(RouterRole.PE)["all"]["hosts"], + "verb": "add_pe_to_p", "is_verification_workflow": "true", } @@ -70,6 +70,60 @@ def verify_p_ibgp(subscription: dict[str, Any]) -> LSOState: } +@step("Verify PE BGP internal mesh neighbors") +def verify_pe_mesh_in_pe(subscription: dict[str, Any]) -> LSOState: + """Verify PE internal mesh neighbors on a PE router.""" + extra_vars = { + "dry_run": True, + "subscription": subscription, + "verb": "add_pe_mesh_to_pe", + "pe_router_list": generate_inventory_for_routers( + router_role=RouterRole.PE, exclude_routers=[subscription["router"]["router_fqdn"]] + )["all"]["hosts"], + "is_verification_workflow": "true", + } + + if not extra_vars["pe_router_list"]: + return { + "playbook_name": "", + "inventory": {"all": {"hosts": {}}}, + "extra_vars": {}, + } + + return { + "playbook_name": "gap_ansible/playbooks/update_ibgp_mesh.yaml", + "inventory": {"all": {"hosts": {subscription["router"]["router_fqdn"]: None}}}, + "extra_vars": extra_vars, + } + + +@step("Verify PE BGP P-ONLY neighbors") +def verify_all_p_in_pe(subscription: dict[str, Any]) -> LSOState: + """Verify P neighbors in P-ONLY group on a PE router.""" + extra_vars = { + "dry_run": True, + "subscription": subscription, + "verb": "add_all_p_to_pe", + "p_router_list": generate_inventory_for_routers( + router_role=RouterRole.P, exclude_routers=[subscription["router"]["router_fqdn"]] + )["all"]["hosts"], + "is_verification_workflow": "true", + } + + if not extra_vars["p_router_list"]: + return { + "playbook_name": "", + "inventory": {"all": {"hosts": {}}}, + "extra_vars": {}, + } + + return { + "playbook_name": "gap_ansible/playbooks/update_ibgp_mesh.yaml", + "inventory": {"all": {"hosts": {subscription["router"]["router_fqdn"]: None}}}, + "extra_vars": extra_vars, + } + + @step("Verify correct LibreNMS entry") def check_librenms_entry_exists(subscription: Router) -> None: """Validate the LibreNMS entry for a Router. @@ -125,6 +179,7 @@ def validate_router() -> StepList: """ is_juniper_router = conditional(lambda state: state["subscription"]["router"]["vendor"] == Vendor.JUNIPER) is_pe_router = conditional(lambda state: state["subscription"]["router"]["router_role"] == RouterRole.PE) + is_p_router = conditional(lambda state: state["subscription"]["router"]["router_role"] == RouterRole.P) return ( begin @@ -137,7 +192,9 @@ def validate_router() -> StepList: >> check_librenms_entry_exists >> is_pe_router(check_kentik_entry_exists) >> anonymous_lso_interaction(verify_base_config) - >> anonymous_lso_interaction(verify_p_ibgp) + >> is_p_router(anonymous_lso_interaction(verify_p_ibgp)) + >> is_pe_router(anonymous_lso_interaction(verify_pe_mesh_in_pe)) + >> is_pe_router(anonymous_lso_interaction(verify_all_p_in_pe)) >> resync >> done ) diff --git a/setup.py b/setup.py index bc1d3bd6177a98ef0ddd71131bbe1b6489297d97..db9de0e1c69a58c958cf8a2b73fbfa3591f07ba9 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import find_packages, setup setup( name="geant-service-orchestrator", - version="2.25", + version="2.26", author="GÉANT Orchestration and Automation Team", author_email="goat@geant.org", description="GÉANT Service Orchestrator", diff --git a/test/cli/test_imports.py b/test/cli/test_imports.py index 62cf1a8d01ed5e0b41dc5f2eb518728f72b546b5..17a1bc13fa3deb71efe359e10c26c82cbb016966 100644 --- a/test/cli/test_imports.py +++ b/test/cli/test_imports.py @@ -7,8 +7,9 @@ import pytest from gso.cli.imports import ( import_edge_port, import_iptrunks, + import_l3_core_service, import_lan_switch_interconnect, - import_nren_l3_core_service, + import_layer_2_circuit_service, import_office_routers, import_opengear, import_routers, @@ -19,12 +20,14 @@ from gso.cli.imports import ( 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.layer_2_circuit import Layer2CircuitType 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.layer_2_circuit import Layer2CircuitServiceType 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.helpers import generate_unique_vc_id, 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 @@ -293,9 +296,9 @@ def edge_port_data(temp_file, faker, router_subscription_factory, partner_factor @pytest.fixture() -def nren_l3_core_service_data(temp_file, faker, partner_factory, edge_port_subscription_factory): - def _nren_l3_core_service_data(**kwargs): - nren_l3_core_service_data = { +def l3_core_service_data(temp_file, faker, partner_factory, edge_port_subscription_factory): + def _l3_core_service_data(**kwargs): + l3_core_service_data = { "partner": partner_factory()["name"], "service_type": "IMPORTED IAS", "service_binding_ports": [ @@ -308,11 +311,21 @@ def nren_l3_core_service_data(temp_file, faker, partner_factory, edge_port_subsc "ipv4_mask": faker.ipv4_netmask(), "ipv6_address": faker.ipv6(), "ipv6_mask": faker.ipv6_netmask(), + "v4_bfd_settings": { + "bfd_enabled": True, + "bfd_interval_rx": faker.pyint(), + "bfd_interval_tx": faker.pyint(), + "bfd_multiplier": faker.pyint(), + }, + "v6_bfd_settings": { + "bfd_enabled": True, + "bfd_interval_rx": faker.pyint(), + "bfd_interval_tx": faker.pyint(), + "bfd_multiplier": faker.pyint(), + }, "bgp_peers": [ { "bfd_enabled": True, - "bfd_interval": faker.pyint(), - "bfd_multiplier": faker.pyint(), "has_custom_policies": True, "authentication_key": faker.password(), "multipath_enabled": False, @@ -325,8 +338,6 @@ def nren_l3_core_service_data(temp_file, faker, partner_factory, edge_port_subsc }, { "bfd_enabled": True, - "bfd_interval": faker.pyint(), - "bfd_multiplier": faker.pyint(), "has_custom_policies": True, "authentication_key": faker.password(), "multipath_enabled": False, @@ -351,8 +362,6 @@ def nren_l3_core_service_data(temp_file, faker, partner_factory, edge_port_subsc "bgp_peers": [ { "bfd_enabled": True, - "bfd_interval": faker.pyint(), - "bfd_multiplier": faker.pyint(), "has_custom_policies": True, "authentication_key": faker.password(), "multipath_enabled": False, @@ -365,8 +374,6 @@ def nren_l3_core_service_data(temp_file, faker, partner_factory, edge_port_subsc }, { "bfd_enabled": True, - "bfd_interval": faker.pyint(), - "bfd_multiplier": faker.pyint(), "has_custom_policies": True, "authentication_key": faker.password(), "multipath_enabled": False, @@ -378,15 +385,57 @@ def nren_l3_core_service_data(temp_file, faker, partner_factory, edge_port_subsc "rtbh_enabled": True, }, ], + "v4_bfd_settings": { + "bfd_enabled": True, + "bfd_interval_rx": faker.pyint(), + "bfd_interval_tx": faker.pyint(), + "bfd_multiplier": faker.pyint(), + }, + "v6_bfd_settings": { + "bfd_enabled": True, + "bfd_interval_rx": faker.pyint(), + "bfd_interval_tx": faker.pyint(), + "bfd_multiplier": faker.pyint(), + }, }, ], } - nren_l3_core_service_data.update(**kwargs) + l3_core_service_data.update(**kwargs) - temp_file.write_text(json.dumps([nren_l3_core_service_data])) - return {"path": str(temp_file), "data": nren_l3_core_service_data} + temp_file.write_text(json.dumps([l3_core_service_data])) + return {"path": str(temp_file), "data": l3_core_service_data} - return _nren_l3_core_service_data + return _l3_core_service_data + + +@pytest.fixture() +def layer_2_circuit_data(temp_file, faker, partner_factory, edge_port_subscription_factory): + def _layer_2_circuit_data(**kwargs): + layer_2_circuit_input_data = { + "partner": partner_factory()["name"], + "service_type": Layer2CircuitServiceType.GEANT_PLUS, + "geant_sid": faker.geant_sid(), + "vc_id": generate_unique_vc_id(), + "layer_2_circuit_side_a": { + "edge_port": edge_port_subscription_factory(), + "vlan_id": faker.vlan_id(), + }, + "layer_2_circuit_side_b": { + "edge_port": edge_port_subscription_factory(), + "vlan_id": faker.vlan_id(), + }, + "layer_2_circuit_type": Layer2CircuitType.TAGGED, + "vlan_range_lower_bound": faker.vlan_id(), + "vlan_range_upper_bound": faker.vlan_id(), + "policer_enabled": False, + } + layer_2_circuit_input_data.update(**kwargs) + + temp_file.write_text(json.dumps([layer_2_circuit_input_data])) + + return {"path": str(temp_file), "data": layer_2_circuit_input_data} + + return _layer_2_circuit_data ########### @@ -621,18 +670,16 @@ def test_import_edge_port_with_invalid_partner(mock_start_process, mock_sleep, e @patch("gso.cli.imports.time.sleep") @patch("gso.cli.imports.start_process") -def test_import_nren_l3_core_service_success(mock_start_process, mock_sleep, nren_l3_core_service_data, capfd): - import_nren_l3_core_service(nren_l3_core_service_data()["path"]) +def test_import_l3_core_service_success(mock_start_process, mock_sleep, l3_core_service_data, capfd): + import_l3_core_service(l3_core_service_data()["path"]) assert mock_start_process.call_count == 1 @patch("gso.cli.imports.time.sleep") @patch("gso.cli.imports.start_process") -def test_import_nren_l3_core_service_with_invalid_partner( - mock_start_process, mock_sleep, nren_l3_core_service_data, capfd -): - broken_data = nren_l3_core_service_data(partner="INVALID") - import_nren_l3_core_service(broken_data["path"]) +def test_import_l3_core_service_with_invalid_partner(mock_start_process, mock_sleep, l3_core_service_data, capfd): + broken_data = l3_core_service_data(partner="INVALID") + import_l3_core_service(broken_data["path"]) captured_output, _ = capfd.readouterr() assert "Partner INVALID not found" in captured_output @@ -641,11 +688,11 @@ def test_import_nren_l3_core_service_with_invalid_partner( @patch("gso.cli.imports.time.sleep") @patch("gso.cli.imports.start_process") -def test_import_nren_l3_core_service_with_invalid_edge_port( - mock_start_process, mock_sleep, faker, nren_l3_core_service_data, edge_port_subscription_factory, capfd +def test_import_l3_core_service_with_invalid_edge_port( + mock_start_process, mock_sleep, faker, l3_core_service_data, edge_port_subscription_factory, capfd ): fake_uuid = faker.uuid4() - broken_data = nren_l3_core_service_data( + broken_data = l3_core_service_data( service_binding_ports=[ { "edge_port": fake_uuid, @@ -674,6 +721,18 @@ def test_import_nren_l3_core_service_with_invalid_edge_port( "rtbh_enabled": True, }, ], + "v4_bfd_settings": { + "bfd_enabled": True, + "bfd_interval_rx": faker.pyint(), + "bfd_interval_tx": faker.pyint(), + "bfd_multiplier": faker.pyint(), + }, + "v6_bfd_settings": { + "bfd_enabled": True, + "bfd_interval_rx": faker.pyint(), + "bfd_interval_tx": faker.pyint(), + "bfd_multiplier": faker.pyint(), + }, }, { "edge_port": edge_port_subscription_factory(), @@ -702,11 +761,44 @@ def test_import_nren_l3_core_service_with_invalid_edge_port( "rtbh_enabled": True, }, ], + "v4_bfd_settings": { + "bfd_enabled": True, + "bfd_interval_rx": faker.pyint(), + "bfd_interval_tx": faker.pyint(), + "bfd_multiplier": faker.pyint(), + }, + "v6_bfd_settings": { + "bfd_enabled": True, + "bfd_interval_rx": faker.pyint(), + "bfd_interval_tx": faker.pyint(), + "bfd_multiplier": faker.pyint(), + }, }, ] ) - import_nren_l3_core_service(broken_data["path"]) + import_l3_core_service(broken_data["path"]) captured_output, _ = capfd.readouterr() assert f"Edge Port {fake_uuid} not found" in captured_output assert mock_start_process.call_count == 0 + + +@patch("gso.cli.imports.time.sleep") +@patch("gso.cli.imports.start_process") +def test_import_layer_2_circuit_success(mock_start_process, mock_sleep, layer_2_circuit_data): + import_layer_2_circuit_service(layer_2_circuit_data()["path"]) + assert mock_start_process.call_count == 1 + assert mock_sleep.call_count == 1 + + +@patch("gso.cli.imports.time.sleep") +@patch("gso.cli.imports.start_process") +def test_import_layer_2_circuit_with_invalid_partner( + mock_start_process, mock_sleep, layer_2_circuit_data, edge_port_subscription_factory, capfd, faker +): + broken_data = layer_2_circuit_data(partner="INVALID") + import_layer_2_circuit_service(broken_data["path"]) + + captured_output, _ = capfd.readouterr() + assert "Partner INVALID not found" in captured_output + assert mock_start_process.call_count == 0 diff --git a/test/conftest.py b/test/conftest.py index 8c8d246c5bd97c16ef7f8db09752c2dca0ffd804..ce44b074025e788e8820ef5a5c8de10b7fb87c8a 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -35,22 +35,7 @@ from urllib3_mock import Responses from gso.main import init_gso_app from gso.services.partners import PartnerSchema, create_partner from gso.utils.types.interfaces import LAGMember, LAGMemberList -from test.fixtures import ( # noqa: F401 - 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, - opengear_subscription_factory, - router_subscription_factory, - service_binding_port_factory, - site_subscription_factory, - super_pop_switch_subscription_factory, - switch_subscription_factory, -) +from test.fixtures import * # noqa: F403 logging.getLogger("faker.factory").setLevel(logging.WARNING) @@ -138,6 +123,11 @@ class FakerProvider(BaseProvider): def vlan_id(self) -> int: return self.generator.random_int(min=1, max=4095) + def bandwidth(self) -> str: + bandwidth_value = self.generator.random_int(1, 1000) + unit = self.generator.random_choices(elements=("K", "M", "G", "T"))[0] + return f"{bandwidth_value}{unit}" + @pytest.fixture(scope="session") def faker() -> Faker: diff --git a/test/fixtures.py b/test/fixtures.py index e2b905c1736879af3087c31d09bc3a605130e43b..da10b0d1a777edfd028878ecb197233aa7e072d5 100644 --- a/test/fixtures.py +++ b/test/fixtures.py @@ -5,9 +5,9 @@ from uuid import uuid4 import pytest from orchestrator import step, workflow from orchestrator.config.assignee import Assignee +from orchestrator.forms import FormPage from orchestrator.types import UUIDstr from orchestrator.workflow import done, init, inputstep -from pydantic_forms.core import FormPage from pydantic_forms.types import FormGenerator from pydantic_forms.validators import Choice diff --git a/test/fixtures/__init__.py b/test/fixtures/__init__.py index 3f183fb2e8778f758d7bba96a459d1df29ac717b..8ecda8a79c736087df8c77019e7814ff72c3b2bf 100644 --- a/test/fixtures/__init__.py +++ b/test/fixtures/__init__.py @@ -1,12 +1,14 @@ 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 ( +from test.fixtures.l3_core_service_fixtures import ( + access_port_factory, + bfd_settings_factory, bgp_session_subscription_factory, - nren_access_port_factory, - nren_l3_core_service_subscription_factory, + l3_core_service_subscription_factory, service_binding_port_factory, ) +from test.fixtures.lan_switch_interconnect_fixtures import lan_switch_interconnect_subscription_factory +from test.fixtures.layer_2_circuit_fixtures import layer_2_circuit_subscription_factory from test.fixtures.office_router_fixtures import office_router_subscription_factory from test.fixtures.opengear_fixtures import opengear_subscription_factory from test.fixtures.router_fixtures import router_subscription_factory @@ -15,13 +17,15 @@ from test.fixtures.super_pop_switch_fixtures import super_pop_switch_subscriptio from test.fixtures.switch_fixtures import switch_subscription_factory __all__ = [ + "access_port_factory", + "bfd_settings_factory", "bgp_session_subscription_factory", "edge_port_subscription_factory", "iptrunk_side_subscription_factory", "iptrunk_subscription_factory", + "l3_core_service_subscription_factory", "lan_switch_interconnect_subscription_factory", - "nren_access_port_factory", - "nren_l3_core_service_subscription_factory", + "layer_2_circuit_subscription_factory", "office_router_subscription_factory", "opengear_subscription_factory", "router_subscription_factory", diff --git a/test/fixtures/iptrunk_fixtures.py b/test/fixtures/iptrunk_fixtures.py index 70ccdeb547d75ae1fc605b9c1f9e9dfb4bafa6ca..5044a2e5904c92023e264e9c5f40cac8a82cb773 100644 --- a/test/fixtures/iptrunk_fixtures.py +++ b/test/fixtures/iptrunk_fixtures.py @@ -25,30 +25,27 @@ def iptrunk_side_subscription_factory(router_subscription_factory, faker): iptrunk_side_ae_members=None, iptrunk_side_ae_members_description=None, ) -> IptrunkSideBlock: - iptrunk_side_node_id = iptrunk_side_node or router_subscription_factory(vendor=Vendor.NOKIA) - iptrunk_side_node = Router.from_subscription(iptrunk_side_node_id).router - iptrunk_side_ae_iface = iptrunk_side_ae_iface or faker.pystr() - iptrunk_side_ae_geant_a_sid = iptrunk_side_ae_geant_a_sid or faker.geant_sid() - iptrunk_side_ae_members = iptrunk_side_ae_members or [ - IptrunkInterfaceBlock.new( - faker.uuid4(), - interface_name=faker.network_interface(), - interface_description=faker.sentence(), - ), - IptrunkInterfaceBlock.new( - faker.uuid4(), - interface_name=faker.network_interface(), - interface_description=faker.sentence(), - ), - ] - return IptrunkSideBlock.new( faker.uuid4(), - iptrunk_side_node=iptrunk_side_node, - iptrunk_side_ae_iface=iptrunk_side_ae_iface, - iptrunk_side_ae_geant_a_sid=iptrunk_side_ae_geant_a_sid, - iptrunk_side_ae_members=iptrunk_side_ae_members, - iptrunk_side_ae_members_description=iptrunk_side_ae_members_description, + iptrunk_side_node=Router.from_subscription( + iptrunk_side_node or router_subscription_factory(vendor=Vendor.NOKIA) + ).router, + iptrunk_side_ae_iface=iptrunk_side_ae_iface or faker.pystr(), + iptrunk_side_ae_geant_a_sid=iptrunk_side_ae_geant_a_sid or faker.geant_sid(), + iptrunk_side_ae_members=iptrunk_side_ae_members + or [ + IptrunkInterfaceBlock.new( + faker.uuid4(), + interface_name=faker.network_interface(), + interface_description=faker.sentence(), + ), + IptrunkInterfaceBlock.new( + faker.uuid4(), + interface_name=faker.network_interface(), + interface_description=faker.sentence(), + ), + ], + iptrunk_side_ae_members_description=iptrunk_side_ae_members_description or faker.sentence(), ) return subscription_create diff --git a/test/fixtures/l3_core_service_fixtures.py b/test/fixtures/l3_core_service_fixtures.py new file mode 100644 index 0000000000000000000000000000000000000000..da94298e8ac6b62507ed61147c80233afed84059 --- /dev/null +++ b/test/fixtures/l3_core_service_fixtures.py @@ -0,0 +1,230 @@ +import random +from uuid import uuid4 + +import pytest +from orchestrator.db import db +from orchestrator.domain import SubscriptionModel +from orchestrator.types import SubscriptionLifecycle, UUIDstr +from pydantic import NonNegativeInt + +from gso.products import ProductName +from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPTypes +from gso.products.product_blocks.l3_core_service import AccessPort +from gso.products.product_blocks.service_binding_port import BFDSettings, ServiceBindingPort +from gso.products.product_types.edge_port import EdgePort +from gso.products.product_types.l3_core_service import ( + ImportedL3CoreService, + L3CoreServiceInactive, + L3CoreServiceType, +) +from gso.services import subscriptions +from gso.utils.shared_enums import APType, SBPType +from gso.utils.types.ip_address import IPAddress + + +@pytest.fixture() +def bfd_settings_factory(faker): + def create_bfd_settings( + bfd_multiplier: int | None = None, + bfd_interval: int | None = None, + *, + bfd_enabled: bool | None = None, + ): + bfd_enabled = bfd_enabled or faker.boolean() + return BFDSettings.new( + subscription_id=uuid4(), + bfd_enabled=bfd_enabled, + bfd_multiplier=bfd_multiplier or (faker.pyint() if bfd_enabled else None), + bfd_interval_rx=bfd_interval or (faker.pyint() if bfd_enabled else None), + bfd_interval_tx=bfd_interval or (faker.pyint() if bfd_enabled else None), + ) + + return create_bfd_settings + + +@pytest.fixture() +def bgp_session_subscription_factory(faker): + def create_bgp_session( + peer_address: IPAddress | None = None, + families: list[IPFamily] | None = None, + authentication_key: str | None = None, + *, + is_multi_hop: bool = False, + has_custom_policies: bool = False, + multipath_enabled: bool | None = True, + prefix_limit: NonNegativeInt | None = None, + send_default_route: bool | None = True, + is_passive: bool | None = False, + rtbh_enabled: bool | None = False, + bfd_enabled: bool | None = False, + ip_type: IPTypes | None = None, + ): + return BGPSession.new( + subscription_id=uuid4(), + peer_address=peer_address or faker.ipv4(), + bfd_enabled=bfd_enabled if bfd_enabled else faker.boolean(), + families=families or [IPFamily.V4UNICAST], + has_custom_policies=has_custom_policies, + authentication_key=authentication_key or faker.password(), + multipath_enabled=multipath_enabled, + prefix_limit=prefix_limit, + send_default_route=send_default_route, + is_multi_hop=is_multi_hop, + rtbh_enabled=rtbh_enabled, + is_passive=is_passive, + ip_type=ip_type if ip_type else faker.random_element(elements=[IPTypes.IPV4, IPTypes.IPV6]), + ) + + return create_bgp_session + + +@pytest.fixture() +def service_binding_port_factory( + faker, bgp_session_subscription_factory, edge_port_subscription_factory, bfd_settings_factory +): + def create_service_binding_port( + bgp_session_list: list[BGPSession] | None = None, + geant_sid: str | None = None, + sbp_type: SBPType = SBPType.L3, + ipv4_address: str | None = None, + ipv4_mask: int | None = None, + ipv6_address: str | None = None, + ipv6_mask: int | None = None, + vlan_id: int | None = None, + edge_port: EdgePort | None = None, + v4_bfd_settings: BFDSettings | None = None, + v6_bfd_settings: BFDSettings | None = None, + *, + custom_firewall_filters: bool = False, + is_tagged: bool = False, + ): + return ServiceBindingPort.new( + subscription_id=uuid4(), + is_tagged=is_tagged, + vlan_id=vlan_id or faker.vlan_id(), + sbp_type=sbp_type, + ipv4_address=ipv4_address or faker.ipv4(), + ipv4_mask=ipv4_mask or faker.ipv4_netmask(), + ipv6_address=ipv6_address or faker.ipv6(), + ipv6_mask=ipv6_mask or faker.ipv6_netmask(), + custom_firewall_filters=custom_firewall_filters, + geant_sid=geant_sid or faker.geant_sid(), + bgp_session_list=bgp_session_list + or [ + bgp_session_subscription_factory(families=[IPFamily.V4UNICAST]), + bgp_session_subscription_factory(families=[IPFamily.V6UNICAST], peer_address=faker.ipv6()), + ], + edge_port=edge_port or EdgePort.from_subscription(edge_port_subscription_factory()).edge_port, + v4_bfd_settings=v4_bfd_settings or bfd_settings_factory(), + v6_bfd_settings=v6_bfd_settings or bfd_settings_factory(), + ) + + return create_service_binding_port + + +@pytest.fixture() +def access_port_factory(faker, service_binding_port_factory): + def create_access_port( + ap_type: APType | None = None, + service_binding_port: ServiceBindingPort | None = None, + ): + return AccessPort.new( + subscription_id=uuid4(), + ap_type=ap_type or random.choice(list(APType)), # noqa: S311 + sbp=service_binding_port or service_binding_port_factory(), + ) + + return create_access_port + + +@pytest.fixture() +def l3_core_service_subscription_factory( + faker, + partner_factory, + access_port_factory, +): + def create_l3_core_service_subscription( + l3_core_service_type: L3CoreServiceType, + description=None, + partner: dict | None = None, + ap_list: list[AccessPort] | None = None, + start_date="2023-05-24T00:00:00+00:00", + status: SubscriptionLifecycle | None = None, + ) -> UUIDstr: + partner = partner or partner_factory() + match l3_core_service_type: + case L3CoreServiceType.GEANT_IP: + product_id = subscriptions.get_product_id_by_name(ProductName.GEANT_IP) + l3_core_service_subscription = L3CoreServiceInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + case L3CoreServiceType.IMPORTED_GEANT_IP: + product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_GEANT_IP) + l3_core_service_subscription = ImportedL3CoreService.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + case L3CoreServiceType.IAS: + product_id = subscriptions.get_product_id_by_name(ProductName.IAS) + l3_core_service_subscription = L3CoreServiceInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + case L3CoreServiceType.IMPORTED_IAS: + product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_IAS) + l3_core_service_subscription = ImportedL3CoreService.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + case L3CoreServiceType.GWS: + product_id = subscriptions.get_product_id_by_name(ProductName.GWS) + l3_core_service_subscription = L3CoreServiceInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + case L3CoreServiceType.IMPORTED_GWS: + product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_GWS) + l3_core_service_subscription = ImportedL3CoreService.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + case L3CoreServiceType.LHCONE: + product_id = subscriptions.get_product_id_by_name(ProductName.LHCONE) + l3_core_service_subscription = L3CoreServiceInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + case L3CoreServiceType.IMPORTED_LHCONE: + product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_LHCONE) + l3_core_service_subscription = ImportedL3CoreService.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + case L3CoreServiceType.COPERNICUS: + product_id = subscriptions.get_product_id_by_name(ProductName.COPERNICUS) + l3_core_service_subscription = L3CoreServiceInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + case L3CoreServiceType.IMPORTED_COPERNICUS: + product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_COPERNICUS) + l3_core_service_subscription = ImportedL3CoreService.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + case _: + msg = f"L3 Core Service type not found: {l3_core_service_type}" + raise ValueError(msg) + + # Default ap_list creation with primary and backup access ports + l3_core_service_subscription.l3_core_service.ap_list = ap_list or [ + access_port_factory(ap_type=APType.PRIMARY), + access_port_factory(ap_type=APType.BACKUP), + ] + + # Update subscription with description, start date, and status + l3_core_service_subscription = SubscriptionModel.from_other_lifecycle( + l3_core_service_subscription, + SubscriptionLifecycle.ACTIVE, + ) + l3_core_service_subscription.description = description or faker.sentence() + l3_core_service_subscription.start_date = start_date + l3_core_service_subscription.status = status or SubscriptionLifecycle.ACTIVE + l3_core_service_subscription.save() + + db.session.commit() + + return str(l3_core_service_subscription.subscription_id) + + return create_l3_core_service_subscription diff --git a/test/fixtures/layer_2_circuit_fixtures.py b/test/fixtures/layer_2_circuit_fixtures.py new file mode 100644 index 0000000000000000000000000000000000000000..28fd88aeb26dfb63d827e1f4741a819633f6af5c --- /dev/null +++ b/test/fixtures/layer_2_circuit_fixtures.py @@ -0,0 +1,124 @@ +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.layer_2_circuit import Layer2CircuitSideBlockInactive, Layer2CircuitType +from gso.products.product_blocks.service_binding_port import ServiceBindingPortInactive +from gso.products.product_types.edge_port import EdgePort +from gso.products.product_types.layer_2_circuit import ( + ImportedLayer2CircuitInactive, + Layer2CircuitInactive, + Layer2CircuitServiceType, +) +from gso.services import subscriptions +from gso.utils.helpers import generate_unique_vc_id +from gso.utils.shared_enums import SBPType +from gso.utils.types.interfaces import BandwidthString +from gso.utils.types.virtual_identifiers import VLAN_ID + + +@pytest.fixture() +def layer_2_circuit_subscription_factory(faker, geant_partner, edge_port_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", + layer_2_circuit_service_type: Layer2CircuitServiceType | None = None, + layer_2_circuit_type: Layer2CircuitType = Layer2CircuitType.TAGGED, + vlan_range_lower_bound: VLAN_ID | None = None, + vlan_range_upper_bound: VLAN_ID | None = None, + policer_bandwidth: BandwidthString | None = None, + policer_burst_rate: BandwidthString | None = None, + layer_2_circuit_side_a_edgeport: UUIDstr | None = None, + vlan_id_side_a: VLAN_ID | None = None, + layer_2_circuit_side_b_edgeport: UUIDstr | None = None, + vlan_id_side_b: VLAN_ID | None = None, + geant_sid: str | None = None, + *, + policer_enabled: bool = False, + ) -> UUIDstr: + # Assign default partner if none provided + if partner is None: + partner = geant_partner + + # Select subscription type based on service type + match layer_2_circuit_service_type: + case Layer2CircuitServiceType.GEANT_PLUS: + product_id = subscriptions.get_product_id_by_name(ProductName.GEANT_PLUS) + subscription = Layer2CircuitInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + case Layer2CircuitServiceType.IMPORTED_GEANT_PLUS: + product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_GEANT_PLUS) + subscription = ImportedLayer2CircuitInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + case Layer2CircuitServiceType.EXPRESSROUTE: + product_id = subscriptions.get_product_id_by_name(ProductName.EXPRESSROUTE) + subscription = Layer2CircuitInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + case Layer2CircuitServiceType.IMPORTED_EXPRESSROUTE: + product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_EXPRESSROUTE) + subscription = ImportedLayer2CircuitInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + case _: + err = ValueError(f"Layer 2 Circuit Service type not found: {layer_2_circuit_service_type}") + raise ValueError(err) + + layer_2_circuit_sides = [] + for edge_port, vlan_id in [ + (layer_2_circuit_side_a_edgeport or edge_port_subscription_factory(), vlan_id_side_a or faker.vlan_id()), + (layer_2_circuit_side_b_edgeport or edge_port_subscription_factory(), vlan_id_side_b or faker.vlan_id()), + ]: + sbp = ServiceBindingPortInactive.new( + uuid4(), + edge_port=EdgePort.from_subscription(edge_port).edge_port, + sbp_type=SBPType.L2, + vlan_id=vlan_id, + geant_sid=geant_sid or faker.geant_sid(), + is_tagged=layer_2_circuit_type == Layer2CircuitType.TAGGED, + custom_firewall_filters=False, + ) + layer_2_circuit_side = Layer2CircuitSideBlockInactive.new(uuid4(), sbp=sbp) + layer_2_circuit_sides.append(layer_2_circuit_side) + + subscription.layer_2_circuit.layer_2_circuit_sides = layer_2_circuit_sides + subscription.layer_2_circuit.virtual_circuit_id = generate_unique_vc_id() + subscription.layer_2_circuit.layer_2_circuit_type = layer_2_circuit_type + if layer_2_circuit_type == Layer2CircuitType.TAGGED: + subscription.layer_2_circuit.vlan_range_lower_bound = vlan_range_lower_bound or faker.vlan_id() + subscription.layer_2_circuit.vlan_range_upper_bound = vlan_range_upper_bound or faker.vlan_id() + else: + subscription.layer_2_circuit.vlan_range_lower_bound = None + subscription.layer_2_circuit.vlan_range_upper_bound = None + + subscription.layer_2_circuit.policer_enabled = policer_enabled + if policer_enabled: + subscription.layer_2_circuit.bandwidth = policer_bandwidth or faker.bandwidth() + subscription.layer_2_circuit.policer_burst_rate = policer_burst_rate or faker.bandwidth() + else: + subscription.layer_2_circuit.bandwidth = None + subscription.layer_2_circuit.policer_burst_rate = None + subscription.description = description or ( + f"{subscription.product.name} - " f"{subscription.layer_2_circuit.virtual_circuit_id}" + ) + + subscription = SubscriptionModel.from_other_lifecycle(subscription, SubscriptionLifecycle.ACTIVE) + subscription.insync = True + 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/nren_l3_core_service_fixtures.py b/test/fixtures/nren_l3_core_service_fixtures.py deleted file mode 100644 index d0fd069a03b4b969e2fd286aaa7f57f5716f10c7..0000000000000000000000000000000000000000 --- a/test/fixtures/nren_l3_core_service_fixtures.py +++ /dev/null @@ -1,173 +0,0 @@ -import random -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.bgp_session import BGPSession, IPFamily -from gso.products.product_blocks.nren_l3_core_service import NRENAccessPort -from gso.products.product_blocks.service_binding_port import ServiceBindingPort -from gso.products.product_types.edge_port import EdgePort -from gso.products.product_types.nren_l3_core_service import ( - ImportedNRENL3CoreService, - NRENL3CoreServiceInactive, - NRENL3CoreServiceType, -) -from gso.services import subscriptions -from gso.utils.shared_enums import APType, SBPType -from gso.utils.types.ip_address import IPAddress - - -@pytest.fixture() -def bgp_session_subscription_factory(faker): - def create_bgp_session( - peer_address: IPAddress | None = None, - bfd_interval: int = 2, - bfd_multiplier: int = 2, - families: list[IPFamily] | None = None, - authentication_key: str | None = None, - *, - is_multi_hop: bool = False, - has_custom_policies: bool = False, - bfd_enabled: bool = True, - multipath_enabled: bool | None = True, - send_default_route: bool | None = True, - is_passive: bool | None = False, - rtbh_enabled: bool | None = False, - ): - return BGPSession.new( - subscription_id=uuid4(), - peer_address=peer_address or faker.ipv4(), - bfd_enabled=bfd_enabled, - families=families or [IPFamily.V4UNICAST], - has_custom_policies=has_custom_policies, - authentication_key=authentication_key or faker.password(), - multipath_enabled=multipath_enabled, - send_default_route=send_default_route, - is_multi_hop=is_multi_hop, - bfd_interval=bfd_interval, - bfd_multiplier=bfd_multiplier, - rtbh_enabled=rtbh_enabled, - is_passive=is_passive, - ) - - return create_bgp_session - - -@pytest.fixture() -def service_binding_port_factory(faker, bgp_session_subscription_factory, edge_port_subscription_factory): - def create_service_binding_port( - bgp_session_list: list[BGPSession] | None = None, - geant_sid: str | None = None, - sbp_type: SBPType = SBPType.L3, - ipv4_address: str | None = None, - ipv4_mask: int | None = None, - ipv6_address: str | None = None, - ipv6_mask: int | None = None, - vlan_id: int | None = None, - edge_port: EdgePort | None = None, - *, - custom_firewall_filters: bool = False, - is_tagged: bool = False, - ): - return ServiceBindingPort.new( - subscription_id=uuid4(), - is_tagged=is_tagged, - vlan_id=vlan_id or faker.vlan_id(), - sbp_type=sbp_type, - ipv4_address=ipv4_address or faker.ipv4(), - ipv4_mask=ipv4_mask or faker.ipv4_netmask(), - ipv6_address=ipv6_address or faker.ipv6(), - ipv6_mask=ipv6_mask or faker.ipv6_netmask(), - custom_firewall_filters=custom_firewall_filters, - geant_sid=geant_sid or faker.geant_sid(), - bgp_session_list=bgp_session_list - or [ - bgp_session_subscription_factory(families=[IPFamily.V4UNICAST]), - bgp_session_subscription_factory(families=[IPFamily.V6UNICAST], peer_address=faker.ipv6()), - ], - edge_port=edge_port or EdgePort.from_subscription(edge_port_subscription_factory()).edge_port, - ) - - return create_service_binding_port - - -@pytest.fixture() -def nren_access_port_factory(faker, service_binding_port_factory): - def create_nren_access_port( - nren_ap_type: APType | None = None, - service_binding_port: ServiceBindingPort | None = None, - ): - return NRENAccessPort.new( - subscription_id=uuid4(), - ap_type=nren_ap_type or random.choice(list(APType)), # noqa: S311 - sbp=service_binding_port or service_binding_port_factory(), - ) - - return create_nren_access_port - - -@pytest.fixture() -def nren_l3_core_service_subscription_factory( - faker, - partner_factory, - nren_access_port_factory, -): - def create_nren_l3_core_service_subscription( - nren_l3_core_service_type: NRENL3CoreServiceType, - description=None, - partner: dict | None = None, - nren_ap_list: list[NRENAccessPort] | None = None, - start_date="2023-05-24T00:00:00+00:00", - status: SubscriptionLifecycle | None = None, - ) -> UUIDstr: - partner = partner or partner_factory() - match nren_l3_core_service_type: - case NRENL3CoreServiceType.GEANT_IP: - product_id = subscriptions.get_product_id_by_name(ProductName.GEANT_IP) - nren_l3_core_service_subscription = NRENL3CoreServiceInactive.from_product_id( - product_id, customer_id=partner["partner_id"], insync=True - ) - case NRENL3CoreServiceType.IMPORTED_GEANT_IP: - product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_GEANT_IP) - nren_l3_core_service_subscription = ImportedNRENL3CoreService.from_product_id( - product_id, customer_id=partner["partner_id"], insync=True - ) - case NRENL3CoreServiceType.IAS: - product_id = subscriptions.get_product_id_by_name(ProductName.IAS) - nren_l3_core_service_subscription = NRENL3CoreServiceInactive.from_product_id( - product_id, customer_id=partner["partner_id"], insync=True - ) - case NRENL3CoreServiceType.IMPORTED_IAS: - product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_IAS) - nren_l3_core_service_subscription = ImportedNRENL3CoreService.from_product_id( - product_id, customer_id=partner["partner_id"], insync=True - ) - case _: - msg = f"NREN L3 Core Service type not found: {nren_l3_core_service_type}" - raise ValueError(msg) - - # Default nren_ap_list creation with primary and backup access ports - nren_l3_core_service_subscription.nren_l3_core_service.nren_ap_list = nren_ap_list or [ - nren_access_port_factory(nren_ap_type=APType.PRIMARY), - nren_access_port_factory(nren_ap_type=APType.BACKUP), - ] - - # Update subscription with description, start date, and status - nren_l3_core_service_subscription = SubscriptionModel.from_other_lifecycle( - nren_l3_core_service_subscription, - SubscriptionLifecycle.ACTIVE, - ) - nren_l3_core_service_subscription.description = description or faker.sentence() - nren_l3_core_service_subscription.start_date = start_date - nren_l3_core_service_subscription.status = status or SubscriptionLifecycle.ACTIVE - nren_l3_core_service_subscription.save() - - db.session.commit() - - return str(nren_l3_core_service_subscription.subscription_id) - - return create_nren_l3_core_service_subscription diff --git a/test/utils/test_helpers.py b/test/utils/test_helpers.py index 880893b628183ff7a131745ca3be7c80164d15fa..e91133780843a15e5b1cba7466f22dbb0700fe99 100644 --- a/test/utils/test_helpers.py +++ b/test/utils/test_helpers.py @@ -8,7 +8,7 @@ from gso.products.product_blocks.router import RouterRole from gso.products.product_types.router import Router from gso.utils.helpers import ( available_interfaces_choices_including_current_members, - generate_inventory_for_active_routers, + generate_inventory_for_routers, ) from gso.utils.shared_enums import Vendor from gso.utils.types.tt_number import validate_tt_number @@ -113,7 +113,7 @@ def test_generate_inventory_for_active_routers_with_single_active_router(router_ } } } - assert generate_inventory_for_active_routers(RouterRole.P) == expected_result + assert generate_inventory_for_routers(RouterRole.P) == expected_result def test_generate_inventory_for_active_routers_with_multiple_routers(router_subscription_factory): @@ -125,9 +125,9 @@ def test_generate_inventory_for_active_routers_with_multiple_routers(router_subs router_subscription_factory(status=SubscriptionLifecycle.TERMINATED) router_subscription_factory(status=SubscriptionLifecycle.INITIAL) # Test the generation of inventory for multiple active P routers. - inventory = generate_inventory_for_active_routers(RouterRole.P) + inventory = generate_inventory_for_routers(RouterRole.P) assert len(inventory["all"]["hosts"]) == 5 - inventory = generate_inventory_for_active_routers(RouterRole.PE) + inventory = generate_inventory_for_routers(RouterRole.PE) assert len(inventory["all"]["hosts"]) == 3 @@ -137,5 +137,5 @@ def test_generate_inventory_for_active_routers_with_excluded_router(router_subsc router_subscription_factory(router_role=RouterRole.P) router = router_subscription_factory(router_role=RouterRole.P) excluded_routers = [Router.from_subscription(router).router.router_fqdn] - inventory = generate_inventory_for_active_routers(RouterRole.P, exclude_routers=excluded_routers) + inventory = generate_inventory_for_routers(RouterRole.P, exclude_routers=excluded_routers) assert len(inventory["all"]["hosts"]) == 5 # 6 P routers, the last one is excluded, so 5 P routers are left. diff --git a/test/workflows/nren_l3_core_service/__init__.py b/test/workflows/l2_circuit/__init__.py similarity index 100% rename from test/workflows/nren_l3_core_service/__init__.py rename to test/workflows/l2_circuit/__init__.py diff --git a/test/workflows/l2_circuit/test_create_imported_layer_2_circuit.py b/test/workflows/l2_circuit/test_create_imported_layer_2_circuit.py new file mode 100644 index 0000000000000000000000000000000000000000..0e4e857ef32e9d002cd175176fa1ba7a463ce318 --- /dev/null +++ b/test/workflows/l2_circuit/test_create_imported_layer_2_circuit.py @@ -0,0 +1,55 @@ +import pytest +from orchestrator.types import SubscriptionLifecycle + +from gso.products.product_blocks.layer_2_circuit import Layer2CircuitType +from gso.products.product_types.layer_2_circuit import Layer2Circuit, Layer2CircuitServiceType +from gso.utils.helpers import generate_unique_vc_id +from test.workflows import assert_complete, extract_state, run_workflow + + +@pytest.mark.parametrize( + "layer_2_service_type", [Layer2CircuitServiceType.GEANT_PLUS, Layer2CircuitServiceType.EXPRESSROUTE] +) +def test_create_imported_layer_2_circuit_success( + faker, partner_factory, edge_port_subscription_factory, layer_2_service_type +): + partner = partner_factory() + edge_port_a = edge_port_subscription_factory(partner=partner) + edge_port_b = edge_port_subscription_factory(partner=partner) + policer_enabled = faker.boolean() + creation_form_input_data = [ + { + "service_type": layer_2_service_type, + "partner": partner["name"], + "layer_2_circuit_type": Layer2CircuitType.TAGGED, + "policer_enabled": policer_enabled, + "vlan_range_lower_bound": faker.vlan_id(), + "vlan_range_upper_bound": faker.vlan_id(), + "vc_id": generate_unique_vc_id(), + "policer_bandwidth": faker.bandwidth() if policer_enabled else None, + "policer_burst_rate": faker.bandwidth() if policer_enabled else None, + "geant_sid": faker.geant_sid(), + "layer_2_circuit_side_a": {"edge_port": edge_port_a, "vlan_id": faker.vlan_id()}, + "layer_2_circuit_side_b": {"edge_port": edge_port_b, "vlan_id": faker.vlan_id()}, + } + ] + + result, _, _ = run_workflow("create_imported_layer_2_circuit", creation_form_input_data) + state = extract_state(result) + assert_complete(result) + subscription = Layer2Circuit.from_subscription(state["subscription_id"]) + assert subscription.status == SubscriptionLifecycle.ACTIVE + assert subscription.layer_2_circuit.layer_2_circuit_type == Layer2CircuitType.TAGGED + assert subscription.layer_2_circuit.virtual_circuit_id == creation_form_input_data[0]["vc_id"] + assert len(subscription.layer_2_circuit.layer_2_circuit_sides) == 2 + assert subscription.layer_2_circuit.layer_2_circuit_sides[0].sbp.is_tagged is True + assert ( + subscription.layer_2_circuit.layer_2_circuit_sides[0].sbp.geant_sid == creation_form_input_data[0]["geant_sid"] + ) + assert ( + str(subscription.layer_2_circuit.layer_2_circuit_sides[0].sbp.edge_port.owner_subscription_id) + == creation_form_input_data[0]["layer_2_circuit_side_a"]["edge_port"] + ) + assert subscription.layer_2_circuit.policer_enabled == policer_enabled + assert subscription.layer_2_circuit.bandwidth == creation_form_input_data[0]["policer_bandwidth"] + assert subscription.layer_2_circuit.policer_burst_rate == creation_form_input_data[0]["policer_burst_rate"] diff --git a/test/workflows/l2_circuit/test_create_layer_2_circuit.py b/test/workflows/l2_circuit/test_create_layer_2_circuit.py new file mode 100644 index 0000000000000000000000000000000000000000..c0d1b2df6aae5014f24e0fe4531a4d52a5007411 --- /dev/null +++ b/test/workflows/l2_circuit/test_create_layer_2_circuit.py @@ -0,0 +1,71 @@ +import pytest +from orchestrator.types import SubscriptionLifecycle + +from gso.products import ProductName +from gso.products.product_blocks.layer_2_circuit import Layer2CircuitType +from gso.products.product_types.layer_2_circuit import Layer2Circuit +from gso.services.subscriptions import get_product_id_by_name +from test.workflows import assert_complete, extract_state, run_workflow + + +@pytest.fixture() +def layer_2_circuit_input(faker, partner_factory, edge_port_subscription_factory, layer_2_circuit_service_type): + partner = partner_factory() + product_id = get_product_id_by_name(layer_2_circuit_service_type) + edge_port_a = edge_port_subscription_factory(partner=partner) + edge_port_b = edge_port_subscription_factory(partner=partner) + policer_enabled = faker.boolean() + return [ + {"product": product_id}, + { + "tt_number": faker.tt_number(), + "partner": partner["partner_id"], + "layer_2_circuit_type": Layer2CircuitType.TAGGED, + "policer_enabled": policer_enabled, + }, + { + "vlan_range_lower_bound": faker.vlan_id(), + "vlan_range_upper_bound": faker.vlan_id(), + "policer_bandwidth": faker.bandwidth() if policer_enabled else None, + "policer_burst_rate": faker.bandwidth() if policer_enabled else None, + "geant_sid": faker.geant_sid(), + "layer_2_circuit_side_a": {"edge_port": edge_port_a, "vlan_id": faker.vlan_id()}, + "layer_2_circuit_side_b": {"edge_port": edge_port_b, "vlan_id": faker.vlan_id()}, + }, + ] + + +@pytest.mark.parametrize("layer_2_circuit_service_type", [ProductName.GEANT_PLUS, ProductName.EXPRESSROUTE]) +@pytest.mark.workflow() +def test_create_layer_2_circuit_success( + layer_2_circuit_service_type, + layer_2_circuit_input, + faker, + partner_factory, + data_config_filename, +): + result, _, _ = run_workflow("create_layer_2_circuit", layer_2_circuit_input) + assert_complete(result) + state = extract_state(result) + subscription = Layer2Circuit.from_subscription(state["subscription_id"]) + assert subscription.status == SubscriptionLifecycle.ACTIVE + assert subscription.layer_2_circuit.virtual_circuit_id is not None + assert len(subscription.layer_2_circuit.layer_2_circuit_sides) == 2 + assert ( + str(subscription.layer_2_circuit.layer_2_circuit_sides[0].sbp.edge_port.owner_subscription_id) + == layer_2_circuit_input[2]["layer_2_circuit_side_a"]["edge_port"] + ) + assert subscription.layer_2_circuit.layer_2_circuit_sides[0].sbp.is_tagged is True + assert subscription.layer_2_circuit.layer_2_circuit_sides[0].sbp.geant_sid == layer_2_circuit_input[2]["geant_sid"] + assert ( + str(subscription.layer_2_circuit.layer_2_circuit_sides[1].sbp.edge_port.owner_subscription_id) + == layer_2_circuit_input[2]["layer_2_circuit_side_b"]["edge_port"] + ) + assert subscription.layer_2_circuit.layer_2_circuit_sides[1].sbp.is_tagged is True + assert subscription.layer_2_circuit.layer_2_circuit_sides[1].sbp.geant_sid == layer_2_circuit_input[2]["geant_sid"] + assert subscription.layer_2_circuit.layer_2_circuit_type == Layer2CircuitType.TAGGED + assert subscription.layer_2_circuit.vlan_range_lower_bound == layer_2_circuit_input[2]["vlan_range_lower_bound"] + assert subscription.layer_2_circuit.vlan_range_upper_bound == layer_2_circuit_input[2]["vlan_range_upper_bound"] + assert subscription.layer_2_circuit.bandwidth == layer_2_circuit_input[2]["policer_bandwidth"] + assert subscription.layer_2_circuit.policer_burst_rate == layer_2_circuit_input[2]["policer_burst_rate"] + assert subscription.layer_2_circuit.policer_enabled == layer_2_circuit_input[1]["policer_enabled"] diff --git a/test/workflows/l2_circuit/test_import_layer_2_circuit.py b/test/workflows/l2_circuit/test_import_layer_2_circuit.py new file mode 100644 index 0000000000000000000000000000000000000000..ba2009a03f68dd63a0ff3b0e89e51bd3ddda55ac --- /dev/null +++ b/test/workflows/l2_circuit/test_import_layer_2_circuit.py @@ -0,0 +1,24 @@ +import pytest +from orchestrator.types import SubscriptionLifecycle + +from gso.products import ProductName +from gso.products.product_types.layer_2_circuit import Layer2Circuit +from test.workflows import assert_complete, run_workflow + + +@pytest.mark.workflow() +@pytest.mark.parametrize( + "layer_2_circuit_service_type", [ProductName.IMPORTED_EXPRESSROUTE, ProductName.IMPORTED_GEANT_PLUS] +) +def test_import_layer_2_circuit_success(layer_2_circuit_service_type, layer_2_circuit_subscription_factory): + imported_layer_2_circuit = layer_2_circuit_subscription_factory( + layer_2_circuit_service_type=layer_2_circuit_service_type + ) + result, _, _ = run_workflow("import_layer_2_circuit", [{"subscription_id": imported_layer_2_circuit}]) + subscription = Layer2Circuit.from_subscription(imported_layer_2_circuit) + + assert_complete(result) + # Remove the "IMPORTED_" prefix with ``[9:]`` + assert subscription.product.name == layer_2_circuit_service_type.value[9:] + assert subscription.status == SubscriptionLifecycle.ACTIVE + assert subscription.insync is True diff --git a/test/workflows/l2_circuit/test_modify_layer_2_circuit.py b/test/workflows/l2_circuit/test_modify_layer_2_circuit.py new file mode 100644 index 0000000000000000000000000000000000000000..3879a0fe085f746990baa4198ebb04ee394750db --- /dev/null +++ b/test/workflows/l2_circuit/test_modify_layer_2_circuit.py @@ -0,0 +1,75 @@ +import pytest +from orchestrator.types import SubscriptionLifecycle + +from gso.products import ProductName +from gso.products.product_blocks.layer_2_circuit import Layer2CircuitType +from gso.products.product_types.layer_2_circuit import Layer2Circuit +from test.workflows import assert_complete, extract_state, run_workflow + + +@pytest.mark.parametrize("layer_2_circuit_service_type", [ProductName.GEANT_PLUS, ProductName.EXPRESSROUTE]) +@pytest.mark.workflow() +def test_modify_layer_2_circuit_change_policer_bandwidth( + layer_2_circuit_service_type, + layer_2_circuit_subscription_factory, + faker, + partner_factory, + data_config_filename, +): + subscription_id = layer_2_circuit_subscription_factory(layer_2_circuit_service_type=layer_2_circuit_service_type) + subscription = Layer2Circuit.from_subscription(subscription_id) + input_form_data = [ + {"subscription_id": subscription_id}, + { + "tt_number": faker.tt_number(), + "layer_2_circuit_type": Layer2CircuitType.TAGGED, + "policer_enabled": False, + }, + { + "vlan_range_lower_bound": subscription.layer_2_circuit.vlan_range_lower_bound, + "vlan_range_upper_bound": subscription.layer_2_circuit.vlan_range_upper_bound, + "policer_bandwidth": None, + "policer_burst_rate": None, + }, + ] + result, _, _ = run_workflow("modify_layer_2_circuit", input_form_data) + assert_complete(result) + assert subscription.status == SubscriptionLifecycle.ACTIVE + assert subscription.layer_2_circuit.policer_enabled is False + assert subscription.layer_2_circuit.bandwidth is None + assert subscription.layer_2_circuit.policer_burst_rate is None + + +@pytest.mark.parametrize("layer_2_circuit_service_type", [ProductName.GEANT_PLUS, ProductName.EXPRESSROUTE]) +@pytest.mark.workflow() +def test_modify_layer_2_circuit_change_circuit_type( + layer_2_circuit_service_type, + layer_2_circuit_subscription_factory, + faker, + partner_factory, + data_config_filename, +): + subscription_id = layer_2_circuit_subscription_factory(layer_2_circuit_service_type=layer_2_circuit_service_type) + subscription = Layer2Circuit.from_subscription(subscription_id) + input_form_data = [ + {"subscription_id": subscription_id}, + { + "tt_number": faker.tt_number(), + "layer_2_circuit_type": Layer2CircuitType.UNTAGGED, + }, + { + "vlan_range_lower_bound": None, + "vlan_range_upper_bound": None, + "policer_bandwidth": subscription.layer_2_circuit.bandwidth, + }, + ] + result, _, _ = run_workflow("modify_layer_2_circuit", input_form_data) + assert_complete(result) + state = extract_state(result) + subscription = Layer2Circuit.from_subscription(state["subscription_id"]) + assert subscription.status == SubscriptionLifecycle.ACTIVE + assert subscription.layer_2_circuit.vlan_range_lower_bound is None + assert subscription.layer_2_circuit.vlan_range_upper_bound is None + assert subscription.layer_2_circuit.layer_2_circuit_type == Layer2CircuitType.UNTAGGED + for layer_2_circuit_side in subscription.layer_2_circuit.layer_2_circuit_sides: + assert layer_2_circuit_side.sbp.is_tagged is False diff --git a/test/workflows/l2_circuit/test_terminate_layer_2_circuit.py b/test/workflows/l2_circuit/test_terminate_layer_2_circuit.py new file mode 100644 index 0000000000000000000000000000000000000000..778582d879d78065921be00b632977af0753ebfa --- /dev/null +++ b/test/workflows/l2_circuit/test_terminate_layer_2_circuit.py @@ -0,0 +1,19 @@ +import pytest + +from gso.products import ProductName +from gso.products.product_types.layer_2_circuit import Layer2Circuit +from test.workflows import assert_complete, extract_state, run_workflow + + +@pytest.mark.workflow() +@pytest.mark.parametrize("layer_2_circuit_service_type", [ProductName.GEANT_PLUS, ProductName.EXPRESSROUTE]) +def test_terminate_layer_2_circuit(layer_2_circuit_service_type, layer_2_circuit_subscription_factory, faker): + subscription_id = layer_2_circuit_subscription_factory(layer_2_circuit_service_type=layer_2_circuit_service_type) + initialt_layer_2_circuit_data = [{"subscription_id": subscription_id}, {"tt_number": faker.tt_number()}] + result, _, _ = run_workflow("terminate_layer_2_circuit", initialt_layer_2_circuit_data) + assert_complete(result) + + state = extract_state(result) + subscription_id = state["subscription_id"] + subscription = Layer2Circuit.from_subscription(subscription_id) + assert subscription.status == "terminated" diff --git a/test/workflows/l3_core_service/__init__.py b/test/workflows/l3_core_service/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/test/workflows/nren_l3_core_service/test_create_imported_nren_l3_core_service.py b/test/workflows/l3_core_service/test_create_imported_l3_core_service.py similarity index 70% rename from test/workflows/nren_l3_core_service/test_create_imported_nren_l3_core_service.py rename to test/workflows/l3_core_service/test_create_imported_l3_core_service.py index 63aab373915f348d3a8e8e7f7bf161c2e1e5c190..626c62378746d7b3625f490325cc7d362a6b4de1 100644 --- a/test/workflows/nren_l3_core_service/test_create_imported_nren_l3_core_service.py +++ b/test/workflows/l3_core_service/test_create_imported_l3_core_service.py @@ -2,13 +2,22 @@ import pytest from orchestrator.types import SubscriptionLifecycle from gso.products.product_blocks.bgp_session import IPFamily -from gso.products.product_types.nren_l3_core_service import ImportedNRENL3CoreService, NRENL3CoreServiceType +from gso.products.product_types.l3_core_service import ImportedL3CoreService, L3CoreServiceType from gso.utils.shared_enums import SBPType from test.workflows import assert_complete, extract_state, run_workflow -@pytest.mark.parametrize("l3_core_service_type", [NRENL3CoreServiceType.GEANT_IP, NRENL3CoreServiceType.IAS]) -def test_create_imported_nren_l3_core_service_success( +@pytest.mark.parametrize( + "l3_core_service_type", + [ + L3CoreServiceType.GEANT_IP, + L3CoreServiceType.IAS, + L3CoreServiceType.GWS, + L3CoreServiceType.LHCONE, + L3CoreServiceType.COPERNICUS, + ], +) +def test_create_imported_l3_core_service_success( faker, partner_factory, edge_port_subscription_factory, l3_core_service_type ): creation_form_input_data = { @@ -27,11 +36,21 @@ def test_create_imported_nren_l3_core_service_success( "ipv6_address": faker.ipv6(), "ipv6_mask": faker.ipv6_netmask(), "custom_firewall_filters": faker.boolean(), + "v4_bfd_settings": { + "bfd_enabled": True, + "bfd_interval_tx": faker.pyint(), + "bfd_interval_rx": faker.pyint(), + "bfd_multiplier": faker.pyint(), + }, + "v6_bfd_settings": { + "bfd_enabled": True, + "bfd_interval_tx": faker.pyint(), + "bfd_interval_rx": faker.pyint(), + "bfd_multiplier": faker.pyint(), + }, "bgp_peers": [ { "bfd_enabled": faker.boolean(), - "bfd_interval": faker.pyint(), - "bfd_multiplier": faker.pyint(), "has_custom_policies": faker.boolean(), "authentication_key": faker.password(), "multipath_enabled": faker.boolean(), @@ -44,8 +63,6 @@ def test_create_imported_nren_l3_core_service_success( }, { "bfd_enabled": faker.boolean(), - "bfd_interval": faker.pyint(), - "bfd_multiplier": faker.pyint(), "has_custom_policies": faker.boolean(), "authentication_key": faker.password(), "multipath_enabled": faker.boolean(), @@ -61,8 +78,8 @@ def test_create_imported_nren_l3_core_service_success( ], } - result, _, _ = run_workflow("create_imported_nren_l3_core_service", [creation_form_input_data]) + result, _, _ = run_workflow("create_imported_l3_core_service", [creation_form_input_data]) state = extract_state(result) - subscription = ImportedNRENL3CoreService.from_subscription(state["subscription_id"]) + subscription = ImportedL3CoreService.from_subscription(state["subscription_id"]) assert_complete(result) assert subscription.status == SubscriptionLifecycle.ACTIVE diff --git a/test/workflows/nren_l3_core_service/test_create_nren_l3_core_service.py b/test/workflows/l3_core_service/test_create_l3_core_service.py similarity index 66% rename from test/workflows/nren_l3_core_service/test_create_nren_l3_core_service.py rename to test/workflows/l3_core_service/test_create_l3_core_service.py index d8eac84e705567a8ef1e5fb0add47457607751f3..149fdf202357a583d52d4010eb7007eb7582413d 100644 --- a/test/workflows/nren_l3_core_service/test_create_nren_l3_core_service.py +++ b/test/workflows/l3_core_service/test_create_l3_core_service.py @@ -4,7 +4,7 @@ import pytest from orchestrator.types import SubscriptionLifecycle from gso.products import ProductName -from gso.products.product_types.nren_l3_core_service import NRENL3CoreService +from gso.products.product_types.l3_core_service import L3CoreService from gso.services.subscriptions import get_product_id_by_name from gso.utils.shared_enums import APType from test.workflows import assert_complete, assert_lso_interaction_success, extract_state, run_workflow @@ -16,8 +16,6 @@ def base_bgp_peer_input(faker): bfd_enabled = faker.boolean() return { "bfd_enabled": bfd_enabled, - "bfd_interval": faker.pyint() if bfd_enabled else None, - "bfd_multiplier": faker.pyint() if bfd_enabled else None, "has_custom_policies": faker.boolean(), "authentication_key": faker.password(), "multipath_enabled": faker.boolean(), @@ -28,10 +26,12 @@ def base_bgp_peer_input(faker): return _base_bgp_peer_input -@pytest.mark.parametrize("l3_core_type", [ProductName.GEANT_IP, ProductName.IAS]) +@pytest.mark.parametrize( + "l3_core_type", [ProductName.GEANT_IP, ProductName.IAS, ProductName.GWS, ProductName.LHCONE, ProductName.COPERNICUS] +) @pytest.mark.workflow() @patch("gso.services.lso_client._send_request") -def test_create_nren_l3_core_service_success( +def test_create_l3_core_service_success( mock_lso_client, l3_core_type, faker, @@ -47,7 +47,7 @@ def test_create_nren_l3_core_service_success( form_input_data = [ {"product": product_id}, {"tt_number": faker.tt_number(), "partner": partner["partner_id"]}, - {"edge_ports": [{"edge_port": edge_port_a, "ap_type": APType.PRIMARY}]}, + {"edge_port": {"edge_port": edge_port_a, "ap_type": APType.PRIMARY}}, { "geant_sid": faker.geant_sid(), "is_tagged": faker.boolean(), @@ -57,24 +57,36 @@ def test_create_nren_l3_core_service_success( "ipv6_address": faker.ipv6(), "ipv6_mask": faker.ipv6_netmask(), "custom_firewall_filters": faker.boolean(), + "v4_bfd_settings": { + "bfd_enabled": True, + "bfd_interval_rx": faker.pyint(), + "bfd_interval_tx": faker.pyint(), + "bfd_multiplier": faker.pyint(), + }, + "v6_bfd_settings": { + "bfd_enabled": True, + "bfd_interval_rx": faker.pyint(), + "bfd_interval_tx": faker.pyint(), + "bfd_multiplier": faker.pyint(), + }, "v4_bgp_peer": base_bgp_peer_input() | {"add_v4_multicast": faker.boolean(), "peer_address": faker.ipv4()}, "v6_bgp_peer": base_bgp_peer_input() | {"add_v6_multicast": faker.boolean(), "peer_address": faker.ipv6()}, }, ] lso_interaction_count = 6 - result, process_stat, step_log = run_workflow("create_nren_l3_core_service", form_input_data) + result, process_stat, step_log = run_workflow("create_l3_core_service", form_input_data) for _ in range(lso_interaction_count): result, step_log = assert_lso_interaction_success(result, process_stat, step_log) assert_complete(result) state = extract_state(result) - subscription = NRENL3CoreService.from_subscription(state["subscription_id"]) + subscription = L3CoreService.from_subscription(state["subscription_id"]) assert mock_lso_client.call_count == lso_interaction_count assert subscription.status == SubscriptionLifecycle.ACTIVE - assert len(subscription.nren_l3_core_service.nren_ap_list) == 1 + assert len(subscription.l3_core_service.ap_list) == 1 assert ( - str(subscription.nren_l3_core_service.nren_ap_list[0].sbp.edge_port.owner_subscription_id) - == form_input_data[2]["edge_ports"][0]["edge_port"] + str(subscription.l3_core_service.ap_list[0].sbp.edge_port.owner_subscription_id) + == form_input_data[2]["edge_port"]["edge_port"] ) diff --git a/test/workflows/l3_core_service/test_import_l3_core_service.py b/test/workflows/l3_core_service/test_import_l3_core_service.py new file mode 100644 index 0000000000000000000000000000000000000000..e3293e6167da1ed02daf053cdde70b4d89d21fe2 --- /dev/null +++ b/test/workflows/l3_core_service/test_import_l3_core_service.py @@ -0,0 +1,27 @@ +import pytest +from orchestrator.types import SubscriptionLifecycle + +from gso.products.product_types.l3_core_service import L3CoreService, L3CoreServiceType +from test.workflows import assert_complete, run_workflow + + +@pytest.mark.parametrize( + "l3_core_service_type", + [ + L3CoreServiceType.IMPORTED_GEANT_IP, + L3CoreServiceType.IMPORTED_IAS, + L3CoreServiceType.IMPORTED_LHCONE, + L3CoreServiceType.IMPORTED_COPERNICUS, + ], +) +@pytest.mark.workflow() +def test_import_l3_core_service_success(l3_core_service_subscription_factory, l3_core_service_type): + imported_l3_core_service = l3_core_service_subscription_factory(l3_core_service_type=l3_core_service_type) + result, _, _ = run_workflow("import_l3_core_service", [{"subscription_id": imported_l3_core_service}]) + subscription = L3CoreService.from_subscription(imported_l3_core_service) + + assert_complete(result) + # Remove the "IMPORTED_" prefix with ``[9:]`` + assert subscription.l3_core_service_type == L3CoreServiceType(l3_core_service_type.value[9:]) + assert subscription.status == SubscriptionLifecycle.ACTIVE + assert subscription.insync is True diff --git a/test/workflows/l3_core_service/test_migrate_l3_core_service.py b/test/workflows/l3_core_service/test_migrate_l3_core_service.py new file mode 100644 index 0000000000000000000000000000000000000000..6109dcca65f26ecec4d29624a4ba4ebe22370323 --- /dev/null +++ b/test/workflows/l3_core_service/test_migrate_l3_core_service.py @@ -0,0 +1,56 @@ +import pytest + +from gso.products.product_types.l3_core_service import L3CoreService, L3CoreServiceType +from test.workflows import assert_complete, extract_state, run_workflow + + +@pytest.mark.parametrize( + "l3_core_service_type", + [ + L3CoreServiceType.GEANT_IP, + L3CoreServiceType.IAS, + L3CoreServiceType.GWS, + L3CoreServiceType.LHCONE, + L3CoreServiceType.COPERNICUS, + ], +) +@pytest.mark.workflow() +def test_migrate_l3_core_service_success( + faker, + edge_port_subscription_factory, + partner_factory, + l3_core_service_subscription_factory, + l3_core_service_type, +): + partner = partner_factory() + subscription_id = l3_core_service_subscription_factory(partner=partner, l3_core_service_type=l3_core_service_type) + new_edge_port_1 = edge_port_subscription_factory(partner=partner) + new_edge_port_2 = edge_port_subscription_factory(partner=partner) + subscription = L3CoreService.from_subscription(subscription_id) + + form_input_data = [ + {"subscription_id": subscription_id}, + { + "tt_number": faker.tt_number(), + "edge_port_selection": [ + { + "old_edge_port": subscription.l3_core_service.ap_list[0].sbp.edge_port.description, + "new_edge_port": new_edge_port_1, + }, + { + "old_edge_port": subscription.l3_core_service.ap_list[1].sbp.edge_port.description, + "new_edge_port": new_edge_port_2, + }, + ], + }, + ] + + result, _, _ = run_workflow("migrate_l3_core_service", form_input_data) + + assert_complete(result) + state = extract_state(result) + subscription = L3CoreService.from_subscription(state["subscription_id"]) + assert subscription.insync is True + assert len(subscription.l3_core_service.ap_list) == 2 + assert str(subscription.l3_core_service.ap_list[0].sbp.edge_port.owner_subscription_id) == new_edge_port_1 + assert str(subscription.l3_core_service.ap_list[1].sbp.edge_port.owner_subscription_id) == new_edge_port_2 diff --git a/test/workflows/l3_core_service/test_modify_l3_core_service.py b/test/workflows/l3_core_service/test_modify_l3_core_service.py new file mode 100644 index 0000000000000000000000000000000000000000..67ff888097afbed5db3403a6ca926cdd00dda54e --- /dev/null +++ b/test/workflows/l3_core_service/test_modify_l3_core_service.py @@ -0,0 +1,307 @@ +import pytest + +from gso.products.product_blocks.bgp_session import IPFamily +from gso.products.product_types.l3_core_service import L3CoreService, L3CoreServiceType +from gso.utils.shared_enums import APType +from test.workflows import extract_state, run_workflow + + +@pytest.mark.parametrize( + "l3_core_service_type", + [ + L3CoreServiceType.GEANT_IP, + L3CoreServiceType.IAS, + L3CoreServiceType.GWS, + L3CoreServiceType.LHCONE, + L3CoreServiceType.COPERNICUS, + ], +) +@pytest.mark.workflow() +def test_modify_l3_core_service_remove_edge_port_success(l3_core_service_subscription_factory, l3_core_service_type): + subscription_id = l3_core_service_subscription_factory(l3_core_service_type=l3_core_service_type) + subscription = L3CoreService.from_subscription(subscription_id) + access_port = subscription.l3_core_service.ap_list[0] + input_form_data = [ + {"subscription_id": subscription_id}, + { + "access_ports": [ + { + "edge_port": str(access_port.sbp.edge_port.owner_subscription_id), + "ap_type": APType.LOAD_BALANCED, + } + ] # The factory generates a subscription with two Access Ports, this will remove the second one. + }, + {}, + ] + + result, _, _ = run_workflow("modify_l3_core_service", input_form_data) + + state = extract_state(result) + subscription = L3CoreService.from_subscription(state["subscription_id"]) + assert len(subscription.l3_core_service.ap_list) == 1 + assert subscription.l3_core_service.ap_list[0].ap_type == APType.LOAD_BALANCED + + +@pytest.mark.parametrize( + "l3_core_service_type", + [ + L3CoreServiceType.GEANT_IP, + L3CoreServiceType.IAS, + L3CoreServiceType.GWS, + L3CoreServiceType.LHCONE, + L3CoreServiceType.COPERNICUS, + ], +) +@pytest.mark.workflow() +def test_modify_l3_core_service_add_new_edge_port_success( + l3_core_service_subscription_factory, + edge_port_subscription_factory, + partner_factory, + faker, + l3_core_service_type, +): + partner = partner_factory() + new_edge_port = edge_port_subscription_factory(partner=partner) + subscription_id = l3_core_service_subscription_factory(partner=partner, l3_core_service_type=l3_core_service_type) + subscription = L3CoreService.from_subscription(subscription_id) + input_form_data = [ + {"subscription_id": subscription_id}, + { + "access_ports": [ + { + "edge_port": str(port.sbp.edge_port.owner_subscription_id), + "ap_type": port.ap_type, + } + for port in subscription.l3_core_service.ap_list + ] + + [ + { + "edge_port": str(new_edge_port), + "ap_type": APType.BACKUP, + } + ] + }, + {}, # The existing SBPs are unchanged + {}, + { # Adding configuration for the new SBP + "geant_sid": faker.geant_sid(), + "vlan_id": faker.vlan_id(), + "ipv4_address": faker.ipv4(), + "ipv6_address": faker.ipv6(), + "v4_bgp_peer": { + "authentication_key": faker.password(), + "peer_address": faker.ipv4(), + "bfd_enabled": False, + "prefix_limit": 1000, + }, + "v6_bgp_peer": { + "authentication_key": faker.password(), + "peer_address": faker.ipv6(), + "bfd_enabled": False, + }, + "v4_bfd_settings": {"bfd_enabled": False}, + "v6_bfd_settings": {"bfd_enabled": False}, + }, + ] + + result, _, _ = run_workflow("modify_l3_core_service", input_form_data) + + state = extract_state(result) + subscription = L3CoreService.from_subscription(state["subscription_id"]) + assert len(subscription.l3_core_service.ap_list) == 3 + + +@pytest.fixture() +def sbp_input_form_data(faker): + def _generate_form_data(): + return { + "geant_sid": faker.geant_sid(), + "is_tagged": True, + "vlan_id": faker.vlan_id(), + "ipv4_address": faker.ipv4(), + "ipv6_address": faker.ipv6(), + "custom_firewall_filters": True, + "v4_bfd_settings": { + "bfd_enabled": True, + "bfd_interval_rx": faker.pyint(), + "bfd_interval_tx": faker.pyint(), + "bfd_multiplier": faker.pyint(), + }, + "v6_bfd_settings": { + "bfd_enabled": True, + "bfd_interval_rx": faker.pyint(), + "bfd_interval_tx": faker.pyint(), + "bfd_multiplier": faker.pyint(), + }, + "v4_bgp_peer": { + "bfd_enabled": True, + "has_custom_policies": True, + "authentication_key": faker.password(), + "multipath_enabled": True, + "send_default_route": True, + "is_passive": True, + "peer_address": faker.ipv4(), + "add_v4_multicast": True, + }, + "v6_bgp_peer": { + "bfd_enabled": True, + "has_custom_policies": True, + "authentication_key": faker.password(), + "multipath_enabled": True, + "send_default_route": True, + "is_passive": True, + "peer_address": faker.ipv6(), + "add_v6_multicast": True, + "prefix_limit": 3000, + }, + } + + return _generate_form_data + + +@pytest.mark.parametrize( + "l3_core_service_type", + [ + L3CoreServiceType.GEANT_IP, + L3CoreServiceType.IAS, + L3CoreServiceType.GWS, + L3CoreServiceType.LHCONE, + L3CoreServiceType.COPERNICUS, + ], +) +@pytest.mark.workflow() +def test_modify_l3_core_service_modify_edge_port_success( + faker, l3_core_service_subscription_factory, l3_core_service_type, sbp_input_form_data +): + subscription_id = l3_core_service_subscription_factory(l3_core_service_type=l3_core_service_type) + subscription = L3CoreService.from_subscription(subscription_id) + new_sbp_data = [sbp_input_form_data(), sbp_input_form_data()] + input_form_data = [ + {"subscription_id": subscription_id}, + { + "access_ports": [ + { + "edge_port": str(port.sbp.edge_port.owner_subscription_id), + "ap_type": port.ap_type, + } + for port in subscription.l3_core_service.ap_list + ] + }, + {**new_sbp_data[0]}, + {**new_sbp_data[1]}, + ] + + result, _, _ = run_workflow("modify_l3_core_service", input_form_data) + + state = extract_state(result) + subscription = L3CoreService.from_subscription(state["subscription_id"]) + assert len(subscription.l3_core_service.ap_list) == 2 + + for i in range(2): + assert subscription.l3_core_service.ap_list[i].sbp.geant_sid == new_sbp_data[i]["geant_sid"] + assert subscription.l3_core_service.ap_list[i].sbp.is_tagged == new_sbp_data[i]["is_tagged"] + assert subscription.l3_core_service.ap_list[i].sbp.vlan_id == new_sbp_data[i]["vlan_id"] + assert str(subscription.l3_core_service.ap_list[i].sbp.ipv4_address) == new_sbp_data[i]["ipv4_address"] + assert str(subscription.l3_core_service.ap_list[i].sbp.ipv6_address) == new_sbp_data[i]["ipv6_address"] + assert ( + subscription.l3_core_service.ap_list[i].sbp.custom_firewall_filters + == new_sbp_data[i]["custom_firewall_filters"] + ) + + assert ( + subscription.l3_core_service.ap_list[i].sbp.bgp_session_list[0].bfd_enabled + == new_sbp_data[i]["v4_bgp_peer"]["bfd_enabled"] + ) + assert ( + subscription.l3_core_service.ap_list[i].sbp.bgp_session_list[0].has_custom_policies + == new_sbp_data[i]["v4_bgp_peer"]["has_custom_policies"] + ) + assert ( + subscription.l3_core_service.ap_list[i].sbp.bgp_session_list[0].authentication_key + == new_sbp_data[i]["v4_bgp_peer"]["authentication_key"] + ) + assert ( + subscription.l3_core_service.ap_list[i].sbp.bgp_session_list[0].multipath_enabled + == new_sbp_data[i]["v4_bgp_peer"]["multipath_enabled"] + ) + assert ( + subscription.l3_core_service.ap_list[i].sbp.bgp_session_list[0].send_default_route + == new_sbp_data[i]["v4_bgp_peer"]["send_default_route"] + ) + assert ( + subscription.l3_core_service.ap_list[i].sbp.bgp_session_list[0].is_passive + == new_sbp_data[i]["v4_bgp_peer"]["is_passive"] + ) + assert ( + str(subscription.l3_core_service.ap_list[i].sbp.bgp_session_list[0].peer_address) + == new_sbp_data[i]["v4_bgp_peer"]["peer_address"] + ) + assert ( + bool(IPFamily.V4MULTICAST in subscription.l3_core_service.ap_list[i].sbp.bgp_session_list[0].families) + == new_sbp_data[i]["v4_bgp_peer"]["add_v4_multicast"] + ) + + assert ( + subscription.l3_core_service.ap_list[i].sbp.bgp_session_list[1].bfd_enabled + == new_sbp_data[i]["v6_bgp_peer"]["bfd_enabled"] + ) + assert ( + subscription.l3_core_service.ap_list[i].sbp.bgp_session_list[1].has_custom_policies + == new_sbp_data[i]["v6_bgp_peer"]["has_custom_policies"] + ) + assert ( + subscription.l3_core_service.ap_list[i].sbp.bgp_session_list[1].authentication_key + == new_sbp_data[i]["v6_bgp_peer"]["authentication_key"] + ) + assert ( + subscription.l3_core_service.ap_list[i].sbp.bgp_session_list[1].multipath_enabled + == new_sbp_data[i]["v6_bgp_peer"]["multipath_enabled"] + ) + assert ( + subscription.l3_core_service.ap_list[i].sbp.bgp_session_list[1].send_default_route + == new_sbp_data[i]["v6_bgp_peer"]["send_default_route"] + ) + assert ( + subscription.l3_core_service.ap_list[i].sbp.bgp_session_list[1].is_passive + == new_sbp_data[i]["v6_bgp_peer"]["is_passive"] + ) + assert ( + str(subscription.l3_core_service.ap_list[i].sbp.bgp_session_list[1].peer_address) + == new_sbp_data[i]["v6_bgp_peer"]["peer_address"] + ) + assert ( + bool(IPFamily.V6MULTICAST in subscription.l3_core_service.ap_list[i].sbp.bgp_session_list[1].families) + == new_sbp_data[i]["v6_bgp_peer"]["add_v6_multicast"] + ) + assert ( + subscription.l3_core_service.ap_list[i].sbp.v4_bfd_settings.bfd_enabled + == new_sbp_data[i]["v4_bfd_settings"]["bfd_enabled"] + ) + assert ( + subscription.l3_core_service.ap_list[i].sbp.v4_bfd_settings.bfd_interval_rx + == new_sbp_data[i]["v4_bfd_settings"]["bfd_interval_rx"] + ) + assert ( + subscription.l3_core_service.ap_list[i].sbp.v4_bfd_settings.bfd_interval_tx + == new_sbp_data[i]["v4_bfd_settings"]["bfd_interval_tx"] + ) + assert ( + subscription.l3_core_service.ap_list[i].sbp.v4_bfd_settings.bfd_multiplier + == new_sbp_data[i]["v4_bfd_settings"]["bfd_multiplier"] + ) + assert ( + subscription.l3_core_service.ap_list[i].sbp.v6_bfd_settings.bfd_enabled + == new_sbp_data[i]["v6_bfd_settings"]["bfd_enabled"] + ) + assert ( + subscription.l3_core_service.ap_list[i].sbp.v6_bfd_settings.bfd_interval_rx + == new_sbp_data[i]["v6_bfd_settings"]["bfd_interval_rx"] + ) + assert ( + subscription.l3_core_service.ap_list[i].sbp.v6_bfd_settings.bfd_interval_tx + == new_sbp_data[i]["v6_bfd_settings"]["bfd_interval_tx"] + ) + assert ( + subscription.l3_core_service.ap_list[i].sbp.v6_bfd_settings.bfd_multiplier + == new_sbp_data[i]["v6_bfd_settings"]["bfd_multiplier"] + ) diff --git a/test/workflows/nren_l3_core_service/test_import_nren_l3_core_service.py b/test/workflows/nren_l3_core_service/test_import_nren_l3_core_service.py deleted file mode 100644 index 13095de6ead5ad679d2ccb9ceb1106d986a79470..0000000000000000000000000000000000000000 --- a/test/workflows/nren_l3_core_service/test_import_nren_l3_core_service.py +++ /dev/null @@ -1,23 +0,0 @@ -import pytest -from orchestrator.types import SubscriptionLifecycle - -from gso.products.product_types.nren_l3_core_service import NRENL3CoreService, NRENL3CoreServiceType -from test.workflows import assert_complete, run_workflow - - -@pytest.mark.parametrize( - "l3_core_service_type", [NRENL3CoreServiceType.IMPORTED_GEANT_IP, NRENL3CoreServiceType.IMPORTED_IAS] -) -@pytest.mark.workflow() -def test_import_nren_l3_core_service_success(nren_l3_core_service_subscription_factory, l3_core_service_type): - imported_nren_l3_core_service = nren_l3_core_service_subscription_factory( - nren_l3_core_service_type=l3_core_service_type - ) - result, _, _ = run_workflow("import_nren_l3_core_service", [{"subscription_id": imported_nren_l3_core_service}]) - subscription = NRENL3CoreService.from_subscription(imported_nren_l3_core_service) - - assert_complete(result) - # Remove the "IMPORTED_" prefix with ``[9:]`` - assert subscription.nren_l3_core_service_type == NRENL3CoreServiceType(l3_core_service_type.value[9:]) - assert subscription.status == SubscriptionLifecycle.ACTIVE - assert subscription.insync is True diff --git a/test/workflows/nren_l3_core_service/test_migrate_nren_l3_core_service.py b/test/workflows/nren_l3_core_service/test_migrate_nren_l3_core_service.py deleted file mode 100644 index a4ad8a9f95a0c834536fae3e76478b65e35264cc..0000000000000000000000000000000000000000 --- a/test/workflows/nren_l3_core_service/test_migrate_nren_l3_core_service.py +++ /dev/null @@ -1,49 +0,0 @@ -import pytest - -from gso.products.product_types.nren_l3_core_service import NRENL3CoreService, NRENL3CoreServiceType -from test.workflows import assert_complete, extract_state, run_workflow - - -@pytest.mark.parametrize("l3_core_service_type", [NRENL3CoreServiceType.GEANT_IP, NRENL3CoreServiceType.IAS]) -@pytest.mark.workflow() -def test_migrate_nren_l3_core_service_success( - faker, - edge_port_subscription_factory, - partner_factory, - nren_l3_core_service_subscription_factory, - l3_core_service_type, -): - partner = partner_factory() - subscription_id = nren_l3_core_service_subscription_factory( - partner=partner, nren_l3_core_service_type=l3_core_service_type - ) - new_edge_port_1 = edge_port_subscription_factory(partner=partner) - new_edge_port_2 = edge_port_subscription_factory(partner=partner) - subscription = NRENL3CoreService.from_subscription(subscription_id) - - form_input_data = [ - {"subscription_id": subscription_id}, - { - "tt_number": faker.tt_number(), - "edge_port_selection": [ - { - "old_edge_port": subscription.nren_l3_core_service.nren_ap_list[0].sbp.edge_port.description, - "new_edge_port": new_edge_port_1, - }, - { - "old_edge_port": subscription.nren_l3_core_service.nren_ap_list[1].sbp.edge_port.description, - "new_edge_port": new_edge_port_2, - }, - ], - }, - ] - - result, _, _ = run_workflow("migrate_nren_l3_core_service", form_input_data) - - assert_complete(result) - state = extract_state(result) - subscription = NRENL3CoreService.from_subscription(state["subscription_id"]) - assert subscription.insync is True - assert len(subscription.nren_l3_core_service.nren_ap_list) == 2 - assert str(subscription.nren_l3_core_service.nren_ap_list[0].sbp.edge_port.owner_subscription_id) == new_edge_port_1 - assert str(subscription.nren_l3_core_service.nren_ap_list[1].sbp.edge_port.owner_subscription_id) == new_edge_port_2 diff --git a/test/workflows/nren_l3_core_service/test_modify_nren_l3_core_service.py b/test/workflows/nren_l3_core_service/test_modify_nren_l3_core_service.py deleted file mode 100644 index 197be36c55baf085b5d624bd9bcee8946c7c157e..0000000000000000000000000000000000000000 --- a/test/workflows/nren_l3_core_service/test_modify_nren_l3_core_service.py +++ /dev/null @@ -1,264 +0,0 @@ -import pytest - -from gso.products.product_blocks.bgp_session import IPFamily -from gso.products.product_types.nren_l3_core_service import NRENL3CoreService, NRENL3CoreServiceType -from gso.utils.shared_enums import APType -from test.workflows import extract_state, run_workflow - - -@pytest.mark.parametrize("l3_core_service_type", [NRENL3CoreServiceType.GEANT_IP, NRENL3CoreServiceType.IAS]) -@pytest.mark.workflow() -def test_modify_nren_l3_core_service_remove_edge_port_success( - nren_l3_core_service_subscription_factory, l3_core_service_type -): - subscription_id = nren_l3_core_service_subscription_factory(nren_l3_core_service_type=l3_core_service_type) - subscription = NRENL3CoreService.from_subscription(subscription_id) - access_port = subscription.nren_l3_core_service.nren_ap_list[0] - input_form_data = [ - {"subscription_id": subscription_id}, - { - "access_ports": [ - { - "edge_port": str(access_port.sbp.edge_port.owner_subscription_id), - "ap_type": APType.LOAD_BALANCED, - } - ] # The factory generates a subscription with two Access Ports, this will remove the second one. - }, - {}, - ] - - result, _, _ = run_workflow("modify_nren_l3_core_service", input_form_data) - - state = extract_state(result) - subscription = NRENL3CoreService.from_subscription(state["subscription_id"]) - assert len(subscription.nren_l3_core_service.nren_ap_list) == 1 - assert subscription.nren_l3_core_service.nren_ap_list[0].ap_type == APType.LOAD_BALANCED - - -@pytest.mark.parametrize("l3_core_service_type", [NRENL3CoreServiceType.GEANT_IP, NRENL3CoreServiceType.IAS]) -@pytest.mark.workflow() -def test_modify_nren_l3_core_service_add_new_edge_port_success( - nren_l3_core_service_subscription_factory, - edge_port_subscription_factory, - partner_factory, - faker, - l3_core_service_type, -): - partner = partner_factory() - new_edge_port = edge_port_subscription_factory(partner=partner) - subscription_id = nren_l3_core_service_subscription_factory( - partner=partner, nren_l3_core_service_type=l3_core_service_type - ) - subscription = NRENL3CoreService.from_subscription(subscription_id) - input_form_data = [ - {"subscription_id": subscription_id}, - { - "access_ports": [ - { - "edge_port": str(port.sbp.edge_port.owner_subscription_id), - "ap_type": port.ap_type, - } - for port in subscription.nren_l3_core_service.nren_ap_list - ] - + [ - { - "edge_port": str(new_edge_port), - "ap_type": APType.BACKUP, - } - ] - }, - {}, # The existing SBPs are unchanged - {}, - { # Adding configuration for the new SBP - "geant_sid": faker.geant_sid(), - "vlan_id": faker.vlan_id(), - "ipv4_address": faker.ipv4(), - "ipv6_address": faker.ipv6(), - "v4_bgp_peer": { - "authentication_key": faker.password(), - "peer_address": faker.ipv4(), - }, - "v6_bgp_peer": { - "authentication_key": faker.password(), - "peer_address": faker.ipv6(), - }, - }, - ] - - result, _, _ = run_workflow("modify_nren_l3_core_service", input_form_data) - - state = extract_state(result) - subscription = NRENL3CoreService.from_subscription(state["subscription_id"]) - assert len(subscription.nren_l3_core_service.nren_ap_list) == 3 - - -@pytest.fixture() -def sbp_input_form_data(faker): - def _generate_form_data(): - return { - "geant_sid": faker.geant_sid(), - "is_tagged": True, - "vlan_id": faker.vlan_id(), - "ipv4_address": faker.ipv4(), - "ipv6_address": faker.ipv6(), - "custom_firewall_filters": True, - "v4_bgp_peer": { - "bfd_enabled": True, - "bfd_interval": faker.pyint(), - "bfd_multiplier": faker.pyint(), - "has_custom_policies": True, - "authentication_key": faker.password(), - "multipath_enabled": True, - "send_default_route": True, - "is_passive": True, - "peer_address": faker.ipv4(), - "add_v4_multicast": True, - }, - "v6_bgp_peer": { - "bfd_enabled": True, - "bfd_interval": faker.pyint(), - "bfd_multiplier": faker.pyint(), - "has_custom_policies": True, - "authentication_key": faker.password(), - "multipath_enabled": True, - "send_default_route": True, - "is_passive": True, - "peer_address": faker.ipv6(), - "add_v6_multicast": True, - }, - } - - return _generate_form_data - - -@pytest.mark.parametrize("l3_core_service_type", [NRENL3CoreServiceType.GEANT_IP, NRENL3CoreServiceType.IAS]) -@pytest.mark.workflow() -def test_modify_nren_l3_core_service_modify_edge_port_success( - faker, nren_l3_core_service_subscription_factory, l3_core_service_type, sbp_input_form_data -): - subscription_id = nren_l3_core_service_subscription_factory(nren_l3_core_service_type=l3_core_service_type) - subscription = NRENL3CoreService.from_subscription(subscription_id) - new_sbp_data = [sbp_input_form_data(), sbp_input_form_data()] - input_form_data = [ - {"subscription_id": subscription_id}, - { - "access_ports": [ - { - "edge_port": str(port.sbp.edge_port.owner_subscription_id), - "ap_type": port.ap_type, - } - for port in subscription.nren_l3_core_service.nren_ap_list - ] - }, - {**new_sbp_data[0]}, - {**new_sbp_data[1]}, - ] - - result, _, _ = run_workflow("modify_nren_l3_core_service", input_form_data) - - state = extract_state(result) - subscription = NRENL3CoreService.from_subscription(state["subscription_id"]) - assert len(subscription.nren_l3_core_service.nren_ap_list) == 2 - - for i in range(2): - assert subscription.nren_l3_core_service.nren_ap_list[i].sbp.geant_sid == new_sbp_data[i]["geant_sid"] - assert subscription.nren_l3_core_service.nren_ap_list[i].sbp.is_tagged == new_sbp_data[i]["is_tagged"] - assert subscription.nren_l3_core_service.nren_ap_list[i].sbp.vlan_id == new_sbp_data[i]["vlan_id"] - assert ( - str(subscription.nren_l3_core_service.nren_ap_list[i].sbp.ipv4_address) == new_sbp_data[i]["ipv4_address"] - ) - assert ( - str(subscription.nren_l3_core_service.nren_ap_list[i].sbp.ipv6_address) == new_sbp_data[i]["ipv6_address"] - ) - assert ( - subscription.nren_l3_core_service.nren_ap_list[i].sbp.custom_firewall_filters - == new_sbp_data[i]["custom_firewall_filters"] - ) - - assert ( - subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[0].bfd_enabled - == new_sbp_data[i]["v4_bgp_peer"]["bfd_enabled"] - ) - assert ( - subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[0].bfd_interval - == new_sbp_data[i]["v4_bgp_peer"]["bfd_interval"] - ) - assert ( - subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[0].bfd_multiplier - == new_sbp_data[i]["v4_bgp_peer"]["bfd_multiplier"] - ) - assert ( - subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[0].has_custom_policies - == new_sbp_data[i]["v4_bgp_peer"]["has_custom_policies"] - ) - assert ( - subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[0].authentication_key - == new_sbp_data[i]["v4_bgp_peer"]["authentication_key"] - ) - assert ( - subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[0].multipath_enabled - == new_sbp_data[i]["v4_bgp_peer"]["multipath_enabled"] - ) - assert ( - subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[0].send_default_route - == new_sbp_data[i]["v4_bgp_peer"]["send_default_route"] - ) - assert ( - subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[0].is_passive - == new_sbp_data[i]["v4_bgp_peer"]["is_passive"] - ) - assert ( - str(subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[0].peer_address) - == new_sbp_data[i]["v4_bgp_peer"]["peer_address"] - ) - assert ( - bool( - IPFamily.V4MULTICAST - in subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[0].families - ) - == new_sbp_data[i]["v4_bgp_peer"]["add_v4_multicast"] - ) - - assert ( - subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[1].bfd_enabled - == new_sbp_data[i]["v6_bgp_peer"]["bfd_enabled"] - ) - assert ( - subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[1].bfd_interval - == new_sbp_data[i]["v6_bgp_peer"]["bfd_interval"] - ) - assert ( - subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[1].bfd_multiplier - == new_sbp_data[i]["v6_bgp_peer"]["bfd_multiplier"] - ) - assert ( - subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[1].has_custom_policies - == new_sbp_data[i]["v6_bgp_peer"]["has_custom_policies"] - ) - assert ( - subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[1].authentication_key - == new_sbp_data[i]["v6_bgp_peer"]["authentication_key"] - ) - assert ( - subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[1].multipath_enabled - == new_sbp_data[i]["v6_bgp_peer"]["multipath_enabled"] - ) - assert ( - subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[1].send_default_route - == new_sbp_data[i]["v6_bgp_peer"]["send_default_route"] - ) - assert ( - subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[1].is_passive - == new_sbp_data[i]["v6_bgp_peer"]["is_passive"] - ) - assert ( - str(subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[1].peer_address) - == new_sbp_data[i]["v6_bgp_peer"]["peer_address"] - ) - assert ( - bool( - IPFamily.V6MULTICAST - in subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[1].families - ) - == new_sbp_data[i]["v6_bgp_peer"]["add_v6_multicast"] - ) diff --git a/test/workflows/router/test_validate_router.py b/test/workflows/router/test_validate_router.py index a72063d916965d62b2048853b720bf9fd21ebf86..1b2e371cebb6cdecd845a4ccc52ee7de285c7afd 100644 --- a/test/workflows/router/test_validate_router.py +++ b/test/workflows/router/test_validate_router.py @@ -4,6 +4,7 @@ import pytest from infoblox_client import objects from orchestrator.types import SubscriptionLifecycle +from gso.products.product_blocks.router import RouterRole from gso.products.product_types.router import Router from gso.utils.shared_enums import Vendor from test.services.conftest import MockedKentikClient @@ -16,6 +17,7 @@ from test.workflows import ( @pytest.mark.parametrize("router_state", [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE]) +@pytest.mark.parametrize("router_role", [RouterRole.P, RouterRole.PE]) @pytest.mark.workflow() @patch("gso.services.infoblox.find_host_by_fqdn") @patch("gso.services.lso_client._send_request") @@ -30,14 +32,15 @@ def test_validate_nokia_router_success( mock_find_host_by_fqdn, router_subscription_factory, faker, - data_config_filename, - geant_partner, router_state, + router_role, ): mock_validate_librenms_device.return_value = None + router_subscription_factory(router_role=RouterRole.P) + router_subscription_factory(router_role=RouterRole.PE) mock_kentik_client.return_value = MockedKentikClient # Run workflow - subscription_id = router_subscription_factory(status=router_state) + subscription_id = router_subscription_factory(status=router_state, router_role=router_role) mock_fqdn = Router.from_subscription(subscription_id).router.router_fqdn mock_v4 = faker.ipv4() mock_find_host_by_fqdn.return_value = objects.HostRecord( @@ -62,7 +65,9 @@ def test_validate_nokia_router_success( state = extract_state(result) subscription_id = state["subscription_id"] - for _ in range(2): + lso_execution_count = 2 if router_role == RouterRole.P else 3 + + for _ in range(lso_execution_count): result, step_log = assert_lso_success(result, process_stat, step_log) assert_complete(result) @@ -71,7 +76,7 @@ def test_validate_nokia_router_success( subscription = Router.from_subscription(subscription_id) assert subscription.status == router_state - assert mock_execute_playbook.call_count == 2 + assert mock_execute_playbook.call_count == lso_execution_count assert mock_find_host_by_fqdn.call_count == 1 assert mock_get_device_by_name.call_count == 1 assert mock_validate_librenms_device.call_count == 1 @@ -79,12 +84,7 @@ def test_validate_nokia_router_success( @pytest.mark.workflow() -def test_validate_juniper_router_success( - router_subscription_factory, - faker, - data_config_filename, - geant_partner, -): +def test_validate_juniper_router_success(router_subscription_factory): # Run workflow subscription_id = router_subscription_factory(vendor=Vendor.JUNIPER)