Skip to content
Snippets Groups Projects
Verified Commit fae59e17 authored by Karel van Klink's avatar Karel van Klink :smiley_cat:
Browse files

Merge behaviour of cancel and terminate workflows

parent f8bc02be
No related branches found
No related tags found
1 merge request!199Remove cancelation workflow
...@@ -38,7 +38,6 @@ ...@@ -38,7 +38,6 @@
"workflow": { "workflow": {
"activate_iptrunk": "Activate IP Trunk", "activate_iptrunk": "Activate IP Trunk",
"activate_router": "Activate router", "activate_router": "Activate router",
"cancel_subscription": "Cancel subscription",
"confirm_info": "Please verify this form looks correct.", "confirm_info": "Please verify this form looks correct.",
"deploy_twamp": "Deploy TWAMP", "deploy_twamp": "Deploy TWAMP",
"migrate_iptrunk": "Migrate IP Trunk", "migrate_iptrunk": "Migrate IP Trunk",
......
"""Initialisation class that imports all workflows into :term:`GSO`.""" """Initialisation class that imports all workflows into :term:`GSO`."""
from orchestrator.services.subscriptions import WF_USABLE_MAP from orchestrator.services.subscriptions import WF_USABLE_MAP
from orchestrator.types import SubscriptionLifecycle
from orchestrator.workflows import LazyWorkflowInstance from orchestrator.workflows import LazyWorkflowInstance
ALL_ALIVE_STATES: list[str] = [
SubscriptionLifecycle.INITIAL,
SubscriptionLifecycle.PROVISIONING,
SubscriptionLifecycle.ACTIVE,
]
WF_USABLE_MAP.update( WF_USABLE_MAP.update(
{ {
"cancel_subscription": ["initial"], "redeploy_base_config": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE],
"redeploy_base_config": ["provisioning", "active"], "update_ibgp_mesh": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE],
"update_ibgp_mesh": ["provisioning", "active"], "activate_router": [SubscriptionLifecycle.PROVISIONING],
"activate_router": ["provisioning"], "deploy_twamp": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE],
"deploy_twamp": ["provisioning", "active"], "modify_trunk_interface": [SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE],
"modify_trunk_interface": ["provisioning", "active"], "activate_iptrunk": [SubscriptionLifecycle.PROVISIONING],
"activate_iptrunk": ["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 ...@@ -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.terminate_router", "terminate_router")
LazyWorkflowInstance("gso.workflows.router.update_ibgp_mesh", "update_ibgp_mesh") LazyWorkflowInstance("gso.workflows.router.update_ibgp_mesh", "update_ibgp_mesh")
LazyWorkflowInstance("gso.workflows.router.modify_connection_strategy", "modify_connection_strategy") 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.create_site", "create_site")
LazyWorkflowInstance("gso.workflows.site.modify_site", "modify_site") LazyWorkflowInstance("gso.workflows.site.modify_site", "modify_site")
LazyWorkflowInstance("gso.workflows.site.terminate_site", "terminate_site") LazyWorkflowInstance("gso.workflows.site.terminate_site", "terminate_site")
......
...@@ -28,18 +28,23 @@ from gso.utils.shared_enums import Vendor ...@@ -28,18 +28,23 @@ from gso.utils.shared_enums import Vendor
from gso.utils.workflow_steps import set_isis_to_max 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.""" """Ask the operator to confirm whether router configuration and :term:`IPAM` resources should be deleted."""
iptrunk = Iptrunk.from_subscription(subscription_id)
class TerminateForm(FormPage): 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 = ( termination_label: Label = (
"Please confirm whether configuration should get removed from the A and B sides of the trunk, and whether " "Please confirm whether configuration should get removed from the A and B sides of the trunk." # type: ignore[assignment]
"IPAM and Netbox resources should be released." # type: ignore[assignment]
) )
tt_number: str
remove_configuration: bool = True remove_configuration: bool = True
clean_up_ipam: bool = True
clean_up_netbox: bool = True
@validator("tt_number", allow_reuse=True) @validator("tt_number", allow_reuse=True)
def validate_tt_number(cls, tt_number: str) -> str: def validate_tt_number(cls, tt_number: str) -> str:
...@@ -148,22 +153,19 @@ def terminate_iptrunk() -> StepList: ...@@ -148,22 +153,19 @@ def terminate_iptrunk() -> StepList:
* Let the operator decide whether to remove configuration from the routers, if so: * 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 * 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 * 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 * Mark the IP trunk interfaces as free in Netbox
* Clear :term:`IPAM` resources, if selected by the operator * Clear :term:`IPAM` resources
* Terminate the subscription in the service database * Terminate the subscription in the service database
""" """
run_config_steps = conditional(lambda state: state["remove_configuration"]) run_config_steps = conditional(lambda state: state["remove_configuration"])
run_ipam_steps = conditional(lambda state: state["clean_up_ipam"])
side_a_is_nokia = conditional( side_a_is_nokia = conditional(
lambda state: state["clean_up_netbox"] lambda state: get_router_vendor(
and get_router_vendor(
state["subscription"]["iptrunk"]["iptrunk_sides"][0]["iptrunk_side_node"]["owner_subscription_id"] state["subscription"]["iptrunk"]["iptrunk_sides"][0]["iptrunk_side_node"]["owner_subscription_id"]
) )
== Vendor.NOKIA == Vendor.NOKIA
) )
side_b_is_nokia = conditional( side_b_is_nokia = conditional(
lambda state: state["clean_up_netbox"] lambda state: get_router_vendor(
and get_router_vendor(
state["subscription"]["iptrunk"]["iptrunk_sides"][1]["iptrunk_side_node"]["owner_subscription_id"] state["subscription"]["iptrunk"]["iptrunk_sides"][1]["iptrunk_side_node"]["owner_subscription_id"]
) )
== Vendor.NOKIA == Vendor.NOKIA
...@@ -175,7 +177,6 @@ def terminate_iptrunk() -> StepList: ...@@ -175,7 +177,6 @@ def terminate_iptrunk() -> StepList:
>> lso_interaction(deprovision_ip_trunk_dry) >> lso_interaction(deprovision_ip_trunk_dry)
>> lso_interaction(deprovision_ip_trunk_real) >> lso_interaction(deprovision_ip_trunk_real)
) )
ipam_steps = init >> deprovision_ip_trunk_ipv4 >> deprovision_ip_trunk_ipv6
return ( return (
init init
...@@ -184,7 +185,8 @@ def terminate_iptrunk() -> StepList: ...@@ -184,7 +185,8 @@ def terminate_iptrunk() -> StepList:
>> run_config_steps(config_steps) >> run_config_steps(config_steps)
>> side_a_is_nokia(netbox_clean_up_side_a) >> side_a_is_nokia(netbox_clean_up_side_a)
>> side_b_is_nokia(netbox_clean_up_side_b) >> 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) >> set_status(SubscriptionLifecycle.TERMINATED)
>> resync >> resync
>> done >> done
......
...@@ -32,13 +32,16 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: ...@@ -32,13 +32,16 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
router = Router.from_subscription(subscription_id) router = Router.from_subscription(subscription_id)
class TerminateForm(FormPage): class TerminateForm(FormPage):
termination_label: Label = ( if router.status == SubscriptionLifecycle.INITIAL:
"Please confirm whether configuration should get removed from the router, and whether IPAM resources should" info_label_2: Label = (
" be released." # type: ignore[assignment] "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 tt_number: str
termination_label: Label = "Please confirm whether configuration should get removed from the router." # type: ignore[assignment]
remove_configuration: bool = True remove_configuration: bool = True
clean_up_ipam: bool = True
user_input = yield TerminateForm user_input = yield TerminateForm
return user_input.dict() | {"router_is_nokia": router.router.vendor == Vendor.NOKIA} return user_input.dict() | {"router_is_nokia": router.router.vendor == Vendor.NOKIA}
...@@ -114,7 +117,6 @@ def terminate_router() -> StepList: ...@@ -114,7 +117,6 @@ def terminate_router() -> StepList:
* Disable and delete configuration on the router, if selected by the operator * Disable and delete configuration on the router, if selected by the operator
* Mark the subscription as terminated in the service database * 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"]) run_config_steps = conditional(lambda state: state["remove_configuration"])
router_is_nokia = conditional(lambda state: state["router_is_nokia"]) router_is_nokia = conditional(lambda state: state["router_is_nokia"])
...@@ -122,7 +124,7 @@ def terminate_router() -> StepList: ...@@ -122,7 +124,7 @@ def terminate_router() -> StepList:
init init
>> store_process_subscription(Target.TERMINATE) >> store_process_subscription(Target.TERMINATE)
>> unsync >> 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_dry))
>> run_config_steps(lso_interaction(remove_config_from_router_real)) >> run_config_steps(lso_interaction(remove_config_from_router_real))
>> router_is_nokia(remove_device_from_netbox) >> router_is_nokia(remove_device_from_netbox)
......
"""Workflows that are shared across multiple products."""
"""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
)
...@@ -18,9 +18,16 @@ from gso.products.product_types.site import Site ...@@ -18,9 +18,16 @@ from gso.products.product_types.site import Site
def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
"""Ask the user for confirmation whether to terminate the selected site.""" """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): 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] termination_label: Label = "Are you sure you want to delete this site?" # type: ignore[assignment]
user_input = yield TerminateForm user_input = yield TerminateForm
......
...@@ -42,7 +42,6 @@ def test_successful_iptrunk_termination( ...@@ -42,7 +42,6 @@ def test_successful_iptrunk_termination(
{ {
"tt_number": faker.tt_number(), "tt_number": faker.tt_number(),
"remove_configuration": True, "remove_configuration": True,
"clean_up_ipam": True,
}, },
] ]
result, process_stat, step_log = run_workflow("terminate_iptrunk", initial_iptrunk_data) result, process_stat, step_log = run_workflow("terminate_iptrunk", initial_iptrunk_data)
......
...@@ -7,15 +7,7 @@ from test.workflows import assert_complete, assert_lso_interaction_success, extr ...@@ -7,15 +7,7 @@ from test.workflows import assert_complete, assert_lso_interaction_success, extr
@pytest.mark.workflow() @pytest.mark.workflow()
@pytest.mark.parametrize( @pytest.mark.parametrize("remove_configuration", [True, False])
("remove_configuration", "clean_up_ipam"),
[
(True, True),
(True, False),
(False, True),
(False, False),
],
)
@patch("gso.services.lso_client._send_request") @patch("gso.services.lso_client._send_request")
@patch("gso.workflows.router.terminate_router.NetboxClient.delete_device") @patch("gso.workflows.router.terminate_router.NetboxClient.delete_device")
@patch("gso.workflows.router.terminate_router.infoblox.delete_host_by_ip") @patch("gso.workflows.router.terminate_router.infoblox.delete_host_by_ip")
...@@ -24,7 +16,6 @@ def test_terminate_router_full_success( ...@@ -24,7 +16,6 @@ def test_terminate_router_full_success(
mock_delete_device, mock_delete_device,
mock_execute_playbook, mock_execute_playbook,
remove_configuration, remove_configuration,
clean_up_ipam,
nokia_router_subscription_factory, nokia_router_subscription_factory,
faker, faker,
data_config_filename, data_config_filename,
...@@ -34,7 +25,6 @@ def test_terminate_router_full_success( ...@@ -34,7 +25,6 @@ def test_terminate_router_full_success(
router_termination_input_form_data = { router_termination_input_form_data = {
"tt_number": faker.tt_number(), "tt_number": faker.tt_number(),
"remove_configuration": remove_configuration, "remove_configuration": remove_configuration,
"clean_up_ipam": clean_up_ipam,
} }
lso_interaction_count = 2 if remove_configuration else 0 lso_interaction_count = 2 if remove_configuration else 0
...@@ -53,5 +43,5 @@ def test_terminate_router_full_success( ...@@ -53,5 +43,5 @@ def test_terminate_router_full_success(
assert subscription.status == "terminated" assert subscription.status == "terminated"
assert mock_delete_device.call_count == 1 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 assert mock_execute_playbook.call_count == lso_interaction_count
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"
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment