From bb30510e2be276e2297d0c71d9fdc9f217d0c729 Mon Sep 17 00:00:00 2001 From: Neda Moeini <neda.moeini@geant.org> Date: Fri, 15 Nov 2024 12:38:05 +0100 Subject: [PATCH] Update import CLI for L2Circuits. --- gso/cli/imports.py | 96 ++++++++++++++++++- .../l2_circuit/import_layer_2_circuit.py | 8 +- test/cli/test_imports.py | 56 ++++++++++- .../l2_circuit/test_import_layer_2_circuit.py | 4 +- 4 files changed, 151 insertions(+), 13 deletions(-) diff --git a/gso/cli/imports.py b/gso/cli/imports.py index 9afc6efb..31993659 100644 --- a/gso/cli/imports.py +++ b/gso/cli/imports.py @@ -17,13 +17,14 @@ from pydantic import BaseModel, ValidationError, field_validator, model_validato from sqlalchemy.exc import SQLAlchemyError from gso.db.models import PartnerTable -from gso.products import ProductType +from gso.products import Layer2CircuitServiceType, 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.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.services.partners import ( PartnerEmail, PartnerName, @@ -38,7 +39,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, @@ -49,7 +50,7 @@ from gso.utils.types.ip_address import ( IPV6Netmask, PortNumber, ) -from gso.utils.types.virtual_identifiers import VLAN_ID +from gso.utils.types.virtual_identifiers import VC_ID, VLAN_ID app: typer.Typer = typer.Typer() @@ -327,6 +328,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, @@ -339,6 +383,7 @@ T = TypeVar( EdgePortImportModel, NRENL3CoreServiceImportModel, LanSwitchInterconnectImportModel, + Layer2CircuitServiceImportModel, ) common_filepath_option = typer.Option( @@ -608,7 +653,7 @@ def import_nren_l3_core_service(filepath: str = common_filepath_option) -> None: 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"]) + service_type = nren_l3_core_service["service_type"] typer.echo(f"Creating imported {service_type} for {partner}") try: @@ -650,3 +695,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/workflows/l2_circuit/import_layer_2_circuit.py b/gso/workflows/l2_circuit/import_layer_2_circuit.py index 7a22d3cc..01224e86 100644 --- a/gso/workflows/l2_circuit/import_layer_2_circuit.py +++ b/gso/workflows/l2_circuit/import_layer_2_circuit.py @@ -18,7 +18,9 @@ def import_layer_2_circuit_subscription(subscription_id: UUIDstr) -> State: 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: + 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." @@ -28,9 +30,7 @@ def import_layer_2_circuit_subscription(subscription_id: UUIDstr) -> State: return {"subscription": new_subscription} -@workflow( - "Import Layer 2 Circuit", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None) -) +@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 ( diff --git a/test/cli/test_imports.py b/test/cli/test_imports.py index 62cf1a8d..bc4325dc 100644 --- a/test/cli/test_imports.py +++ b/test/cli/test_imports.py @@ -8,6 +8,7 @@ from gso.cli.imports import ( import_edge_port, import_iptrunks, import_lan_switch_interconnect, + import_layer_2_circuit_service, import_nren_l3_core_service, import_office_routers, import_opengear, @@ -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 @@ -389,6 +392,36 @@ def nren_l3_core_service_data(temp_file, faker, partner_factory, edge_port_subsc return _nren_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 + + ########### # TESTS # ########### @@ -710,3 +743,24 @@ def test_import_nren_l3_core_service_with_invalid_edge_port( 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/workflows/l2_circuit/test_import_layer_2_circuit.py b/test/workflows/l2_circuit/test_import_layer_2_circuit.py index 31b31066..ba2009a0 100644 --- a/test/workflows/l2_circuit/test_import_layer_2_circuit.py +++ b/test/workflows/l2_circuit/test_import_layer_2_circuit.py @@ -14,9 +14,7 @@ def test_import_layer_2_circuit_success(layer_2_circuit_service_type, layer_2_ci 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}] - ) + result, _, _ = run_workflow("import_layer_2_circuit", [{"subscription_id": imported_layer_2_circuit}]) subscription = Layer2Circuit.from_subscription(imported_layer_2_circuit) assert_complete(result) -- GitLab