diff --git a/Changelog.md b/Changelog.md index 999e99e0a464426f71a805a05cbd139ced1fe74d..c760ba0c8b4c4ef9e51d68fdd0ad0673895cefad 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## [2.5] - 2024-07-16 +- Added import Opengear workflow. +- NAT-616 +- NAT-617: Restored ISIS metric to new node +- NAT-619: Fixed BFD update logic bug + ## [2.4] - 2024-06-25 - Fixed the issue with client_credentials grant type token in Authentication part. diff --git a/docs/source/module/workflows/index.rst b/docs/source/module/workflows/index.rst index 9ef57ae8414ab3e0d46154237f5aadc7b0817ede..d3f0d0a2eff42a8f3693d7c04f83e05ad2bdf6c1 100644 --- a/docs/source/module/workflows/index.rst +++ b/docs/source/module/workflows/index.rst @@ -17,3 +17,4 @@ Subpackages router/index site/index super_pop_switch/index + opengear/index diff --git a/docs/source/module/workflows/opengear/create_imported_opengear.rst b/docs/source/module/workflows/opengear/create_imported_opengear.rst new file mode 100644 index 0000000000000000000000000000000000000000..07765a76e09ca0923cd3466049798992672ba063 --- /dev/null +++ b/docs/source/module/workflows/opengear/create_imported_opengear.rst @@ -0,0 +1,6 @@ +``gso.workflows.opengear.create_imported_opengear`` +=================================================================== + +.. automodule:: gso.workflows.opengear.create_imported_opengear + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/opengear/import_opengear.rst b/docs/source/module/workflows/opengear/import_opengear.rst new file mode 100644 index 0000000000000000000000000000000000000000..9834c5b5303d40983b088a555d3a3051f58cb618 --- /dev/null +++ b/docs/source/module/workflows/opengear/import_opengear.rst @@ -0,0 +1,6 @@ +``gso.workflows.opengear.import_opengear`` +========================================================== + +.. automodule:: gso.workflows.opengear.import_opengear + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/opengear/index.rst b/docs/source/module/workflows/opengear/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..69acd4555a2f3c62880b7b799079b147922d0c95 --- /dev/null +++ b/docs/source/module/workflows/opengear/index.rst @@ -0,0 +1,16 @@ +``gso.workflows.opengear`` +================================== + +.. automodule:: gso.workflows.opengear + :members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + :maxdepth: 2 + :titlesonly: + + create_imported_opengear + import_opengear diff --git a/gso/cli/imports.py b/gso/cli/imports.py index c4dc1adf2cd9080a4401ceb34415de674d1d561e..f9dc660bd9b26058099954a9ed03aa583cf27846 100644 --- a/gso/cli/imports.py +++ b/gso/cli/imports.py @@ -145,8 +145,25 @@ class IptrunkImportModel(BaseModel): return self +class OpenGearImportModel(BaseModel): + """Required fields for importing an existing :class:`gso.products.product_types.opengear`.""" + + partner: str + opengear_site: str + opengear_hostname: str + opengear_wan_address: IPv4AddressType + opengear_wan_netmask: IPv4AddressType + opengear_wan_gateway: IPv4AddressType + + T = TypeVar( - "T", SiteImportModel, RouterImportModel, IptrunkImportModel, SuperPopSwitchImportModel, OfficeRouterImportModel + "T", + SiteImportModel, + RouterImportModel, + IptrunkImportModel, + SuperPopSwitchImportModel, + OfficeRouterImportModel, + OpenGearImportModel, ) common_filepath_option = typer.Option( @@ -263,6 +280,18 @@ def import_office_routers(filepath: str = common_filepath_option) -> None: ) +@app.command() +def import_opengear(filepath: str = common_filepath_option) -> None: + """Import Opengear into GSO.""" + _generic_import_product( + Path(filepath), + ProductType.IMPORTED_OPENGEAR, + "opengear", + "opengear_hostname", + OpenGearImportModel, + ) + + @app.command() def import_iptrunks(filepath: str = common_filepath_option) -> None: """Import IP trunks into GSO.""" diff --git a/gso/main.py b/gso/main.py old mode 100644 new mode 100755 diff --git a/gso/migrations/versions/2023-11-21_e8378fbcfbf3_add_initial_products.py b/gso/migrations/versions/2023-11-21_e8378fbcfbf3_add_initial_products.py index d0c1a38893f1355a597a121cd39d553b6a847325..b91dfc6795d87f858dc9dde7fc097bc7215634e2 100644 --- a/gso/migrations/versions/2023-11-21_e8378fbcfbf3_add_initial_products.py +++ b/gso/migrations/versions/2023-11-21_e8378fbcfbf3_add_initial_products.py @@ -10,9 +10,9 @@ from alembic import op # revision identifiers, used by Alembic. revision = 'e8378fbcfbf3' -down_revision = 'da5c9f4cce1c' -branch_labels = None -depends_on = None +down_revision = None +branch_labels = ("data",) +depends_on = "da5c9f4cce1c" def upgrade() -> None: diff --git a/gso/migrations/versions/2024-06-19_fvd7mfcfbs1q_update_modify_isis_workflow.py b/gso/migrations/versions/2024-06-19_fvd7mfcfbs1q_update_modify_isis_workflow.py index f3a1ae003a77aba2735742588f2355eca100c93b..96a18957725701658d2f21aa09780001fa4ba1bc 100644 --- a/gso/migrations/versions/2024-06-19_fvd7mfcfbs1q_update_modify_isis_workflow.py +++ b/gso/migrations/versions/2024-06-19_fvd7mfcfbs1q_update_modify_isis_workflow.py @@ -1,4 +1,4 @@ -"""Add upstream migrations as a dependency. +""" Modify ISIS metric workflow description Revision ID: fvd7mfcfbs1q Revises: diff --git a/gso/migrations/versions/2024-06-27_ccc7ac05063b_add_importedopengear_product.py b/gso/migrations/versions/2024-06-27_ccc7ac05063b_add_importedopengear_product.py new file mode 100644 index 0000000000000000000000000000000000000000..f6bee99fdfcd271f125504f50173db8c1fc7424d --- /dev/null +++ b/gso/migrations/versions/2024-06-27_ccc7ac05063b_add_importedopengear_product.py @@ -0,0 +1,47 @@ +"""Add ImportedOpenGear product.. + +Revision ID: ccc7ac05063b +Revises: fvd7mfcfbs1q +Create Date: 2024-06-27 11:07:11.122519 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'ccc7ac05063b' +down_revision = 'fvd7mfcfbs1q' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + conn = op.get_bind() + conn.execute(sa.text(""" +INSERT INTO products (name, description, product_type, tag, status) VALUES ('Imported Opengear', 'Imported Opengear Product', 'ImportedOpengear', 'IMPORTED_OPENGEAR', 'active') RETURNING products.product_id + """)) + conn.execute(sa.text(""" +INSERT INTO product_product_blocks (product_id, product_block_id) VALUES ((SELECT products.product_id FROM products WHERE products.name IN ('Imported Opengear')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('OpengearBlock'))) + """)) + + +def downgrade() -> None: + conn = op.get_bind() + conn.execute(sa.text(""" +DELETE FROM product_product_blocks WHERE product_product_blocks.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Imported Opengear')) AND product_product_blocks.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('OpengearBlock')) + """)) + 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 Opengear')))) + """)) + 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 Opengear'))) + """)) + 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 Opengear'))) + """)) + conn.execute(sa.text(""" +DELETE FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Imported Opengear')) + """)) + conn.execute(sa.text(""" +DELETE FROM products WHERE products.name IN ('Imported Opengear') + """)) diff --git a/gso/migrations/versions/2024-06-27_f6a38f9e9e18_add_import_opengear_workflows.py b/gso/migrations/versions/2024-06-27_f6a38f9e9e18_add_import_opengear_workflows.py new file mode 100644 index 0000000000000000000000000000000000000000..ff8a2ab3b1cb05131f352dd2cb480e39c4f2c9cf --- /dev/null +++ b/gso/migrations/versions/2024-06-27_f6a38f9e9e18_add_import_opengear_workflows.py @@ -0,0 +1,45 @@ +"""Add import Opengear workflows.. + +Revision ID: f6a38f9e9e18 +Revises: ccc7ac05063b +Create Date: 2024-06-27 11:48:05.331149 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'f6a38f9e9e18' +down_revision = 'ccc7ac05063b' +branch_labels = None +depends_on = None + + +from orchestrator.migrations.helpers import create_workflow, delete_workflow + +new_workflows = [ + { + "name": "create_imported_opengear", + "target": "CREATE", + "description": "Import Opengear", + "product_type": "ImportedOpengear" + }, + { + "name": "import_opengear", + "target": "MODIFY", + "description": "Import Opengear", + "product_type": "ImportedOpengear" + } +] + + +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/products/__init__.py b/gso/products/__init__.py index e81b36b8bd9d1777edac64aea4b4da77cba49b4c..9278fbe752d1fc4614f89c3f60c72ef3021908b7 100644 --- a/gso/products/__init__.py +++ b/gso/products/__init__.py @@ -11,7 +11,7 @@ from pydantic_forms.types import strEnum from gso.products.product_types.iptrunk import ImportedIptrunk, Iptrunk from gso.products.product_types.lan_switch_interconnect import LanSwitchInterconnect from gso.products.product_types.office_router import ImportedOfficeRouter, OfficeRouter -from gso.products.product_types.opengear import Opengear +from gso.products.product_types.opengear import ImportedOpengear, Opengear from gso.products.product_types.pop_vlan import PopVlan from gso.products.product_types.router import ImportedRouter, Router from gso.products.product_types.site import ImportedSite, Site @@ -36,6 +36,7 @@ class ProductName(strEnum): IMPORTED_SUPER_POP_SWITCH = "Imported super PoP switch" IMPORTED_OFFICE_ROUTER = "Imported office router" OPENGEAR = "Opengear" + IMPORTED_OPENGEAR = "Imported Opengear" class ProductType(strEnum): @@ -55,6 +56,7 @@ class ProductType(strEnum): IMPORTED_SUPER_POP_SWITCH = ImportedSuperPopSwitch.__name__ IMPORTED_OFFICE_ROUTER = ImportedOfficeRouter.__name__ OPENGEAR = Opengear.__name__ + IMPORTED_OPENGEAR = Opengear.__name__ SUBSCRIPTION_MODEL_REGISTRY.update( @@ -73,5 +75,6 @@ SUBSCRIPTION_MODEL_REGISTRY.update( ProductName.IMPORTED_SUPER_POP_SWITCH.value: ImportedSuperPopSwitch, ProductName.IMPORTED_OFFICE_ROUTER.value: ImportedOfficeRouter, ProductName.OPENGEAR.value: Opengear, + ProductName.IMPORTED_OPENGEAR.value: ImportedOpengear, }, ) diff --git a/gso/products/product_types/opengear.py b/gso/products/product_types/opengear.py index a752f07e958748584b4642f62776b8e88ab1fa54..816ff2cccdf8de64701601387a31d7d6399094a7 100644 --- a/gso/products/product_types/opengear.py +++ b/gso/products/product_types/opengear.py @@ -22,3 +22,17 @@ class Opengear(OpengearProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): """An Opengear that is currently active.""" opengear: OpengearBlock + + +class ImportedOpengearInactive(SubscriptionModel, is_base=True): + """An imported, inactive Opengear.""" + + opengear: OpengearBlockInactive + + +class ImportedOpengear( + ImportedOpengearInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE] +): + """An imported Opengear that is currently active.""" + + opengear: OpengearBlock diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py index e10c8b7f415f22cfdc0348ea42b445ba198f3f35..cf2eca0f71b724ca482b15107c7cf1844f554918 100644 --- a/gso/workflows/__init__.py +++ b/gso/workflows/__init__.py @@ -59,3 +59,7 @@ LazyWorkflowInstance( # Office router workflows LazyWorkflowInstance("gso.workflows.office_router.import_office_router", "import_office_router") LazyWorkflowInstance("gso.workflows.office_router.create_imported_office_router", "create_imported_office_router") + +# Opengear workflows +LazyWorkflowInstance("gso.workflows.opengear.create_imported_opengear", "create_imported_opengear") +LazyWorkflowInstance("gso.workflows.opengear.import_opengear", "import_opengear") diff --git a/gso/workflows/iptrunk/create_iptrunk.py b/gso/workflows/iptrunk/create_iptrunk.py index 440bb8e2ac0eef37df5af277864e126d61245214..30f2c46b172707fa905055339db060e57f81f440 100644 --- a/gso/workflows/iptrunk/create_iptrunk.py +++ b/gso/workflows/iptrunk/create_iptrunk.py @@ -494,11 +494,15 @@ def netbox_allocate_side_b_interfaces(subscription: IptrunkInactive) -> None: @step("Create a new SharePoint checklist item") -def create_new_sharepoint_checklist(subscription: IptrunkProvisioning, tt_number: str) -> State: +def create_new_sharepoint_checklist(subscription: IptrunkProvisioning, tt_number: str, process_id: UUIDstr) -> State: """Create a new checklist item in SharePoint for approving this IPtrunk.""" new_list_item_url = SharePointClient().add_list_item( - "ip_trunk", - {"Title": f"{subscription.description} - {subscription.iptrunk.geant_s_sid}", "TT_NUMBER": tt_number}, + list_name="ip_trunk", + fields={ + "Title": f"{subscription.description} - {subscription.iptrunk.geant_s_sid}", + "TT_NUMBER": tt_number, + "GAP_PROCESS_URL": f"{load_oss_params().GENERAL.public_hostname}/workflows/{process_id}", + }, ) return {"checklist_url": new_list_item_url} diff --git a/gso/workflows/iptrunk/migrate_iptrunk.py b/gso/workflows/iptrunk/migrate_iptrunk.py index 11294d9e2c50fb29b55eb99370162fb6d12c39aa..a92294934f94ef949fbd912f32ce65ab598e57b6 100644 --- a/gso/workflows/iptrunk/migrate_iptrunk.py +++ b/gso/workflows/iptrunk/migrate_iptrunk.py @@ -25,7 +25,7 @@ from pydantic import AfterValidator, ConfigDict, field_validator from pydantic_forms.validators import ReadOnlyField, validate_unique_list from pynetbox.models.dcim import Interfaces -from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock +from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock, IptrunkType from gso.products.product_types.iptrunk import Iptrunk from gso.products.product_types.router import Router from gso.services import infoblox @@ -266,6 +266,34 @@ def check_ip_trunk_optical_levels_post( return {"subscription": subscription} +@step("Check LLDP on the trunk endpoints") +def check_ip_trunk_lldp( + subscription: Iptrunk, + callback_route: str, + new_node: Router, + new_lag_member_interfaces: list[dict], + replace_index: int, +) -> State: + """Check LLDP on the new trunk endpoints.""" + extra_vars = { + "wfo_ip_trunk_json": json.loads(json_dumps(subscription)), + "new_node": json.loads(json_dumps(new_node)), + "new_lag_member_interfaces": new_lag_member_interfaces, + "replace_index": replace_index, + "check": "lldp", + } + + execute_playbook( + playbook_name="iptrunks_checks.yaml", + callback_route=callback_route, + inventory=f"{subscription.iptrunk.iptrunk_sides[1 - replace_index].iptrunk_side_node.router_fqdn}\n" + f"{new_node.router.router_fqdn}\n", + extra_vars=extra_vars, + ) + + return {"subscription": subscription} + + @step("[DRY RUN] Disable configuration on old router") def disable_old_config_dry( subscription: Iptrunk, @@ -416,6 +444,90 @@ def deploy_new_config_real( return {"subscription": subscription} +@step("[DRY RUN] Update BFD on the remaining side") +def update_remaining_side_bfd_dry( + subscription: Iptrunk, + callback_route: str, + new_node: Router, + replace_index: int, + process_id: UUIDstr, + tt_number: str, +) -> State: + """Perform a dry run of updating configuration on the remaining router.""" + extra_vars = { + "wfo_trunk_json": json.loads(json_dumps(subscription)), + "new_node": json.loads(json_dumps(new_node)), + "replace_index": replace_index, + "verb": "update", + "config_object": "bfd_update", + "dry_run": True, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} " f"- Update BFD config.", + } + + execute_playbook( + playbook_name="iptrunks_migration.yaml", + callback_route=callback_route, + inventory=subscription.iptrunk.iptrunk_sides[1 - replace_index].iptrunk_side_node.router_fqdn, + extra_vars=extra_vars, + ) + + return {"subscription": subscription} + + +@step("[FOR REAL] Update BFD on the remaining side") +def update_remaining_side_bfd_real( + subscription: Iptrunk, + callback_route: str, + new_node: Router, + replace_index: int, + process_id: UUIDstr, + tt_number: str, +) -> State: + """Update configuration on the remaining router.""" + extra_vars = { + "wfo_trunk_json": json.loads(json_dumps(subscription)), + "new_node": json.loads(json_dumps(new_node)), + "replace_index": replace_index, + "verb": "update", + "config_object": "bfd_update", + "dry_run": False, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} " f"- Update BFD config.", + } + + execute_playbook( + playbook_name="iptrunks_migration.yaml", + callback_route=callback_route, + inventory=subscription.iptrunk.iptrunk_sides[1 - replace_index].iptrunk_side_node.router_fqdn, + extra_vars=extra_vars, + ) + + return {"subscription": subscription} + + +@step("Check BFD session over trunk") +def check_ip_trunk_bfd( + subscription: Iptrunk, + callback_route: str, + new_node: Router, + replace_index: int, +) -> State: + """Check BFD session across the new trunk.""" + extra_vars = { + "wfo_ip_trunk_json": json.loads(json_dumps(subscription)), + "new_node": json.loads(json_dumps(new_node)), + "check": "bfd", + } + + execute_playbook( + playbook_name="iptrunks_checks.yaml", + callback_route=callback_route, + inventory=subscription.iptrunk.iptrunk_sides[1 - replace_index].iptrunk_side_node.router_fqdn, + extra_vars=extra_vars, + ) + + return {"subscription": subscription} + + @inputstep("Wait for confirmation", assignee=Assignee.SYSTEM) def confirm_continue_move_fiber() -> FormGenerator: """Wait for confirmation from an operator that the physical fiber has been moved.""" @@ -726,6 +838,9 @@ def migrate_iptrunk() -> StepList: == Vendor.NOKIA ) should_restore_isis_metric = conditional(lambda state: state["restore_isis_metric"]) + trunk_type_is_leased = conditional( + lambda state: state["subscription"]["iptrunk"]["iptrunk_type"] == IptrunkType.LEASED + ) return ( init @@ -739,17 +854,21 @@ def migrate_iptrunk() -> StepList: >> lso_interaction(disable_old_config_real) >> lso_interaction(deploy_new_config_dry) >> lso_interaction(deploy_new_config_real) + >> trunk_type_is_leased(lso_interaction(update_remaining_side_bfd_dry)) + >> trunk_type_is_leased(lso_interaction(update_remaining_side_bfd_real)) >> confirm_continue_move_fiber >> lso_interaction(check_ip_trunk_optical_levels_post) + >> lso_interaction(check_ip_trunk_lldp) + >> trunk_type_is_leased(lso_interaction(check_ip_trunk_bfd)) >> lso_interaction(check_ip_trunk_connectivity) >> lso_interaction(deploy_new_isis) >> lso_interaction(check_ip_trunk_isis) - >> should_restore_isis_metric(confirm_continue_restore_isis) - >> should_restore_isis_metric(lso_interaction(restore_isis_metric)) >> lso_interaction(delete_old_config_dry) >> lso_interaction(delete_old_config_real) >> update_ipam >> update_subscription_model + >> should_restore_isis_metric(confirm_continue_restore_isis) + >> should_restore_isis_metric(lso_interaction(restore_isis_metric)) >> old_side_is_nokia(netbox_remove_old_interfaces) >> new_side_is_nokia(netbox_allocate_new_interfaces) >> resync diff --git a/gso/workflows/opengear/__init__.py b/gso/workflows/opengear/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..111b88820f4d9488cd8701f1a4bcd38a1f3f6705 --- /dev/null +++ b/gso/workflows/opengear/__init__.py @@ -0,0 +1 @@ +"""Workflows related to Opengear subscriptions.""" diff --git a/gso/workflows/opengear/create_imported_opengear.py b/gso/workflows/opengear/create_imported_opengear.py new file mode 100644 index 0000000000000000000000000000000000000000..e88e329a44b505eaca4466078e9a68da6835e459 --- /dev/null +++ b/gso/workflows/opengear/create_imported_opengear.py @@ -0,0 +1,83 @@ +"""A creation workflow that adds an existing opengear to the service database.""" + +from orchestrator import workflow +from orchestrator.forms import FormPage +from orchestrator.targets import Target +from orchestrator.types import FormGenerator, State, SubscriptionLifecycle +from orchestrator.workflow import StepList, done, init, step +from orchestrator.workflows.steps import resync, set_status, store_process_subscription +from pydantic import ConfigDict + +from gso.products import ProductName +from gso.products.product_types.opengear import ImportedOpengearInactive +from gso.services.partners import get_partner_by_name +from gso.services.subscriptions import get_product_id_by_name, get_site_by_name +from gso.utils.shared_enums import IPv4AddressType + + +@step("Create subscription") +def create_subscription(partner: str) -> State: + """Create a new subscription object.""" + partner_id = get_partner_by_name(partner)["partner_id"] + product_id = get_product_id_by_name(ProductName.IMPORTED_OPENGEAR) + subscription = ImportedOpengearInactive.from_product_id(product_id, partner_id) + + return { + "subscription": subscription, + "subscription_id": subscription.subscription_id, + } + + +def initial_input_form_generator() -> FormGenerator: + """Generate a form that is filled in using information passed through the :term:`API` endpoint.""" + + class ImportOpengear(FormPage): + model_config = ConfigDict(title="Import Opengear") + + partner: str + opengear_site: str + opengear_hostname: str + opengear_wan_address: IPv4AddressType + opengear_wan_netmask: IPv4AddressType + opengear_wan_gateway: IPv4AddressType + + user_input = yield ImportOpengear + + return user_input.dict() + + +@step("Initialize subscription") +def initialize_subscription( + subscription: ImportedOpengearInactive, + opengear_site: str, + opengear_hostname: str, + opengear_wan_address: IPv4AddressType | None, + opengear_wan_netmask: IPv4AddressType | None, + opengear_wan_gateway: IPv4AddressType | None, +) -> State: + """Initialise the Imported Opengear subscription using input data.""" + subscription.opengear.opengear_site = get_site_by_name(opengear_site).site + subscription.opengear.opengear_hostname = opengear_hostname + subscription.opengear.opengear_wan_address = opengear_wan_address + subscription.opengear.opengear_wan_netmask = opengear_wan_netmask + subscription.opengear.opengear_wan_gateway = opengear_wan_gateway + + return {"subscription": subscription} + + +@workflow( + "Import Opengear", + initial_input_form=initial_input_form_generator, + target=Target.CREATE, +) +def create_imported_opengear() -> StepList: + """Import an Opengear without provisioning it.""" + return ( + init + >> create_subscription + >> store_process_subscription(Target.CREATE) + >> initialize_subscription + >> set_status(SubscriptionLifecycle.ACTIVE) + >> resync + >> done + ) diff --git a/gso/workflows/opengear/import_opengear.py b/gso/workflows/opengear/import_opengear.py new file mode 100644 index 0000000000000000000000000000000000000000..d9bb75306a39161dc03c78ca47cc70449cd56850 --- /dev/null +++ b/gso/workflows/opengear/import_opengear.py @@ -0,0 +1,27 @@ +"""A modification workflow for migrating an ImportedOpengear to an Opengear subscription.""" + +from orchestrator.targets import Target +from orchestrator.types import State, UUIDstr +from orchestrator.workflow import StepList, done, init, step, workflow +from orchestrator.workflows.steps import resync, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form + +from gso.products import ProductName +from gso.products.product_types.opengear import ImportedOpengear, Opengear +from gso.services.subscriptions import get_product_id_by_name + + +@step("Create new Opengear subscription") +def import_opengear_subscription(subscription_id: UUIDstr) -> State: + """Take an ImportedOpengear subscription, and turn it into an Opengear subscription.""" + old_opengear = ImportedOpengear.from_subscription(subscription_id) + product_id = get_product_id_by_name(ProductName.OPENGEAR) + new_subscription = Opengear.from_other_product(old_opengear, product_id) # type: ignore[arg-type] + + return {"subscription": new_subscription} + + +@workflow("Import Opengear", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None)) +def import_opengear() -> StepList: + """Modify an ImportedOpengear subscription into an Opengear subscription to complete the import.""" + return init >> store_process_subscription(Target.MODIFY) >> unsync >> import_opengear_subscription >> resync >> done diff --git a/gso/workflows/router/create_router.py b/gso/workflows/router/create_router.py index 233d982c0ea1a275b12b9540399c1bbd4d8b20dd..700f3f9c86112562d1a1ef127dad209da22b96be 100644 --- a/gso/workflows/router/create_router.py +++ b/gso/workflows/router/create_router.py @@ -21,6 +21,7 @@ from gso.services.lso_client import lso_interaction from gso.services.netbox_client import NetboxClient from gso.services.partners import get_partner_by_name from gso.services.sharepoint import SharePointClient +from gso.settings import load_oss_params from gso.utils.helpers import generate_fqdn, iso_from_ipv4 from gso.utils.shared_enums import PortNumber, Vendor from gso.utils.workflow_steps import ( @@ -224,10 +225,15 @@ def prompt_insert_in_radius(subscription: RouterInactive) -> FormGenerator: @step("Create a new SharePoint checklist") -def create_new_sharepoint_checklist(subscription: RouterProvisioning, tt_number: str) -> State: +def create_new_sharepoint_checklist(subscription: RouterProvisioning, tt_number: str, process_id: UUIDstr) -> State: """Create a new checklist in SharePoint for approving this router.""" new_list_item_url = SharePointClient().add_list_item( - "p_router", {"Title": subscription.router.router_fqdn, "TT_NUMBER": tt_number} + list_name="p_router", + fields={ + "Title": subscription.router.router_fqdn, + "TT_NUMBER": tt_number, + "GAP_PROCESS_URL": f"{load_oss_params().GENERAL.public_hostname}/workflows/{process_id}", + }, ) return {"checklist_url": new_list_item_url} diff --git a/setup.py b/setup.py index 646364102ed9634cc655a6882a8becf2cf3d02a7..64423cc3bf393a932ae4d2cd3b2c46c8e98e062f 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import find_packages, setup setup( name="geant-service-orchestrator", - version="2.4", + version="2.5", author="GÉANT Orchestration and Automation Team", author_email="goat@geant.org", description="GÉANT Service Orchestrator", diff --git a/test/cli/conftest.py b/test/cli/conftest.py index e002fa13c19973dfbe733aa47fba34981558f116..92d726fae5aaa0adf7a51eb4b4c4f0bc768b14c3 100644 --- a/test/cli/conftest.py +++ b/test/cli/conftest.py @@ -3,6 +3,7 @@ from test.fixtures import ( # noqa: F401 iptrunk_subscription_factory, nokia_router_subscription_factory, office_router_subscription_factory, + opengear_subscription_factory, site_subscription_factory, super_pop_switch_subscription_factory, ) diff --git a/test/cli/test_imports.py b/test/cli/test_imports.py index 3cd45db7d4715f046a0ab885b6b146f3b56f9db4..f25c64a98f6ea546394ac2b213ed668b4d8b24a1 100644 --- a/test/cli/test_imports.py +++ b/test/cli/test_imports.py @@ -7,6 +7,7 @@ import pytest from gso.cli.imports import ( import_iptrunks, import_office_routers, + import_opengear, import_routers, import_sites, import_super_pop_switches, @@ -175,6 +176,24 @@ def office_router_data(temp_file, faker, site_subscription_factory): return _office_router_data +@pytest.fixture() +def opengear_data(temp_file, faker, site_subscription_factory): + def _opengear_data(**kwargs): + opengear_data = { + "opengear_site": Site.from_subscription(site_subscription_factory()).site.site_name, + "opengear_hostname": faker.domain_name(levels=4), + "opengear_wan_address": str(faker.ipv4()), + "opengear_wan_netmask": str(faker.ipv4()), + "opengear_wan_gateway": str(faker.ipv4()), + } + opengear_data.update(**kwargs) + + temp_file.write_text(json.dumps([opengear_data])) + return {"path": str(temp_file), "data": opengear_data} + + return _opengear_data + + ########### # TESTS # ########### @@ -339,3 +358,9 @@ def test_import_office_router_success(mock_start_process, office_router_data): def test_import_super_pop_switch_success(mock_start_process, super_pop_switch_data): import_super_pop_switches(super_pop_switch_data()["path"]) assert mock_start_process.call_count == 1 + + +@patch("gso.cli.imports.start_process") +def test_import_opengear_success(mock_start_process, opengear_data): + import_opengear(opengear_data()["path"]) + assert mock_start_process.call_count == 1 diff --git a/test/conftest.py b/test/conftest.py index 8fae41e74198cd47d0cc6543b60b3b459b6be273..d0391c02ab4c83b0c3d0cc98a418dabe11c9e5ad 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -42,6 +42,7 @@ from test.fixtures import ( # noqa: F401 juniper_router_subscription_factory, nokia_router_subscription_factory, office_router_subscription_factory, + opengear_subscription_factory, site_subscription_factory, super_pop_switch_subscription_factory, test_workflow, diff --git a/test/fixtures.py b/test/fixtures.py index 316a43c338f20601fa4235f0cac11ceacc2317f4..4a996e7d63fa7f8f85af9235c5021aab7cdd44c7 100644 --- a/test/fixtures.py +++ b/test/fixtures.py @@ -25,6 +25,7 @@ from gso.products.product_blocks.router import RouterRole from gso.products.product_blocks.site import SiteTier from gso.products.product_types.iptrunk import ImportedIptrunkInactive, IptrunkInactive from gso.products.product_types.office_router import ImportedOfficeRouterInactive, OfficeRouterInactive +from gso.products.product_types.opengear import ImportedOpengearInactive, OpengearInactive from gso.products.product_types.router import ImportedRouterInactive, Router, RouterInactive from gso.products.product_types.site import ImportedSiteInactive, Site, SiteInactive from gso.products.product_types.super_pop_switch import ImportedSuperPopSwitchInactive, SuperPopSwitchInactive @@ -278,7 +279,7 @@ def iptrunk_subscription_factory(iptrunk_side_subscription_factory, faker, geant start_date="2023-05-24T00:00:00+00:00", geant_s_sid=None, iptrunk_description=None, - iptrunk_type=IptrunkType.DARK_FIBER, + iptrunk_type=IptrunkType.LEASED, iptrunk_speed=PhysicalPortCapacity.ONE_GIGABIT_PER_SECOND, iptrunk_isis_metric=None, iptrunk_ipv4_network=None, @@ -463,6 +464,65 @@ def super_pop_switch_subscription_factory(site_subscription_factory, faker, gean return subscription_create +@pytest.fixture() +def opengear_subscription_factory(site_subscription_factory, faker, geant_partner): + def subscription_create( + description=None, + start_date="2023-05-24T00:00:00+00:00", + opengear_site=None, + opengear_hostname=None, + opengear_wan_address=None, + opengear_wan_netmask=None, + opengear_wan_gateway=None, + status: SubscriptionLifecycle | None = None, + partner: dict | None = None, + *, + is_imported: bool | None = True, + ) -> UUIDstr: + if partner is None: + partner = geant_partner + + description = description or faker.text(max_nb_chars=30) + opengear_site = opengear_site or site_subscription_factory() + opengear_hostname = opengear_hostname or faker.domain_name(levels=4) + opengear_wan_address = opengear_wan_address or faker.ipv4() + opengear_wan_netmask = opengear_wan_netmask or faker.ipv4() + opengear_wan_gateway = opengear_wan_gateway or faker.ipv4() + + if is_imported: + product_id = subscriptions.get_product_id_by_name(ProductName.OPENGEAR) + opengear_subscription = OpengearInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + else: + product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_OPENGEAR) + opengear_subscription = ImportedOpengearInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + + opengear_subscription.opengear.opengear_site = Site.from_subscription(opengear_site).site + opengear_subscription.opengear.opengear_hostname = opengear_hostname + opengear_subscription.opengear.opengear_wan_address = opengear_wan_address + opengear_subscription.opengear.opengear_wan_netmask = opengear_wan_netmask + opengear_subscription.opengear.opengear_wan_gateway = opengear_wan_gateway + + opengear_subscription = SubscriptionModel.from_other_lifecycle( + opengear_subscription, SubscriptionLifecycle.ACTIVE + ) + opengear_subscription.description = description + opengear_subscription.start_date = start_date + + if status: + opengear_subscription.status = status + + opengear_subscription.save() + db.session.commit() + + return str(opengear_subscription.subscription_id) + + return subscription_create + + @pytest.fixture() def test_workflow(generic_subscription_1: UUIDstr, generic_product_type_1) -> Generator: _, generic_product_one = generic_product_type_1 diff --git a/test/workflows/iptrunk/test_migrate_iptrunk.py b/test/workflows/iptrunk/test_migrate_iptrunk.py index cd46d72100d3432cc868cdd8bb6cb710d0e74c44..5a8f49b3ad3e54bf106fafbb0d4392be8c696937 100644 --- a/test/workflows/iptrunk/test_migrate_iptrunk.py +++ b/test/workflows/iptrunk/test_migrate_iptrunk.py @@ -149,19 +149,19 @@ def test_migrate_iptrunk_success( result, process_stat, step_log = run_workflow("migrate_iptrunk", migrate_form_input) - for _ in range(6): + for _ in range(8): result, step_log = assert_lso_interaction_success(result, process_stat, step_log) assert_suspended(result) result, step_log = resume_workflow(process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) - for _ in range(4): + for _ in range(8): result, step_log = assert_lso_interaction_success(result, process_stat, step_log) assert_suspended(result) result, step_log = resume_workflow(process_stat, step_log, input_data=USER_CONFIRM_EMPTY_FORM) - for _ in range(3): + for _ in range(1): result, step_log = assert_lso_interaction_success(result, process_stat, step_log) assert_complete(result) @@ -171,7 +171,7 @@ def test_migrate_iptrunk_success( subscription = Iptrunk.from_subscription(subscription_id) assert subscription.status == "active" - assert mock_execute_playbook.call_count == 13 + assert mock_execute_playbook.call_count == 17 assert mock_create_host_by_ip.call_count == 1 assert mock_delete_host_by_ip.call_count == 1 diff --git a/test/workflows/opengear/__init__.py b/test/workflows/opengear/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/test/workflows/opengear/test_create_imported_opengear.py b/test/workflows/opengear/test_create_imported_opengear.py new file mode 100644 index 0000000000000000000000000000000000000000..0f6d9bf171103c5b5152937b47114a9b666bad5c --- /dev/null +++ b/test/workflows/opengear/test_create_imported_opengear.py @@ -0,0 +1,33 @@ +import pytest +from orchestrator.types import SubscriptionLifecycle + +from gso.products import ImportedOpengear, ProductName +from gso.products.product_types.site import Site +from test.workflows import ( + assert_complete, + extract_state, + run_workflow, +) + + +@pytest.fixture() +def workflow_input_data(faker, site_subscription_factory): + return { + "partner": "GEANT", + "opengear_site": Site.from_subscription(site_subscription_factory()).site.site_name, + "opengear_hostname": faker.hostname(), + "opengear_wan_address": faker.ipv4(), + "opengear_wan_netmask": faker.ipv4(), + "opengear_wan_gateway": faker.ipv4(), + } + + +@pytest.mark.workflow() +def test_create_imported_opengear_success(workflow_input_data): + result, _, _ = run_workflow("create_imported_opengear", [workflow_input_data]) + state = extract_state(result) + imported_opengear = ImportedOpengear.from_subscription(state["subscription_id"]) + + assert_complete(result) + assert imported_opengear.product.name == ProductName.IMPORTED_OPENGEAR + assert imported_opengear.status == SubscriptionLifecycle.ACTIVE diff --git a/test/workflows/opengear/test_import_opengear.py b/test/workflows/opengear/test_import_opengear.py new file mode 100644 index 0000000000000000000000000000000000000000..37c7d1718444341b8f645942b0c6baaf24f7e3a6 --- /dev/null +++ b/test/workflows/opengear/test_import_opengear.py @@ -0,0 +1,18 @@ +import pytest +from orchestrator.types import SubscriptionLifecycle + +from gso.products import ProductName +from gso.products.product_types.opengear import Opengear +from test.workflows import assert_complete, run_workflow + + +@pytest.mark.workflow() +def test_import_office_router_success(opengear_subscription_factory): + imported_opengear = opengear_subscription_factory(is_imported=False) + result, _, _ = run_workflow("import_opengear", [{"subscription_id": imported_opengear}]) + subscription = Opengear.from_subscription(imported_opengear) + + assert_complete(result) + assert subscription.product.name == ProductName.OPENGEAR + assert subscription.status == SubscriptionLifecycle.ACTIVE + assert subscription.insync