From fae59e1794f11b131ef3e93d761011a425ae8a2a Mon Sep 17 00:00:00 2001 From: Karel van Klink <karel.vanklink@geant.org> Date: Fri, 5 Apr 2024 14:57:58 +0200 Subject: [PATCH] Merge behaviour of cancel and terminate workflows --- gso/translations/en-GB.json | 1 - gso/workflows/__init__.py | 24 ++++++---- gso/workflows/iptrunk/terminate_iptrunk.py | 32 +++++++------ gso/workflows/router/terminate_router.py | 16 ++++--- gso/workflows/shared/__init__.py | 1 - gso/workflows/shared/cancel_subscription.py | 47 ------------------- gso/workflows/site/terminate_site.py | 9 +++- .../iptrunk/test_terminate_iptrunk.py | 1 - .../workflows/router/test_terminate_router.py | 14 +----- test/workflows/shared/__init__.py | 0 test/workflows/shared/cancel_subscription.py | 30 ------------ 11 files changed, 52 insertions(+), 123 deletions(-) delete mode 100644 gso/workflows/shared/__init__.py delete mode 100644 gso/workflows/shared/cancel_subscription.py delete mode 100644 test/workflows/shared/__init__.py delete mode 100644 test/workflows/shared/cancel_subscription.py diff --git a/gso/translations/en-GB.json b/gso/translations/en-GB.json index fe687f34..63b31e76 100644 --- a/gso/translations/en-GB.json +++ b/gso/translations/en-GB.json @@ -38,7 +38,6 @@ "workflow": { "activate_iptrunk": "Activate IP Trunk", "activate_router": "Activate router", - "cancel_subscription": "Cancel subscription", "confirm_info": "Please verify this form looks correct.", "deploy_twamp": "Deploy TWAMP", "migrate_iptrunk": "Migrate IP Trunk", diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py index 05848a32..1e89ec89 100644 --- a/gso/workflows/__init__.py +++ b/gso/workflows/__init__.py @@ -1,17 +1,26 @@ """Initialisation class that imports all workflows into :term:`GSO`.""" from orchestrator.services.subscriptions import WF_USABLE_MAP +from orchestrator.types import SubscriptionLifecycle from orchestrator.workflows import LazyWorkflowInstance +ALL_ALIVE_STATES: list[str] = [ + SubscriptionLifecycle.INITIAL, + SubscriptionLifecycle.PROVISIONING, + SubscriptionLifecycle.ACTIVE, +] + WF_USABLE_MAP.update( { - "cancel_subscription": ["initial"], - "redeploy_base_config": ["provisioning", "active"], - "update_ibgp_mesh": ["provisioning", "active"], - "activate_router": ["provisioning"], - "deploy_twamp": ["provisioning", "active"], - "modify_trunk_interface": ["provisioning", "active"], - "activate_iptrunk": ["provisioning"], + "redeploy_base_config": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE], + "update_ibgp_mesh": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE], + "activate_router": [SubscriptionLifecycle.PROVISIONING], + "deploy_twamp": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE], + "modify_trunk_interface": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE], + "activate_iptrunk": [SubscriptionLifecycle.PROVISIONING], + "terminate_site": ALL_ALIVE_STATES, + "terminate_router": ALL_ALIVE_STATES, + "terminate_iptrunk": ALL_ALIVE_STATES, } ) @@ -28,7 +37,6 @@ LazyWorkflowInstance("gso.workflows.router.redeploy_base_config", "redeploy_base LazyWorkflowInstance("gso.workflows.router.terminate_router", "terminate_router") LazyWorkflowInstance("gso.workflows.router.update_ibgp_mesh", "update_ibgp_mesh") LazyWorkflowInstance("gso.workflows.router.modify_connection_strategy", "modify_connection_strategy") -LazyWorkflowInstance("gso.workflows.shared.cancel_subscription", "cancel_subscription") LazyWorkflowInstance("gso.workflows.site.create_site", "create_site") LazyWorkflowInstance("gso.workflows.site.modify_site", "modify_site") LazyWorkflowInstance("gso.workflows.site.terminate_site", "terminate_site") diff --git a/gso/workflows/iptrunk/terminate_iptrunk.py b/gso/workflows/iptrunk/terminate_iptrunk.py index 7a2afe6c..a2cb6727 100644 --- a/gso/workflows/iptrunk/terminate_iptrunk.py +++ b/gso/workflows/iptrunk/terminate_iptrunk.py @@ -28,18 +28,23 @@ from gso.utils.shared_enums import Vendor from gso.utils.workflow_steps import set_isis_to_max -def initial_input_form_generator() -> FormGenerator: +def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: """Ask the operator to confirm whether router configuration and :term:`IPAM` resources should be deleted.""" + iptrunk = Iptrunk.from_subscription(subscription_id) class TerminateForm(FormPage): + if iptrunk.status == SubscriptionLifecycle.INITIAL: + info_label_2: Label = ( + "This will immediately mark the subscription as terminated, preventing any other workflows from " # type:ignore[assignment] + "interacting with this product subscription." + ) + info_label_3: Label = "ONLY EXECUTE THIS WORKFLOW WHEN YOU ARE ABSOLUTELY SURE WHAT YOU ARE DOING." # type:ignore[assignment] + + tt_number: str termination_label: Label = ( - "Please confirm whether configuration should get removed from the A and B sides of the trunk, and whether " - "IPAM and Netbox resources should be released." # type: ignore[assignment] + "Please confirm whether configuration should get removed from the A and B sides of the trunk." # type: ignore[assignment] ) - tt_number: str remove_configuration: bool = True - clean_up_ipam: bool = True - clean_up_netbox: bool = True @validator("tt_number", allow_reuse=True) def validate_tt_number(cls, tt_number: str) -> str: @@ -148,22 +153,19 @@ def terminate_iptrunk() -> StepList: * Let the operator decide whether to remove configuration from the routers, if so: * Set the :term:`ISIS` metric of the IP trunk to an arbitrarily high value * Disable and remove configuration from the routers, first as a dry run - * Mark the IP trunk interfaces as free in Netbox, if selected by the operator - * Clear :term:`IPAM` resources, if selected by the operator + * Mark the IP trunk interfaces as free in Netbox + * Clear :term:`IPAM` resources * Terminate the subscription in the service database """ run_config_steps = conditional(lambda state: state["remove_configuration"]) - run_ipam_steps = conditional(lambda state: state["clean_up_ipam"]) side_a_is_nokia = conditional( - lambda state: state["clean_up_netbox"] - and get_router_vendor( + lambda state: get_router_vendor( state["subscription"]["iptrunk"]["iptrunk_sides"][0]["iptrunk_side_node"]["owner_subscription_id"] ) == Vendor.NOKIA ) side_b_is_nokia = conditional( - lambda state: state["clean_up_netbox"] - and get_router_vendor( + lambda state: get_router_vendor( state["subscription"]["iptrunk"]["iptrunk_sides"][1]["iptrunk_side_node"]["owner_subscription_id"] ) == Vendor.NOKIA @@ -175,7 +177,6 @@ def terminate_iptrunk() -> StepList: >> lso_interaction(deprovision_ip_trunk_dry) >> lso_interaction(deprovision_ip_trunk_real) ) - ipam_steps = init >> deprovision_ip_trunk_ipv4 >> deprovision_ip_trunk_ipv6 return ( init @@ -184,7 +185,8 @@ def terminate_iptrunk() -> StepList: >> run_config_steps(config_steps) >> side_a_is_nokia(netbox_clean_up_side_a) >> side_b_is_nokia(netbox_clean_up_side_b) - >> run_ipam_steps(ipam_steps) + >> deprovision_ip_trunk_ipv4 + >> deprovision_ip_trunk_ipv6 >> set_status(SubscriptionLifecycle.TERMINATED) >> resync >> done diff --git a/gso/workflows/router/terminate_router.py b/gso/workflows/router/terminate_router.py index 8abc8877..0d46f9ab 100644 --- a/gso/workflows/router/terminate_router.py +++ b/gso/workflows/router/terminate_router.py @@ -32,13 +32,16 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: router = Router.from_subscription(subscription_id) class TerminateForm(FormPage): - termination_label: Label = ( - "Please confirm whether configuration should get removed from the router, and whether IPAM resources should" - " be released." # type: ignore[assignment] - ) + if router.status == SubscriptionLifecycle.INITIAL: + info_label_2: Label = ( + "This will immediately mark the subscription as terminated, preventing any other workflows from " # type:ignore[assignment] + "interacting with this product subscription." + ) + info_label_3: Label = "ONLY EXECUTE THIS WORKFLOW WHEN YOU ARE ABSOLUTELY SURE WHAT YOU ARE DOING." # type:ignore[assignment] + tt_number: str + termination_label: Label = "Please confirm whether configuration should get removed from the router." # type: ignore[assignment] remove_configuration: bool = True - clean_up_ipam: bool = True user_input = yield TerminateForm return user_input.dict() | {"router_is_nokia": router.router.vendor == Vendor.NOKIA} @@ -114,7 +117,6 @@ def terminate_router() -> StepList: * Disable and delete configuration on the router, if selected by the operator * Mark the subscription as terminated in the service database """ - run_ipam_steps = conditional(lambda state: state["clean_up_ipam"]) run_config_steps = conditional(lambda state: state["remove_configuration"]) router_is_nokia = conditional(lambda state: state["router_is_nokia"]) @@ -122,7 +124,7 @@ def terminate_router() -> StepList: init >> store_process_subscription(Target.TERMINATE) >> unsync - >> run_ipam_steps(deprovision_loopback_ips) + >> deprovision_loopback_ips >> run_config_steps(lso_interaction(remove_config_from_router_dry)) >> run_config_steps(lso_interaction(remove_config_from_router_real)) >> router_is_nokia(remove_device_from_netbox) diff --git a/gso/workflows/shared/__init__.py b/gso/workflows/shared/__init__.py deleted file mode 100644 index daa25805..00000000 --- a/gso/workflows/shared/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Workflows that are shared across multiple products.""" diff --git a/gso/workflows/shared/cancel_subscription.py b/gso/workflows/shared/cancel_subscription.py deleted file mode 100644 index 79bb0590..00000000 --- a/gso/workflows/shared/cancel_subscription.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Cancel a subscription that is in the initial lifecycle state.""" - -from orchestrator.forms import FormPage -from orchestrator.forms.validators import Label -from orchestrator.targets import Target -from orchestrator.types import FormGenerator, SubscriptionLifecycle, UUIDstr -from orchestrator.workflow import StepList, done, init, workflow -from orchestrator.workflows.steps import resync, set_status, store_process_subscription, unsync -from orchestrator.workflows.utils import wrap_modify_initial_input_form - - -def _initial_input_form(subscription_id: UUIDstr) -> FormGenerator: - class CancelSubscriptionForm(FormPage): - info_label: Label = f"Canceling subscription with ID {subscription_id}" # type:ignore[assignment] - info_label_2: Label = ( - "This will immediately mark the subscription as terminated, preventing any other workflows from interacting" # type:ignore[assignment] - " with this product subscription." - ) - info_label_3: Label = "ONLY EXECUTE THIS WORKFLOW WHEN YOU ARE ABSOLUTELY SURE WHAT YOU ARE DOING." # type:ignore[assignment] - info_label_4: Label = "THIS WORKFLOW IS IRREVERSIBLE AND MAY HAVE UNFORESEEN CONSEQUENCES." # type:ignore[assignment] - - yield CancelSubscriptionForm - - return {"subscription_id": subscription_id} - - -@workflow( - "Cancel an initial subscription", - initial_input_form=wrap_modify_initial_input_form(_initial_input_form), - target=Target.TERMINATE, -) -def cancel_subscription() -> StepList: - """Cancel an initial subscription, taking it from the ``INITIAL`` state straight to ``TERMINATED``. - - This workflow can be used when a creation workflow has failed, and the process needs to be restarted. This workflow - will prevent a stray subscription, forever stuck in the initial state, to stick around. - - * Update the subscription lifecycle state to ``TERMINATED``. - """ - return ( - init - >> store_process_subscription(Target.TERMINATE) - >> unsync - >> set_status(SubscriptionLifecycle.TERMINATED) - >> resync - >> done - ) diff --git a/gso/workflows/site/terminate_site.py b/gso/workflows/site/terminate_site.py index 91be194f..96e807b4 100644 --- a/gso/workflows/site/terminate_site.py +++ b/gso/workflows/site/terminate_site.py @@ -18,9 +18,16 @@ from gso.products.product_types.site import Site def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: """Ask the user for confirmation whether to terminate the selected site.""" - Site.from_subscription(subscription_id) + site = Site.from_subscription(subscription_id) class TerminateForm(FormPage): + if site.status == SubscriptionLifecycle.INITIAL: + info_label_2: Label = ( + "This will immediately mark the subscription as terminated, preventing any other workflows from " # type:ignore[assignment] + "interacting with this product subscription." + ) + info_label_3: Label = "ONLY EXECUTE THIS WORKFLOW WHEN YOU ARE ABSOLUTELY SURE WHAT YOU ARE DOING." # type:ignore[assignment] + termination_label: Label = "Are you sure you want to delete this site?" # type: ignore[assignment] user_input = yield TerminateForm diff --git a/test/workflows/iptrunk/test_terminate_iptrunk.py b/test/workflows/iptrunk/test_terminate_iptrunk.py index 05a94456..c026e84b 100644 --- a/test/workflows/iptrunk/test_terminate_iptrunk.py +++ b/test/workflows/iptrunk/test_terminate_iptrunk.py @@ -42,7 +42,6 @@ def test_successful_iptrunk_termination( { "tt_number": faker.tt_number(), "remove_configuration": True, - "clean_up_ipam": True, }, ] result, process_stat, step_log = run_workflow("terminate_iptrunk", initial_iptrunk_data) diff --git a/test/workflows/router/test_terminate_router.py b/test/workflows/router/test_terminate_router.py index bc978824..e5282682 100644 --- a/test/workflows/router/test_terminate_router.py +++ b/test/workflows/router/test_terminate_router.py @@ -7,15 +7,7 @@ from test.workflows import assert_complete, assert_lso_interaction_success, extr @pytest.mark.workflow() -@pytest.mark.parametrize( - ("remove_configuration", "clean_up_ipam"), - [ - (True, True), - (True, False), - (False, True), - (False, False), - ], -) +@pytest.mark.parametrize("remove_configuration", [True, False]) @patch("gso.services.lso_client._send_request") @patch("gso.workflows.router.terminate_router.NetboxClient.delete_device") @patch("gso.workflows.router.terminate_router.infoblox.delete_host_by_ip") @@ -24,7 +16,6 @@ def test_terminate_router_full_success( mock_delete_device, mock_execute_playbook, remove_configuration, - clean_up_ipam, nokia_router_subscription_factory, faker, data_config_filename, @@ -34,7 +25,6 @@ def test_terminate_router_full_success( router_termination_input_form_data = { "tt_number": faker.tt_number(), "remove_configuration": remove_configuration, - "clean_up_ipam": clean_up_ipam, } lso_interaction_count = 2 if remove_configuration else 0 @@ -53,5 +43,5 @@ def test_terminate_router_full_success( assert subscription.status == "terminated" assert mock_delete_device.call_count == 1 - assert mock_delete_host_by_ip.call_count == (1 if clean_up_ipam else 0) + assert mock_delete_host_by_ip.call_count == 1 assert mock_execute_playbook.call_count == lso_interaction_count diff --git a/test/workflows/shared/__init__.py b/test/workflows/shared/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/test/workflows/shared/cancel_subscription.py b/test/workflows/shared/cancel_subscription.py deleted file mode 100644 index 9963b8f2..00000000 --- a/test/workflows/shared/cancel_subscription.py +++ /dev/null @@ -1,30 +0,0 @@ -import pytest -from orchestrator.domain.base import SubscriptionModel -from orchestrator.types import SubscriptionLifecycle - -from test.workflows import assert_complete, extract_state, run_workflow - - -@pytest.mark.parametrize( - "subscription_factory", - [ - "site_subscription_factory", - "juniper_router_subscription_factory", - "nokia_router_subscription_factory", - "iptrunk_subscription_factory", - ], -) -@pytest.mark.workflow() -def test_cancel_workflow_success(subscription_factory, geant_partner, request): - subscription_id = request.getfixturevalue(subscription_factory)( - status=SubscriptionLifecycle.INITIAL, - partner=geant_partner, - ) - initial_site_data = [{"subscription_id": subscription_id}, {}] - result, _, _ = run_workflow("cancel_subscription", initial_site_data) - assert_complete(result) - - state = extract_state(result) - subscription_id = state["subscription_id"] - subscription = SubscriptionModel.from_subscription(subscription_id) - assert subscription.status == "terminated" -- GitLab