From 5ab29db8e7594b21cd2e72e5c11fbf8ba3bd2279 Mon Sep 17 00:00:00 2001 From: Neda Moeini <neda.moeini@geant.org> Date: Tue, 12 Nov 2024 15:58:25 +0100 Subject: [PATCH] Add create_imported_layer_2_circuit product, WF and unit tests --- ...29_72a4f7aa499d_add_l2circuit_workflows.py | 6 ++ gso/workflows/__init__.py | 1 + .../create_imported_layer_2_circuit.py | 101 +++++++++++++----- .../l2_circuit/import_layer_2_circuit.py | 1 + .../test_create_imported_layer_2_circuit.py | 55 ++++++++++ 5 files changed, 139 insertions(+), 25 deletions(-) create mode 100644 gso/workflows/l2_circuit/import_layer_2_circuit.py create mode 100644 test/workflows/l2_circuit/test_create_imported_layer_2_circuit.py 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 index 3dfffe01..c6891acc 100644 --- a/gso/migrations/versions/2024-10-29_72a4f7aa499d_add_l2circuit_workflows.py +++ b/gso/migrations/versions/2024-10-29_72a4f7aa499d_add_l2circuit_workflows.py @@ -35,6 +35,12 @@ new_workflows = [ "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" } ] diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py index 76527718..f1910726 100644 --- a/gso/workflows/__init__.py +++ b/gso/workflows/__init__.py @@ -129,3 +129,4 @@ LazyWorkflowInstance("gso.workflows.nren_l3_core_service.migrate_nren_l3_core_se 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") diff --git a/gso/workflows/l2_circuit/create_imported_layer_2_circuit.py b/gso/workflows/l2_circuit/create_imported_layer_2_circuit.py index 5c7e30c8..1a94e4ef 100644 --- a/gso/workflows/l2_circuit/create_imported_layer_2_circuit.py +++ b/gso/workflows/l2_circuit/create_imported_layer_2_circuit.py @@ -1,22 +1,28 @@ """A creation workflow that adds an existing Layer 2 Circuit to the database.""" -from typing import Self +from typing import Any, Self +from uuid import uuid4 -from orchestrator import step +from orchestrator import step, workflow from orchestrator.forms import FormPage -from orchestrator.types import FormGenerator, State +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 Layer2CircuitType +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 ( - ImportedLayer2Circuit, 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 @@ -26,36 +32,23 @@ def initial_input_form_generator() -> FormGenerator: class ServiceBindingPortInput(BaseModel): edge_port: UUIDstr - geant_sid: str - is_tagged: bool vlan_id: VLAN_ID - custom_firewall_filters: bool 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 - virtual_circuit_id: VC_ID 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 - bandwidth: BandwidthString | None = None - - @model_validator(mode="after") - def partner_id_matches_edge_port_owner(self) -> Self: - """Validate that the entered partner owns both Edge Ports.""" - partner_id = get_partner_by_name(self.partner)["partner_id"] - if partner_id != self.layer_2_circuit_side_a.edge_port: - msg = f"Selected Edge Port on side A is not owned by partner {self.partner}." - raise ValueError(msg) - if partner_id != self.layer_2_circuit_side_b.edge_port: - msg = f"Selected Edge Port on side B is not owned by partner {self.partner}." - raise ValueError(msg) - return self + 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: @@ -70,6 +63,17 @@ def initial_input_form_generator() -> FormGenerator: 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() @@ -79,7 +83,7 @@ def create_subscription(partner: str, service_type: Layer2CircuitServiceType) -> """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 = ImportedLayer2Circuit.from_product_id(product_id, partner_id) + subscription = ImportedLayer2CircuitInactive.from_product_id(product_id, partner_id) return {"subscription": subscription, "subscription_id": subscription.subscription_id} @@ -87,10 +91,57 @@ def create_subscription(partner: str, service_type: Layer2CircuitServiceType) -> @step("Initialize subscription") def initialize_subscription( subscription: ImportedLayer2CircuitInactive, - vlan_range_lower_bound: VLAN_ID, - vlan_range_upper_bound: VLAN_ID, + 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/import_layer_2_circuit.py b/gso/workflows/l2_circuit/import_layer_2_circuit.py new file mode 100644 index 00000000..0aea5132 --- /dev/null +++ b/gso/workflows/l2_circuit/import_layer_2_circuit.py @@ -0,0 +1 @@ +"""Import a Layer 2 Circuit.""" 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 00000000..0e4e857e --- /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"] -- GitLab