diff --git a/Changelog.md b/Changelog.md index 58e216a048bbd7da5b30b386935f8d658d7bb95a..d490a8bfe7769d4024704346facc5b5b18eaac93 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,24 +1,28 @@ # Changelog +# [3.0] - 2025-04-23 +- Breaking change: reworked all the Layer 3 Core services, splitting them up into separate product types. +- Allow for creating an Edge Port on a Juniper router. +- Add IAS flavour as an attribute to the IAS product. + ## [2.48] - 2025-04-14 -- Make updating iBGP and SBP meshes optional in mesh update workflow +- Make updating iBGP and SBP meshes optional in mesh update workflow. ## [2.47] - 2025-04-14 -- Make generation of new virtual circuit ID optional in layer 2 circuit migration workflow -- Add REST API endpoint for getting the list of available versions +- Make generation of new virtual circuit ID optional in layer 2 circuit migration workflow. +- Add REST API endpoint for getting the list of available versions. ## [2.46] - 2025-04-08 - Integrated a new l2circuit migration workflow within the database. -- Enhancements to Layer 2 Circuit Workflows +- Enhancements to Layer 2 Circuit Workflows. - Fixed a bug in the prefix list validation logic when an FQDN list is unexpectedly empty. -- Code Improvements and Utility Updates +- Code Improvements and Utility Updates. - Added SDP steps to the test_terminate_router to ensure proper execution. - Reworked SDP functions within workflow_steps for improved process clarity. - Integrated additional SDP steps in the terminate_router process. - Adjusted SDP steps in the promote_p_to_pe workflow. - Updated SDP steps within the update_ibgp_mesh workflow. - ## [2.45] - 2025-04-02 - Add email notifications to Kentik-related steps in workflows. - Improve Kentik license handling in Router termination workflow. @@ -234,7 +238,7 @@ ## [2.1] - 2024-06-17 - Fixed tiny bugs in migrate IP trunk. -## [2.0] - 2024-05-23 +# [2.0] - 2024-05-23 - Upgraded `orchestrator-core` to version 2! - Added Opengear products. - Added separate imported product types for importing existing subscriptions. @@ -259,7 +263,7 @@ - Fixed the AttributeError in the migrate_iptrunk workflow. - Improved the delete device in Netbox functionality. -## [1.0] - 2024-03-28 +# [1.0] - 2024-03-28 - PHASE 1 initial release ## [0.9] - 2024-03-20 diff --git a/gso/cli/imports.py b/gso/cli/imports.py index 85db5b756101ab06f7aa86cd54d696586f75641d..6a82ddecc513fc71496de598d83d2499272bab5a 100644 --- a/gso/cli/imports.py +++ b/gso/cli/imports.py @@ -18,15 +18,15 @@ from pydantic_forms.types import UUIDstr from sqlalchemy.exc import SQLAlchemyError from gso.db.models import PartnerTable -from gso.products import ProductType +from gso.products import ProductName, 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.ias import IASFlavor 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.edge_port import EdgePort -from gso.products.product_types.l3_core_service import L3CoreServiceType from gso.products.product_types.layer_2_circuit import Layer2CircuitServiceType from gso.services.partners import ( PartnerEmail, @@ -53,6 +53,7 @@ from gso.utils.types.ip_address import ( PortNumber, ) from gso.utils.types.virtual_identifiers import VC_ID, VLAN_ID +from gso.workflows.l3_core_service.shared import L3_CREAT_IMPORTED_WF_MAP, L3ProductNameType app: typer.Typer = typer.Typer() IMPORT_WAIT_MESSAGE = "Waiting for the dust to settle before importing new products..." @@ -289,7 +290,7 @@ class L3CoreServiceImportModel(BaseModel): partner: str service_binding_ports: list[ServiceBindingPort] - service_type: L3CoreServiceType + product_name: L3ProductNameType @field_validator("partner") def check_if_partner_exists(cls, value: str) -> str: @@ -314,6 +315,12 @@ class L3CoreServiceImportModel(BaseModel): return value +class IASImportModel(L3CoreServiceImportModel): + """Import IAS model.""" + + ias_flavor: IASFlavor = IASFlavor.IAS_PS_OPT_OUT + + class LanSwitchInterconnectRouterSideImportModel(BaseModel): """Import LAN Switch Interconnect Router side model.""" @@ -664,16 +671,20 @@ def import_l3_core_service(filepath: str = common_filepath_option) -> None: l3_core_service_list = _read_data(Path(filepath)) 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}") + partner = l3_core_service[0]["partner"] + product_name = l3_core_service[0]["product_name"] + typer.echo(f"Creating imported {product_name} for {partner}") try: - 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"]] + if product_name == ProductName.IAS.value: + initial_data = IASImportModel(**l3_core_service[0], **l3_core_service[1]).model_dump() + else: + initial_data = L3CoreServiceImportModel(**l3_core_service[0], **l3_core_service[1]).model_dump() + + start_process(L3_CREAT_IMPORTED_WF_MAP[product_name], [initial_data]) + edge_ports = [sbp["edge_port"] for sbp in l3_core_service[0]["service_binding_ports"]] successfully_imported_data.append(edge_ports) - typer.echo(f"Successfully created imported {service_type} for {partner}") + typer.echo(f"Successfully created imported {product_name} for {partner}") except ValidationError as e: typer.echo(f"Validation error: {e}") diff --git a/gso/migrations/versions/2025-04-08_e1afa3790f32_re_model_ias.py b/gso/migrations/versions/2025-04-08_e1afa3790f32_re_model_ias.py new file mode 100644 index 0000000000000000000000000000000000000000..d2e5b6d42eb3735671a0b2e4f014c62e9bf178f7 --- /dev/null +++ b/gso/migrations/versions/2025-04-08_e1afa3790f32_re_model_ias.py @@ -0,0 +1,382 @@ +"""Re-model L3CoreServices. + + GEANT_IP = "GÉANT IP" + IMPORTED_GEANT_IP = "IMPORTED GÉANT IP" + LHCONE = "LHCONE" + IMPORTED_LHCONE = "IMPORTED LHCONE" + COPERNICUS = "COPERNICUS" + IMPORTED_COPERNICUS = "IMPORTED COPERNICUS" + +Revision ID: e1afa3790f32 +Revises: b96b0ecf6906 +Create Date: 2025-03-17 10:23:45.917222 +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic +revision = "e1afa3790f32" +down_revision = "3541c7e57284" +branch_labels = None +depends_on = None + +_L3_CORE_SERVICES = [ + { + "product": { + "name": "IAS", + "type": "IAS", + }, + "imported_product": { + "name": "Imported IAS", + "type": "ImportedIAS" + }, + "product_block": { + "name": "IASBlock", + "description": "An Internet Access Service for general internet access", + "tag": "IAS", + }, + }, + { + "product": { + "name": "GÉANT IP", + "type": "GeantIP" + }, + "imported_product": { + "name": "Imported GÉANT IP", + "type": "ImportedGeantIP" + }, + "product_block": { + "name": "GeantIPBlock", + "description": "A GÉANT IP product block", + "tag": "G_IP", + }, + }, + { + "product": { + "name": "LHCOne", + "type": "LHCOne" + }, + "imported_product": { + "name": "Imported LHCOne", + "type": "ImportedLHCOne" + }, + "product_block": { + "name": "LHCOneBlock", + "description": "A LHCOne product block", + "tag": "LHC", + }, + }, + { + "product": { + "name": "Copernicus", + "type": "Copernicus" + }, + "imported_product": { + "name": "Imported Copernicus", + "type": "ImportedCopernicus" + }, + "product_block": { + "name": "CopernicusBlock", + "description": "A Copernicus product block", + "tag": "COP", + }, + }, +] + + +def upgrade() -> None: + """Perform the upgrade for L3CoreServices remodeling.""" + conn = op.get_bind() + + for l3 in _L3_CORE_SERVICES: + + conn.execute( + sa.text( + f""" + DELETE FROM fixed_inputs + WHERE fixed_inputs.product_id IN ( + SELECT products.product_id + FROM products + WHERE products.name IN ( + '{l3["product"]["name"]}', + '{l3["imported_product"]["name"]}' + ) + ) + AND fixed_inputs.name = 'l3_core_service_type'; + """ + ) + ) + + conn.execute( + sa.text( + f""" + INSERT INTO product_blocks (name, description, tag, status) + VALUES ( + '{l3["product_block"]["name"]}', + '{l3["product_block"]["description"]}', + '{l3["product_block"]["tag"]}', + 'active' + ) + RETURNING product_blocks.product_block_id; + """ + ) + ) + + if l3["product"]["name"] == "IAS": + conn.execute( + sa.text( + """ + INSERT INTO resource_types (resource_type, description) + VALUES ('ias_flavor', 'The flavor of the IAS service') + RETURNING resource_types.resource_type_id; + """ + ) + ) + + conn.execute( + sa.text( + f""" + INSERT INTO product_block_relations + VALUES ( + ( + SELECT product_blocks.product_block_id + FROM product_blocks + WHERE name = '{l3["product_block"]["name"]}' + ), + ( + SELECT product_blocks.product_block_id + FROM product_blocks + WHERE name = 'L3CoreServiceBlock' + ), + NULL, + NULL + ); + """ + ) + ) + + conn.execute( + sa.text( + f""" + UPDATE product_product_blocks + SET product_block_id = ( + SELECT product_block_id + FROM product_blocks + WHERE name = '{l3["product_block"]["name"]}' + ) + WHERE product_block_id = ( + SELECT product_block_id + FROM product_blocks + WHERE name = 'L3CoreServiceBlock' + ) + AND product_id IN ( + SELECT product_id + FROM products + WHERE name IN ( + '{l3["product"]["name"]}', + '{l3["imported_product"]["name"]}' + ) + ); + """ + ) + ) + + conn.execute( + sa.text( + f""" + UPDATE products + SET product_type = '{l3["product"]["type"]}' + WHERE product_type = 'L3CoreService' + AND name = '{l3["product"]["name"]}'; + """ + ) + ) + + conn.execute( + sa.text( + f""" + UPDATE products + SET product_type = '{l3["imported_product"]["type"]}' + WHERE product_type = 'ImportedL3CoreService' + AND name = '{l3["imported_product"]["name"]}'; + """ + ) + ) + + conn.execute( + sa.text( + f""" + -- Step 1: Insert new subscription_instances for '{l3["product_block"]["name"]}' and return their IDs + WITH inserted AS ( + INSERT INTO subscription_instances (subscription_id, product_block_id) + SELECT + s.subscription_id, + ( + SELECT product_block_id + FROM product_blocks + WHERE name = '{l3["product_block"]["name"]}' + ) AS product_block_id + FROM subscriptions s + WHERE s.product_id = ( + SELECT product_id + FROM products + WHERE products.name = '{l3["product"]["name"]}' + ) + RETURNING subscription_instance_id, subscription_id + ) + + -- Step 2: Link newly inserted instances to the existing L3CoreServiceBlock instance + INSERT INTO subscription_instance_relations ( + in_use_by_id, + depends_on_id, + order_id, + domain_model_attr + ) + SELECT + i.subscription_instance_id AS in_use_by_id, + e.subscription_instance_id AS depends_on_id, + 0 AS order_id, + 'l3_core' AS domain_model_attr + FROM inserted i + JOIN subscription_instances e + ON e.subscription_id = i.subscription_id + AND e.product_block_id = ( + SELECT product_block_id + FROM product_blocks + WHERE name = 'L3CoreServiceBlock' + ); + """ + ) + ) + + if l3["product"]["name"] == "IAS": + 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 ('IASBlock') + ), + ( + SELECT resource_types.resource_type_id + FROM resource_types + WHERE resource_types.resource_type IN ('ias_flavor') + ) + ); + """ + ) + ) + + conn.execute( + sa.text( + """ + WITH subscription_instance_ids AS ( + SELECT subscription_instances.subscription_instance_id + FROM subscription_instances + WHERE subscription_instances.product_block_id IN ( + SELECT product_blocks.product_block_id + FROM product_blocks + WHERE product_blocks.name = 'IASBlock' + ) + ) + INSERT INTO subscription_instance_values ( + subscription_instance_id, + resource_type_id, + value + ) + SELECT + subscription_instance_ids.subscription_instance_id, + resource_types.resource_type_id, + 'IASPS Opt-OUT' + FROM resource_types + CROSS JOIN subscription_instance_ids + WHERE resource_types.resource_type = 'ias_flavor'; + """ + ) + ) + + conn.execute( + sa.text( + """ + DELETE FROM processes + WHERE processes.pid IN ( + SELECT processes_subscriptions.pid + FROM processes_subscriptions + WHERE processes_subscriptions.subscription_id IN ( + SELECT subscriptions.subscription_id + FROM subscriptions + WHERE subscriptions.product_id IN ( + SELECT products.product_id + FROM products + WHERE products.name IN ('Imported GWS', '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 ('Imported GWS', '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 ('Imported GWS', 'GWS') + ) + ); + """ + ) + ) + + conn.execute( + sa.text( + """ + DELETE FROM subscriptions + WHERE subscriptions.product_id IN ( + SELECT products.product_id + FROM products + WHERE products.name IN ('Imported GWS', 'GWS') + ); + """ + ) + ) + + conn.execute( + sa.text( + """ + DELETE FROM products + WHERE products.name IN ('Imported GWS', 'GWS'); + """ + ) + ) + + +def downgrade() -> None: + """Perform the downgrade (no actions defined).""" + pass diff --git a/gso/migrations/versions/2025-04-09_9fbb3c4411ea_add_new_l3_core_services_wfs.py b/gso/migrations/versions/2025-04-09_9fbb3c4411ea_add_new_l3_core_services_wfs.py new file mode 100644 index 0000000000000000000000000000000000000000..fb2cc89b1b9a468cd823e3107da1da96463a4947 --- /dev/null +++ b/gso/migrations/versions/2025-04-09_9fbb3c4411ea_add_new_l3_core_services_wfs.py @@ -0,0 +1,205 @@ +"""Add new L3 core services wfs. + +Revision ID: 9fbb3c4411ea +Revises: e1afa3790f32 +Create Date: 2025-03-26 13:39:41.657079 + +""" +from alembic import op + +# revision identifiers, used by Alembic. +revision = '9fbb3c4411ea' +down_revision = 'e1afa3790f32' +branch_labels = None +depends_on = None + +from orchestrator.migrations.helpers import create_workflow, delete_workflow + +new_workflows = [ + { + "name": "create_ias", + "target": "CREATE", + "description": "Create IAS", + "product_type": "IAS" + }, + { + "name": "modify_ias", + "target": "MODIFY", + "description": "Modify IAS", + "product_type": "IAS" + }, + { + "name": "terminate_ias", + "target": "TERMINATE", + "description": "Terminate IAS", + "product_type": "IAS" + }, + { + "name": "migrate_ias", + "target": "MODIFY", + "description": "Migrate IAS", + "product_type": "IAS" + }, + { + "name": "create_imported_ias", + "target": "CREATE", + "description": "Create Imported IAS", + "product_type": "ImportedIAS" + }, + { + "name": "import_ias", + "target": "MODIFY", + "description": "Import IAS", + "product_type": "ImportedIAS" + }, + { + "name": "validate_ias", + "target": "SYSTEM", + "description": "Validate IAS", + "product_type": "IAS" + }, + { + "name": "create_geant_ip", + "target": "CREATE", + "description": "Create GÉANT IP", + "product_type": "GeantIP" + }, + { + "name": "modify_geant_ip", + "target": "MODIFY", + "description": "Modify GÉANT IP", + "product_type": "GeantIP" + }, + { + "name": "terminate_geant_ip", + "target": "TERMINATE", + "description": "Terminate GÉANT IP", + "product_type": "GeantIP" + }, + { + "name": "migrate_geant_ip", + "target": "MODIFY", + "description": "Migrate GÉANT IP", + "product_type": "GeantIP" + }, + { + "name": "create_imported_geant_ip", + "target": "CREATE", + "description": "Create Imported GÉANT IP", + "product_type": "ImportedGeantIP" + }, + { + "name": "import_geant_ip", + "target": "MODIFY", + "description": "Import GÉANT IP", + "product_type": "ImportedGeantIP" + }, + { + "name": "validate_geant_ip", + "target": "SYSTEM", + "description": "Validate GÉANT IP", + "product_type": "GeantIP" + }, + { + "name": "validate_geant_ip_prefix_list", + "target": "SYSTEM", + "description": "Validate GÉANT IP Prefix-List", + "product_type": "GeantIP" + }, + { + "name": "create_lhcone", + "target": "CREATE", + "description": "Create LHCOne", + "product_type": "LHCOne" + }, + { + "name": "modify_lhcone", + "target": "MODIFY", + "description": "Modify LHCOne", + "product_type": "LHCOne" + }, + { + "name": "terminate_lhcone", + "target": "TERMINATE", + "description": "Terminate LHCOne", + "product_type": "LHCOne" + }, + { + "name": "migrate_lhcone", + "target": "MODIFY", + "description": "Migrate LHCOne", + "product_type": "LHCOne" + }, + { + "name": "create_imported_lhcone", + "target": "CREATE", + "description": "Create Imported LHCOne", + "product_type": "ImportedLHCOne" + }, + { + "name": "import_lhcone", + "target": "MODIFY", + "description": "Import LHCOne", + "product_type": "ImportedLHCOne" + }, + { + "name": "validate_lhcone", + "target": "SYSTEM", + "description": "Validate LHCOne", + "product_type": "LHCOne" + }, + { + "name": "create_copernicus", + "target": "CREATE", + "description": "Create Copernicus", + "product_type": "Copernicus" + }, + { + "name": "modify_copernicus", + "target": "MODIFY", + "description": "Modify Copernicus", + "product_type": "Copernicus" + }, + { + "name": "terminate_copernicus", + "target": "TERMINATE", + "description": "Terminate Copernicus", + "product_type": "Copernicus" + }, + { + "name": "migrate_copernicus", + "target": "MODIFY", + "description": "Migrate Copernicus", + "product_type": "Copernicus" + }, + { + "name": "create_imported_copernicus", + "target": "CREATE", + "description": "Create Imported Copernicus", + "product_type": "ImportedCopernicus" + }, + { + "name": "import_copernicus", + "target": "MODIFY", + "description": "Import Copernicus", + "product_type": "ImportedCopernicus" + }, + { + "name": "validate_copernicus", + "target": "SYSTEM", + "description": "Validate Copernicus", + "product_type": "Copernicus" + }, +] + + +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/2025-04-10_c38adde1a18e_update_wf_in_process_table.py b/gso/migrations/versions/2025-04-10_c38adde1a18e_update_wf_in_process_table.py new file mode 100644 index 0000000000000000000000000000000000000000..005d1f1824cd8b6f183ef627d23e39fd41cfae73 --- /dev/null +++ b/gso/migrations/versions/2025-04-10_c38adde1a18e_update_wf_in_process_table.py @@ -0,0 +1,152 @@ +"""Update workflows in process table and delete old processes and workflows + +Revision ID: c38adde1a18e +Revises: 9fbb3c4411ea +Create Date: 2025-03-26 21:38:13.445657 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'c38adde1a18e' +down_revision = '9fbb3c4411ea' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + """Update workflow IDs in the processes table based on product_type and current workflow.""" + conn = op.get_bind() + + # Mapping for all workflow updates: + update_mappings = [ + # IAS updates + {"new_workflow": "create_ias", "old_workflow": "create_l3_core_service", "product_type": "IAS"}, + {"new_workflow": "modify_ias", "old_workflow": "modify_l3_core_service", "product_type": "IAS"}, + {"new_workflow": "terminate_ias", "old_workflow": "terminate_l3_core_service", "product_type": "IAS"}, + {"new_workflow": "migrate_ias", "old_workflow": "migrate_l3_core_service", "product_type": "IAS"}, + {"new_workflow": "validate_ias", "old_workflow": "validate_l3_core_service", "product_type": "IAS"}, + {"new_workflow": "create_imported_ias", "old_workflow": "create_imported_l3_core_service", "product_type": "IAS"}, + {"new_workflow": "import_ias", "old_workflow": "import_l3_core_service", "product_type": "IAS"}, + + # GeantIP updates + {"new_workflow": "create_geant_ip", "old_workflow": "create_l3_core_service", "product_type": "GeantIP"}, + {"new_workflow": "modify_geant_ip", "old_workflow": "modify_l3_core_service", "product_type": "GeantIP"}, + {"new_workflow": "terminate_geant_ip", "old_workflow": "terminate_l3_core_service", "product_type": "GeantIP"}, + {"new_workflow": "migrate_geant_ip", "old_workflow": "migrate_l3_core_service", "product_type": "GeantIP"}, + {"new_workflow": "validate_geant_ip", "old_workflow": "validate_l3_core_service", "product_type": "GeantIP"}, + {"new_workflow": "create_imported_geant_ip", "old_workflow": "create_imported_l3_core_service", "product_type": "GeantIP"}, + {"new_workflow": "import_geant_ip", "old_workflow": "import_l3_core_service", "product_type": "GeantIP"}, + {"new_workflow": "validate_geant_ip_prefix_list", "old_workflow": "validate_prefix_list", "product_type": "GeantIP"}, + + # LHCOne updates + {"new_workflow": "create_lhcone", "old_workflow": "create_l3_core_service", "product_type": "LHCOne"}, + {"new_workflow": "modify_lhcone", "old_workflow": "modify_l3_core_service", "product_type": "LHCOne"}, + {"new_workflow": "terminate_lhcone", "old_workflow": "terminate_l3_core_service", "product_type": "LHCOne"}, + {"new_workflow": "migrate_lhcone", "old_workflow": "migrate_l3_core_service", "product_type": "LHCOne"}, + {"new_workflow": "validate_lhcone", "old_workflow": "validate_l3_core_service", "product_type": "LHCOne"}, + {"new_workflow": "create_imported_lhcone", "old_workflow": "create_imported_l3_core_service", + "product_type": "LHCOne"}, + {"new_workflow": "import_lhcone", "old_workflow": "import_l3_core_service", "product_type": "LHCOne"}, + + # Copernicus updates + {"new_workflow": "create_copernicus", "old_workflow": "create_l3_core_service", "product_type": "Copernicus"}, + {"new_workflow": "modify_copernicus", "old_workflow": "modify_l3_core_service", "product_type": "Copernicus"}, + {"new_workflow": "terminate_copernicus", "old_workflow": "terminate_l3_core_service", "product_type": "Copernicus"}, + {"new_workflow": "migrate_copernicus", "old_workflow": "migrate_l3_core_service", "product_type": "Copernicus"}, + {"new_workflow": "validate_copernicus", "old_workflow": "validate_l3_core_service","product_type": "Copernicus"}, + {"new_workflow": "create_imported_copernicus", "old_workflow": "create_imported_l3_core_service", "product_type": "Copernicus"}, + {"new_workflow": "import_copernicus", "old_workflow": "import_l3_core_service", "product_type": "Copernicus"}, + ] + + # SQL template with parameters + sql_template = """ +UPDATE processes pr +SET workflow_id = ( + SELECT workflow_id + FROM workflows + WHERE name = :new_workflow +) +FROM processes_subscriptions ps +JOIN subscriptions s ON ps.subscription_id = s.subscription_id +JOIN products p ON s.product_id = p.product_id +WHERE pr.pid = ps.pid + AND p.product_type = :product_type + AND pr.workflow_id = ( + SELECT workflow_id + FROM workflows + WHERE name = :old_workflow + ); + """ + + # Execute the update for each mapping by passing the mapping as a parameter dictionary. + for mapping in update_mappings: + conn.execute(sa.text(sql_template), mapping) + + # Delete input states referencing to orphaned workflows. + conn.execute( + sa.text( + """ +DELETE FROM input_states +WHERE input_states.pid IN (SELECT p.pid + FROM processes p + JOIN workflows w + ON p.workflow_id = w.workflow_id + JOIN processes_subscriptions ps + ON ps.pid = p.pid + JOIN subscriptions s + ON s.subscription_id = ps.subscription_id + JOIN products pro + ON s.product_id = pro.product_id + WHERE w."name" = 'validate_prefix_list' + AND pro."name" != 'GÉANT IP'); + """ + ) + ) + + # Delete processes refering to orphaned workflows. + conn.execute( + sa.text( + """ +DELETE FROM processes +WHERE processes.pid IN (SELECT p.pid + FROM processes p + JOIN workflows w + ON p.workflow_id = w.workflow_id + JOIN processes_subscriptions ps + ON ps.pid = p.pid + JOIN subscriptions s + ON s.subscription_id = ps.subscription_id + JOIN products pro + ON s.product_id = pro.product_id + WHERE w."name" = 'validate_prefix_list' + AND pro."name" != 'GÉANT IP'); + """ + ) + ) + + # Delete workflows for products that no longer exist. + conn.execute( + sa.text( + """ +DELETE FROM workflows +WHERE name IN ( + 'create_l3_core_service', + 'modify_l3_core_service', + 'terminate_l3_core_service', + 'migrate_l3_core_service', + 'validate_l3_core_service', + 'create_imported_l3_core_service', + 'import_l3_core_service', + 'validate_prefix_list' +); + """ + ) + ) + + +def downgrade() -> None: + """No downgrade available.""" + pass diff --git a/gso/migrations/versions/2025-04-18_fffe36624681_update_ip_trunk_descriptions.py b/gso/migrations/versions/2025-04-18_fffe36624681_update_ip_trunk_descriptions.py new file mode 100644 index 0000000000000000000000000000000000000000..4402b1ef376d441a9e093a4a466c63aeed44501d --- /dev/null +++ b/gso/migrations/versions/2025-04-18_fffe36624681_update_ip_trunk_descriptions.py @@ -0,0 +1,38 @@ +"""Update IP Trunk descriptions. + +Removes an outdated to `geant_s_sid` in IP Trunk descriptions. This field has been renamed and adds unnecessary clutter +to the description field. + +Revision ID: fffe36624681 +Revises: c38adde1a18e +Create Date: 2025-04-18 09:41:28.463681 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'fffe36624681' +down_revision = 'c38adde1a18e' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + conn = op.get_bind() + conn.execute( + sa.text( + """ +UPDATE subscriptions +SET description = Replace(description, 'geant_s_sid:', '') +WHERE product_id = (SELECT product_id + FROM products p + WHERE p.tag = 'IPTRUNK'); + """ + ) + ) + + +def downgrade() -> None: + # Do nothing + pass diff --git a/gso/products/__init__.py b/gso/products/__init__.py index 2d73ad8981b36080b6341b6e66e9709596a0b889..fff581523777c10fc3461bb16ce72cdd1b906548 100644 --- a/gso/products/__init__.py +++ b/gso/products/__init__.py @@ -8,11 +8,14 @@ from orchestrator.domain import SUBSCRIPTION_MODEL_REGISTRY from pydantic_forms.types import strEnum +from gso.products.product_types.copernicus import Copernicus, ImportedCopernicus from gso.products.product_types.edge_port import EdgePort, ImportedEdgePort +from gso.products.product_types.geant_ip import GeantIP, ImportedGeantIP +from gso.products.product_types.ias import IAS, ImportedIAS 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.layer_2_circuit import ImportedLayer2Circuit, Layer2Circuit, Layer2CircuitServiceType +from gso.products.product_types.lhcone import ImportedLHCOne, LHCOne 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 @@ -61,9 +64,6 @@ class ProductName(strEnum): IAS = "IAS" """Internet Access Services.""" IMPORTED_IAS = "Imported IAS" - GWS = "GWS" - """GÉANT Web Services.""" - IMPORTED_GWS = "Imported GWS" LHCONE = "LHCOne" """LHCOne.""" IMPORTED_LHCONE = "Imported LHCOne" @@ -80,7 +80,6 @@ class ProductName(strEnum): """VRFs.""" -L3_CORE_SERVICE_PRODUCT_TYPE = L3CoreService.__name__ L2_CIRCUIT_PRODUCT_TYPE = Layer2Circuit.__name__ @@ -106,21 +105,19 @@ class ProductType(strEnum): IMPORTED_OPENGEAR = ImportedOpengear.__name__ EDGE_PORT = EdgePort.__name__ IMPORTED_EDGE_PORT = ImportedEdgePort.__name__ - GEANT_IP = L3_CORE_SERVICE_PRODUCT_TYPE - IMPORTED_GEANT_IP = ImportedL3CoreService.__name__ - IAS = L3_CORE_SERVICE_PRODUCT_TYPE - IMPORTED_IAS = ImportedL3CoreService.__name__ - GWS = L3_CORE_SERVICE_PRODUCT_TYPE - IMPORTED_GWS = ImportedL3CoreService.__name__ - LHCONE = L3_CORE_SERVICE_PRODUCT_TYPE - IMPORTED_LHCONE = ImportedL3CoreService.__name__ - COPERNICUS = L3_CORE_SERVICE_PRODUCT_TYPE - IMPORTED_COPERNICUS = ImportedL3CoreService.__name__ GEANT_PLUS = L2_CIRCUIT_PRODUCT_TYPE IMPORTED_GEANT_PLUS = ImportedLayer2Circuit.__name__ EXPRESSROUTE = L2_CIRCUIT_PRODUCT_TYPE IMPORTED_EXPRESSROUTE = ImportedLayer2Circuit.__name__ VRF = VRF.__name__ + IAS = IAS.__name__ + IMPORTED_IAS = ImportedIAS.__name__ + GEANT_IP = GeantIP.__name__ + IMPORTED_GEANT_IP = ImportedGeantIP.__name__ + LHCONE = LHCOne.__name__ + IMPORTED_LHCONE = ImportedLHCOne.__name__ + COPERNICUS = Copernicus.__name__ + IMPORTED_COPERNICUS = ImportedCopernicus.__name__ SUBSCRIPTION_MODEL_REGISTRY.update( @@ -144,16 +141,14 @@ SUBSCRIPTION_MODEL_REGISTRY.update( ProductName.IMPORTED_OPENGEAR.value: ImportedOpengear, ProductName.EDGE_PORT.value: EdgePort, ProductName.IMPORTED_EDGE_PORT.value: ImportedEdgePort, - 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_IP.value: GeantIP, + ProductName.IMPORTED_GEANT_IP.value: ImportedGeantIP, + ProductName.IAS.value: IAS, + ProductName.IMPORTED_IAS.value: ImportedIAS, + ProductName.LHCONE.value: LHCOne, + ProductName.IMPORTED_LHCONE.value: ImportedLHCOne, + ProductName.COPERNICUS.value: Copernicus, + ProductName.IMPORTED_COPERNICUS.value: ImportedCopernicus, ProductName.GEANT_PLUS.value: Layer2Circuit, ProductName.IMPORTED_GEANT_PLUS.value: ImportedLayer2Circuit, ProductName.EXPRESSROUTE.value: Layer2Circuit, @@ -162,4 +157,4 @@ SUBSCRIPTION_MODEL_REGISTRY.update( }, ) -__all__ = ["ProductName", "ProductType"] +__all__ = ["L2_CIRCUIT_PRODUCT_TYPE", "ProductName", "ProductType"] diff --git a/gso/products/product_blocks/copernicus.py b/gso/products/product_blocks/copernicus.py new file mode 100644 index 0000000000000000000000000000000000000000..d62d39000ee93799e5256160f0eff2e5b97a4cb6 --- /dev/null +++ b/gso/products/product_blocks/copernicus.py @@ -0,0 +1,30 @@ +"""Product blocks for Copernicus products.""" + +from orchestrator.domain.base import ProductBlockModel +from orchestrator.types import SubscriptionLifecycle + +from gso.products.product_blocks.l3_core_service import ( + L3CoreServiceBlock, + L3CoreServiceBlockInactive, + L3CoreServiceBlockProvisioning, +) + + +class CopernicusBlockInactive( + ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="CopernicusBlock" +): + """An inactive Copernicus product block. See `CopernicusBlock`.""" + + l3_core: L3CoreServiceBlockInactive + + +class CopernicusBlockProvisioning(CopernicusBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """A provisioning Copernicus product block. See `CopernicusBlock`.""" + + l3_core: L3CoreServiceBlockProvisioning + + +class CopernicusBlock(CopernicusBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """An active Copernicus product block.""" + + l3_core: L3CoreServiceBlock diff --git a/gso/products/product_blocks/geant_ip.py b/gso/products/product_blocks/geant_ip.py new file mode 100644 index 0000000000000000000000000000000000000000..74256629870a7342151bbdc48ff069bb23bab644 --- /dev/null +++ b/gso/products/product_blocks/geant_ip.py @@ -0,0 +1,30 @@ +"""Product blocks for GÉANT IP products.""" + +from orchestrator.domain.base import ProductBlockModel +from orchestrator.types import SubscriptionLifecycle + +from gso.products.product_blocks.l3_core_service import ( + L3CoreServiceBlock, + L3CoreServiceBlockInactive, + L3CoreServiceBlockProvisioning, +) + + +class GeantIPBlockInactive( + ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="GeantIPBlock" +): + """An inactive GeantIP product block. See `GeantIPBlock`.""" + + l3_core: L3CoreServiceBlockInactive + + +class GeantIPBlockProvisioning(GeantIPBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """A provisioning GeantIP product block. See `GeantIPBlock`.""" + + l3_core: L3CoreServiceBlockProvisioning + + +class GeantIPBlock(GeantIPBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """An active GeantIP product block.""" + + l3_core: L3CoreServiceBlock diff --git a/gso/products/product_blocks/ias.py b/gso/products/product_blocks/ias.py new file mode 100644 index 0000000000000000000000000000000000000000..f57d76fcb8541369b0381e24f09f0caaa2790090 --- /dev/null +++ b/gso/products/product_blocks/ias.py @@ -0,0 +1,40 @@ +"""Product blocks for IAS products.""" + +from orchestrator.domain.base import ProductBlockModel +from orchestrator.types import SubscriptionLifecycle +from pydantic_forms.types import strEnum + +from gso.products.product_blocks.l3_core_service import ( + L3CoreServiceBlock, + L3CoreServiceBlockInactive, + L3CoreServiceBlockProvisioning, +) + + +class IASFlavor(strEnum): + """IAS flavors.""" + + IAS_PS_OPT_OUT = "IASPS Opt-OUT" + IAS_PS_OPT_IN = "IASPS Opt-IN" + IASGWS = "IASGWS" + + +class IASBlockInactive(ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="IASBlock"): + """An inactive IAS product block. See `IASBlock`.""" + + l3_core: L3CoreServiceBlockInactive + ias_flavor: IASFlavor = IASFlavor.IAS_PS_OPT_OUT + + +class IASBlockProvisioning(IASBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """A provisioning IAS product block. See `IASBlock`.""" + + l3_core: L3CoreServiceBlockProvisioning + ias_flavor: IASFlavor + + +class IASBlock(IASBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """An active IAS product block.""" + + l3_core: L3CoreServiceBlock + ias_flavor: IASFlavor diff --git a/gso/products/product_blocks/lhcone.py b/gso/products/product_blocks/lhcone.py new file mode 100644 index 0000000000000000000000000000000000000000..016a2a8f4758315f2762fde5159f1fb0fdb1fc07 --- /dev/null +++ b/gso/products/product_blocks/lhcone.py @@ -0,0 +1,30 @@ +"""Product blocks for LHCONE AS products.""" + +from orchestrator.domain.base import ProductBlockModel +from orchestrator.types import SubscriptionLifecycle + +from gso.products.product_blocks.l3_core_service import ( + L3CoreServiceBlock, + L3CoreServiceBlockInactive, + L3CoreServiceBlockProvisioning, +) + + +class LHCOneBlockInactive( + ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="LHCOneBlock" +): + """An inactive LHCOne product block. See `LHCOneBlock`.""" + + l3_core: L3CoreServiceBlockInactive + + +class LHCOneBlockProvisioning(LHCOneBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """A provisioning LHCOne product block. See `LHCOneBlock`.""" + + l3_core: L3CoreServiceBlockProvisioning + + +class LHCOneBlock(LHCOneBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """An active LHCOne product block.""" + + l3_core: L3CoreServiceBlock diff --git a/gso/products/product_types/copernicus.py b/gso/products/product_types/copernicus.py new file mode 100644 index 0000000000000000000000000000000000000000..570938b7a193a26cc23fa8311d4e11d40b242572 --- /dev/null +++ b/gso/products/product_types/copernicus.py @@ -0,0 +1,79 @@ +"""Product type for Copernicus.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from orchestrator.types import SubscriptionLifecycle + +from gso.products.product_blocks.copernicus import ( + CopernicusBlock, + CopernicusBlockInactive, + CopernicusBlockProvisioning, +) +from gso.products.product_types.l3_core_service import BaseL3SubscriptionModel + +if TYPE_CHECKING: + from gso.products.product_blocks.l3_core_service import L3CoreServiceBlockInactive + + +class CopernicusInactive(BaseL3SubscriptionModel, is_base=True): + """A Copernicus product that is inactive.""" + + copernicus: CopernicusBlockInactive + + @property + def l3_core(self) -> L3CoreServiceBlockInactive: + """Getter: Retrieve the l3_core from the copernicus attribute.""" + return self.copernicus.l3_core + + @l3_core.setter + def l3_core(self, value: L3CoreServiceBlockInactive) -> None: + """Setter: Set the l3_core on the copernicus attribute.""" + self.copernicus.l3_core = value + + @property + def service_name_attribute(self) -> str: + """Get the service name.""" + return "copernicus" + + +class CopernicusProvisioning(CopernicusInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """A Copernicus product that is being provisioned.""" + + copernicus: CopernicusBlockProvisioning + + +class Copernicus(CopernicusProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """A Copernicus product that is active.""" + + copernicus: CopernicusBlock + + +class ImportedCopernicusInactive(BaseL3SubscriptionModel, is_base=True): + """An imported Copernicus product that is inactive.""" + + copernicus: CopernicusBlockInactive + + @property + def l3_core(self) -> L3CoreServiceBlockInactive: + """Getter: Retrieve the l3_core from the copernicus attribute.""" + return self.copernicus.l3_core + + @l3_core.setter + def l3_core(self, value: L3CoreServiceBlockInactive) -> None: + """Setter: Set the l3_core on the copernicus attribute.""" + self.copernicus.l3_core = value + + @property + def service_name_attribute(self) -> str: + """Get the service name.""" + return "copernicus" + + +class ImportedCopernicus( + ImportedCopernicusInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE] +): + """An imported Copernicus product that is active.""" + + copernicus: CopernicusBlock diff --git a/gso/products/product_types/geant_ip.py b/gso/products/product_types/geant_ip.py new file mode 100644 index 0000000000000000000000000000000000000000..2710f1afe2c92133c6de1d88ba255483592a1f43 --- /dev/null +++ b/gso/products/product_types/geant_ip.py @@ -0,0 +1,81 @@ +"""Product type for GÉANT IP.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from orchestrator.types import SubscriptionLifecycle + +from gso.products.product_blocks.geant_ip import ( + GeantIPBlock, + GeantIPBlockInactive, + GeantIPBlockProvisioning, +) +from gso.products.product_types.l3_core_service import BaseL3SubscriptionModel + +if TYPE_CHECKING: + from gso.products.product_blocks.l3_core_service import ( + L3CoreServiceBlockInactive, + ) + + +class GeantIPInactive(BaseL3SubscriptionModel, is_base=True): + """A GeantIP product that is inactive.""" + + geant_ip: GeantIPBlockInactive + + @property + def l3_core(self) -> L3CoreServiceBlockInactive: + """Getter: Retrieve the l3_core from the geant_ip attribute.""" + return self.geant_ip.l3_core + + @l3_core.setter + def l3_core(self, value: L3CoreServiceBlockInactive) -> None: + """Setter: Set the l3_core on the geant_ip attribute.""" + self.geant_ip.l3_core = value + + @property + def service_name_attribute(self) -> str: + """Get the service name.""" + return "geant_ip" + + +class GeantIPProvisioning(GeantIPInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """A GeantIP product that is being provisioned.""" + + geant_ip: GeantIPBlockProvisioning + + +class GeantIP(GeantIPProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """A GeantIP product that is active.""" + + geant_ip: GeantIPBlock + + +class ImportedGeantIPInactive(BaseL3SubscriptionModel, is_base=True): + """An imported GeantIP product that is inactive.""" + + geant_ip: GeantIPBlockInactive + + @property + def l3_core(self) -> L3CoreServiceBlockInactive: + """Getter: Retrieve the l3_core from the geant_ip attribute.""" + return self.geant_ip.l3_core + + @l3_core.setter + def l3_core(self, value: L3CoreServiceBlockInactive) -> None: + """Setter: Set the l3_core on the geant_ip attribute.""" + self.geant_ip.l3_core = value + + @property + def service_name_attribute(self) -> str: + """Get the service name.""" + return "geant_ip" + + +class ImportedGeantIP( + ImportedGeantIPInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE] +): + """An imported GeantIP product that is active.""" + + geant_ip: GeantIPBlock diff --git a/gso/products/product_types/ias.py b/gso/products/product_types/ias.py new file mode 100644 index 0000000000000000000000000000000000000000..b70ba11307d05563278b2a8ea984915f2a26ca22 --- /dev/null +++ b/gso/products/product_types/ias.py @@ -0,0 +1,79 @@ +"""Product type for IAS.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from orchestrator.types import SubscriptionLifecycle + +from gso.products.product_blocks.ias import ( + IASBlock, + IASBlockInactive, + IASBlockProvisioning, +) +from gso.products.product_types.l3_core_service import BaseL3SubscriptionModel + +if TYPE_CHECKING: + from gso.products.product_blocks.l3_core_service import ( + L3CoreServiceBlockInactive, + ) + + +class IASInactive(BaseL3SubscriptionModel, is_base=True): + """An IAS product that is inactive.""" + + ias: IASBlockInactive + + @property + def l3_core(self) -> L3CoreServiceBlockInactive: + """Getter: Retrieve the l3_core from the ``ias`` attribute.""" + return self.ias.l3_core + + @l3_core.setter + def l3_core(self, value: L3CoreServiceBlockInactive) -> None: + """Setter: Set the l3_core on the ``ias`` attribute.""" + self.ias.l3_core = value + + @property + def service_name_attribute(self) -> str: + """Get the service name.""" + return "ias" + + +class IASProvisioning(IASInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """An IAS product that is being provisioned.""" + + ias: IASBlockProvisioning + + +class IAS(IASProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """An IAS product that is active.""" + + ias: IASBlock + + +class ImportedIASInactive(BaseL3SubscriptionModel, is_base=True): + """An imported IAS product that is inactive.""" + + ias: IASBlockInactive + + @property + def l3_core(self) -> L3CoreServiceBlockInactive: + """Getter: Retrieve the l3_core from the ``ias`` attribute.""" + return self.ias.l3_core + + @l3_core.setter + def l3_core(self, value: L3CoreServiceBlockInactive) -> None: + """Setter: Set the l3_core on the ``ias`` attribute.""" + self.ias.l3_core = value + + @property + def service_name_attribute(self) -> str: + """Get the service name.""" + return "ias" + + +class ImportedIAS(ImportedIASInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE]): + """An imported IAS product that is active.""" + + ias: IASBlock diff --git a/gso/products/product_types/l3_core_service.py b/gso/products/product_types/l3_core_service.py index 34f9beccdaecbde31ee1d8b5c3fe4c1cbda494ca..5026060ec04bcc56eacde7440109705b0dc045a2 100644 --- a/gso/products/product_types/l3_core_service.py +++ b/gso/products/product_types/l3_core_service.py @@ -1,87 +1,30 @@ -"""L3 Core Service product type.""" +"""Base class for L3 products.""" -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" - """GÉANT IP.""" - IMPORTED_GEANT_IP = "IMPORTED GÉANT IP" - IAS = "IAS" - """Internet Access Serive.""" - IMPORTED_IAS = "IMPORTED IAS" - GWS = "GWS" - """GÉANT Web Services.""" - IMPORTED_GWS = "IMPORTED GWS" - LHCONE = "LHCONE" - """LHCOne.""" - IMPORTED_LHCONE = "IMPORTED LHCONE" - COPERNICUS = "COPERNICUS" - """Copernicus.""" - IMPORTED_COPERNICUS = "IMPORTED COPERNICUS" - - -L3_CORE_SERVICE_TYPES = [ - L3CoreServiceType.GEANT_IP, - L3CoreServiceType.IAS, - L3CoreServiceType.GWS, - L3CoreServiceType.LHCONE, - L3CoreServiceType.COPERNICUS, -] -IMPORTED_L3_CORE_SERVICE_TYPES = [ - L3CoreServiceType.IMPORTED_GEANT_IP, - L3CoreServiceType.IMPORTED_IAS, - L3CoreServiceType.IMPORTED_GWS, - L3CoreServiceType.IMPORTED_LHCONE, - L3CoreServiceType.IMPORTED_COPERNICUS, -] +from __future__ import annotations +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING -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 +from orchestrator.domain import SubscriptionModel +if TYPE_CHECKING: + from gso.products.product_blocks.l3_core_service import L3CoreServiceBlockInactive -class ImportedL3CoreServiceInactive(SubscriptionModel, is_base=True): - """An imported, inactive L3 Core Service subscription.""" - l3_core_service_type: L3CoreServiceType - l3_core_service: L3CoreServiceBlockInactive +class BaseL3SubscriptionModel(SubscriptionModel, ABC): + """Base class for L3 products.""" + @property + @abstractmethod + def l3_core(self) -> L3CoreServiceBlockInactive: + """Getter: Should be implemented by subclasses.""" -class ImportedL3CoreService( - ImportedL3CoreServiceInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE] -): - """An imported L3 Core Service subscription.""" + @l3_core.setter + @abstractmethod + def l3_core(self, value: L3CoreServiceBlockInactive) -> None: + """Setter: Should be implemented by subclasses.""" - l3_core_service_type: L3CoreServiceType - l3_core_service: L3CoreServiceBlock + @property + @abstractmethod + def service_name_attribute(self) -> str: + """Get the service name.""" diff --git a/gso/products/product_types/lhcone.py b/gso/products/product_types/lhcone.py new file mode 100644 index 0000000000000000000000000000000000000000..6e130a2edc0939208c3dfc006295f9fca86121de --- /dev/null +++ b/gso/products/product_types/lhcone.py @@ -0,0 +1,81 @@ +"""Product type for LHCONE.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from orchestrator.types import SubscriptionLifecycle + +from gso.products.product_blocks.lhcone import ( + LHCOneBlock, + LHCOneBlockInactive, + LHCOneBlockProvisioning, +) +from gso.products.product_types.l3_core_service import BaseL3SubscriptionModel + +if TYPE_CHECKING: + from gso.products.product_blocks.l3_core_service import ( + L3CoreServiceBlockInactive, + ) + + +class LHCOneInactive(BaseL3SubscriptionModel, is_base=True): + """A LHCOne product that is inactive.""" + + lhcone: LHCOneBlockInactive + + @property + def l3_core(self) -> L3CoreServiceBlockInactive: + """Getter: Retrieve the l3_core from the lhcone attribute.""" + return self.lhcone.l3_core + + @l3_core.setter + def l3_core(self, value: L3CoreServiceBlockInactive) -> None: + """Setter: Set the l3_core on the lhcone attribute.""" + self.lhcone.l3_core = value + + @property + def service_name_attribute(self) -> str: + """Get the service name.""" + return "lhcone" + + +class LHCOneProvisioning(LHCOneInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """A LHCOne product that is being provisioned.""" + + lhcone: LHCOneBlockProvisioning + + +class LHCOne(LHCOneProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """A LHCOne product that is active.""" + + lhcone: LHCOneBlock + + +class ImportedLHCOneInactive(BaseL3SubscriptionModel, is_base=True): + """An imported LHCOne product that is inactive.""" + + lhcone: LHCOneBlockInactive + + @property + def l3_core(self) -> L3CoreServiceBlockInactive: + """Getter: Retrieve the l3_core from the lhcone attribute.""" + return self.lhcone.l3_core + + @l3_core.setter + def l3_core(self, value: L3CoreServiceBlockInactive) -> None: + """Setter: Set the l3_core on the lhcone attribute.""" + self.lhcone.l3_core = value + + @property + def service_name_attribute(self) -> str: + """Get the service name.""" + return "lhcone" + + +class ImportedLHCOne( + ImportedLHCOneInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE] +): + """An imported LHCOne product that is active.""" + + lhcone: LHCOneBlock diff --git a/gso/services/subscriptions.py b/gso/services/subscriptions.py index 182e54a9783f28b3b286a729c87f17d5fb93fd42..6ae731410e60b1a60e0a80b3b5cd11508e101aab 100644 --- a/gso/services/subscriptions.py +++ b/gso/services/subscriptions.py @@ -23,8 +23,9 @@ from pydantic_forms.types import UUIDstr from sqlalchemy import and_, text from sqlalchemy.exc import SQLAlchemyError -from gso.products import L2_CIRCUIT_PRODUCT_TYPE, L3_CORE_SERVICE_PRODUCT_TYPE, ProductName, ProductType +from gso.products import L2_CIRCUIT_PRODUCT_TYPE, ProductName, ProductType from gso.products.product_types.site import Site +from gso.workflows.l3_core_service.shared import L3_CORE_SERVICE_PRODUCT_TYPES SubscriptionType = dict[str, Any] @@ -197,7 +198,7 @@ def get_active_l3_services_linked_to_edge_port(edge_port_id: UUIDstr) -> list[Su .join(ProductTable) .filter( and_( - ProductTable.product_type.in_([L3_CORE_SERVICE_PRODUCT_TYPE]), + ProductTable.product_type.in_(L3_CORE_SERVICE_PRODUCT_TYPES), SubscriptionTable.status == SubscriptionLifecycle.ACTIVE, ) ) @@ -210,8 +211,6 @@ def get_active_l3_services_linked_to_edge_port(edge_port_id: UUIDstr) -> list[Su def get_active_layer_3_services_on_router(subscription_id: UUID) -> list[SubscriptionModel]: """Get all active Layer 3 services that insist on a given router `subscription_id`. - TODO: Update this method when refactoring layer 3 services. - Args: subscription_id: Subscription ID of a Router. diff --git a/gso/translations/en-GB.json b/gso/translations/en-GB.json index 02ea8449fd9eefba694548940ab8b83a6a61a85d..98d936fa0b0bbc830395531474ccf0e9b866d619 100644 --- a/gso/translations/en-GB.json +++ b/gso/translations/en-GB.json @@ -83,7 +83,10 @@ "create_edge_port": "Create Edge Port", "create_imported_edge_port": "NOT FOR HUMANS -- Import existing Edge Port", "create_imported_iptrunk": "NOT FOR HUMANS -- Import existing IP trunk", - "create_imported_l3_core_service": "NOT FOR HUMANS -- Import existing L3 Core Service", + "create_imported_geant_ip": "NOT FOR HUMANS -- Import existing GÉANT IP", + "create_imported_ias": "NOT FOR HUMANS -- Import existing IAS", + "create_imported_lhcone": "NOT FOR HUMANS -- Import existing LHCOne", + "create_imported_copernicus": "NOT FOR HUMANS -- Import existing Copernicus", "create_imported_lan_switch_interconnect": "NOT FOR HUMANS -- Import existing LAN Switch Interconnect", "create_imported_layer_2_circuit": "NOT FOR HUMANS -- Import existing Layer 2 Circuit", "create_imported_office_router": "NOT FOR HUMANS -- Import existing office router", @@ -93,7 +96,10 @@ "create_imported_super_pop_switch": "NOT FOR HUMANS -- Import existing super PoP switch", "create_imported_switch": "NOT FOR HUMANS -- Import existing Switch", "create_iptrunk": "Create IP Trunk", - "create_l3_core_service": "Create L3 Core Service", + "create_geant_ip": "Create GÉANT IP", + "create_lhcone": "Create LHCOne", + "create_copernicus": "Create Copernicus", + "create_ias": "Create IAS", "create_lan_switch_interconnect": "Create LAN Switch Interconnect", "create_layer_2_circuit": "Create Layer 2 Circuit", "create_router": "Create Router", @@ -103,7 +109,10 @@ "deploy_twamp": "Deploy TWAMP", "import_edge_port": "NOT FOR HUMANS -- Finalize import into an Edge Port", "import_iptrunk": "NOT FOR HUMANS -- Finalize import into an IP trunk product", - "import_l3_core_service": "NOT FOR HUMANS -- Finalize import into an L3 Core Service", + "import_geant_ip": "NOT FOR HUMANS -- Finalize import into a GÉANT IP product", + "import_ias": "NOT FOR HUMANS -- Finalize import into an IAS product", + "import_lhcone": "NOT FOR HUMANS -- Finalize import into a LHCOne product", + "import_copernicus": "NOT FOR HUMANS -- Finalize import into a Copernicus product", "import_lan_switch_interconnect": "NOT FOR HUMANS -- Finalize import into a LAN Switch Interconnect", "import_layer_2_circuit": "NOT FOR HUMANS -- Finalize import into a Layer 2 Circuit product", "import_office_router": "NOT FOR HUMANS -- Finalize import into an Office router product", @@ -115,11 +124,17 @@ "migrate_edge_port": "Migrate Edge Port", "migrate_iptrunk": "Migrate IP Trunk", "migrate_layer_2_circuit": "Migrate Layer 2 Circuit", - "migrate_l3_core_service": "Migrate L3 Core Service", + "migrate_ias": "Migrate IAS", + "migrate_lhcone": "Migrate LHCOne", + "migrate_copernicus": "Migrate Copernicus", + "migrate_geant_ip": "Migrate GÉANT IP", "modify_connection_strategy": "Modify connection strategy", "modify_edge_port": "Modify Edge Port", "modify_isis_metric": "Modify the ISIS metric", - "modify_l3_core_service": "Modify L3 Core Service", + "modify_ias": "Modify IAS", + "modify_lhcone": "Modify LHCOne", + "modify_copernicus": "Modify Copernicus", + "modify_geant_ip": "Modify GÉANT IP", "modify_layer_2_circuit": "Modify Layer 2 Circuit", "modify_router_kentik_license": "Modify device license in Kentik", "modify_site": "Modify Site", @@ -137,7 +152,10 @@ "task_validate_geant_products": "Validation task for GEANT products", "terminate_edge_port": "Terminate Edge Port", "terminate_iptrunk": "Terminate IP Trunk", - "terminate_l3_core_service": "Terminate L3 Core Service", + "terminate_geant_ip": "Terminate GÉANT IP", + "terminate_ias": "Terminate IAS", + "terminate_lhcone": "Terminate LHCOne", + "terminate_copernicus": "Terminate Copernicus", "terminate_lan_switch_interconnect": "Terminate LAN Switch Interconnect", "terminate_layer_2_circuit": "Terminate Layer 2 Circuit", "terminate_router": "Terminate Router", @@ -147,9 +165,12 @@ "update_ibgp_mesh": "Update iBGP mesh", "validate_edge_port": "Validate Edge Port", "validate_iptrunk": "Validate IP Trunk configuration", - "validate_l3_core_service": "Validate L3 Core Service", + "validate_geant_ip": "Validate GÉANT IP", + "validate_ias": "Validate IAS", + "validate_lhcone": "Validate LHCOne", + "validate_copernicus": "Validate Copernicus", "validate_lan_switch_interconnect": "Validate LAN Switch Interconnect", - "validate_prefix_list": "Validate Prefix-List", + "validate_geant_ip_prefix_list": "Validate GÉANT IP Prefix-List", "validate_router": "Validate Router configuration", "validate_switch": "Validate Switch configuration" } diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py index e977c62c25e434852c999d91cd0b2239a3da3e6b..f69279021b2fae317e08094b3aae013ff14bcda7 100644 --- a/gso/workflows/__init__.py +++ b/gso/workflows/__init__.py @@ -118,15 +118,44 @@ LazyWorkflowInstance("gso.workflows.edge_port.create_imported_edge_port", "creat LazyWorkflowInstance("gso.workflows.edge_port.import_edge_port", "import_edge_port") LazyWorkflowInstance("gso.workflows.edge_port.migrate_edge_port", "migrate_edge_port") -# 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") -LazyWorkflowInstance("gso.workflows.l3_core_service.validate_l3_core_service", "validate_l3_core_service") -LazyWorkflowInstance("gso.workflows.l3_core_service.terminate_l3_core_service", "terminate_l3_core_service") -LazyWorkflowInstance("gso.workflows.l3_core_service.validate_prefix_list", "validate_prefix_list") +# IAS workflows +LazyWorkflowInstance("gso.workflows.l3_core_service.ias.create_ias", "create_ias") +LazyWorkflowInstance("gso.workflows.l3_core_service.ias.modify_ias", "modify_ias") +LazyWorkflowInstance("gso.workflows.l3_core_service.ias.terminate_ias", "terminate_ias") +LazyWorkflowInstance("gso.workflows.l3_core_service.ias.migrate_ias", "migrate_ias") +LazyWorkflowInstance("gso.workflows.l3_core_service.ias.create_imported_ias", "create_imported_ias") +LazyWorkflowInstance("gso.workflows.l3_core_service.ias.import_ias", "import_ias") +LazyWorkflowInstance("gso.workflows.l3_core_service.ias.validate_ias", "validate_ias") + +# Copernicus workflows +LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.create_copernicus", "create_copernicus") +LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.modify_copernicus", "modify_copernicus") +LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.terminate_copernicus", "terminate_copernicus") +LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.migrate_copernicus", "migrate_copernicus") +LazyWorkflowInstance( + "gso.workflows.l3_core_service.copernicus.create_imported_copernicus", "create_imported_copernicus" +) +LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.import_copernicus", "import_copernicus") +LazyWorkflowInstance("gso.workflows.l3_core_service.copernicus.validate_copernicus", "validate_copernicus") + +# LHCOne workflows +LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.create_lhcone", "create_lhcone") +LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.modify_lhcone", "modify_lhcone") +LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.terminate_lhcone", "terminate_lhcone") +LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.migrate_lhcone", "migrate_lhcone") +LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.create_imported_lhcone", "create_imported_lhcone") +LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.import_lhcone", "import_lhcone") +LazyWorkflowInstance("gso.workflows.l3_core_service.lhcone.validate_lhcone", "validate_lhcone") + +# GÉANT IP workflows +LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.create_geant_ip", "create_geant_ip") +LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.modify_geant_ip", "modify_geant_ip") +LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.terminate_geant_ip", "terminate_geant_ip") +LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.migrate_geant_ip", "migrate_geant_ip") +LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.create_imported_geant_ip", "create_imported_geant_ip") +LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.import_geant_ip", "import_geant_ip") +LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.validate_geant_ip", "validate_geant_ip") +LazyWorkflowInstance("gso.workflows.l3_core_service.geant_ip.validate_prefix_list", "validate_geant_ip_prefix_list") # Layer 2 Circuit workflows LazyWorkflowInstance("gso.workflows.l2_circuit.create_layer_2_circuit", "create_layer_2_circuit") diff --git a/gso/workflows/edge_port/create_edge_port.py b/gso/workflows/edge_port/create_edge_port.py index 2455ee3de58a5b7b71dfaf0702422d497c8fbc33..d221c969fa3065ee7bf78de9585662e67b9b3307 100644 --- a/gso/workflows/edge_port/create_edge_port.py +++ b/gso/workflows/edge_port/create_edge_port.py @@ -31,8 +31,9 @@ from gso.utils.helpers import ( partner_choice, validate_edge_port_number_of_members_based_on_lacp, ) +from gso.utils.shared_enums import Vendor from gso.utils.types.geant_ids import IMPORTED_GA_ID -from gso.utils.types.interfaces import LAGMember, PhysicalPortCapacity +from gso.utils.types.interfaces import JuniperLAGMember, LAGMember, PhysicalPortCapacity from gso.utils.types.tt_number import TTNumber from gso.utils.workflow_steps import start_moodi, stop_moodi from gso.workflows.shared import create_summary_form @@ -50,7 +51,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: service_type: EdgePortType enable_lacp: bool = False speed: PhysicalPortCapacity - encapsulation: EncapsulationType = EncapsulationType.DOT1Q + encapsulation: EncapsulationType | str = EncapsulationType.DOT1Q # TODO: remove type hint workaround number_of_members: int minimum_links: int mac_address: str | None = None @@ -81,13 +82,16 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: initial_user_input = yield CreateEdgePortForm - class EdgePortLAGMember(LAGMember): + class NokiaEdgePortLAGMember(LAGMember): interface_name: available_interfaces_choices( # type: ignore[valid-type] initial_user_input.node, initial_user_input.speed ) + selected_router = Router.from_subscription(initial_user_input.node) + lag_member = JuniperLAGMember if selected_router.router.vendor == Vendor.JUNIPER else NokiaEdgePortLAGMember + lag_ae_members = Annotated[ - list[EdgePortLAGMember], + list[lag_member], # type: ignore[valid-type] AfterValidator(validate_unique_list), Len( min_length=initial_user_input.number_of_members, @@ -98,7 +102,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: class SelectInterfaceForm(FormPage): model_config = ConfigDict(title="Select Interfaces") - name: available_service_lags_choices(initial_user_input.node) # type: ignore[valid-type] + name: available_service_lags_choices(initial_user_input.node) or str # type: ignore[valid-type] description: str | None = None ae_members: lag_ae_members diff --git a/gso/workflows/l3_core_service/create_imported_l3_core_service.py b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py similarity index 56% rename from gso/workflows/l3_core_service/create_imported_l3_core_service.py rename to gso/workflows/l3_core_service/base_create_imported_l3_core_service.py index 70753dd08b427c3fc0930ada60a64713a636e4b8..3f8bf13f324771ea4dd2c4f33dffbafb65d2c26e 100644 --- a/gso/workflows/l3_core_service/create_imported_l3_core_service.py +++ b/gso/workflows/l3_core_service/base_create_imported_l3_core_service.py @@ -2,28 +2,21 @@ from uuid import uuid4 -from orchestrator import workflow +from orchestrator.domain import SubscriptionModel from orchestrator.forms import SubmitFormPage -from orchestrator.targets import Target -from orchestrator.types import 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 orchestrator.workflow import step from pydantic import BaseModel, NonNegativeInt from pydantic_forms.types import FormGenerator, UUIDstr -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 AccessPortInactive +from gso.products.product_blocks.l3_core_service import AccessPortInactive, L3CoreServiceBlockInactive 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.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.geant_ids import IMPORTED_GS_ID from gso.utils.types.ip_address import IPAddress, IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask from gso.utils.types.virtual_identifiers import VLAN_ID +from gso.workflows.l3_core_service.shared import L3ProductNameType def initial_input_form_generator() -> FormGenerator: @@ -70,37 +63,15 @@ def initial_input_form_generator() -> FormGenerator: class ImportL3CoreServiceForm(SubmitFormPage): partner: str service_binding_ports: list[ServiceBindingPort] - service_type: L3CoreServiceType + product_name: L3ProductNameType user_input = yield ImportL3CoreServiceForm return user_input.model_dump() -@step("Create subscription") -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 - 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: ImportedL3CoreServiceInactive, service_binding_ports: list) -> dict: +def initialize_subscription(subscription: SubscriptionModel, 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")) @@ -115,7 +86,7 @@ def initialize_subscription(subscription: ImportedL3CoreServiceInactive, service ) for session in bgp_peers ] - service_binding_port_subscription = ServiceBindingPortInactive.new( + service_binding_port_block = ServiceBindingPortInactive.new( subscription_id=uuid4(), edge_port=edge_port_subscription.edge_port, bgp_session_list=sbp_bgp_session_list, @@ -123,31 +94,17 @@ def initialize_subscription(subscription: ImportedL3CoreServiceInactive, service v6_bfd_settings=BFDSettings.new(subscription_id=uuid4(), **(service_binding_port.pop("v6_bfd_settings"))), **service_binding_port, ) - subscription.l3_core_service.ap_list.append( - AccessPortInactive.new( - subscription_id=uuid4(), - ap_type=service_binding_port["ap_type"], - sbp=service_binding_port_subscription, - custom_service_name=service_binding_port.get("custom_service_name"), - ) + + subscription.l3_core = L3CoreServiceBlockInactive.new( # type: ignore[attr-defined] + subscription_id=uuid4(), + ap_list=[ + AccessPortInactive.new( + subscription_id=uuid4(), + ap_type=service_binding_port["ap_type"], + sbp=service_binding_port_block, + custom_service_name=service_binding_port.get("custom_service_name"), + ) + ], ) return {"subscription": subscription} - - -@workflow( - "Create imported L3 Core Service", - initial_input_form=initial_input_form_generator, - target=Target.CREATE, -) -def create_imported_l3_core_service() -> StepList: - """Import a GÉANT IP 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/l3_core_service/create_l3_core_service.py b/gso/workflows/l3_core_service/base_create_l3_core_service.py similarity index 80% rename from gso/workflows/l3_core_service/create_l3_core_service.py rename to gso/workflows/l3_core_service/base_create_l3_core_service.py index bea3b0e6bdd0af56c23d4f16d3c35cf448825a28..6d8c451ea82e3af081020977c05943e06797c4d2 100644 --- a/gso/workflows/l3_core_service/create_l3_core_service.py +++ b/gso/workflows/l3_core_service/base_create_l3_core_service.py @@ -1,25 +1,21 @@ -"""Create a new L3 Core Service subscription including GÉANT IP and IAS.""" +"""Base workflow for creating a new L3 Core Service.""" from typing import Any from uuid import uuid4 +from orchestrator.domain import SubscriptionModel from orchestrator.forms import FormPage, SubmitFormPage from orchestrator.forms.validators import Label -from orchestrator.targets import Target -from orchestrator.types import SubscriptionLifecycle -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 orchestrator.workflow import step from pydantic import BaseModel, ConfigDict, Field, NonNegativeInt, computed_field, model_validator from pydantic_forms.types import FormGenerator, State, UUIDstr from pydantic_forms.validators import Divider 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.l3_core_service import AccessPortInactive, L3CoreServiceBlockInactive 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.l3_core_service import L3CoreService, L3CoreServiceInactive -from gso.services.lso_client import LSOState, lso_interaction +from gso.services.lso_client import LSOState from gso.services.partners import get_partner_by_id from gso.services.sharepoint import SharePointClient from gso.services.subscriptions import generate_unique_id @@ -33,7 +29,6 @@ from gso.utils.types.geant_ids import IMPORTED_GS_ID 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 -from gso.utils.workflow_steps import prompt_sharepoint_checklist_url, start_moodi, stop_moodi def initial_input_form_generator(product_name: str) -> FormGenerator: @@ -165,31 +160,30 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: ) -@step("Create subscription") -def create_subscription(product: UUIDstr, partner: str) -> State: - """Create a new subscription object in the database.""" - subscription = L3CoreServiceInactive.from_product_id(product, partner) - - return {"subscription": subscription, "subscription_id": subscription.subscription_id} - - @step("Initialize subscription") def initialize_subscription( - 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.""" + subscription: SubscriptionModel, + edge_port: dict, + binding_port_input: dict, + product_name: str, +) -> dict: + """Initialize a service binding port for a given service type in the subscription model.""" edge_port_fqdn_list = [] 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"] ] + sbp_gs_id = ( generate_unique_id(prefix="GS") if binding_port_input.pop("generate_gs_id", False) - else binding_port_input.pop("gs_id") + else binding_port_input.pop("gs_id", None) ) + binding_port_input.pop("gs_id", None) + service_binding_port = ServiceBindingPortInactive.new( subscription_id=uuid4(), v4_bfd_settings=BFDSettings.new(subscription_id=uuid4(), **(binding_port_input.pop("v4_bfd_settings"))), @@ -200,18 +194,23 @@ def initialize_subscription( edge_port=edge_port_subscription.edge_port, gs_id=sbp_gs_id, ) - subscription.l3_core_service.ap_list.append( - AccessPortInactive.new( - subscription_id=uuid4(), - ap_type=edge_port["ap_type"], - sbp=service_binding_port, - custom_service_name=edge_port.get("custom_service_name"), - ) + + subscription.l3_core = L3CoreServiceBlockInactive.new( # type: ignore[attr-defined] + subscription_id=uuid4(), + ap_list=[ + AccessPortInactive.new( + subscription_id=uuid4(), + ap_type=edge_port["ap_type"], + sbp=service_binding_port, + custom_service_name=edge_port.get("custom_service_name"), + ) + ], ) edge_port_fqdn_list.append(edge_port_subscription.edge_port.node.router_fqdn) partner_name = get_partner_by_id(subscription.customer_id).name subscription.description = f"{product_name} service for {partner_name}" + return {"subscription": subscription, "edge_port_fqdn_list": edge_port_fqdn_list, "partner_name": partner_name} @@ -317,7 +316,7 @@ 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 BGP peers.""" + """Check the correct deployment of BGP peers.""" extra_vars = {"subscription": subscription, "verb": "check", "object": "bgp"} return { @@ -328,62 +327,30 @@ def check_bgp_peers(subscription: dict[str, Any], edge_port_fqdn_list: list[str] @step("Update Infoblox") -def update_dns_records(subscription: L3CoreService) -> State: +def update_dns_records(subscription: SubscriptionModel) -> State: """Update DNS records in Infoblox.""" # TODO: implement return {"subscription": subscription} @step("Create a new SharePoint checklist item") -def create_new_sharepoint_checklist(subscription: L3CoreService, tt_number: TTNumber, process_id: UUIDstr) -> State: +def create_new_sharepoint_checklist( + subscription: SubscriptionModel, + tt_number: TTNumber, + process_id: UUIDstr, +) -> State: """Create a new checklist item in SharePoint for approving this L3 Core Service.""" - new_ep = subscription.l3_core_service.ap_list[0].sbp.edge_port + new_ep = subscription.l3_core.ap_list[0].sbp.edge_port # type: ignore[attr-defined] new_list_item_url = SharePointClient().add_list_item( list_name="l3_core_service", fields={ "Title": f"{subscription.description}", "TT_NUMBER": tt_number, "ACTIVITY_TYPE": "Creation", - "PRODUCT_TYPE": subscription.l3_core_service_type, + "PRODUCT_TYPE": subscription.product.name, "LOCATION": f"{new_ep.edge_port_name} {new_ep.edge_port_description} on {new_ep.node.router_fqdn}", "GAP_PROCESS_URL": f"{load_oss_params().GENERAL.public_hostname}/workflows/{process_id}", }, ) return {"checklist_url": new_list_item_url} - - -@workflow( - "Create L3 Core Service", - initial_input_form=wrap_create_initial_input_form(initial_input_form_generator), - target=Target.CREATE, -) -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 - * Deploy BGP peers - * Update DNS records - * Set the subscription in a provisioning state in the database - """ - return ( - begin - >> create_subscription - >> store_process_subscription(Target.CREATE) - >> initialize_subscription - >> start_moodi() - >> lso_interaction(provision_sbp_dry) - >> lso_interaction(provision_sbp_real) - >> lso_interaction(check_sbp_functionality) - >> lso_interaction(deploy_bgp_peers_dry) - >> lso_interaction(deploy_bgp_peers_real) - >> lso_interaction(check_bgp_peers) - >> update_dns_records - >> set_status(SubscriptionLifecycle.ACTIVE) - >> resync - >> create_new_sharepoint_checklist - >> prompt_sharepoint_checklist_url - >> stop_moodi() - >> done - ) diff --git a/gso/workflows/l3_core_service/migrate_l3_core_service.py b/gso/workflows/l3_core_service/base_migrate_l3_core_service.py similarity index 80% rename from gso/workflows/l3_core_service/migrate_l3_core_service.py rename to gso/workflows/l3_core_service/base_migrate_l3_core_service.py index 3f97983c5d72ffff255257cc735bd800d3e09785..3ae59b1fd6a9b608d3011fd6c42d798ecac6ba5c 100644 --- a/gso/workflows/l3_core_service/migrate_l3_core_service.py +++ b/gso/workflows/l3_core_service/base_migrate_l3_core_service.py @@ -11,38 +11,36 @@ destination Edge Port that this service should be placed on. All other Access Po import json from typing import Any -from orchestrator import workflow from orchestrator.config.assignee import Assignee +from orchestrator.domain import SubscriptionModel from orchestrator.forms import FormPage, SubmitFormPage -from orchestrator.targets import Target from orchestrator.utils.errors import ProcessFailureError from orchestrator.utils.json import json_dumps -from orchestrator.workflow import StepList, begin, conditional, done, inputstep, step -from orchestrator.workflows.steps import resync, store_process_subscription, unsync -from orchestrator.workflows.utils import wrap_modify_initial_input_form +from orchestrator.workflow import inputstep, step from pydantic import ConfigDict, Field from pydantic_forms.types import FormGenerator, State, UUIDstr from pydantic_forms.validators import Choice, Divider, Label from gso.products.product_types.edge_port import EdgePort -from gso.products.product_types.l3_core_service import L3CoreService -from gso.services.lso_client import LSOState, lso_interaction +from gso.services.lso_client import LSOState from gso.services.partners import get_partner_by_id from gso.services.subscriptions import get_active_edge_port_subscriptions from gso.utils.types.tt_number import TTNumber -from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, SKIP_MOODI_KEY, start_moodi, stop_moodi +from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, SKIP_MOODI_KEY from gso.workflows.shared import create_summary_form -def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: +def initial_input_form(subscription_id: UUIDstr) -> FormGenerator: """Gather input from the operator on what destination Edge Ports this L3 Core Service should be migrated to.""" - subscription = L3CoreService.from_subscription(subscription_id) + subscription = SubscriptionModel.from_subscription(subscription_id) partner_id = subscription.customer_id + ap_list = subscription.l3_core.ap_list # type: ignore[attr-defined] + current_ep_list = { str( ap.sbp.edge_port.owner_subscription_id ): f"{EdgePort.from_subscription(ap.sbp.edge_port.owner_subscription_id).description} ({ap.ap_type})" - for ap in subscription.l3_core_service.ap_list + for ap in ap_list } source_edge_port_selector = Choice( "Select an Edge Port", @@ -50,7 +48,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: ) class L3CoreServiceSourceEdgePortSelectionForm(FormPage): - model_config = ConfigDict(title=f"Migrating a(n) {subscription.l3_core_service_type} AP to a new Edge Port") + model_config = ConfigDict(title=f"Migrating a(n) {subscription.product.name} AP to a new Edge Port") tt_number: TTNumber divider: Divider = Field(None, exclude=True) @@ -61,7 +59,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: source_ep_user_input = yield L3CoreServiceSourceEdgePortSelectionForm def _destination_edge_port_selector(pid: UUIDstr) -> Choice: - existing_ep_list = [ap.sbp.edge_port.owner_subscription_id for ap in subscription.l3_core_service.ap_list] + existing_ep_list = [ap.sbp.edge_port.owner_subscription_id for ap in ap_list] edge_port_subscriptions = list( filter( lambda ep: bool(ep.customer_id == pid) and ep.subscription_id not in existing_ep_list, @@ -79,7 +77,6 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: destination_edge_port: _destination_edge_port_selector(partner_id) | str # type: ignore[valid-type] destination_ep_user_input = yield L3CoreServiceEdgePortSelectionForm - if source_ep_user_input.is_human_initiated_wf: summary_input = { "source_edge_port": EdgePort.from_subscription(source_ep_user_input.source_edge_port).description, @@ -87,12 +84,10 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: destination_ep_user_input.destination_edge_port ).description, } - yield from create_summary_form( - summary_input, subscription.l3_core_service_type.value, list(summary_input.keys()) - ) + yield from create_summary_form(summary_input, subscription.product.name, list(summary_input.keys())) return ( - {"subscription_id": subscription_id, "subscription": subscription} + {"subscription_id": subscription.subscription_id, "subscription": subscription} | source_ep_user_input.model_dump() | destination_ep_user_input.model_dump() | { @@ -103,7 +98,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: @step("Inject Partner Name") -def inject_partner_name(subscription: L3CoreService) -> LSOState: +def inject_partner_name(subscription: SubscriptionModel) -> LSOState: """Resolve and inject partner name into the state.""" partner_name = get_partner_by_id(subscription.customer_id).name @@ -112,7 +107,11 @@ def inject_partner_name(subscription: L3CoreService) -> LSOState: @step("Show BGP neighbors") def show_bgp_neighbors( - subscription: L3CoreService, process_id: UUIDstr, tt_number: TTNumber, source_edge_port: EdgePort, partner_name: str + subscription: SubscriptionModel, + process_id: UUIDstr, + tt_number: TTNumber, + source_edge_port: EdgePort, + partner_name: str, ) -> LSOState: """List all BGP neighbors on the source router, to present an expected base-line for the new one.""" source_access_port_fqdn = source_edge_port.edge_port.node.router_fqdn @@ -134,7 +133,7 @@ def show_bgp_neighbors( @step("[DRY RUN] Deactivate BGP session on the source router") def deactivate_bgp_dry( - subscription: L3CoreService, + subscription: SubscriptionModel, process_id: UUIDstr, tt_number: TTNumber, source_access_port_fqdn: str, @@ -157,7 +156,7 @@ def deactivate_bgp_dry( @step("[FOR REAL] Deactivate BGP session on the source router") def deactivate_bgp_real( - subscription: L3CoreService, + subscription: SubscriptionModel, process_id: UUIDstr, tt_number: TTNumber, source_access_port_fqdn: str, @@ -200,7 +199,7 @@ def inform_operator_traffic_check() -> FormGenerator: @step("[DRY RUN] Deactivate SBP config on the source router") def deactivate_sbp_dry( - subscription: L3CoreService, + subscription: SubscriptionModel, process_id: UUIDstr, tt_number: TTNumber, source_access_port_fqdn: str, @@ -222,7 +221,7 @@ def deactivate_sbp_dry( @step("[FOR REAL] Deactivate SBP config on the source router") def deactivate_sbp_real( - subscription: L3CoreService, + subscription: SubscriptionModel, process_id: UUIDstr, tt_number: TTNumber, source_access_port_fqdn: str, @@ -245,23 +244,26 @@ def deactivate_sbp_real( @step("Generate updated subscription model") def generate_scoped_subscription_model( - subscription: L3CoreService, source_edge_port: EdgePort, destination_edge_port: EdgePort + subscription: SubscriptionModel, + source_edge_port: EdgePort, + destination_edge_port: EdgePort, ) -> State: """Calculate what the updated subscription model will look like, but don't update the actual subscription yet. The new subscription is used for running Ansible playbooks remotely, but the updated subscription model is not stored yet, to avoid issues recovering when the workflow is aborted. """ + service_name = subscription.service_name_attribute # type: ignore[attr-defined] updated_subscription = json.loads(json_dumps(subscription)) - for index, ap in enumerate(updated_subscription["l3_core_service"]["ap_list"]): + for index, ap in enumerate(updated_subscription[service_name]["l3_core"]["ap_list"]): if ap["sbp"]["edge_port"]["owner_subscription_id"] == str(source_edge_port.subscription_id): # We have found the AP that is to be replaced, we can return all the necessary information to the state. # First, remove all unneeded unchanged APs that should not be included when executing a playbook. - updated_subscription["l3_core_service"]["ap_list"] = [ - updated_subscription["l3_core_service"]["ap_list"][index] + updated_subscription[service_name]["l3_core"]["ap_list"] = [ + updated_subscription[service_name]["l3_core"]["ap_list"][index] ] # Then replace the AP that is migrated such that it includes the destination EP instead of the source one. - updated_subscription["l3_core_service"]["ap_list"][0]["sbp"]["edge_port"] = json.loads( + updated_subscription[service_name]["l3_core"]["ap_list"][0]["sbp"]["edge_port"] = json.loads( json_dumps(destination_edge_port.edge_port) ) return {"scoped_subscription": updated_subscription, "replaced_ap_index": index} @@ -369,7 +371,7 @@ def deploy_bgp_session_real( @step("Update Infoblox") -def update_dns_records(subscription: L3CoreService) -> State: +def update_dns_records(subscription: SubscriptionModel) -> State: """Update DNS records in Infoblox.""" # TODO: implement return {"subscription": subscription} @@ -377,43 +379,12 @@ def update_dns_records(subscription: L3CoreService) -> State: @step("Update subscription model") def update_subscription_model( - subscription: L3CoreService, destination_edge_port: EdgePort, replaced_ap_index: int + subscription: SubscriptionModel, + destination_edge_port: EdgePort, + replaced_ap_index: int, ) -> State: """Update the subscription model with the destination Edge Port attached to the Access Port that is migrated.""" - subscription.l3_core_service.ap_list[replaced_ap_index].sbp.edge_port = destination_edge_port.edge_port + ap_list = subscription.l3_core.ap_list # type: ignore[attr-defined] + ap_list[replaced_ap_index].sbp.edge_port = destination_edge_port.edge_port return {"subscription": subscription, "__remove_keys": ["replaced_ap_index"]} - - -@workflow( - "Migrate L3 Core Service", - initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), - target=Target.MODIFY, -) -def migrate_l3_core_service() -> StepList: - """Migrate a L3 Core Service to a destination Edge Port.""" - is_human_initiated_wf = conditional(lambda state: bool(state.get(IS_HUMAN_INITIATED_WF_KEY))) - - return ( - begin - >> store_process_subscription(Target.MODIFY) - >> inject_partner_name - >> is_human_initiated_wf(lso_interaction(show_bgp_neighbors)) # TODO: send OTRS email with pre-check results - >> is_human_initiated_wf(lso_interaction(deactivate_bgp_dry)) - >> is_human_initiated_wf(lso_interaction(deactivate_bgp_real)) - >> is_human_initiated_wf(lso_interaction(deactivate_sbp_dry)) - >> is_human_initiated_wf(lso_interaction(deactivate_sbp_real)) - >> is_human_initiated_wf(inform_operator_traffic_check) - >> unsync - >> generate_scoped_subscription_model - >> start_moodi() # TODO: include results from first LSO run - >> lso_interaction(deploy_destination_ep_dry) - >> lso_interaction(deploy_destination_ep_real) - >> lso_interaction(deploy_bgp_session_dry) - >> lso_interaction(deploy_bgp_session_real) - >> update_dns_records - >> update_subscription_model - >> resync - >> stop_moodi() - >> done - ) diff --git a/gso/workflows/l3_core_service/modify_l3_core_service.py b/gso/workflows/l3_core_service/base_modify_l3_core_service.py similarity index 88% rename from gso/workflows/l3_core_service/modify_l3_core_service.py rename to gso/workflows/l3_core_service/base_modify_l3_core_service.py index 427717fdf037de34953c935fffbc3ec33ef75f93..4185ef26965e3fe003c51dcf8cdb0c0f61db64b4 100644 --- a/gso/workflows/l3_core_service/modify_l3_core_service.py +++ b/gso/workflows/l3_core_service/base_modify_l3_core_service.py @@ -1,19 +1,11 @@ -"""A modification workflow for a L3 Core Service subscription. - -Only one operation can be performed per workflow run. This is enforced through the input form at the start of the -workflow. One access port can either be added, removed, or modified. Every one of these operations requires a separate -maintenance ticket and therefore should be in separate workflow runs. -""" +"""Base functionality for modifying an L3 Core Service subscription.""" from typing import Any, TypeAlias, cast from uuid import UUID, uuid4 -from orchestrator import begin, conditional, done, step, workflow +from orchestrator import step +from orchestrator.domain import SubscriptionModel from orchestrator.forms import FormPage -from orchestrator.targets import Target -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 BaseModel, ConfigDict, Field, NonNegativeInt, computed_field, field_validator, model_validator from pydantic_forms.types import FormGenerator, State, UUIDstr, strEnum from pydantic_forms.validators import Choice, Divider, Label, ReadOnlyField @@ -22,7 +14,6 @@ from gso.products.product_blocks.bgp_session import BGPSession, IPFamily, IPType 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 L3CoreService from gso.services.subscriptions import generate_unique_id, get_active_edge_port_subscriptions from gso.utils.shared_enums import APType, SBPType from gso.utils.types.geant_ids import IMPORTED_GS_ID @@ -41,7 +32,7 @@ class Operation(strEnum): def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: """Get input about added, removed, and modified Access Ports.""" - subscription = L3CoreService.from_subscription(subscription_id) + subscription = SubscriptionModel.from_subscription(subscription_id) product_name = subscription.product.name class OperationSelectionForm(FormPage): @@ -52,7 +43,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: def access_port_selector() -> TypeAlias: """Generate a dropdown selector for choosing an Access Port in an input form.""" - access_ports = subscription.l3_core_service.ap_list + access_ports = subscription.l3_core.ap_list # type: ignore[attr-defined] options = { str(access_port.subscription_instance_id): ( f"{access_port.sbp.gs_id} on " @@ -135,7 +126,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: str(edge_port.subscription_id): edge_port.description for edge_port in edge_ports if edge_port.subscription_id - not in [ap.sbp.edge_port.owner_subscription_id for ap in subscription.l3_core_service.ap_list] + not in [ap.sbp.edge_port.owner_subscription_id for ap in subscription.l3_core.ap_list] # type: ignore[attr-defined] } return cast( @@ -158,7 +149,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: ap_type=access_port.ap_type.value, custom_service_name=access_port.custom_service_name or "", ) - for access_port in subscription.l3_core_service.ap_list + for access_port in subscription.l3_core.ap_list # type: ignore[attr-defined] ], default_type=list[AccessPortListItem], ), @@ -212,9 +203,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: @field_validator("edge_port") def selected_edge_port_is_new(cls, value: UUIDstr) -> UUIDstr: - if value in [ - str(ap.sbp.edge_port.owner_subscription_id) for ap in subscription.l3_core_service.ap_list - ]: + if value in [str(ap.sbp.edge_port.owner_subscription_id) for ap in subscription.l3_core.ap_list]: # type: ignore[attr-defined] error_message = ( f"This {product_name} service is already deployed on " f"{EdgePort.from_subscription(value).description}." @@ -223,7 +212,10 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: return value user_input = yield AddAccessPortForm - return {"operation": initial_input.operation, "added_access_port": user_input} + return { + "operation": initial_input.operation, + "added_access_port": user_input.model_dump(), + } case Operation.REMOVE: @@ -237,7 +229,10 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: access_port: access_port_selector() # type: ignore[valid-type] user_input = yield RemoveAccessPortForm - return {"operation": initial_input.operation, "removed_access_port": user_input.access_port} + return { + "operation": initial_input.operation, + "removed_access_port": user_input.access_port, + } case Operation.EDIT: @@ -362,7 +357,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: return { "operation": initial_input.operation, "modified_access_port": user_input.access_port, - "modified_sbp": binding_port_input_form, + "modified_sbp": binding_port_input_form.model_dump(), } case _: @@ -371,7 +366,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: @step("Instantiate new Service Binding Ports") -def create_new_sbp(subscription: L3CoreService, added_access_port: dict[str, Any]) -> State: +def create_new_sbp(subscription: SubscriptionModel, added_access_port: dict[str, Any]) -> State: """Add new SBP to the L3 Core Service subscription.""" edge_port = EdgePort.from_subscription(added_access_port.pop("edge_port")) bgp_session_list = [ @@ -396,7 +391,7 @@ def create_new_sbp(subscription: L3CoreService, added_access_port: dict[str, Any edge_port=edge_port.edge_port, gs_id=sbp_gs_id, ) - subscription.l3_core_service.ap_list.append( + subscription.l3_core.ap_list.append( # type: ignore[attr-defined] AccessPort.new( subscription_id=uuid4(), ap_type=added_access_port["ap_type"], @@ -409,20 +404,24 @@ def create_new_sbp(subscription: L3CoreService, added_access_port: dict[str, Any @step("Clean up removed Edge Ports") -def remove_old_sbp(subscription: L3CoreService, removed_access_port: UUIDstr) -> State: - """Remove old SBP product blocks from the GÉANT IP subscription.""" - subscription.l3_core_service.ap_list.remove(AccessPort.from_db(UUID(removed_access_port))) +def remove_old_sbp(subscription: SubscriptionModel, removed_access_port: UUIDstr) -> State: + """Remove old SBP product blocks from the specific L3 core service subscription.""" + subscription.l3_core.ap_list.remove(AccessPort.from_db(UUID(removed_access_port))) # type: ignore[attr-defined] return {"subscription": subscription} @step("Modify existing Service Binding Ports") def modify_existing_sbp( - subscription: L3CoreService, modified_access_port: UUIDstr, modified_sbp: dict[str, Any] + subscription: SubscriptionModel, + modified_access_port: UUIDstr, + modified_sbp: dict[str, Any], ) -> State: """Update the subscription model.""" current_ap = next( - ap for ap in subscription.l3_core_service.ap_list if str(ap.subscription_instance_id) == modified_access_port + ap + for ap in subscription.l3_core.ap_list # type: ignore[attr-defined] + if str(ap.subscription_instance_id) == modified_access_port ) v4_peer = next(peer for peer in current_ap.sbp.bgp_session_list if IPFamily.V4UNICAST in peer.families) for attribute in modified_sbp["v4_bgp_peer"]: @@ -449,26 +448,3 @@ def modify_existing_sbp( current_ap.custom_service_name = modified_sbp["custom_service_name"] return {"subscription": subscription} - - -@workflow( - "Modify L3 Core Service", - initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), - target=Target.MODIFY, -) -def modify_l3_core_service() -> StepList: - """Modify an L3 Core Service subscription.""" - access_port_is_added = conditional(lambda state: state["operation"] == Operation.ADD) - access_port_is_removed = conditional(lambda state: state["operation"] == Operation.REMOVE) - access_port_is_modified = conditional(lambda state: state["operation"] == Operation.EDIT) - - return ( - begin - >> store_process_subscription(Target.MODIFY) - >> unsync - >> access_port_is_added(create_new_sbp) - >> access_port_is_removed(remove_old_sbp) - >> access_port_is_modified(modify_existing_sbp) - >> resync - >> done - ) diff --git a/gso/workflows/l3_core_service/validate_l3_core_service.py b/gso/workflows/l3_core_service/base_validate_l3_core_service.py similarity index 66% rename from gso/workflows/l3_core_service/validate_l3_core_service.py rename to gso/workflows/l3_core_service/base_validate_l3_core_service.py index 0b02e34f6695578755c7c167755d8837a4f9fe63..f1e5f3a0045ca9c1a5bc64685428e2e97b2c77cf 100644 --- a/gso/workflows/l3_core_service/validate_l3_core_service.py +++ b/gso/workflows/l3_core_service/base_validate_l3_core_service.py @@ -2,21 +2,19 @@ from typing import Any -from orchestrator.targets import Target -from orchestrator.workflow import StepList, begin, done, step, workflow -from orchestrator.workflows.steps import resync, store_process_subscription, unsync -from orchestrator.workflows.utils import wrap_modify_initial_input_form -from pydantic_forms.types import State, UUIDstr +from orchestrator.domain import SubscriptionModel +from orchestrator.workflow import step +from pydantic_forms.types import UUIDstr -from gso.products.product_types.l3_core_service import L3CoreService -from gso.services.lso_client import LSOState, anonymous_lso_interaction +from gso.services.lso_client import LSOState from gso.services.partners import get_partner_by_id @step("Prepare list of all Access Ports") -def build_fqdn_list(subscription: L3CoreService) -> State: +def build_fqdn_list(subscription: SubscriptionModel) -> dict[str, list[str]]: """Build the list of all FQDNs that are in the list of access ports of a L3 Core Service subscription.""" - ap_fqdn_list = [ap.sbp.edge_port.node.router_fqdn for ap in subscription.l3_core_service.ap_list] + ap_list = subscription.l3_core.ap_list # type: ignore[attr-defined] + ap_fqdn_list = [ap.sbp.edge_port.node.router_fqdn for ap in ap_list] return {"ap_fqdn_list": ap_fqdn_list} @@ -64,19 +62,3 @@ def validate_bgp_peers(subscription: dict[str, Any], process_id: UUIDstr, ap_fqd def validate_dns_records() -> None: """Validate DNS records in Infoblox.""" # TODO: implement - - -@workflow("Validate L3 Core Service", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None))) -def validate_l3_core_service() -> StepList: - """Validate an existing L3 Core Service subscription.""" - return ( - begin - >> store_process_subscription(Target.SYSTEM) - >> unsync - >> build_fqdn_list - >> anonymous_lso_interaction(validate_sbp_config) - >> anonymous_lso_interaction(validate_bgp_peers) - >> validate_dns_records - >> resync - >> done - ) diff --git a/gso/workflows/l3_core_service/validate_prefix_list.py b/gso/workflows/l3_core_service/base_validate_prefix_list.py similarity index 66% rename from gso/workflows/l3_core_service/validate_prefix_list.py rename to gso/workflows/l3_core_service/base_validate_prefix_list.py index 9c9b6cd4321e07d78ce98524487d880f8b1cfb54..1771426c9fa5a701051f64b363c346ba3a282c84 100644 --- a/gso/workflows/l3_core_service/validate_prefix_list.py +++ b/gso/workflows/l3_core_service/base_validate_prefix_list.py @@ -3,17 +3,14 @@ from typing import Any from orchestrator.config.assignee import Assignee +from orchestrator.domain import SubscriptionModel from orchestrator.forms import SubmitFormPage -from orchestrator.targets import Target -from orchestrator.workflow import StepList, begin, conditional, done, inputstep, step, workflow -from orchestrator.workflows.steps import resync, store_process_subscription, unsync -from orchestrator.workflows.utils import wrap_modify_initial_input_form +from orchestrator.workflow import inputstep, step from pydantic import Field from pydantic_forms.types import FormGenerator, State, UUIDstr from pydantic_forms.validators import Label -from gso.products.product_types.l3_core_service import L3CoreService, L3CoreServiceType -from gso.services.lso_client import LSOState, anonymous_lso_interaction, lso_interaction +from gso.services.lso_client import LSOState from gso.services.partners import get_partner_by_id from gso.utils.shared_enums import Vendor @@ -21,11 +18,10 @@ from gso.utils.shared_enums import Vendor @step("Prepare list of all Access Ports") def build_fqdn_list(subscription_id: UUIDstr) -> State: """Build the list of all FQDNs in the access ports of L3 Core Service subscription, excluding Juniper devices.""" - subscription = L3CoreService.from_subscription(subscription_id) + subscription = SubscriptionModel.from_subscription(subscription_id) + ap_list = subscription.l3_core.ap_list # type: ignore[attr-defined] ap_fqdn_list = [ - ap.sbp.edge_port.node.router_fqdn - for ap in subscription.l3_core_service.ap_list - if ap.sbp.edge_port.node.vendor != Vendor.JUNIPER + ap.sbp.edge_port.node.router_fqdn for ap in ap_list if ap.sbp.edge_port.node.vendor != Vendor.JUNIPER ] return {"ap_fqdn_list": ap_fqdn_list, "subscription": subscription} @@ -51,7 +47,8 @@ def validate_prefix_lists_dry(subscription: dict[str, Any], process_id: UUIDstr, @step("Evaluate validation of Prefix-Lists") -def _evaluate_result_has_diff(callback_result: dict) -> State: +def evaluate_result_has_diff(callback_result: dict) -> State: + """Evaluate the result of the playbook that validates prefix-lists.""" return {"callback_result": callback_result, "prefix_list_drift": bool(callback_result["return_code"] != 0)} @@ -106,36 +103,3 @@ def deploy_prefix_lists_real(subscription: dict[str, Any], process_id: UUIDstr, "inventory": {"all": {"hosts": dict.fromkeys(ap_fqdn_list)}}, "extra_vars": extra_vars, } - - -@workflow("Validate Prefix-List", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None))) -def validate_prefix_list() -> StepList: - """Validate prefix-lists for an existing L3 Core Service subscription.""" - prefix_list_should_be_validated = conditional( - lambda state: state["subscription"]["l3_core_service_type"] == L3CoreServiceType.GEANT_IP - ) - fqdn_list_is_empty = conditional(lambda state: state["ap_fqdn_list"] == []) - prefix_list_has_drifted = conditional(lambda state: bool(state["prefix_list_drift"])) - - redeploy_prefix_list_steps = ( - begin - >> unsync - >> await_operator - >> lso_interaction(deploy_prefix_lists_dry) - >> lso_interaction(deploy_prefix_lists_real) - >> resync - ) - prefix_list_validation_steps = ( - begin - >> anonymous_lso_interaction(validate_prefix_lists_dry, _evaluate_result_has_diff) - >> prefix_list_has_drifted(redeploy_prefix_list_steps) - ) - - return ( - begin - >> store_process_subscription(Target.SYSTEM) - >> build_fqdn_list - >> fqdn_list_is_empty(done) - >> prefix_list_should_be_validated(prefix_list_validation_steps) - >> done - ) diff --git a/gso/workflows/l3_core_service/copernicus/__init__.py b/gso/workflows/l3_core_service/copernicus/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2b2a70aad17271ebf70d4fd7f7f9fe7635b9bd75 --- /dev/null +++ b/gso/workflows/l3_core_service/copernicus/__init__.py @@ -0,0 +1 @@ +"""Copernicus service workflows.""" diff --git a/gso/workflows/l3_core_service/copernicus/create_copernicus.py b/gso/workflows/l3_core_service/copernicus/create_copernicus.py new file mode 100644 index 0000000000000000000000000000000000000000..c011f1865aef6e7cba2f06e6d378c7ec390bff9f --- /dev/null +++ b/gso/workflows/l3_core_service/copernicus/create_copernicus.py @@ -0,0 +1,61 @@ +"""Create Copernicus subscription workflow.""" + +from orchestrator.targets import Target +from orchestrator.types import SubscriptionLifecycle +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_forms.types import State, UUIDstr + +from gso.products.product_types.copernicus import CopernicusInactive +from gso.services.lso_client import lso_interaction +from gso.utils.workflow_steps import prompt_sharepoint_checklist_url, start_moodi, stop_moodi +from gso.workflows.l3_core_service.base_create_l3_core_service import ( + check_bgp_peers, + check_sbp_functionality, + create_new_sharepoint_checklist, + deploy_bgp_peers_dry, + deploy_bgp_peers_real, + initial_input_form_generator, + initialize_subscription, + provision_sbp_dry, + provision_sbp_real, + update_dns_records, +) + + +@step("Create subscription") +def create_subscription(product: UUIDstr, partner: str) -> State: + """Create a new subscription object in the database.""" + subscription = CopernicusInactive.from_product_id(product, partner) + + return {"subscription": subscription, "subscription_id": subscription.subscription_id} + + +@workflow( + "Create Copernicus", + initial_input_form=wrap_create_initial_input_form(initial_input_form_generator), + target=Target.CREATE, +) +def create_copernicus() -> StepList: + """Create a new Copernicus subscription.""" + return ( + begin + >> create_subscription + >> store_process_subscription(Target.CREATE) + >> initialize_subscription + >> start_moodi() + >> lso_interaction(provision_sbp_dry) + >> lso_interaction(provision_sbp_real) + >> lso_interaction(check_sbp_functionality) + >> lso_interaction(deploy_bgp_peers_dry) + >> lso_interaction(deploy_bgp_peers_real) + >> lso_interaction(check_bgp_peers) + >> update_dns_records + >> set_status(SubscriptionLifecycle.ACTIVE) + >> resync + >> create_new_sharepoint_checklist + >> prompt_sharepoint_checklist_url + >> stop_moodi() + >> done + ) diff --git a/gso/workflows/l3_core_service/copernicus/create_imported_copernicus.py b/gso/workflows/l3_core_service/copernicus/create_imported_copernicus.py new file mode 100644 index 0000000000000000000000000000000000000000..e5dad58095470bc16592a573b9b4b0264f5ccfaf --- /dev/null +++ b/gso/workflows/l3_core_service/copernicus/create_imported_copernicus.py @@ -0,0 +1,43 @@ +"""A creation workflow for adding an existing Copernicus to the service database.""" + +from orchestrator import workflow +from orchestrator.targets import Target +from orchestrator.types import SubscriptionLifecycle +from orchestrator.workflow import StepList, begin, done, step +from orchestrator.workflows.steps import resync, set_status, store_process_subscription + +from gso.products import ProductName +from gso.products.product_types.copernicus import ImportedCopernicusInactive +from gso.services.partners import get_partner_by_name +from gso.services.subscriptions import get_product_id_by_name +from gso.workflows.l3_core_service.base_create_imported_l3_core_service import ( + initial_input_form_generator, + initialize_subscription, +) + + +@step("Create subscription") +def create_subscription(partner: str) -> dict: + """Create a new subscription object in the database.""" + partner_id = get_partner_by_name(partner).partner_id + product_id = get_product_id_by_name(ProductName.IMPORTED_COPERNICUS) + subscription = ImportedCopernicusInactive.from_product_id(product_id, partner_id) + return {"subscription": subscription, "subscription_id": subscription.subscription_id} + + +@workflow( + "Create Imported Copernicus", + initial_input_form=initial_input_form_generator, + target=Target.CREATE, +) +def create_imported_copernicus() -> StepList: + """Import a Copernicus 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/l3_core_service/copernicus/import_copernicus.py b/gso/workflows/l3_core_service/copernicus/import_copernicus.py new file mode 100644 index 0000000000000000000000000000000000000000..0b23e716f4c20200c4afeccee77402a82b95657b --- /dev/null +++ b/gso/workflows/l3_core_service/copernicus/import_copernicus.py @@ -0,0 +1,32 @@ +"""A modification workflow for migrating an `ImportedCopernicus` to a `Copernicus` subscription.""" + +from orchestrator.targets import Target +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 pydantic_forms.types import State, UUIDstr + +from gso.products import ProductName +from gso.products.product_types.copernicus import Copernicus, ImportedCopernicus +from gso.services.partners import get_partner_by_id +from gso.services.subscriptions import get_product_id_by_name + + +@step("Create a Copernicus subscription") +def import_copernicus_subscription(subscription_id: UUIDstr) -> State: + """Take an imported subscription, and turn it into a Copernicus subscription.""" + old_l3_core_service = ImportedCopernicus.from_subscription(subscription_id) + new_product_id = get_product_id_by_name(ProductName.COPERNICUS) + new_subscription = Copernicus.from_other_product(old_l3_core_service, new_product_id) # type: ignore[arg-type] + new_subscription.description = ( + f"{new_subscription.product.name} service for {get_partner_by_id(new_subscription.customer_id).name}" + ) + return {"subscription": new_subscription} + + +@workflow("Import Copernicus", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None)) +def import_copernicus() -> StepList: + """Modify an imported subscription into a Copernicus subscription to complete the import.""" + return ( + init >> store_process_subscription(Target.MODIFY) >> unsync >> import_copernicus_subscription >> resync >> done + ) diff --git a/gso/workflows/l3_core_service/copernicus/migrate_copernicus.py b/gso/workflows/l3_core_service/copernicus/migrate_copernicus.py new file mode 100644 index 0000000000000000000000000000000000000000..17b8156d899a192825bbb4a04e0df6a13afc0bd4 --- /dev/null +++ b/gso/workflows/l3_core_service/copernicus/migrate_copernicus.py @@ -0,0 +1,69 @@ +"""A modification workflow that migrates a Copernicus Service to a new Edge Port. + +In one run of a migration workflow, only a single Access Port can be replaced. This is due to the nature of these +services. When a service is dually homed, for example, only one of the Access Ports should be migrated. The other one +will remain the way it is. + +At the start of the workflow, the operator will select one Access Port that is to be migrated, and will then select a +destination Edge Port that this service should be placed on. All other Access Ports will be left as-is. +""" + +from orchestrator import workflow +from orchestrator.targets import Target +from orchestrator.workflow import StepList, begin, conditional, done +from orchestrator.workflows.steps import resync, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form + +from gso.services.lso_client import lso_interaction +from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, start_moodi, stop_moodi +from gso.workflows.l3_core_service.base_migrate_l3_core_service import ( + deactivate_bgp_dry, + deactivate_bgp_real, + deactivate_sbp_dry, + deactivate_sbp_real, + deploy_bgp_session_dry, + deploy_bgp_session_real, + deploy_destination_ep_dry, + deploy_destination_ep_real, + generate_scoped_subscription_model, + inform_operator_traffic_check, + initial_input_form, + inject_partner_name, + show_bgp_neighbors, + update_dns_records, + update_subscription_model, +) + + +@workflow( + "Migrate Copernicus", + initial_input_form=wrap_modify_initial_input_form(initial_input_form), + target=Target.MODIFY, +) +def migrate_copernicus() -> StepList: + """Migrate a Copernicus Service to a destination Edge Port.""" + is_human_initiated_wf = conditional(lambda state: bool(state.get(IS_HUMAN_INITIATED_WF_KEY))) + + return ( + begin + >> store_process_subscription(Target.MODIFY) + >> inject_partner_name + >> is_human_initiated_wf(lso_interaction(show_bgp_neighbors)) # TODO: send OTRS email with pre-check results + >> is_human_initiated_wf(lso_interaction(deactivate_bgp_dry)) + >> is_human_initiated_wf(lso_interaction(deactivate_bgp_real)) + >> is_human_initiated_wf(lso_interaction(deactivate_sbp_dry)) + >> is_human_initiated_wf(lso_interaction(deactivate_sbp_real)) + >> is_human_initiated_wf(inform_operator_traffic_check) + >> unsync + >> generate_scoped_subscription_model + >> start_moodi() # TODO: include results from first LSO run + >> lso_interaction(deploy_destination_ep_dry) + >> lso_interaction(deploy_destination_ep_real) + >> lso_interaction(deploy_bgp_session_dry) + >> lso_interaction(deploy_bgp_session_real) + >> update_dns_records + >> update_subscription_model + >> resync + >> stop_moodi() + >> done + ) diff --git a/gso/workflows/l3_core_service/copernicus/modify_copernicus.py b/gso/workflows/l3_core_service/copernicus/modify_copernicus.py new file mode 100644 index 0000000000000000000000000000000000000000..6aacfa9dfffd67d846857627fdf04927edf8c565 --- /dev/null +++ b/gso/workflows/l3_core_service/copernicus/modify_copernicus.py @@ -0,0 +1,38 @@ +"""Modification workflow for a Copernicus subscription.""" + +from orchestrator import begin, conditional, done, workflow +from orchestrator.targets import Target +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 gso.workflows.l3_core_service.base_modify_l3_core_service import ( + Operation, + create_new_sbp, + initial_input_form_generator, + modify_existing_sbp, + remove_old_sbp, +) + + +@workflow( + "Modify Copernicus", + initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), + target=Target.MODIFY, +) +def modify_copernicus() -> StepList: + """Modify Copernicus subscription.""" + access_port_is_added = conditional(lambda state: state["operation"] == Operation.ADD) + access_port_is_removed = conditional(lambda state: state["operation"] == Operation.REMOVE) + access_port_is_modified = conditional(lambda state: state["operation"] == Operation.EDIT) + + return ( + begin + >> store_process_subscription(Target.MODIFY) + >> unsync + >> access_port_is_added(create_new_sbp) + >> access_port_is_removed(remove_old_sbp) + >> access_port_is_modified(modify_existing_sbp) + >> resync + >> done + ) diff --git a/gso/workflows/l3_core_service/copernicus/terminate_copernicus.py b/gso/workflows/l3_core_service/copernicus/terminate_copernicus.py new file mode 100644 index 0000000000000000000000000000000000000000..9d9539d6f59e119b3631f4a2b0a7c3aefbb620b8 --- /dev/null +++ b/gso/workflows/l3_core_service/copernicus/terminate_copernicus.py @@ -0,0 +1,41 @@ +"""Workflow for terminating an Copernicus subscription.""" + +from orchestrator import begin, workflow +from orchestrator.forms import SubmitFormPage +from orchestrator.targets import Target +from orchestrator.types import SubscriptionLifecycle +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, UUIDstr + +from gso.products.product_types.copernicus import Copernicus +from gso.utils.types.tt_number import TTNumber + + +def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: + """Initial input form generator for terminating a Copernicus subscription.""" + subscription = Copernicus.from_subscription(subscription_id) + + class TerminateForm(SubmitFormPage): + tt_number: TTNumber + + user_input = yield TerminateForm + return {"subscription": subscription} | user_input.model_dump() + + +@workflow( + "Terminate Copernicus", + initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), + target=Target.TERMINATE, +) +def terminate_copernicus() -> StepList: + """Terminate a Copernicus subscription.""" + return ( + begin + >> store_process_subscription(Target.TERMINATE) + >> unsync + >> set_status(SubscriptionLifecycle.TERMINATED) + >> resync + >> done + ) diff --git a/gso/workflows/l3_core_service/copernicus/validate_copernicus.py b/gso/workflows/l3_core_service/copernicus/validate_copernicus.py new file mode 100644 index 0000000000000000000000000000000000000000..820329369071485c10676c631a32cf8514cb7197 --- /dev/null +++ b/gso/workflows/l3_core_service/copernicus/validate_copernicus.py @@ -0,0 +1,30 @@ +"""Validation workflow for Copernicus subscription objects.""" + +from orchestrator.targets import Target +from orchestrator.workflow import StepList, begin, done, workflow +from orchestrator.workflows.steps import resync, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form + +from gso.services.lso_client import anonymous_lso_interaction +from gso.workflows.l3_core_service.base_validate_l3_core_service import ( + build_fqdn_list, + validate_bgp_peers, + validate_dns_records, + validate_sbp_config, +) + + +@workflow("Validate Copernicus", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None))) +def validate_copernicus() -> StepList: + """Validate an existing Copernicus subscription.""" + return ( + begin + >> store_process_subscription(Target.SYSTEM) + >> unsync + >> build_fqdn_list + >> anonymous_lso_interaction(validate_sbp_config) + >> anonymous_lso_interaction(validate_bgp_peers) + >> validate_dns_records + >> resync + >> done + ) diff --git a/gso/workflows/l3_core_service/geant_ip/__init__.py b/gso/workflows/l3_core_service/geant_ip/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5db92fcc1aa4ae47e1944561e473ad805c6efdfc --- /dev/null +++ b/gso/workflows/l3_core_service/geant_ip/__init__.py @@ -0,0 +1 @@ +"""GÉANT IP service workflows.""" diff --git a/gso/workflows/l3_core_service/geant_ip/create_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/create_geant_ip.py new file mode 100644 index 0000000000000000000000000000000000000000..18315f20b14b781c7a444586a815caea011d6edf --- /dev/null +++ b/gso/workflows/l3_core_service/geant_ip/create_geant_ip.py @@ -0,0 +1,61 @@ +"""Create GÉANT IP subscription workflow.""" + +from orchestrator.targets import Target +from orchestrator.types import SubscriptionLifecycle +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_forms.types import State, UUIDstr + +from gso.products.product_types.geant_ip import GeantIPInactive +from gso.services.lso_client import lso_interaction +from gso.utils.workflow_steps import prompt_sharepoint_checklist_url, start_moodi, stop_moodi +from gso.workflows.l3_core_service.base_create_l3_core_service import ( + check_bgp_peers, + check_sbp_functionality, + create_new_sharepoint_checklist, + deploy_bgp_peers_dry, + deploy_bgp_peers_real, + initial_input_form_generator, + initialize_subscription, + provision_sbp_dry, + provision_sbp_real, + update_dns_records, +) + + +@step("Create subscription") +def create_subscription(product: UUIDstr, partner: str) -> State: + """Create a new subscription object in the database.""" + subscription = GeantIPInactive.from_product_id(product, partner) + + return {"subscription": subscription, "subscription_id": subscription.subscription_id} + + +@workflow( + "Create GÉANT IP", + initial_input_form=wrap_create_initial_input_form(initial_input_form_generator), + target=Target.CREATE, +) +def create_geant_ip() -> StepList: + """Create a new GÉANT IP subscription.""" + return ( + begin + >> create_subscription + >> store_process_subscription(Target.CREATE) + >> initialize_subscription + >> start_moodi() + >> lso_interaction(provision_sbp_dry) + >> lso_interaction(provision_sbp_real) + >> lso_interaction(check_sbp_functionality) + >> lso_interaction(deploy_bgp_peers_dry) + >> lso_interaction(deploy_bgp_peers_real) + >> lso_interaction(check_bgp_peers) + >> update_dns_records + >> set_status(SubscriptionLifecycle.ACTIVE) + >> resync + >> create_new_sharepoint_checklist + >> prompt_sharepoint_checklist_url + >> stop_moodi() + >> done + ) diff --git a/gso/workflows/l3_core_service/geant_ip/create_imported_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/create_imported_geant_ip.py new file mode 100644 index 0000000000000000000000000000000000000000..7a1775034da2a754b100ed8cdb37e1ece007469a --- /dev/null +++ b/gso/workflows/l3_core_service/geant_ip/create_imported_geant_ip.py @@ -0,0 +1,43 @@ +"""A creation workflow for adding an existing Imported GÉANT IP to the service database.""" + +from orchestrator import workflow +from orchestrator.targets import Target +from orchestrator.types import SubscriptionLifecycle +from orchestrator.workflow import StepList, begin, done, step +from orchestrator.workflows.steps import resync, set_status, store_process_subscription + +from gso.products import ProductName +from gso.products.product_types.geant_ip import ImportedGeantIPInactive +from gso.services.partners import get_partner_by_name +from gso.services.subscriptions import get_product_id_by_name +from gso.workflows.l3_core_service.base_create_imported_l3_core_service import ( + initial_input_form_generator, + initialize_subscription, +) + + +@step("Create subscription") +def create_subscription(partner: str) -> dict: + """Create a new subscription object in the database.""" + partner_id = get_partner_by_name(partner).partner_id + product_id = get_product_id_by_name(ProductName.IMPORTED_GEANT_IP) + subscription = ImportedGeantIPInactive.from_product_id(product_id, partner_id) + return {"subscription": subscription, "subscription_id": subscription.subscription_id} + + +@workflow( + "Create Imported GÉANT IP", + initial_input_form=initial_input_form_generator, + target=Target.CREATE, +) +def create_imported_geant_ip() -> StepList: + """Import a GÉANT IP 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/l3_core_service/geant_ip/import_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/import_geant_ip.py new file mode 100644 index 0000000000000000000000000000000000000000..d7bd7bef409ad10abbc8374f8fb4b066083c33b2 --- /dev/null +++ b/gso/workflows/l3_core_service/geant_ip/import_geant_ip.py @@ -0,0 +1,30 @@ +"""A modification workflow for migrating an `ImportedGeantIP` to a `GeantIP` subscription.""" + +from orchestrator.targets import Target +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 pydantic_forms.types import State, UUIDstr + +from gso.products import ProductName +from gso.products.product_types.geant_ip import GeantIP, ImportedGeantIP +from gso.services.partners import get_partner_by_id +from gso.services.subscriptions import get_product_id_by_name + + +@step("Create a GÉANT IP subscription") +def import_geant_ip_subscription(subscription_id: UUIDstr) -> State: + """Take an imported subscription, and turn it into a GÉANT IP subscription.""" + old_l3_core_service = ImportedGeantIP.from_subscription(subscription_id) + new_product_id = get_product_id_by_name(ProductName.GEANT_IP) + new_subscription = GeantIP.from_other_product(old_l3_core_service, new_product_id) # type: ignore[arg-type] + new_subscription.description = ( + f"{new_subscription.product.name} service for {get_partner_by_id(new_subscription.customer_id).name}" + ) + return {"subscription": new_subscription} + + +@workflow("Import GÉANT IP", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None)) +def import_geant_ip() -> StepList: + """Modify an imported subscription into a GÉANT IP subscription to complete the import.""" + return init >> store_process_subscription(Target.MODIFY) >> unsync >> import_geant_ip_subscription >> resync >> done diff --git a/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py new file mode 100644 index 0000000000000000000000000000000000000000..b7c42e67e0e42cb6cf79169fa2ec9b0d5deff56f --- /dev/null +++ b/gso/workflows/l3_core_service/geant_ip/migrate_geant_ip.py @@ -0,0 +1,69 @@ +"""A modification workflow that migrates a GÉANT IP Service to a new Edge Port. + +In one run of a migration workflow, only a single Access Port can be replaced. This is due to the nature of these +services. When a service is dually homed, for example, only one of the Access Ports should be migrated. The other one +will remain the way it is. + +At the start of the workflow, the operator will select one Access Port that is to be migrated, and will then select a +destination Edge Port that this service should be placed on. All other Access Ports will be left as-is. +""" + +from orchestrator import workflow +from orchestrator.targets import Target +from orchestrator.workflow import StepList, begin, conditional, done +from orchestrator.workflows.steps import resync, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form + +from gso.services.lso_client import lso_interaction +from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, start_moodi, stop_moodi +from gso.workflows.l3_core_service.base_migrate_l3_core_service import ( + deactivate_bgp_dry, + deactivate_bgp_real, + deactivate_sbp_dry, + deactivate_sbp_real, + deploy_bgp_session_dry, + deploy_bgp_session_real, + deploy_destination_ep_dry, + deploy_destination_ep_real, + generate_scoped_subscription_model, + inform_operator_traffic_check, + initial_input_form, + inject_partner_name, + show_bgp_neighbors, + update_dns_records, + update_subscription_model, +) + + +@workflow( + "Migrate GÉANT IP", + initial_input_form=wrap_modify_initial_input_form(initial_input_form), + target=Target.MODIFY, +) +def migrate_geant_ip() -> StepList: + """Migrate a GÉANT IP Service to a destination Edge Port.""" + is_human_initiated_wf = conditional(lambda state: bool(state.get(IS_HUMAN_INITIATED_WF_KEY))) + + return ( + begin + >> store_process_subscription(Target.MODIFY) + >> inject_partner_name + >> is_human_initiated_wf(lso_interaction(show_bgp_neighbors)) # TODO: send OTRS email with pre-check results + >> is_human_initiated_wf(lso_interaction(deactivate_bgp_dry)) + >> is_human_initiated_wf(lso_interaction(deactivate_bgp_real)) + >> is_human_initiated_wf(lso_interaction(deactivate_sbp_dry)) + >> is_human_initiated_wf(lso_interaction(deactivate_sbp_real)) + >> is_human_initiated_wf(inform_operator_traffic_check) + >> unsync + >> generate_scoped_subscription_model + >> start_moodi() # TODO: include results from first LSO run + >> lso_interaction(deploy_destination_ep_dry) + >> lso_interaction(deploy_destination_ep_real) + >> lso_interaction(deploy_bgp_session_dry) + >> lso_interaction(deploy_bgp_session_real) + >> update_dns_records + >> update_subscription_model + >> resync + >> stop_moodi() + >> done + ) diff --git a/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py new file mode 100644 index 0000000000000000000000000000000000000000..a2b07c08fceccd7fdeb073be48f16042e97e6c02 --- /dev/null +++ b/gso/workflows/l3_core_service/geant_ip/modify_geant_ip.py @@ -0,0 +1,38 @@ +"""Modification workflow for a GÉANT IP subscription.""" + +from orchestrator import begin, conditional, done, workflow +from orchestrator.targets import Target +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 gso.workflows.l3_core_service.base_modify_l3_core_service import ( + Operation, + create_new_sbp, + initial_input_form_generator, + modify_existing_sbp, + remove_old_sbp, +) + + +@workflow( + "Modify GÉANT IP", + initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), + target=Target.MODIFY, +) +def modify_geant_ip() -> StepList: + """Modify GÉANT IP subscription.""" + access_port_is_added = conditional(lambda state: state["operation"] == Operation.ADD) + access_port_is_removed = conditional(lambda state: state["operation"] == Operation.REMOVE) + access_port_is_modified = conditional(lambda state: state["operation"] == Operation.EDIT) + + return ( + begin + >> store_process_subscription(Target.MODIFY) + >> unsync + >> access_port_is_added(create_new_sbp) + >> access_port_is_removed(remove_old_sbp) + >> access_port_is_modified(modify_existing_sbp) + >> resync + >> done + ) diff --git a/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py new file mode 100644 index 0000000000000000000000000000000000000000..b96474dd618fae98f0fd50798cac382d3dca9f3c --- /dev/null +++ b/gso/workflows/l3_core_service/geant_ip/terminate_geant_ip.py @@ -0,0 +1,42 @@ +"""Workflow for terminating an GÉANT IP subscription.""" + +from orchestrator import begin, workflow +from orchestrator.forms import SubmitFormPage +from orchestrator.targets import Target +from orchestrator.types import SubscriptionLifecycle +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, UUIDstr + +from gso.products.product_types.geant_ip import GeantIP +from gso.utils.types.tt_number import TTNumber + + +def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: + """Initial input form generator for terminating a GÉANT IP subscription.""" + subscription = GeantIP.from_subscription(subscription_id) + + class TerminateForm(SubmitFormPage): + tt_number: TTNumber + + user_input = yield TerminateForm + + return {"subscription": subscription} | user_input.model_dump() + + +@workflow( + "Terminate GÉANT IP", + initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), + target=Target.TERMINATE, +) +def terminate_geant_ip() -> StepList: + """Terminate an GÉANT IP subscription.""" + return ( + begin + >> store_process_subscription(Target.TERMINATE) + >> unsync + >> set_status(SubscriptionLifecycle.TERMINATED) + >> resync + >> done + ) diff --git a/gso/workflows/l3_core_service/geant_ip/validate_geant_ip.py b/gso/workflows/l3_core_service/geant_ip/validate_geant_ip.py new file mode 100644 index 0000000000000000000000000000000000000000..217c3d6c6f45ca2ceb5dc98a273f26ac7b787863 --- /dev/null +++ b/gso/workflows/l3_core_service/geant_ip/validate_geant_ip.py @@ -0,0 +1,30 @@ +"""Validation workflow for GÉANT IP subscription objects.""" + +from orchestrator.targets import Target +from orchestrator.workflow import StepList, begin, done, workflow +from orchestrator.workflows.steps import resync, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form + +from gso.services.lso_client import anonymous_lso_interaction +from gso.workflows.l3_core_service.base_validate_l3_core_service import ( + build_fqdn_list, + validate_bgp_peers, + validate_dns_records, + validate_sbp_config, +) + + +@workflow("Validate GÉANT IP", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None))) +def validate_geant_ip() -> StepList: + """Validate an existing Copernicus subscription.""" + return ( + begin + >> store_process_subscription(Target.SYSTEM) + >> unsync + >> build_fqdn_list + >> anonymous_lso_interaction(validate_sbp_config) + >> anonymous_lso_interaction(validate_bgp_peers) + >> validate_dns_records + >> resync + >> done + ) diff --git a/gso/workflows/l3_core_service/geant_ip/validate_prefix_list.py b/gso/workflows/l3_core_service/geant_ip/validate_prefix_list.py new file mode 100644 index 0000000000000000000000000000000000000000..5f259b576889bbfcb801d7b77dce71413426a9d1 --- /dev/null +++ b/gso/workflows/l3_core_service/geant_ip/validate_prefix_list.py @@ -0,0 +1,48 @@ +"""Prefix Validation workflow for GÉANT IP subscription objects.""" + +from orchestrator.targets import Target +from orchestrator.workflow import StepList, begin, conditional, done, workflow +from orchestrator.workflows.steps import resync, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form + +from gso.services.lso_client import anonymous_lso_interaction, lso_interaction +from gso.workflows.l3_core_service.base_validate_prefix_list import ( + await_operator, + build_fqdn_list, + deploy_prefix_lists_dry, + deploy_prefix_lists_real, + evaluate_result_has_diff, + validate_prefix_lists_dry, +) + + +@workflow( + "Validate GÉANT IP Prefix-List", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None)) +) +def validate_geant_ip_prefix_list() -> StepList: + """Validate prefix-lists for an existing GÉANT IP subscription.""" + fqdn_list_is_empty = conditional(lambda state: state["ap_fqdn_list"] == []) + prefix_list_has_drifted = conditional(lambda state: bool(state["prefix_list_drift"])) + + redeploy_prefix_list_steps = ( + begin + >> unsync + >> await_operator + >> lso_interaction(deploy_prefix_lists_dry) + >> lso_interaction(deploy_prefix_lists_real) + >> resync + ) + prefix_list_validation_steps = ( + begin + >> anonymous_lso_interaction(validate_prefix_lists_dry, evaluate_result_has_diff) + >> prefix_list_has_drifted(redeploy_prefix_list_steps) + ) + + return ( + begin + >> store_process_subscription(Target.SYSTEM) + >> build_fqdn_list + >> fqdn_list_is_empty(done) + >> prefix_list_validation_steps + >> done + ) diff --git a/gso/workflows/l3_core_service/ias/__init__.py b/gso/workflows/l3_core_service/ias/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a39a157a3406035c3c75cfe60eef43d0e6122e4c --- /dev/null +++ b/gso/workflows/l3_core_service/ias/__init__.py @@ -0,0 +1 @@ +"""IAS Service Workflow.""" diff --git a/gso/workflows/l3_core_service/ias/create_ias.py b/gso/workflows/l3_core_service/ias/create_ias.py new file mode 100644 index 0000000000000000000000000000000000000000..911ffb82b74a92d91dbfb8329410affee05028c0 --- /dev/null +++ b/gso/workflows/l3_core_service/ias/create_ias.py @@ -0,0 +1,80 @@ +"""Create a new IAS workflow.""" + +from orchestrator.forms import FormPage +from orchestrator.targets import Target +from orchestrator.types import SubscriptionLifecycle +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_forms.types import FormGenerator, State, UUIDstr + +from gso.products.product_blocks.ias import IASFlavor +from gso.products.product_types.ias import IASInactive +from gso.services.lso_client import lso_interaction +from gso.utils.workflow_steps import prompt_sharepoint_checklist_url, start_moodi, stop_moodi +from gso.workflows.l3_core_service.base_create_l3_core_service import ( + check_bgp_peers, + check_sbp_functionality, + create_new_sharepoint_checklist, + deploy_bgp_peers_dry, + deploy_bgp_peers_real, + initialize_subscription, + provision_sbp_dry, + provision_sbp_real, + update_dns_records, +) +from gso.workflows.l3_core_service.base_create_l3_core_service import ( + initial_input_form_generator as base_initial_input_form_generator, +) +from gso.workflows.l3_core_service.ias.shared import update_ias_subscription_model + + +def initial_input_form_generator(product_name: str) -> FormGenerator: + """Initial input form generator for creating a new IAS subscription.""" + initial_generator = base_initial_input_form_generator(product_name) + initial_user_input = yield from initial_generator + + # Additional IAS step + class IASExtraForm(FormPage): + ias_flavor: IASFlavor | str = IASFlavor.IAS_PS_OPT_OUT # TODO: remove type hint workaround + + ias_extra = yield IASExtraForm + return initial_user_input | ias_extra.model_dump() + + +@step("Create subscription") +def create_subscription(product: UUIDstr, partner: str) -> State: + """Create a new subscription object in the database.""" + subscription = IASInactive.from_product_id(product, partner) + + return {"subscription": subscription, "subscription_id": subscription.subscription_id} + + +@workflow( + "Create IAS", + initial_input_form=wrap_create_initial_input_form(initial_input_form_generator), + target=Target.CREATE, +) +def create_ias() -> StepList: + """Create a new IAS subscription.""" + return ( + begin + >> create_subscription + >> store_process_subscription(Target.CREATE) + >> initialize_subscription + >> update_ias_subscription_model + >> start_moodi() + >> lso_interaction(provision_sbp_dry) + >> lso_interaction(provision_sbp_real) + >> lso_interaction(check_sbp_functionality) + >> lso_interaction(deploy_bgp_peers_dry) + >> lso_interaction(deploy_bgp_peers_real) + >> lso_interaction(check_bgp_peers) + >> update_dns_records + >> set_status(SubscriptionLifecycle.ACTIVE) + >> resync + >> create_new_sharepoint_checklist + >> prompt_sharepoint_checklist_url + >> stop_moodi() + >> done + ) diff --git a/gso/workflows/l3_core_service/ias/create_imported_ias.py b/gso/workflows/l3_core_service/ias/create_imported_ias.py new file mode 100644 index 0000000000000000000000000000000000000000..fb64b6077f2b24300f4c7d5c957e822dfaf585be --- /dev/null +++ b/gso/workflows/l3_core_service/ias/create_imported_ias.py @@ -0,0 +1,64 @@ +"""A creation workflow for adding an existing Imported IAS to the service database.""" + +from orchestrator import workflow +from orchestrator.forms import FormPage +from orchestrator.targets import Target +from orchestrator.types import SubscriptionLifecycle +from orchestrator.workflow import StepList, begin, done, step +from orchestrator.workflows.steps import resync, set_status, store_process_subscription +from pydantic_forms.types import FormGenerator + +from gso.products import ProductName +from gso.products.product_blocks.ias import IASFlavor +from gso.products.product_types.ias import ImportedIASInactive +from gso.services.partners import get_partner_by_name +from gso.services.subscriptions import get_product_id_by_name +from gso.workflows.l3_core_service.base_create_imported_l3_core_service import ( + initial_input_form_generator as base_initial_input_form_generator, +) +from gso.workflows.l3_core_service.base_create_imported_l3_core_service import ( + initialize_subscription, +) + + +def initial_input_form_generator() -> FormGenerator: + """Initial input form generator for creating a new imported IAS subscription.""" + initial_generator = base_initial_input_form_generator() + initial_user_input = yield from initial_generator + + # Additional IAS step + class IASExtraForm(FormPage): + ias_flavor: IASFlavor = IASFlavor.IAS_PS_OPT_OUT + + ias_extra = yield IASExtraForm + return initial_user_input | ias_extra.model_dump() + + +@step("Create subscription") +def create_subscription(partner: str, ias_flavor: IASFlavor) -> dict: + """Create a new subscription object in the database.""" + partner_id = get_partner_by_name(partner).partner_id + product_id = get_product_id_by_name(ProductName.IMPORTED_IAS) + subscription = ImportedIASInactive.from_product_id(product_id, partner_id) + + subscription.ias.ias_flavor = ias_flavor + + return {"subscription": subscription, "subscription_id": subscription.subscription_id} + + +@workflow( + "Create Imported IAS", + initial_input_form=initial_input_form_generator, + target=Target.CREATE, +) +def create_imported_ias() -> StepList: + """Import an IAS 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/l3_core_service/ias/import_ias.py b/gso/workflows/l3_core_service/ias/import_ias.py new file mode 100644 index 0000000000000000000000000000000000000000..130c01a8416cbc48de656660eb2872b145f0ee75 --- /dev/null +++ b/gso/workflows/l3_core_service/ias/import_ias.py @@ -0,0 +1,30 @@ +"""A modification workflow for migrating an `ImportedIAS` to an `IAS` subscription.""" + +from orchestrator.targets import Target +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 pydantic_forms.types import State, UUIDstr + +from gso.products import ProductName +from gso.products.product_types.ias import IAS, ImportedIAS +from gso.services.partners import get_partner_by_id +from gso.services.subscriptions import get_product_id_by_name + + +@step("Create an IAS subscription") +def import_ias_subscription(subscription_id: UUIDstr) -> State: + """Take an imported subscription, and turn it into an IAS subscription.""" + old_l3_core_service = ImportedIAS.from_subscription(subscription_id) + new_product_id = get_product_id_by_name(ProductName.IAS) + new_subscription = IAS.from_other_product(old_l3_core_service, new_product_id) # type: ignore[arg-type] + new_subscription.description = ( + f"{new_subscription.product.name} service for {get_partner_by_id(new_subscription.customer_id).name}" + ) + return {"subscription": new_subscription} + + +@workflow("Import IAS", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None)) +def import_ias() -> StepList: + """Modify an imported subscription into an IAS subscription to complete the import.""" + return init >> store_process_subscription(Target.MODIFY) >> unsync >> import_ias_subscription >> resync >> done diff --git a/gso/workflows/l3_core_service/ias/migrate_ias.py b/gso/workflows/l3_core_service/ias/migrate_ias.py new file mode 100644 index 0000000000000000000000000000000000000000..b430d832b2b376da69d1e9e80fde2984a5d7872b --- /dev/null +++ b/gso/workflows/l3_core_service/ias/migrate_ias.py @@ -0,0 +1,69 @@ +"""A modification workflow that migrates an IAS Service to a new Edge Port. + +In one run of a migration workflow, only a single Access Port can be replaced. This is due to the nature of these +services. When a service is dually homed, for example, only one of the Access Ports should be migrated. The other one +will remain the way it is. + +At the start of the workflow, the operator will select one Access Port that is to be migrated, and will then select a +destination Edge Port that this service should be placed on. All other Access Ports will be left as-is. +""" + +from orchestrator import workflow +from orchestrator.targets import Target +from orchestrator.workflow import StepList, begin, conditional, done +from orchestrator.workflows.steps import resync, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form + +from gso.services.lso_client import lso_interaction +from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, start_moodi, stop_moodi +from gso.workflows.l3_core_service.base_migrate_l3_core_service import ( + deactivate_bgp_dry, + deactivate_bgp_real, + deactivate_sbp_dry, + deactivate_sbp_real, + deploy_bgp_session_dry, + deploy_bgp_session_real, + deploy_destination_ep_dry, + deploy_destination_ep_real, + generate_scoped_subscription_model, + inform_operator_traffic_check, + initial_input_form, + inject_partner_name, + show_bgp_neighbors, + update_dns_records, + update_subscription_model, +) + + +@workflow( + "Migrate IAS", + initial_input_form=wrap_modify_initial_input_form(initial_input_form), + target=Target.MODIFY, +) +def migrate_ias() -> StepList: + """Migrate an IAS Service to a destination Edge Port.""" + is_human_initiated_wf = conditional(lambda state: bool(state.get(IS_HUMAN_INITIATED_WF_KEY))) + + return ( + begin + >> store_process_subscription(Target.MODIFY) + >> inject_partner_name + >> is_human_initiated_wf(lso_interaction(show_bgp_neighbors)) # TODO: send OTRS email with pre-check results + >> is_human_initiated_wf(lso_interaction(deactivate_bgp_dry)) + >> is_human_initiated_wf(lso_interaction(deactivate_bgp_real)) + >> is_human_initiated_wf(lso_interaction(deactivate_sbp_dry)) + >> is_human_initiated_wf(lso_interaction(deactivate_sbp_real)) + >> is_human_initiated_wf(inform_operator_traffic_check) + >> unsync + >> generate_scoped_subscription_model + >> start_moodi() # TODO: include results from first LSO run + >> lso_interaction(deploy_destination_ep_dry) + >> lso_interaction(deploy_destination_ep_real) + >> lso_interaction(deploy_bgp_session_dry) + >> lso_interaction(deploy_bgp_session_real) + >> update_dns_records + >> update_subscription_model + >> resync + >> stop_moodi() + >> done + ) diff --git a/gso/workflows/l3_core_service/ias/modify_ias.py b/gso/workflows/l3_core_service/ias/modify_ias.py new file mode 100644 index 0000000000000000000000000000000000000000..41425074fa5c0899237a04f5d817a23ff6373be8 --- /dev/null +++ b/gso/workflows/l3_core_service/ias/modify_ias.py @@ -0,0 +1,60 @@ +"""Modification workflow for a IAS subscription.""" + +from orchestrator import begin, conditional, done, workflow +from orchestrator.targets import Target +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_forms.core import FormPage +from pydantic_forms.types import FormGenerator, UUIDstr + +from gso.products.product_blocks.ias import IASFlavor +from gso.products.product_types.ias import IAS +from gso.workflows.l3_core_service.base_modify_l3_core_service import ( + Operation, + create_new_sbp, + initial_input_form_generator, + modify_existing_sbp, + remove_old_sbp, +) +from gso.workflows.l3_core_service.ias.shared import update_ias_subscription_model + + +def modify_ias_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: + """Initial form generator for modifying the custom attributes of an existing IAS subscription.""" + initial_generator = initial_input_form_generator(subscription_id) + initial_user_input = yield from initial_generator + + subscription = IAS.from_subscription(subscription_id) + + # Additional IAS step + class IASExtraForm(FormPage): + # TODO: remove type hint workaround + ias_flavor: IASFlavor | str = subscription.ias.ias_flavor + + ias_extra = yield IASExtraForm + return initial_user_input | ias_extra.model_dump() + + +@workflow( + "Modify IAS", + initial_input_form=wrap_modify_initial_input_form(modify_ias_input_form_generator), + target=Target.MODIFY, +) +def modify_ias() -> StepList: + """Modify IAS subscription.""" + access_port_is_added = conditional(lambda state: state["operation"] == Operation.ADD) + access_port_is_removed = conditional(lambda state: state["operation"] == Operation.REMOVE) + access_port_is_modified = conditional(lambda state: state["operation"] == Operation.EDIT) + + return ( + begin + >> store_process_subscription(Target.MODIFY) + >> unsync + >> update_ias_subscription_model + >> access_port_is_added(create_new_sbp) + >> access_port_is_removed(remove_old_sbp) + >> access_port_is_modified(modify_existing_sbp) + >> resync + >> done + ) diff --git a/gso/workflows/l3_core_service/ias/shared.py b/gso/workflows/l3_core_service/ias/shared.py new file mode 100644 index 0000000000000000000000000000000000000000..09b2c3fa59b424e56951c49414e40b8160eb30f5 --- /dev/null +++ b/gso/workflows/l3_core_service/ias/shared.py @@ -0,0 +1,15 @@ +"""Shared logic for IAS service workflows.""" + +from orchestrator import step +from orchestrator.domain import SubscriptionModel +from pydantic_forms.types import State + +from gso.products.product_blocks.ias import IASFlavor + + +@step("Update IAS-specific attributes") +def update_ias_subscription_model(subscription: SubscriptionModel, ias_flavor: IASFlavor) -> State: + """Update the subscription model of an IAS subscription with a new IAS flavour.""" + subscription.ias.ias_flavor = ias_flavor # type: ignore[attr-defined] + + return {"subscription": subscription} diff --git a/gso/workflows/l3_core_service/terminate_l3_core_service.py b/gso/workflows/l3_core_service/ias/terminate_ias.py similarity index 57% rename from gso/workflows/l3_core_service/terminate_l3_core_service.py rename to gso/workflows/l3_core_service/ias/terminate_ias.py index 8cae6c430b4a635906cf80fb3d67d25444a9a8ef..5cc25a6e22cc9a987df33ca8e9b2ec4880bd5bd8 100644 --- a/gso/workflows/l3_core_service/terminate_l3_core_service.py +++ b/gso/workflows/l3_core_service/ias/terminate_ias.py @@ -1,4 +1,4 @@ -"""Workflow for terminating a Layer 3 Core Service.""" +"""Workflow for terminating an IAS subscription.""" from orchestrator import begin, workflow from orchestrator.forms import SubmitFormPage @@ -9,27 +9,29 @@ from orchestrator.workflows.steps import resync, set_status, store_process_subsc from orchestrator.workflows.utils import wrap_modify_initial_input_form from pydantic_forms.types import FormGenerator, UUIDstr -from gso.products.product_types.l3_core_service import L3CoreService +from gso.products.product_types.ias import IAS from gso.utils.types.tt_number import TTNumber -def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator: - subscription = L3CoreService.from_subscription(subscription_id) +def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: + """Initial input form generator for terminating an IAS subscription.""" + subscription = IAS.from_subscription(subscription_id) class TerminateForm(SubmitFormPage): tt_number: TTNumber - yield TerminateForm - return {"subscription": subscription} + user_input = yield TerminateForm + + return {"subscription": subscription} | user_input.model_dump() @workflow( - "Terminate Layer 3 Core Service", - initial_input_form=wrap_modify_initial_input_form(_input_form_generator), + "Terminate IAS", + initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), target=Target.TERMINATE, ) -def terminate_l3_core_service() -> StepList: - """Terminate a Layer 3 Core Service subscription.""" +def terminate_ias() -> StepList: + """Terminate an IAS subscription.""" return ( begin >> store_process_subscription(Target.TERMINATE) diff --git a/gso/workflows/l3_core_service/ias/validate_ias.py b/gso/workflows/l3_core_service/ias/validate_ias.py new file mode 100644 index 0000000000000000000000000000000000000000..95c06042a79abc383580564953c171daea128f92 --- /dev/null +++ b/gso/workflows/l3_core_service/ias/validate_ias.py @@ -0,0 +1,30 @@ +"""Validation workflow for IAS subscription objects.""" + +from orchestrator.targets import Target +from orchestrator.workflow import StepList, begin, done, workflow +from orchestrator.workflows.steps import resync, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form + +from gso.services.lso_client import anonymous_lso_interaction +from gso.workflows.l3_core_service.base_validate_l3_core_service import ( + build_fqdn_list, + validate_bgp_peers, + validate_dns_records, + validate_sbp_config, +) + + +@workflow("Validate IAS", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None))) +def validate_ias() -> StepList: + """Validate an existing IAS subscription.""" + return ( + begin + >> store_process_subscription(Target.SYSTEM) + >> unsync + >> build_fqdn_list + >> anonymous_lso_interaction(validate_sbp_config) + >> anonymous_lso_interaction(validate_bgp_peers) + >> validate_dns_records + >> resync + >> done + ) diff --git a/gso/workflows/l3_core_service/import_l3_core_service.py b/gso/workflows/l3_core_service/import_l3_core_service.py deleted file mode 100644 index 1c9d85def744dfe543560ef12ce8e309022bb308..0000000000000000000000000000000000000000 --- a/gso/workflows/l3_core_service/import_l3_core_service.py +++ /dev/null @@ -1,55 +0,0 @@ -"""A modification workflow for migrating an `ImportedGeantIP` to a `GeantIP` subscription.""" - -from orchestrator.targets import Target -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 pydantic_forms.types import State, UUIDstr - -from gso.products import ProductName -from gso.products.product_types.l3_core_service import ( - ImportedL3CoreService, - L3CoreService, - L3CoreServiceType, -) -from gso.services.partners import get_partner_by_id -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] - new_subscription.description = ( - f"{new_subscription.product.name} service for {get_partner_by_id(new_subscription.customer_id).name}" - ) - 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/l3_core_service/lhcone/__init__.py b/gso/workflows/l3_core_service/lhcone/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..848a9f86186778582e275f2a228e8acf70784227 --- /dev/null +++ b/gso/workflows/l3_core_service/lhcone/__init__.py @@ -0,0 +1 @@ +"""LHCONE service workflows.""" diff --git a/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py b/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py new file mode 100644 index 0000000000000000000000000000000000000000..adc0fb663f916aafcf53d41a0fe3a88e53f231a2 --- /dev/null +++ b/gso/workflows/l3_core_service/lhcone/create_imported_lhcone.py @@ -0,0 +1,43 @@ +"""A creation workflow for adding an existing Imported LHCOne to the service database.""" + +from orchestrator import workflow +from orchestrator.targets import Target +from orchestrator.types import SubscriptionLifecycle +from orchestrator.workflow import StepList, begin, done, step +from orchestrator.workflows.steps import resync, set_status, store_process_subscription + +from gso.products import ProductName +from gso.products.product_types.lhcone import ImportedLHCOneInactive +from gso.services.partners import get_partner_by_name +from gso.services.subscriptions import get_product_id_by_name +from gso.workflows.l3_core_service.base_create_imported_l3_core_service import ( + initial_input_form_generator, + initialize_subscription, +) + + +@step("Create subscription") +def create_subscription(partner: str) -> dict: + """Create a new subscription object in the database.""" + partner_id = get_partner_by_name(partner).partner_id + product_id = get_product_id_by_name(ProductName.IMPORTED_LHCONE) + subscription = ImportedLHCOneInactive.from_product_id(product_id, partner_id) + return {"subscription": subscription, "subscription_id": subscription.subscription_id} + + +@workflow( + "Create Imported LHCOne", + initial_input_form=initial_input_form_generator, + target=Target.CREATE, +) +def create_imported_lhcone() -> StepList: + """Import a LHCOne 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/l3_core_service/lhcone/create_lhcone.py b/gso/workflows/l3_core_service/lhcone/create_lhcone.py new file mode 100644 index 0000000000000000000000000000000000000000..7e90e04c0dbb54c2142e95f03cc0b7688b58ad79 --- /dev/null +++ b/gso/workflows/l3_core_service/lhcone/create_lhcone.py @@ -0,0 +1,61 @@ +"""Create LHCOne subscription workflow.""" + +from orchestrator.targets import Target +from orchestrator.types import SubscriptionLifecycle +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_forms.types import State, UUIDstr + +from gso.products.product_types.lhcone import LHCOneInactive +from gso.services.lso_client import lso_interaction +from gso.utils.workflow_steps import prompt_sharepoint_checklist_url, start_moodi, stop_moodi +from gso.workflows.l3_core_service.base_create_l3_core_service import ( + check_bgp_peers, + check_sbp_functionality, + create_new_sharepoint_checklist, + deploy_bgp_peers_dry, + deploy_bgp_peers_real, + initial_input_form_generator, + initialize_subscription, + provision_sbp_dry, + provision_sbp_real, + update_dns_records, +) + + +@step("Create subscription") +def create_subscription(product: UUIDstr, partner: str) -> State: + """Create a new subscription object in the database.""" + subscription = LHCOneInactive.from_product_id(product, partner) + + return {"subscription": subscription, "subscription_id": subscription.subscription_id} + + +@workflow( + "Create LHCOne", + initial_input_form=wrap_create_initial_input_form(initial_input_form_generator), + target=Target.CREATE, +) +def create_lhcone() -> StepList: + """Create a new LHCONE subscription.""" + return ( + begin + >> create_subscription + >> store_process_subscription(Target.CREATE) + >> initialize_subscription + >> start_moodi() + >> lso_interaction(provision_sbp_dry) + >> lso_interaction(provision_sbp_real) + >> lso_interaction(check_sbp_functionality) + >> lso_interaction(deploy_bgp_peers_dry) + >> lso_interaction(deploy_bgp_peers_real) + >> lso_interaction(check_bgp_peers) + >> update_dns_records + >> set_status(SubscriptionLifecycle.ACTIVE) + >> resync + >> create_new_sharepoint_checklist + >> prompt_sharepoint_checklist_url + >> stop_moodi() + >> done + ) diff --git a/gso/workflows/l3_core_service/lhcone/import_lhcone.py b/gso/workflows/l3_core_service/lhcone/import_lhcone.py new file mode 100644 index 0000000000000000000000000000000000000000..bf4cdae263437d40e46b1c780e1db28a00d18ca5 --- /dev/null +++ b/gso/workflows/l3_core_service/lhcone/import_lhcone.py @@ -0,0 +1,30 @@ +"""A modification workflow for migrating an `ImportedLHCOne` to an `LHCOne` subscription.""" + +from orchestrator.targets import Target +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 pydantic_forms.types import State, UUIDstr + +from gso.products import ProductName +from gso.products.product_types.lhcone import ImportedLHCOne, LHCOne +from gso.services.partners import get_partner_by_id +from gso.services.subscriptions import get_product_id_by_name + + +@step("Create a LHCOne subscription") +def import_lhcone_subscription(subscription_id: UUIDstr) -> State: + """Take an imported subscription, and turn it into a LHCOne subscription.""" + old_l3_core_service = ImportedLHCOne.from_subscription(subscription_id) + new_product_id = get_product_id_by_name(ProductName.LHCONE) + new_subscription = LHCOne.from_other_product(old_l3_core_service, new_product_id) # type: ignore[arg-type] + new_subscription.description = ( + f"{new_subscription.product.name} service for {get_partner_by_id(new_subscription.customer_id).name}" + ) + return {"subscription": new_subscription} + + +@workflow("Import LHCOne", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None)) +def import_lhcone() -> StepList: + """Modify an imported subscription into a LHCOne subscription to complete the import.""" + return init >> store_process_subscription(Target.MODIFY) >> unsync >> import_lhcone_subscription >> resync >> done diff --git a/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py b/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py new file mode 100644 index 0000000000000000000000000000000000000000..18d3a2ddc33efeb55de0b0ec9d01ae32052f89d5 --- /dev/null +++ b/gso/workflows/l3_core_service/lhcone/migrate_lhcone.py @@ -0,0 +1,69 @@ +"""A modification workflow that migrates an LHCOne Service to a new Edge Port. + +In one run of a migration workflow, only a single Access Port can be replaced. This is due to the nature of these +services. When a service is dually homed, for example, only one of the Access Ports should be migrated. The other one +will remain the way it is. + +At the start of the workflow, the operator will select one Access Port that is to be migrated, and will then select a +destination Edge Port that this service should be placed on. All other Access Ports will be left as-is. +""" + +from orchestrator import workflow +from orchestrator.targets import Target +from orchestrator.workflow import StepList, begin, conditional, done +from orchestrator.workflows.steps import resync, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form + +from gso.services.lso_client import lso_interaction +from gso.utils.workflow_steps import IS_HUMAN_INITIATED_WF_KEY, start_moodi, stop_moodi +from gso.workflows.l3_core_service.base_migrate_l3_core_service import ( + deactivate_bgp_dry, + deactivate_bgp_real, + deactivate_sbp_dry, + deactivate_sbp_real, + deploy_bgp_session_dry, + deploy_bgp_session_real, + deploy_destination_ep_dry, + deploy_destination_ep_real, + generate_scoped_subscription_model, + inform_operator_traffic_check, + initial_input_form, + inject_partner_name, + show_bgp_neighbors, + update_dns_records, + update_subscription_model, +) + + +@workflow( + "Migrate LHCOne", + initial_input_form=wrap_modify_initial_input_form(initial_input_form), + target=Target.MODIFY, +) +def migrate_lhcone() -> StepList: + """Migrate a LHCOne Service to a destination Edge Port.""" + is_human_initiated_wf = conditional(lambda state: bool(state.get(IS_HUMAN_INITIATED_WF_KEY))) + + return ( + begin + >> store_process_subscription(Target.MODIFY) + >> inject_partner_name + >> is_human_initiated_wf(lso_interaction(show_bgp_neighbors)) # TODO: send OTRS email with pre-check results + >> is_human_initiated_wf(lso_interaction(deactivate_bgp_dry)) + >> is_human_initiated_wf(lso_interaction(deactivate_bgp_real)) + >> is_human_initiated_wf(lso_interaction(deactivate_sbp_dry)) + >> is_human_initiated_wf(lso_interaction(deactivate_sbp_real)) + >> is_human_initiated_wf(inform_operator_traffic_check) + >> unsync + >> generate_scoped_subscription_model + >> start_moodi() # TODO: include results from first LSO run + >> lso_interaction(deploy_destination_ep_dry) + >> lso_interaction(deploy_destination_ep_real) + >> lso_interaction(deploy_bgp_session_dry) + >> lso_interaction(deploy_bgp_session_real) + >> update_dns_records + >> update_subscription_model + >> resync + >> stop_moodi() + >> done + ) diff --git a/gso/workflows/l3_core_service/lhcone/modify_lhcone.py b/gso/workflows/l3_core_service/lhcone/modify_lhcone.py new file mode 100644 index 0000000000000000000000000000000000000000..da79e86bdef550d3a269e59302b6e3494571f7f2 --- /dev/null +++ b/gso/workflows/l3_core_service/lhcone/modify_lhcone.py @@ -0,0 +1,38 @@ +"""Modification workflow for a LHCOne subscription.""" + +from orchestrator import begin, conditional, done, workflow +from orchestrator.targets import Target +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 gso.workflows.l3_core_service.base_modify_l3_core_service import ( + Operation, + create_new_sbp, + initial_input_form_generator, + modify_existing_sbp, + remove_old_sbp, +) + + +@workflow( + "Modify LHCOne", + initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), + target=Target.MODIFY, +) +def modify_lhcone() -> StepList: + """Modify LHCOne subscription.""" + access_port_is_added = conditional(lambda state: state["operation"] == Operation.ADD) + access_port_is_removed = conditional(lambda state: state["operation"] == Operation.REMOVE) + access_port_is_modified = conditional(lambda state: state["operation"] == Operation.EDIT) + + return ( + begin + >> store_process_subscription(Target.MODIFY) + >> unsync + >> access_port_is_added(create_new_sbp) + >> access_port_is_removed(remove_old_sbp) + >> access_port_is_modified(modify_existing_sbp) + >> resync + >> done + ) diff --git a/gso/workflows/l3_core_service/lhcone/terminate_lhcone.py b/gso/workflows/l3_core_service/lhcone/terminate_lhcone.py new file mode 100644 index 0000000000000000000000000000000000000000..021e2b70bd3cff4c8ecee47a59e98b9985cbda53 --- /dev/null +++ b/gso/workflows/l3_core_service/lhcone/terminate_lhcone.py @@ -0,0 +1,42 @@ +"""Workflow for terminating an LHCOne subscription.""" + +from orchestrator import begin, workflow +from orchestrator.forms import SubmitFormPage +from orchestrator.targets import Target +from orchestrator.types import SubscriptionLifecycle +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, UUIDstr + +from gso.products.product_types.lhcone import LHCOne +from gso.utils.types.tt_number import TTNumber + + +def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: + """Initial input form generator for terminating an LHCOne subscription.""" + subscription = LHCOne.from_subscription(subscription_id) + + class TerminateForm(SubmitFormPage): + tt_number: TTNumber + + user_input = yield TerminateForm + + return {"subscription": subscription} | user_input.model_dump() + + +@workflow( + "Terminate LHCOne", + initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), + target=Target.TERMINATE, +) +def terminate_lhcone() -> StepList: + """Terminate an LHCOne subscription.""" + return ( + begin + >> store_process_subscription(Target.TERMINATE) + >> unsync + >> set_status(SubscriptionLifecycle.TERMINATED) + >> resync + >> done + ) diff --git a/gso/workflows/l3_core_service/lhcone/validate_lhcone.py b/gso/workflows/l3_core_service/lhcone/validate_lhcone.py new file mode 100644 index 0000000000000000000000000000000000000000..b379dc33d935425abf271d6b0431b35bbaef67bc --- /dev/null +++ b/gso/workflows/l3_core_service/lhcone/validate_lhcone.py @@ -0,0 +1,30 @@ +"""Validation workflow for LHCONE subscription objects.""" + +from orchestrator.targets import Target +from orchestrator.workflow import StepList, begin, done, workflow +from orchestrator.workflows.steps import resync, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form + +from gso.services.lso_client import anonymous_lso_interaction +from gso.workflows.l3_core_service.base_validate_l3_core_service import ( + build_fqdn_list, + validate_bgp_peers, + validate_dns_records, + validate_sbp_config, +) + + +@workflow("Validate LHCOne", target=Target.SYSTEM, initial_input_form=(wrap_modify_initial_input_form(None))) +def validate_lhcone() -> StepList: + """Validate an existing LHCone subscription.""" + return ( + begin + >> store_process_subscription(Target.SYSTEM) + >> unsync + >> build_fqdn_list + >> anonymous_lso_interaction(validate_sbp_config) + >> anonymous_lso_interaction(validate_bgp_peers) + >> validate_dns_records + >> resync + >> done + ) diff --git a/gso/workflows/l3_core_service/shared.py b/gso/workflows/l3_core_service/shared.py new file mode 100644 index 0000000000000000000000000000000000000000..00c3ea8bac27489c6bbfc8506b1cb2642281f1df --- /dev/null +++ b/gso/workflows/l3_core_service/shared.py @@ -0,0 +1,73 @@ +"""Shared logic for L3 Core Service.""" + +from typing import Literal + +from gso.products import ProductName +from gso.products.product_types.copernicus import Copernicus +from gso.products.product_types.geant_ip import GeantIP +from gso.products.product_types.ias import IAS +from gso.products.product_types.lhcone import LHCOne + +L3_PRODUCT_NAMES = [ + ProductName.GEANT_IP, + ProductName.IAS, + ProductName.LHCONE, + ProductName.COPERNICUS, +] +L3_CORE_SERVICE_PRODUCT_TYPES = [GeantIP.__name__, IAS.__name__, LHCOne.__name__, Copernicus.__name__] +assert len(L3_PRODUCT_NAMES) == len( # noqa: S101 + L3_CORE_SERVICE_PRODUCT_TYPES +), "The number of L3 product names does not match the number of L3 core service product types." + +L3ProductNameType = Literal[ProductName.IAS, ProductName.LHCONE, ProductName.COPERNICUS, ProductName.GEANT_IP] + +L3_CREAT_IMPORTED_WF_MAP = { + ProductName.COPERNICUS: "create_imported_copernicus", + ProductName.GEANT_IP: "create_imported_geant_ip", + ProductName.IAS: "create_imported_ias", + ProductName.LHCONE: "create_imported_lhcone", +} + +L3_CREATION_WF_MAP = { + ProductName.COPERNICUS: "create_copernicus", + ProductName.GEANT_IP: "create_geant_ip", + ProductName.IAS: "create_ias", + ProductName.LHCONE: "create_lhcone", +} + +L3_MIGRATION_WF_MAP = { + ProductName.COPERNICUS: "migrate_copernicus", + ProductName.GEANT_IP: "migrate_geant_ip", + ProductName.IAS: "migrate_ias", + ProductName.LHCONE: "migrate_lhcone", +} + + +L3_MODIFICATION_WF_MAP = { + ProductName.COPERNICUS: "modify_copernicus", + ProductName.GEANT_IP: "modify_geant_ip", + ProductName.IAS: "modify_ias", + ProductName.LHCONE: "modify_lhcone", +} + + +L3_IMPORT_WF_MAP = { + ProductName.COPERNICUS: "import_copernicus", + ProductName.GEANT_IP: "import_geant_ip", + ProductName.IAS: "import_ias", + ProductName.LHCONE: "import_lhcone", +} + +L3_VALIDATION_WF_MAP = { + ProductName.GEANT_IP: "validate_geant_ip", + ProductName.IAS: "validate_ias", + ProductName.LHCONE: "validate_lhcone", + ProductName.COPERNICUS: "validate_copernicus", +} + +L3_TERMINATION_WF_MAP = { + ProductName.COPERNICUS: "terminate_copernicus", + ProductName.GEANT_IP: "terminate_geant_ip", + ProductName.IAS: "terminate_ias", + ProductName.LHCONE: "terminate_lhcone", +} diff --git a/gso/workflows/lan_switch_interconnect/terminate_lan_switch_interconnect.py b/gso/workflows/lan_switch_interconnect/terminate_lan_switch_interconnect.py index b878b7d6afc27ee0eebbdf76b18708b586a72148..b35211f4fc838a7773507f57918fd273ff602b01 100644 --- a/gso/workflows/lan_switch_interconnect/terminate_lan_switch_interconnect.py +++ b/gso/workflows/lan_switch_interconnect/terminate_lan_switch_interconnect.py @@ -29,8 +29,9 @@ def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator: tt_number: TTNumber - yield TerminateForm - return {"subscription": lan_switch_interconnect} + user_input = yield TerminateForm + + return {"subscription": lan_switch_interconnect} | user_input.model_dump() @step("Release IPAM resources") diff --git a/gso/workflows/switch/terminate_switch.py b/gso/workflows/switch/terminate_switch.py index 01f149b1dae9717a955afbb78636942ce5d26e71..11acaabd044fe079b6873d0dbf81227c68aed7dd 100644 --- a/gso/workflows/switch/terminate_switch.py +++ b/gso/workflows/switch/terminate_switch.py @@ -30,8 +30,9 @@ def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator: tt_number: TTNumber - yield TerminateForm - return {"subscription": switch} + user_input = yield TerminateForm + + return {"subscription": switch} | user_input.model_dump() @step("Remove switch from Netbox") diff --git a/setup.py b/setup.py index 53cdde31a7ea5f6ba40ab98f665003c52c5f84d7..44b4f9e5879d24193991457627c9cbb5ae48e788 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import find_packages, setup setup( name="geant-service-orchestrator", - version="2.48", + version="3.0", 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 26ab3bdd918da528e4c2f1564a8ced089caf803c..ff5126f342cb1d80d501c68134453bf6858f7df0 100644 --- a/test/cli/test_imports.py +++ b/test/cli/test_imports.py @@ -17,8 +17,10 @@ from gso.cli.imports import ( import_super_pop_switches, import_switches, ) +from gso.products import ProductName from gso.products.product_blocks.bgp_session import IPFamily from gso.products.product_blocks.edge_port import EdgePortType, EncapsulationType +from gso.products.product_blocks.ias import IASFlavor 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 @@ -292,9 +294,12 @@ def edge_port_data(temp_file, faker, router_subscription_factory, partner_factor @pytest.fixture() def l3_core_service_data(temp_file, faker, partner_factory, edge_port_subscription_factory): def _l3_core_service_data(**kwargs): + extra_ias_data = { + "ias_flavor": IASFlavor.IAS_PS_OPT_OUT, + } l3_core_service_data = { "partner": partner_factory()["name"], - "service_type": "IMPORTED IAS", + "product_name": ProductName.IAS.value, "service_binding_ports": [ { "edge_port": str(edge_port_subscription_factory().subscription_id), @@ -396,8 +401,15 @@ def l3_core_service_data(temp_file, faker, partner_factory, edge_port_subscripti } l3_core_service_data.update(**kwargs) - temp_file.write_text(json.dumps([l3_core_service_data])) - return {"path": str(temp_file), "data": l3_core_service_data} + temp_file.write_text( + json.dumps([ + [ + l3_core_service_data, + extra_ias_data, + ] + ]) + ) + return {"path": str(temp_file)} return _l3_core_service_data diff --git a/test/conftest.py b/test/conftest.py index 926cd183b0b50593f65b390d8a357eb73e337fd6..2d2c1a784d6d89b546a13d4c691968f05fcb1484 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -134,7 +134,7 @@ class FakerProvider(BaseProvider): return self.generator.numerify("ae@#") def nokia_lag_interface_name(self) -> str: - return self.generator.numerify("lag-@#") + return self.generator.numerify("lag-3#") def link_members_juniper(self) -> LAGMemberList[LAGMember]: iface_amount = self.generator.random_int(min=2, max=5) diff --git a/test/fixtures/__init__.py b/test/fixtures/__init__.py index 8118b583393f649a4b7c1a17cf03b4870ed51f65..b2ee468ffd7c698d291e1390dfd79ae19b10f1be 100644 --- a/test/fixtures/__init__.py +++ b/test/fixtures/__init__.py @@ -4,7 +4,14 @@ from test.fixtures.l3_core_service_fixtures import ( access_port_factory, bfd_settings_factory, bgp_session_subscription_factory, + copernicus_subscription_factory, + geant_ip_subscription_factory, + ias_subscription_factory, + l3_core_block_factory, l3_core_service_subscription_factory, + lhcone_subscription_factory, + make_subscription_factory, + save_l3_core_subscription, service_binding_port_factory, ) from test.fixtures.lan_switch_interconnect_fixtures import lan_switch_interconnect_subscription_factory @@ -21,15 +28,22 @@ __all__ = [ "access_port_factory", "bfd_settings_factory", "bgp_session_subscription_factory", + "copernicus_subscription_factory", "edge_port_subscription_factory", + "geant_ip_subscription_factory", + "ias_subscription_factory", "iptrunk_side_subscription_factory", "iptrunk_subscription_factory", + "l3_core_block_factory", "l3_core_service_subscription_factory", "lan_switch_interconnect_subscription_factory", "layer_2_circuit_subscription_factory", + "lhcone_subscription_factory", + "make_subscription_factory", "office_router_subscription_factory", "opengear_subscription_factory", "router_subscription_factory", + "save_l3_core_subscription", "service_binding_port_factory", "site_subscription_factory", "super_pop_switch_subscription_factory", diff --git a/test/fixtures/l3_core_service_fixtures.py b/test/fixtures/l3_core_service_fixtures.py index fc72ca3998ccec8c03e893f4178c452fbabf6e6f..04e9c175f48585a42f4c48e4687f00757ad7b0e0 100644 --- a/test/fixtures/l3_core_service_fixtures.py +++ b/test/fixtures/l3_core_service_fixtures.py @@ -7,20 +7,29 @@ from orchestrator.domain import SubscriptionModel from orchestrator.types import SubscriptionLifecycle from pydantic import NonNegativeInt -from gso.products import ProductName +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.ias import IASFlavor +from gso.products.product_blocks.l3_core_service import AccessPort, L3CoreServiceBlockInactive from gso.products.product_blocks.service_binding_port import BFDSettings, ServiceBindingPort +from gso.products.product_types.copernicus import CopernicusInactive, ImportedCopernicusInactive 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.products.product_types.geant_ip import GeantIPInactive, ImportedGeantIPInactive +from gso.products.product_types.ias import IASInactive, ImportedIASInactive +from gso.products.product_types.lhcone import ImportedLHCOneInactive, LHCOneInactive +from gso.services.subscriptions import get_product_id_by_name from gso.utils.shared_enums import APType, SBPType from gso.utils.types.ip_address import IPAddress +PRODUCT_IMPORTED_MAP = { + ProductName.IAS: ProductName.IMPORTED_IAS, + ProductName.GEANT_IP: ProductName.IMPORTED_GEANT_IP, + ProductName.COPERNICUS: ProductName.IMPORTED_COPERNICUS, + ProductName.LHCONE: ProductName.IMPORTED_LHCONE, +} + @pytest.fixture() def bfd_settings_factory(faker): @@ -123,6 +132,23 @@ def service_binding_port_factory( return create_service_binding_port +@pytest.fixture() +def l3_core_block_factory(access_port_factory): + def factory(ap_list: list[AccessPort] | None = None): + # Default ap_list creation with primary and backup access ports + ap_list = ap_list or [ + access_port_factory(ap_type=APType.PRIMARY), + access_port_factory(ap_type=APType.BACKUP), + ] + + return L3CoreServiceBlockInactive.new( + subscription_id=uuid4(), + ap_list=ap_list, + ) + + return factory + + @pytest.fixture() def access_port_factory(faker, service_binding_port_factory): def create_access_port( @@ -141,93 +167,142 @@ def access_port_factory(faker, service_binding_port_factory): @pytest.fixture() -def l3_core_service_subscription_factory( - faker, +def make_subscription_factory( partner_factory, - access_port_factory, + save_l3_core_subscription, ): - def create_l3_core_service_subscription( - l3_core_service_type: L3CoreServiceType, + def create_subscription( + product_name: ProductName, + imported_class, + native_class, 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, + *, + is_imported: bool | None = False, ) -> SubscriptionModel: 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) + product_id = get_product_id_by_name(PRODUCT_IMPORTED_MAP[product_name] if is_imported else product_name) + subscription_class = imported_class if is_imported else native_class - # 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), - ] + subscription = subscription_class.from_product_id(product_id, customer_id=partner["partner_id"], insync=True) + + return save_l3_core_subscription(ap_list, description, subscription, start_date, status) + + return create_subscription + + +@pytest.fixture() +def save_l3_core_subscription(access_port_factory, faker, l3_core_block_factory): + def _save_subscription(ap_list, description, subscription, start_date, status): + subscription.l3_core = l3_core_block_factory(ap_list) # Update subscription with description, start date, and status - l3_core_service_subscription = SubscriptionModel.from_other_lifecycle( - l3_core_service_subscription, + subscription = SubscriptionModel.from_other_lifecycle( + 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() - + subscription.description = description or faker.sentence() + subscription.start_date = start_date + subscription.status = status or SubscriptionLifecycle.ACTIVE + subscription.save() db.session.commit() + return subscription + + return _save_subscription + + +@pytest.fixture() +def ias_subscription_factory(faker, partner_factory, access_port_factory, save_l3_core_subscription): + def create_ias_subscription( + 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, + ias_flavor: IASFlavor | None = IASFlavor.IAS_PS_OPT_OUT, + *, + is_imported: bool | None = False, + ) -> SubscriptionModel: + partner = partner or partner_factory() + + if is_imported: + product_id = get_product_id_by_name(ProductName.IMPORTED_IAS) + subscription = ImportedIASInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + else: + product_id = get_product_id_by_name(ProductName.IAS) + subscription = IASInactive.from_product_id(product_id, customer_id=partner["partner_id"], insync=True) + + subscription.ias.ias_flavor = ias_flavor + + return save_l3_core_subscription(ap_list, description, subscription, start_date, status) + + return create_ias_subscription + + +@pytest.fixture() +def geant_ip_subscription_factory(make_subscription_factory): + def factory(*args, **kwargs): + return make_subscription_factory( + product_name=ProductName.GEANT_IP, + imported_class=ImportedGeantIPInactive, + native_class=GeantIPInactive, + *args, # noqa: B026 + **kwargs, + ) + + return factory + + +@pytest.fixture() +def copernicus_subscription_factory(make_subscription_factory): + def factory(*args, **kwargs): + return make_subscription_factory( + product_name=ProductName.COPERNICUS, + imported_class=ImportedCopernicusInactive, + native_class=CopernicusInactive, + *args, # noqa: B026 + **kwargs, + ) + + return factory + + +@pytest.fixture() +def lhcone_subscription_factory(make_subscription_factory): + def factory(*args, **kwargs): + return make_subscription_factory( + product_name=ProductName.LHCONE, + imported_class=ImportedLHCOneInactive, + native_class=LHCOneInactive, + *args, # noqa: B026 + **kwargs, + ) + + return factory + + +@pytest.fixture() +def l3_core_service_subscription_factory( + ias_subscription_factory, + geant_ip_subscription_factory, + copernicus_subscription_factory, + lhcone_subscription_factory, +) -> callable: + def factory(product_name: ProductName, *args, **kwargs): + if product_name == ProductName.IAS: + return ias_subscription_factory(*args, **kwargs) + + if product_name == ProductName.GEANT_IP: + return geant_ip_subscription_factory(*args, **kwargs) + + if product_name == ProductName.COPERNICUS: + return copernicus_subscription_factory(*args, **kwargs) - return l3_core_service_subscription + return lhcone_subscription_factory(*args, **kwargs) - return create_l3_core_service_subscription + return factory diff --git a/test/workflows/edge_port/test_create_edge_port.py b/test/workflows/edge_port/test_create_edge_port.py index 941995bcd0693ec945da844f2052402eaf8cba75..b907a4138ae89ea52c335b849edd087326775641 100644 --- a/test/workflows/edge_port/test_create_edge_port.py +++ b/test/workflows/edge_port/test_create_edge_port.py @@ -47,51 +47,57 @@ def _netbox_client_mock(): @pytest.fixture() def input_form_wizard_data(request, router_subscription_factory, partner_factory, faker): - create_edge_port_step = { - "tt_number": faker.tt_number(), - "node": str(router_subscription_factory(vendor=Vendor.NOKIA).subscription_id), - "partner": partner_factory(name="GAAR", email=faker.email())["partner_id"], - "service_type": EdgePortType.PUBLIC, - "enable_lacp": True, - "speed": PhysicalPortCapacity.HUNDRED_GIGABIT_PER_SECOND, - "encapsulation": EncapsulationType.DOT1Q, - "number_of_members": 2, - "minimum_links": 1, - "custom_service_name": faker.sentence(), - "generate_ga_id": False, - "ga_id": "GA-12345", - } - create_edge_port_interface_step = { - "name": "lag-21", - "description": faker.sentence(), - "ae_members": [ - { - "interface_name": f"Interface{interface}", - "interface_description": faker.sentence(), - } - for interface in range(2) - ], - } - summary_view_step = {} - - return [ - create_edge_port_step, - create_edge_port_interface_step, - summary_view_step, - ] + def input_form_factory(vendor: Vendor = Vendor.NOKIA): + partner = partner_factory(name="GAAR", email=faker.email()) + create_edge_port_step = { + "tt_number": faker.tt_number(), + "node": str(router_subscription_factory(vendor=vendor, partner=partner).subscription_id), + "partner": partner["partner_id"], + "service_type": EdgePortType.PUBLIC, + "enable_lacp": True, + "speed": PhysicalPortCapacity.HUNDRED_GIGABIT_PER_SECOND, + "encapsulation": EncapsulationType.DOT1Q, + "number_of_members": 2, + "minimum_links": 1, + "custom_service_name": faker.sentence(), + "generate_ga_id": False, + "ga_id": "GA-12345", + } + create_edge_port_interface_step = { + "name": faker.nokia_lag_interface_name(), + "description": faker.sentence(), + "ae_members": [ + { + "interface_name": faker.network_interface() if vendor == Vendor.JUNIPER else f"Interface{i}", + "interface_description": faker.sentence(), + } + for i in range(2) + ], + } + summary_view_step = {} + + return [ + create_edge_port_step, + create_edge_port_interface_step, + summary_view_step, + ] + + return input_form_factory @pytest.mark.workflow() +@pytest.mark.parametrize("router_vendor", [*Vendor.values()]) @patch("gso.services.lso_client._send_request") def test_successful_edge_port_creation( mock_execute_playbook, + router_vendor, input_form_wizard_data, faker, _netbox_client_mock, # noqa: PT019 test_client, ): product_id = get_product_id_by_name(ProductName.EDGE_PORT) - initial_data = [{"product": product_id}, *input_form_wizard_data] + initial_data = [{"product": product_id}, *input_form_wizard_data(vendor=router_vendor)] result, process_stat, step_log = run_workflow("create_edge_port", initial_data) for _ in range(3): @@ -106,9 +112,12 @@ def test_successful_edge_port_creation( subscription = EdgePort.from_subscription(subscription_id) assert subscription.status == "active" - router_fqdn = Router.from_subscription(input_form_wizard_data[0]["node"]).router.router_fqdn + router_fqdn = Router.from_subscription(initial_data[1]["node"]).router.router_fqdn assert subscription.edge_port.ga_id == "GA-12345" - assert subscription.description == f"Edge Port lag-21 on {router_fqdn}, GAAR, {subscription.edge_port.ga_id}" + assert ( + subscription.description + == f"Edge Port {initial_data[2]["name"]} on {router_fqdn}, GAAR, {subscription.edge_port.ga_id}" + ) assert len(subscription.edge_port.edge_port_ae_members) == 2 assert mock_execute_playbook.call_count == 4 @@ -123,7 +132,7 @@ def test_successful_edge_port_creation_with_auto_ga_id_creation( test_client, ): product_id = get_product_id_by_name(ProductName.EDGE_PORT) - initial_data = [{"product": product_id}, *input_form_wizard_data] + initial_data = [{"product": product_id}, *input_form_wizard_data()] initial_data[1]["generate_ga_id"] = True initial_data[1]["ga_id"] = None result, process_stat, step_log = run_workflow("create_edge_port", initial_data) @@ -152,8 +161,8 @@ def test_edge_port_creation_with_invalid_input( ): product_id = get_product_id_by_name(ProductName.EDGE_PORT) # If the number of members is greater than 1 then LACP must be enabled. - input_form_wizard_data[0]["enable_lacp"] = False - initial_data = [{"product": product_id}, *input_form_wizard_data] + initial_data = [{"product": product_id}, *input_form_wizard_data()] + initial_data[1]["enable_lacp"] = False with pytest.raises(FormValidationError) as error: run_workflow("create_edge_port", initial_data) @@ -175,7 +184,7 @@ def test_edge_port_creation_with_existing_ga_id( ): subscription = edge_port_subscription_factory(ga_id="GA-12345") product_id = get_product_id_by_name(ProductName.EDGE_PORT) - initial_data = [{"product": product_id}, *input_form_wizard_data] + initial_data = [{"product": product_id}, *input_form_wizard_data()] with pytest.raises(FormValidationError) as error: run_workflow("create_edge_port", initial_data) diff --git a/test/workflows/edge_port/test_migrate_edge_port.py b/test/workflows/edge_port/test_migrate_edge_port.py index 6c7c55d790bbbe6ef2b4a7c8743041c9c26389c4..821e7e82b9c8e071008c706ab736e9d164ac26be 100644 --- a/test/workflows/edge_port/test_migrate_edge_port.py +++ b/test/workflows/edge_port/test_migrate_edge_port.py @@ -4,9 +4,9 @@ import pytest from gso.products import Layer2CircuitServiceType from gso.products.product_types.edge_port import EdgePort -from gso.products.product_types.l3_core_service import L3CoreServiceType from gso.products.product_types.router import Router from gso.utils.shared_enums import Vendor +from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES from test import USER_CONFIRM_EMPTY_FORM from test.services.conftest import MockedNetboxClient from test.workflows import ( @@ -89,16 +89,17 @@ def test_successful_edge_port_migration( layer_2_circuit_subscription_factory, ): edge_port = edge_port_subscription_factory(partner=partner) - for service_type in [service_type for service_type in L3CoreServiceType if not service_type.startswith("IMPORTED")]: - l3_core_service = l3_core_service_subscription_factory(partner=partner, l3_core_service_type=service_type) - l3_core_service.l3_core_service.ap_list[0].sbp.edge_port = edge_port.edge_port + for product_name in L3_PRODUCT_NAMES: + l3_core_service = l3_core_service_subscription_factory(partner=partner, product_name=product_name) + sbp = l3_core_service.l3_core.ap_list[0].sbp + sbp.edge_port = edge_port.edge_port l3_core_service.save() - for service_type in [ + for product_name in [ service_type for service_type in Layer2CircuitServiceType if not service_type.startswith("IMPORTED") ]: layer_2_circuit_subscription_factory( - partner=partner, layer_2_circuit_side_a_edgeport=edge_port, layer_2_circuit_service_type=service_type + partner=partner, layer_2_circuit_side_a_edgeport=edge_port, layer_2_circuit_service_type=product_name ) initial_data = [{"subscription_id": str(edge_port.subscription_id)}, *input_form_wizard_data] diff --git a/test/workflows/l3_core_service/test_create_imported_l3_core_service.py b/test/workflows/l3_core_service/test_create_imported_l3_core_service.py index 5350814fae155b403edce5bdaadba7a784cfda94..4032f1a081bf91473f73ae2cdf6b4891996e60c7 100644 --- a/test/workflows/l3_core_service/test_create_imported_l3_core_service.py +++ b/test/workflows/l3_core_service/test_create_imported_l3_core_service.py @@ -1,19 +1,24 @@ import pytest +from orchestrator.domain import SubscriptionModel from orchestrator.types import SubscriptionLifecycle +from gso.products import ProductName from gso.products.product_blocks.bgp_session import IPFamily -from gso.products.product_types.l3_core_service import L3_CORE_SERVICE_TYPES, ImportedL3CoreService +from gso.products.product_blocks.ias import IASFlavor from gso.utils.shared_enums import SBPType +from gso.workflows.l3_core_service.shared import L3_CREAT_IMPORTED_WF_MAP, L3_PRODUCT_NAMES +from test.fixtures.l3_core_service_fixtures import PRODUCT_IMPORTED_MAP from test.workflows import assert_complete, extract_state, run_workflow -@pytest.mark.parametrize("l3_core_service_type", L3_CORE_SERVICE_TYPES) -def test_create_imported_l3_core_service_success( - faker, partner_factory, edge_port_subscription_factory, l3_core_service_type -): +@pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES) +def test_create_imported_l3_core_service_success(faker, partner_factory, edge_port_subscription_factory, product_name): + extra_ias_data = { + "ias_flavor": IASFlavor.IASGWS, + } creation_form_input_data = { "partner": partner_factory()["name"], - "service_type": l3_core_service_type, + "product_name": product_name, "service_binding_ports": [ { "edge_port": edge_port_subscription_factory().subscription_id, @@ -69,8 +74,15 @@ def test_create_imported_l3_core_service_success( ], } - result, _, _ = run_workflow("create_imported_l3_core_service", [creation_form_input_data]) + input_data = ( + [creation_form_input_data, extra_ias_data] if product_name == ProductName.IAS else [creation_form_input_data] + ) + result, _, _ = run_workflow(f"{L3_CREAT_IMPORTED_WF_MAP[product_name]}", input_data) state = extract_state(result) - subscription = ImportedL3CoreService.from_subscription(state["subscription_id"]) + subscription = SubscriptionModel.from_subscription(state["subscription_id"]) assert_complete(result) assert subscription.status == SubscriptionLifecycle.ACTIVE + assert subscription.product.name == PRODUCT_IMPORTED_MAP[product_name] + + if product_name == ProductName.IAS: + assert subscription.ias.ias_flavor == IASFlavor.IASGWS diff --git a/test/workflows/l3_core_service/test_create_l3_core_service.py b/test/workflows/l3_core_service/test_create_l3_core_service.py index b3c43e59715fa9dafd916a94700a074afbc722f2..20d307f4977b83c011b77e88e2b24eca8cf57de9 100644 --- a/test/workflows/l3_core_service/test_create_l3_core_service.py +++ b/test/workflows/l3_core_service/test_create_l3_core_service.py @@ -1,12 +1,14 @@ from unittest.mock import patch import pytest +from orchestrator.domain import SubscriptionModel from orchestrator.types import SubscriptionLifecycle from gso.products import ProductName -from gso.products.product_types.l3_core_service import L3CoreService +from gso.products.product_blocks.ias import IASFlavor from gso.services.subscriptions import get_product_id_by_name from gso.utils.shared_enums import APType +from gso.workflows.l3_core_service.shared import L3_CREATION_WF_MAP, L3_PRODUCT_NAMES from test import USER_CONFIRM_EMPTY_FORM from test.services.conftest import MockedSharePointClient from test.workflows import ( @@ -35,23 +37,21 @@ def base_bgp_peer_input(faker): return _base_bgp_peer_input -@pytest.mark.parametrize( - "l3_core_type", [ProductName.GEANT_IP, ProductName.IAS, ProductName.GWS, ProductName.LHCONE, ProductName.COPERNICUS] -) +@pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES) @pytest.mark.workflow() @patch("gso.services.lso_client._send_request") -@patch("gso.workflows.l3_core_service.create_l3_core_service.SharePointClient") +@patch("gso.workflows.l3_core_service.base_create_l3_core_service.SharePointClient") def test_create_l3_core_service_success( mock_sharepoint_client, mock_lso_client, - l3_core_type, + product_name, faker, partner_factory, edge_port_subscription_factory, base_bgp_peer_input, ): partner = partner_factory() - product_id = get_product_id_by_name(l3_core_type) + product_id = get_product_id_by_name(product_name) edge_port_a = str(edge_port_subscription_factory(partner=partner).subscription_id) mock_sharepoint_client.return_value = MockedSharePointClient @@ -85,9 +85,16 @@ def test_create_l3_core_service_success( "v6_bgp_peer": base_bgp_peer_input() | {"add_v6_multicast": faker.boolean(), "peer_address": faker.ipv6()}, }, ] + + if product_name == ProductName.IAS: + extra_ias_data = { + "ias_flavor": IASFlavor.IASGWS, + } + form_input_data.append(extra_ias_data) + lso_interaction_count = 7 - result, process_stat, step_log = run_workflow("create_l3_core_service", form_input_data) + result, process_stat, step_log = run_workflow(L3_CREATION_WF_MAP[product_name], form_input_data) for _ in range(lso_interaction_count): result, step_log = assert_lso_interaction_success(result, process_stat, step_log) @@ -98,13 +105,18 @@ def test_create_l3_core_service_success( assert_complete(result) state = extract_state(result) - subscription = L3CoreService.from_subscription(state["subscription_id"]) + subscription = SubscriptionModel.from_subscription(state["subscription_id"]) + + assert subscription.product.name == product_name assert mock_lso_client.call_count == lso_interaction_count + 1 assert subscription.status == SubscriptionLifecycle.ACTIVE - assert len(subscription.l3_core_service.ap_list) == 1 + assert len(subscription.l3_core.ap_list) == 1 assert ( - str(subscription.l3_core_service.ap_list[0].sbp.edge_port.owner_subscription_id) + str(subscription.l3_core.ap_list[0].sbp.edge_port.owner_subscription_id) == form_input_data[2]["edge_port"]["edge_port"] ) - assert subscription.l3_core_service.ap_list[0].sbp.gs_id == "GS-12345" + assert subscription.l3_core.ap_list[0].sbp.gs_id == "GS-12345" assert mock_sharepoint_client.call_count == 1 + + if product_name == ProductName.IAS: + assert subscription.ias.ias_flavor == IASFlavor.IASGWS 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 index c04515cf80eaa615ccb35a37b3a0a4145c751388..7bb36d27fc49e749cdaf2708542d098da7adfa43 100644 --- a/test/workflows/l3_core_service/test_import_l3_core_service.py +++ b/test/workflows/l3_core_service/test_import_l3_core_service.py @@ -1,21 +1,23 @@ import pytest +from orchestrator.domain import SubscriptionModel from orchestrator.types import SubscriptionLifecycle -from gso.products.product_types.l3_core_service import IMPORTED_L3_CORE_SERVICE_TYPES, L3CoreService, L3CoreServiceType +from gso.workflows.l3_core_service.shared import L3_IMPORT_WF_MAP, L3_PRODUCT_NAMES +from test.fixtures.l3_core_service_fixtures import PRODUCT_IMPORTED_MAP from test.workflows import assert_complete, run_workflow -@pytest.mark.parametrize("l3_core_service_type", IMPORTED_L3_CORE_SERVICE_TYPES) +@pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES) @pytest.mark.workflow() -def test_import_l3_core_service_success(l3_core_service_subscription_factory, l3_core_service_type): - imported_l3_core_service = str( - l3_core_service_subscription_factory(l3_core_service_type=l3_core_service_type).subscription_id - ) - result, _, _ = run_workflow("import_l3_core_service", [{"subscription_id": imported_l3_core_service}]) - subscription = L3CoreService.from_subscription(imported_l3_core_service) +def test_import_l3_core_service_success(l3_core_service_subscription_factory, product_name): + imported_subscription = l3_core_service_subscription_factory(product_name=product_name, is_imported=True) + assert imported_subscription.product.name == PRODUCT_IMPORTED_MAP[product_name] + imported_l3_core_service = str(imported_subscription.subscription_id) + result, _, _ = run_workflow(L3_IMPORT_WF_MAP[product_name], [{"subscription_id": 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:]) + + subscription = SubscriptionModel.from_subscription(imported_l3_core_service) + assert subscription.product.name == product_name 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 index 04a69df97d99cfb3ca4fac7d98bc59f63550687e..18c5682e777ccef18fc95e3a235b829e9814c7e6 100644 --- a/test/workflows/l3_core_service/test_migrate_l3_core_service.py +++ b/test/workflows/l3_core_service/test_migrate_l3_core_service.py @@ -1,10 +1,11 @@ from unittest.mock import patch import pytest +from orchestrator.domain import SubscriptionModel from gso.products.product_types.edge_port import EdgePort -from gso.products.product_types.l3_core_service import L3_CORE_SERVICE_TYPES, L3CoreService from gso.utils.shared_enums import APType +from gso.workflows.l3_core_service.shared import L3_MIGRATION_WF_MAP, L3_PRODUCT_NAMES from test import USER_CONFIRM_EMPTY_FORM from test.workflows import ( assert_complete, @@ -17,7 +18,7 @@ from test.workflows import ( @pytest.mark.workflow() -@pytest.mark.parametrize("l3_core_service_type", L3_CORE_SERVICE_TYPES) +@pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES) @patch("gso.services.lso_client._send_request") def test_migrate_l3_core_service_success( mock_execute_playbook, @@ -25,29 +26,28 @@ def test_migrate_l3_core_service_success( edge_port_subscription_factory, partner_factory, l3_core_service_subscription_factory, - l3_core_service_type, + product_name, access_port_factory, ): partner = partner_factory() subscription_id = str( l3_core_service_subscription_factory( - partner=partner, l3_core_service_type=l3_core_service_type, ap_list=[access_port_factory()] + partner=partner, product_name=product_name, ap_list=[access_port_factory()] ).subscription_id ) destination_edge_port = str(edge_port_subscription_factory(partner=partner).subscription_id) - subscription = L3CoreService.from_subscription(subscription_id) - + subscription = SubscriptionModel.from_subscription(subscription_id) form_input_data = [ {"subscription_id": subscription_id}, { "tt_number": faker.tt_number(), - "source_edge_port": subscription.l3_core_service.ap_list[0].sbp.edge_port.owner_subscription_id, + "source_edge_port": subscription.l3_core.ap_list[0].sbp.edge_port.owner_subscription_id, }, {"destination_edge_port": destination_edge_port}, {}, ] - result, process_stat, step_log = run_workflow("migrate_l3_core_service", form_input_data) + result, process_stat, step_log = run_workflow(L3_MIGRATION_WF_MAP[product_name], form_input_data) for _ in range(5): result, step_log = assert_lso_interaction_success(result, process_stat, step_log) @@ -61,15 +61,15 @@ def test_migrate_l3_core_service_success( assert_complete(result) state = extract_state(result) - subscription = L3CoreService.from_subscription(state["subscription_id"]) + subscription = SubscriptionModel.from_subscription(state["subscription_id"]) assert mock_execute_playbook.call_count == 11 assert subscription.insync - assert len(subscription.l3_core_service.ap_list) == 1 - assert str(subscription.l3_core_service.ap_list[0].sbp.edge_port.owner_subscription_id) == destination_edge_port + assert len(subscription.l3_core.ap_list) == 1 + assert str(subscription.l3_core.ap_list[0].sbp.edge_port.owner_subscription_id) == destination_edge_port @pytest.mark.workflow() -@pytest.mark.parametrize("l3_core_service_type", L3_CORE_SERVICE_TYPES) +@pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES) @patch("gso.services.lso_client._send_request") def test_migrate_l3_core_service_scoped_emission( mock_execute_playbook, @@ -78,30 +78,29 @@ def test_migrate_l3_core_service_scoped_emission( access_port_factory, partner_factory, l3_core_service_subscription_factory, - l3_core_service_type, + product_name, ): partner = partner_factory() custom_ap_list = [access_port_factory(ap_type=APType.LOAD_BALANCED) for _ in range(5)] - subscription_id = str( - l3_core_service_subscription_factory( - partner=partner, l3_core_service_type=l3_core_service_type, ap_list=custom_ap_list - ).subscription_id + subscription = l3_core_service_subscription_factory( + partner=partner, product_name=product_name, ap_list=custom_ap_list ) - destination_edge_port = str(edge_port_subscription_factory(partner=partner).subscription_id) - subscription = L3CoreService.from_subscription(subscription_id) - source_edge_port = subscription.l3_core_service.ap_list[3].sbp.edge_port.owner_subscription_id + service_name = subscription.service_name_attribute + destination_edge_port_id = str(edge_port_subscription_factory(partner=partner).subscription_id) + ap_list = subscription.l3_core.ap_list + source_edge_port = ap_list[3].sbp.edge_port.owner_subscription_id form_input_data = [ - {"subscription_id": subscription_id}, + {"subscription_id": str(subscription.subscription_id)}, { "tt_number": faker.tt_number(), "source_edge_port": source_edge_port, }, - {"destination_edge_port": destination_edge_port}, + {"destination_edge_port": destination_edge_port_id}, {}, ] - result, process_stat, step_log = run_workflow("migrate_l3_core_service", form_input_data) + result, process_stat, step_log = run_workflow(L3_MIGRATION_WF_MAP[product_name], form_input_data) result, step_log = assert_lso_interaction_success(result, process_stat, step_log) @@ -124,10 +123,12 @@ def test_migrate_l3_core_service_scoped_emission( assert len(state["inventory"]["all"]["hosts"].keys()) == 1 transmitted_destination_ep_fqdn = next(iter(state["inventory"]["all"]["hosts"].keys())) assert ( - EdgePort.from_subscription(destination_edge_port).edge_port.node.router_fqdn == transmitted_destination_ep_fqdn + EdgePort.from_subscription(destination_edge_port_id).edge_port.node.router_fqdn + == transmitted_destination_ep_fqdn ) assert ( - state["subscription"]["l3_core_service"] == state["__old_subscriptions__"][subscription_id]["l3_core_service"] + state["subscription"][service_name]["l3_core"] + == state["__old_subscriptions__"][str(subscription.subscription_id)][service_name]["l3_core"] ) # Subscription is unchanged for now for _ in range(4): @@ -137,8 +138,9 @@ def test_migrate_l3_core_service_scoped_emission( assert_complete(result) state = extract_state(result) - subscription = L3CoreService.from_subscription(state["subscription_id"]) + subscription = SubscriptionModel.from_subscription(state["subscription_id"]) assert mock_execute_playbook.call_count == 11 assert subscription.insync - assert len(subscription.l3_core_service.ap_list) == 5 - assert str(subscription.l3_core_service.ap_list[3].sbp.edge_port.owner_subscription_id) == destination_edge_port + ap_list = subscription.l3_core.ap_list + assert len(ap_list) == 5 + assert str(ap_list[3].sbp.edge_port.owner_subscription_id) == destination_edge_port_id 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 index 51d9b8aefabeb1431d277cf46408d5f66d91e218..0b2a9f3f9377b3c2513f8db551d5c45fd860677c 100644 --- a/test/workflows/l3_core_service/test_modify_l3_core_service.py +++ b/test/workflows/l3_core_service/test_modify_l3_core_service.py @@ -1,44 +1,54 @@ import pytest +from orchestrator.domain import SubscriptionModel +from gso.products import ProductName from gso.products.product_blocks.bgp_session import IPFamily -from gso.products.product_types.l3_core_service import L3_CORE_SERVICE_TYPES, L3CoreService +from gso.products.product_blocks.ias import IASFlavor from gso.utils.shared_enums import APType -from gso.workflows.l3_core_service.modify_l3_core_service import Operation +from gso.workflows.l3_core_service.base_modify_l3_core_service import Operation +from gso.workflows.l3_core_service.shared import L3_MODIFICATION_WF_MAP, L3_PRODUCT_NAMES from test.workflows import extract_state, run_workflow -@pytest.mark.parametrize("l3_core_service_type", L3_CORE_SERVICE_TYPES) +@pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES) @pytest.mark.workflow() -def test_modify_l3_core_service_remove_edge_port_success( - faker, l3_core_service_subscription_factory, l3_core_service_type -): - subscription = l3_core_service_subscription_factory(l3_core_service_type=l3_core_service_type) - access_port = subscription.l3_core_service.ap_list[0] +def test_modify_l3_core_service_remove_edge_port_success(faker, l3_core_service_subscription_factory, product_name): + subscription = l3_core_service_subscription_factory(product_name=product_name) + access_port = subscription.l3_core.ap_list[0] input_form_data = [ {"subscription_id": str(subscription.subscription_id)}, {"tt_number": faker.tt_number(), "operation": Operation.REMOVE}, {"access_port": str(access_port.subscription_instance_id)}, ] - result, _, _ = run_workflow("modify_l3_core_service", input_form_data) + if product_name == ProductName.IAS: + extra_ias_data = { + "ias_flavor": IASFlavor.IASGWS, + } + input_form_data.append(extra_ias_data) + + result, _, _ = run_workflow(L3_MODIFICATION_WF_MAP[product_name], 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.BACKUP + subscription = SubscriptionModel.from_subscription(state["subscription_id"]) + ap_list = subscription.l3_core.ap_list + assert len(ap_list) == 1 + assert ap_list[0].ap_type == APType.BACKUP + if product_name == ProductName.IAS: + assert subscription.ias.ias_flavor == IASFlavor.IASGWS -@pytest.mark.parametrize("l3_core_service_type", L3_CORE_SERVICE_TYPES) +@pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES) @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, + product_name, ): partner = partner_factory() - subscription = l3_core_service_subscription_factory(partner=partner, l3_core_service_type=l3_core_service_type) + subscription = l3_core_service_subscription_factory(partner=partner, product_name=product_name) new_edge_port = edge_port_subscription_factory(partner=partner).subscription_id input_form_data = [ {"subscription_id": str(subscription.subscription_id)}, @@ -70,11 +80,18 @@ def test_modify_l3_core_service_add_new_edge_port_success( }, ] - result, _, _ = run_workflow("modify_l3_core_service", input_form_data) + if product_name == ProductName.IAS: + extra_ias_data = { + "ias_flavor": IASFlavor.IASGWS, + } + input_form_data.append(extra_ias_data) + + result, _, _ = run_workflow(L3_MODIFICATION_WF_MAP[product_name], input_form_data) state = extract_state(result) - subscription = L3CoreService.from_subscription(state["subscription_id"]) - new_ap = subscription.l3_core_service.ap_list[-1] + subscription = SubscriptionModel.from_subscription(state["subscription_id"]) + ap_list = subscription.l3_core.ap_list + new_ap = ap_list[-1] assert new_ap.ap_type == APType.BACKUP assert new_ap.sbp.gs_id == input_form_data[2]["gs_id"] assert new_ap.sbp.vlan_id == input_form_data[2]["vlan_id"] @@ -82,7 +99,9 @@ def test_modify_l3_core_service_add_new_edge_port_success( assert new_ap.sbp.ipv4_mask == input_form_data[2]["ipv4_mask"] assert str(new_ap.sbp.ipv6_address) == input_form_data[2]["ipv6_address"] assert new_ap.sbp.ipv6_mask == input_form_data[2]["ipv6_mask"] - assert len(subscription.l3_core_service.ap_list) == 3 + assert len(ap_list) == 3 + if product_name == ProductName.IAS: + assert subscription.ias.ias_flavor == IASFlavor.IASGWS @pytest.fixture() @@ -129,121 +148,75 @@ def sbp_input_form_data(faker): return _generate_form_data -@pytest.mark.parametrize("l3_core_service_type", L3_CORE_SERVICE_TYPES) +@pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES) @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 + faker, l3_core_service_subscription_factory, product_name, sbp_input_form_data ): - subscription = l3_core_service_subscription_factory(l3_core_service_type=l3_core_service_type) + subscription = l3_core_service_subscription_factory(product_name=product_name) new_sbp_data = sbp_input_form_data() + ap_list = subscription.l3_core.ap_list input_form_data = [ {"subscription_id": str(subscription.subscription_id)}, {"tt_number": faker.tt_number(), "operation": Operation.EDIT}, - {"access_port": subscription.l3_core_service.ap_list[0].subscription_instance_id}, + {"access_port": ap_list[0].subscription_instance_id}, {**new_sbp_data}, ] - result, _, _ = run_workflow("modify_l3_core_service", input_form_data) + if product_name == ProductName.IAS: + extra_ias_data = { + "ias_flavor": IASFlavor.IASGWS, + } + input_form_data.append(extra_ias_data) - state = extract_state(result) - subscription = L3CoreService.from_subscription(state["subscription_id"]) - assert len(subscription.l3_core_service.ap_list) == 2 - - assert subscription.l3_core_service.ap_list[0].sbp.gs_id == new_sbp_data["gs_id"] - assert subscription.l3_core_service.ap_list[0].sbp.is_tagged == new_sbp_data["is_tagged"] - assert subscription.l3_core_service.ap_list[0].sbp.vlan_id == new_sbp_data["vlan_id"] - assert str(subscription.l3_core_service.ap_list[0].sbp.ipv4_address) == new_sbp_data["ipv4_address"] - assert subscription.l3_core_service.ap_list[0].sbp.ipv4_mask == new_sbp_data["ipv4_mask"] - assert str(subscription.l3_core_service.ap_list[0].sbp.ipv6_address) == new_sbp_data["ipv6_address"] - assert subscription.l3_core_service.ap_list[0].sbp.ipv6_mask == new_sbp_data["ipv6_mask"] - assert ( - subscription.l3_core_service.ap_list[0].sbp.custom_firewall_filters == new_sbp_data["custom_firewall_filters"] - ) + result, _, _ = run_workflow(L3_MODIFICATION_WF_MAP[product_name], input_form_data) - assert ( - subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[0].bfd_enabled - == new_sbp_data["v4_bgp_bfd_enabled"] - ) - assert ( - subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[0].has_custom_policies - == new_sbp_data["v4_bgp_has_custom_policies"] - ) - assert ( - subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[0].authentication_key - == new_sbp_data["v4_bgp_authentication_key"] - ) - assert ( - subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[0].multipath_enabled - == new_sbp_data["v4_bgp_multipath_enabled"] - ) - assert ( - subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[0].send_default_route - == new_sbp_data["v4_bgp_send_default_route"] - ) - assert ( - subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[0].is_passive == new_sbp_data["v4_bgp_is_passive"] - ) - assert ( - str(subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[0].peer_address) - == new_sbp_data["v4_bgp_peer_address"] - ) - assert ( - bool(IPFamily.V4MULTICAST in subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[0].families) + state = extract_state(result) + subscription = SubscriptionModel.from_subscription(state["subscription_id"]) + ap_list = subscription.l3_core.ap_list + assert len(ap_list) == 2 + + access_port = ap_list[0] + assert access_port.sbp.gs_id == new_sbp_data["gs_id"] + assert access_port.sbp.is_tagged == new_sbp_data["is_tagged"] + assert access_port.sbp.vlan_id == new_sbp_data["vlan_id"] + assert str(access_port.sbp.ipv4_address) == new_sbp_data["ipv4_address"] + assert access_port.sbp.ipv4_mask == new_sbp_data["ipv4_mask"] + assert str(access_port.sbp.ipv6_address) == new_sbp_data["ipv6_address"] + assert access_port.sbp.ipv6_mask == new_sbp_data["ipv6_mask"] + assert access_port.sbp.custom_firewall_filters == new_sbp_data["custom_firewall_filters"] + + assert access_port.sbp.bgp_session_list[0].bfd_enabled == new_sbp_data["v4_bgp_bfd_enabled"] + assert access_port.sbp.bgp_session_list[0].has_custom_policies == new_sbp_data["v4_bgp_has_custom_policies"] + assert access_port.sbp.bgp_session_list[0].authentication_key == new_sbp_data["v4_bgp_authentication_key"] + assert access_port.sbp.bgp_session_list[0].multipath_enabled == new_sbp_data["v4_bgp_multipath_enabled"] + assert access_port.sbp.bgp_session_list[0].send_default_route == new_sbp_data["v4_bgp_send_default_route"] + assert access_port.sbp.bgp_session_list[0].is_passive == new_sbp_data["v4_bgp_is_passive"] + assert str(access_port.sbp.bgp_session_list[0].peer_address) == new_sbp_data["v4_bgp_peer_address"] + assert ( + bool(IPFamily.V4MULTICAST in access_port.sbp.bgp_session_list[0].families) == new_sbp_data["v4_bgp_add_v4_multicast"] ) + assert access_port.sbp.bgp_session_list[1].bfd_enabled == new_sbp_data["v6_bgp_bfd_enabled"] + assert access_port.sbp.bgp_session_list[1].has_custom_policies == new_sbp_data["v6_bgp_has_custom_policies"] + assert access_port.sbp.bgp_session_list[1].authentication_key == new_sbp_data["v6_bgp_authentication_key"] + assert access_port.sbp.bgp_session_list[1].multipath_enabled == new_sbp_data["v6_bgp_multipath_enabled"] + assert access_port.sbp.bgp_session_list[1].send_default_route == new_sbp_data["v6_bgp_send_default_route"] + assert access_port.sbp.bgp_session_list[1].is_passive == new_sbp_data["v6_bgp_is_passive"] + assert str(access_port.sbp.bgp_session_list[1].peer_address) == new_sbp_data["v6_bgp_peer_address"] assert ( - subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[1].bfd_enabled - == new_sbp_data["v6_bgp_bfd_enabled"] - ) - assert ( - subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[1].has_custom_policies - == new_sbp_data["v6_bgp_has_custom_policies"] - ) - assert ( - subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[1].authentication_key - == new_sbp_data["v6_bgp_authentication_key"] - ) - assert ( - subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[1].multipath_enabled - == new_sbp_data["v6_bgp_multipath_enabled"] - ) - assert ( - subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[1].send_default_route - == new_sbp_data["v6_bgp_send_default_route"] - ) - assert ( - subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[1].is_passive == new_sbp_data["v6_bgp_is_passive"] - ) - assert ( - str(subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[1].peer_address) - == new_sbp_data["v6_bgp_peer_address"] - ) - assert ( - bool(IPFamily.V6MULTICAST in subscription.l3_core_service.ap_list[0].sbp.bgp_session_list[1].families) + bool(IPFamily.V6MULTICAST in access_port.sbp.bgp_session_list[1].families) == new_sbp_data["v6_bgp_add_v6_multicast"] ) - assert subscription.l3_core_service.ap_list[0].sbp.v4_bfd_settings.bfd_enabled == new_sbp_data["v4_bfd_enabled"] - assert ( - subscription.l3_core_service.ap_list[0].sbp.v4_bfd_settings.bfd_interval_rx - == new_sbp_data["v4_bfd_interval_rx"] - ) - assert ( - subscription.l3_core_service.ap_list[0].sbp.v4_bfd_settings.bfd_interval_tx - == new_sbp_data["v4_bfd_interval_tx"] - ) - assert ( - subscription.l3_core_service.ap_list[0].sbp.v4_bfd_settings.bfd_multiplier == new_sbp_data["v4_bfd_multiplier"] - ) - assert subscription.l3_core_service.ap_list[0].sbp.v6_bfd_settings.bfd_enabled == new_sbp_data["v6_bfd_enabled"] - assert ( - subscription.l3_core_service.ap_list[0].sbp.v6_bfd_settings.bfd_interval_rx - == new_sbp_data["v6_bfd_interval_rx"] - ) - assert ( - subscription.l3_core_service.ap_list[0].sbp.v6_bfd_settings.bfd_interval_tx - == new_sbp_data["v6_bfd_interval_tx"] - ) - assert ( - subscription.l3_core_service.ap_list[0].sbp.v6_bfd_settings.bfd_multiplier == new_sbp_data["v6_bfd_multiplier"] - ) + assert access_port.sbp.v4_bfd_settings.bfd_enabled == new_sbp_data["v4_bfd_enabled"] + assert access_port.sbp.v4_bfd_settings.bfd_interval_rx == new_sbp_data["v4_bfd_interval_rx"] + assert access_port.sbp.v4_bfd_settings.bfd_interval_tx == new_sbp_data["v4_bfd_interval_tx"] + assert access_port.sbp.v4_bfd_settings.bfd_multiplier == new_sbp_data["v4_bfd_multiplier"] + assert access_port.sbp.v6_bfd_settings.bfd_enabled == new_sbp_data["v6_bfd_enabled"] + assert access_port.sbp.v6_bfd_settings.bfd_interval_rx == new_sbp_data["v6_bfd_interval_rx"] + assert access_port.sbp.v6_bfd_settings.bfd_interval_tx == new_sbp_data["v6_bfd_interval_tx"] + assert access_port.sbp.v6_bfd_settings.bfd_multiplier == new_sbp_data["v6_bfd_multiplier"] + + if product_name == ProductName.IAS: + assert subscription.ias.ias_flavor == IASFlavor.IASGWS diff --git a/test/workflows/l3_core_service/test_terminate_l3_core_service.py b/test/workflows/l3_core_service/test_terminate_l3_core_service.py index 3ac23351e30dd90bb8ffbe2b2fac01bc3147dbd9..6fd3e68f31f7a2eec274d22a0da577236256d53c 100644 --- a/test/workflows/l3_core_service/test_terminate_l3_core_service.py +++ b/test/workflows/l3_core_service/test_terminate_l3_core_service.py @@ -1,20 +1,19 @@ import pytest +from orchestrator.domain import SubscriptionModel -from gso.products.product_types.l3_core_service import L3_CORE_SERVICE_TYPES, L3CoreService +from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES, L3_TERMINATION_WF_MAP from test.workflows import assert_complete, extract_state, run_workflow @pytest.mark.workflow() -@pytest.mark.parametrize("l3_core_service_type", L3_CORE_SERVICE_TYPES) -def test_terminate_l3_core_service(l3_core_service_type, l3_core_service_subscription_factory, faker): - subscription_id = str( - l3_core_service_subscription_factory(l3_core_service_type=l3_core_service_type).subscription_id - ) +@pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES) +def test_terminate_l3_core_service(product_name, l3_core_service_subscription_factory, faker): + subscription_id = str(l3_core_service_subscription_factory(product_name=product_name).subscription_id) initial_form_data = [{"subscription_id": subscription_id}, {"tt_number": faker.tt_number()}] - result, _, _ = run_workflow("terminate_l3_core_service", initial_form_data) + result, _, _ = run_workflow(L3_TERMINATION_WF_MAP[product_name], initial_form_data) assert_complete(result) state = extract_state(result) subscription_id = state["subscription_id"] - subscription = L3CoreService.from_subscription(subscription_id) + subscription = SubscriptionModel.from_subscription(subscription_id) assert subscription.status == "terminated" diff --git a/test/workflows/l3_core_service/test_validate_l3_core_service.py b/test/workflows/l3_core_service/test_validate_l3_core_service.py index cd6db8369446ee77ec73b9c0c70acbac217f3dea..ad6a06ca2ebf68737ca0e8a84173782a5d3f2caa 100644 --- a/test/workflows/l3_core_service/test_validate_l3_core_service.py +++ b/test/workflows/l3_core_service/test_validate_l3_core_service.py @@ -1,29 +1,26 @@ from unittest.mock import patch import pytest +from orchestrator.domain import SubscriptionModel -from gso.products.product_types.l3_core_service import L3_CORE_SERVICE_TYPES, L3CoreService +from gso.workflows.l3_core_service.shared import L3_PRODUCT_NAMES, L3_VALIDATION_WF_MAP from test.workflows import assert_complete, assert_lso_success, extract_state, run_workflow @pytest.mark.workflow() @patch("gso.services.lso_client._send_request") -@pytest.mark.parametrize("l3_core_service_type", L3_CORE_SERVICE_TYPES) -def test_validate_l3_core_service( - mock_lso_interaction, l3_core_service_subscription_factory, faker, l3_core_service_type -): - subscription_id = str( - l3_core_service_subscription_factory(l3_core_service_type=l3_core_service_type).subscription_id - ) +@pytest.mark.parametrize("product_name", L3_PRODUCT_NAMES) +def test_validate_l3_core_service(mock_lso_interaction, l3_core_service_subscription_factory, faker, product_name): + subscription_id = str(l3_core_service_subscription_factory(product_name=product_name).subscription_id) initial_l3_core_service_data = [{"subscription_id": subscription_id}] - result, process_stat, step_log = run_workflow("validate_l3_core_service", initial_l3_core_service_data) + result, process_stat, step_log = run_workflow(L3_VALIDATION_WF_MAP[product_name], initial_l3_core_service_data) result, step_log = assert_lso_success(result, process_stat, step_log) result, _ = assert_lso_success(result, process_stat, step_log) assert_complete(result) state = extract_state(result) subscription_id = state["subscription_id"] - subscription = L3CoreService.from_subscription(subscription_id) + subscription = SubscriptionModel.from_subscription(subscription_id) assert subscription.status == "active" assert subscription.insync is True assert mock_lso_interaction.call_count == 2 diff --git a/test/workflows/l3_core_service/test_validate_prefix_list.py b/test/workflows/l3_core_service/test_validate_prefix_list.py index 5ec52d8e58a29f9dd6f7c05a587a5ae83cf63a01..a2baecb6839c73125d52da4eda76ac84f9c4e389 100644 --- a/test/workflows/l3_core_service/test_validate_prefix_list.py +++ b/test/workflows/l3_core_service/test_validate_prefix_list.py @@ -2,7 +2,7 @@ from unittest.mock import patch import pytest -from gso.products.product_types.l3_core_service import L3_CORE_SERVICE_TYPES, L3CoreService, L3CoreServiceType +from gso.products.product_types.geant_ip import GeantIP from gso.utils.shared_enums import APType, Vendor from test import USER_CONFIRM_EMPTY_FORM from test.workflows import ( @@ -19,48 +19,41 @@ from test.workflows import ( @pytest.mark.workflow() @patch("gso.services.lso_client._send_request") -@pytest.mark.parametrize("l3_core_service_type", L3_CORE_SERVICE_TYPES) -def test_validate_prefix_list_success( - mock_lso_interaction, l3_core_service_subscription_factory, faker, l3_core_service_type -): - should_run_validation = l3_core_service_type == L3CoreServiceType.GEANT_IP - subscription_id = str( - l3_core_service_subscription_factory(l3_core_service_type=l3_core_service_type).subscription_id - ) +def test_validate_prefix_list_success(mock_lso_interaction, geant_ip_subscription_factory, faker): + subscription_id = str(geant_ip_subscription_factory().subscription_id) initial_l3_core_service_data = [{"subscription_id": subscription_id}] # Run the workflow and extract results - result, process_stat, step_log = run_workflow("validate_prefix_list", initial_l3_core_service_data) + result, process_stat, step_log = run_workflow("validate_geant_ip_prefix_list", initial_l3_core_service_data) - # If validation should run, assert LSO success - if should_run_validation: - result, step_log = assert_lso_success(result, process_stat, step_log) - assert_complete(result) + result, step_log = assert_lso_success(result, process_stat, step_log) + assert_complete(result) # Extract the state and validate subscription attributes state = extract_state(result) subscription_id = state["subscription_id"] - subscription = L3CoreService.from_subscription(subscription_id) + subscription = GeantIP.from_subscription(subscription_id) assert subscription.status == "active" assert subscription.insync is True + # Verify the subscription has no Juniper devices + for ap in subscription.geant_ip.l3_core.ap_list: + assert ap.sbp.edge_port.node.vendor != Vendor.JUNIPER # Verify the number of LSO interactions - assert mock_lso_interaction.call_count == (1 if should_run_validation else 0) + assert mock_lso_interaction.call_count == 1 @pytest.mark.workflow() @patch("gso.services.lso_client._send_request") -def test_validate_prefix_list_with_diff(mock_lso_interaction, l3_core_service_subscription_factory, faker): +def test_validate_prefix_list_with_diff(mock_lso_interaction, geant_ip_subscription_factory, faker): """Test case where playbook_has_diff qualifies and additional steps are executed.""" - subscription_id = str( - l3_core_service_subscription_factory(l3_core_service_type=L3CoreServiceType.GEANT_IP).subscription_id - ) + subscription_id = str(geant_ip_subscription_factory().subscription_id) initial_l3_core_service_data = [{"subscription_id": subscription_id}] # Run the workflow and extract results - result, process_stat, step_log = run_workflow("validate_prefix_list", initial_l3_core_service_data) + result, process_stat, step_log = run_workflow("validate_geant_ip_prefix_list", initial_l3_core_service_data) # Assert LSO success and workflow completion result, step_log = assert_lso_failure(result, process_stat, step_log) # Interaction has failed, we will need to redeploy this prefix list state = extract_state(result) subscription_id = state["subscription_id"] - subscription = L3CoreService.from_subscription(subscription_id) + subscription = GeantIP.from_subscription(subscription_id) assert not subscription.insync assert_suspended(result) @@ -72,7 +65,7 @@ def test_validate_prefix_list_with_diff(mock_lso_interaction, l3_core_service_su # Extract the state and validate subscription attributes state = extract_state(result) subscription_id = state["subscription_id"] - subscription = L3CoreService.from_subscription(subscription_id) + subscription = GeantIP.from_subscription(subscription_id) assert subscription.status == "active" assert subscription.insync is True # Verify the number of LSO interactions @@ -81,21 +74,19 @@ def test_validate_prefix_list_with_diff(mock_lso_interaction, l3_core_service_su @pytest.mark.workflow() @patch("gso.services.lso_client._send_request") -def test_validate_prefix_list_without_diff(mock_lso_interaction, l3_core_service_subscription_factory, faker): +def test_validate_prefix_list_without_diff(mock_lso_interaction, geant_ip_subscription_factory, faker): """Test case where playbook_has_diff does not qualify and skips additional steps.""" - subscription_id = str( - l3_core_service_subscription_factory(l3_core_service_type=L3CoreServiceType.GEANT_IP).subscription_id - ) + subscription_id = str(geant_ip_subscription_factory().subscription_id) initial_l3_core_service_data = [{"subscription_id": subscription_id}] # Run the workflow and extract results - result, process_stat, step_log = run_workflow("validate_prefix_list", initial_l3_core_service_data) + result, process_stat, step_log = run_workflow("validate_geant_ip_prefix_list", initial_l3_core_service_data) # Assert LSO success and workflow completion result, step_log = assert_lso_success(result, process_stat, step_log) assert_complete(result) # Extract the state and validate subscription attributes state = extract_state(result) subscription_id = state["subscription_id"] - subscription = L3CoreService.from_subscription(subscription_id) + subscription = GeantIP.from_subscription(subscription_id) assert subscription.status == "active" assert subscription.insync is True # Verify the number of LSO interactions @@ -104,7 +95,7 @@ def test_validate_prefix_list_without_diff(mock_lso_interaction, l3_core_service @pytest.mark.workflow() def test_validate_prefix_skip_on_juniper( - l3_core_service_subscription_factory, + geant_ip_subscription_factory, access_port_factory, service_binding_port_factory, edge_port_subscription_factory, @@ -138,19 +129,15 @@ def test_validate_prefix_skip_on_juniper( ), ), ] - subscription_id = str( - l3_core_service_subscription_factory( - l3_core_service_type=L3CoreServiceType.GEANT_IP, ap_list=ap_list - ).subscription_id - ) + subscription_id = str(geant_ip_subscription_factory(ap_list=ap_list).subscription_id) initial_l3_core_service_data = [{"subscription_id": subscription_id}] # Run the workflow and extract results # Assert workflow completion since it is skipped if it is on a Juniper - result, _, _ = run_workflow("validate_prefix_list", initial_l3_core_service_data) + result, _, _ = run_workflow("validate_geant_ip_prefix_list", initial_l3_core_service_data) assert_complete(result) # Extract the state and validate subscription attributes state = extract_state(result) subscription_id = state["subscription_id"] - subscription = L3CoreService.from_subscription(subscription_id) + subscription = GeantIP.from_subscription(subscription_id) assert subscription.status == "active" assert subscription.insync is True