From 951c5f2f81d2d83306116d3ebad2d403b84d88aa Mon Sep 17 00:00:00 2001 From: Karel van Klink <karel.vanklink@geant.org> Date: Tue, 27 Feb 2024 17:07:24 +0100 Subject: [PATCH] Add IP Trunk activation workflow, including unit test. IP Trunk workflows can execute on PROVISIONING state --- ...47f61d_add_ip_trunk_activation_workflow.py | 39 ++++++++++++ gso/workflows/__init__.py | 4 ++ gso/workflows/iptrunk/activate_iptrunk.py | 59 +++++++++++++++++++ test/fixtures.py | 5 ++ .../iptrunk/test_activate_iptrunk.py | 36 +++++++++++ 5 files changed, 143 insertions(+) create mode 100644 gso/migrations/versions/2024-02-27_5bea5647f61d_add_ip_trunk_activation_workflow.py create mode 100644 gso/workflows/iptrunk/activate_iptrunk.py create mode 100644 test/workflows/iptrunk/test_activate_iptrunk.py diff --git a/gso/migrations/versions/2024-02-27_5bea5647f61d_add_ip_trunk_activation_workflow.py b/gso/migrations/versions/2024-02-27_5bea5647f61d_add_ip_trunk_activation_workflow.py new file mode 100644 index 00000000..c7b03f9f --- /dev/null +++ b/gso/migrations/versions/2024-02-27_5bea5647f61d_add_ip_trunk_activation_workflow.py @@ -0,0 +1,39 @@ +"""Add IP Trunk activation workflow. + +Revision ID: 5bea5647f61d +Revises: 113a81d2a40a +Create Date: 2024-02-27 17:01:57.300326 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '5bea5647f61d' +down_revision = '113a81d2a40a' +branch_labels = None +depends_on = None + + +from orchestrator.migrations.helpers import create_workflow, delete_workflow + +new_workflows = [ + { + "name": "activate_iptrunk", + "target": "MODIFY", + "description": "Activate an IP Trunk", + "product_type": "Iptrunk" + } +] + + +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/workflows/__init__.py b/gso/workflows/__init__.py index bbc40e87..3566d7fc 100644 --- a/gso/workflows/__init__.py +++ b/gso/workflows/__init__.py @@ -8,9 +8,13 @@ WF_USABLE_MAP.update( "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"], } ) +LazyWorkflowInstance("gso.workflows.iptrunk.activate_iptrunk", "activate_iptrunk") LazyWorkflowInstance("gso.workflows.iptrunk.create_iptrunk", "create_iptrunk") LazyWorkflowInstance("gso.workflows.iptrunk.deploy_twamp", "deploy_twamp") LazyWorkflowInstance("gso.workflows.iptrunk.modify_isis_metric", "modify_isis_metric") diff --git a/gso/workflows/iptrunk/activate_iptrunk.py b/gso/workflows/iptrunk/activate_iptrunk.py new file mode 100644 index 00000000..f686a8cb --- /dev/null +++ b/gso/workflows/iptrunk/activate_iptrunk.py @@ -0,0 +1,59 @@ +"""Activate IP trunk takes a provisioning trunk to the active lifecycle state.""" + +from orchestrator.config.assignee import Assignee +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, inputstep, workflow +from orchestrator.workflows.steps import resync, set_status, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form + +from gso.products.product_types.iptrunk import Iptrunk + + +def _initial_input_form(subscription_id: UUIDstr) -> FormGenerator: + trunk = Iptrunk.from_subscription(subscription_id) + + class ActivateTrunkForm(FormPage): + info_label: Label = "Start approval process for IP trunk activation." # type:ignore[assignment] + + user_input = yield ActivateTrunkForm + + return user_input.dict() | {"subscription": trunk} + + +@inputstep("Verify checklist completion", assignee=Assignee.SYSTEM) +def verify_complete_checklist() -> FormGenerator: + """Show a form for the operator to input a link to the completed checklist.""" + + class VerifyCompleteForm(FormPage): + info_label: Label = "Verify that the checklist has been completed. Then continue this workflow." # type: ignore[assignment] + checklist_url: str = "" + + user_input = yield VerifyCompleteForm + + return {"checklist_url": user_input.dict()["checklist_url"]} + + +@workflow( + "Activate an IP Trunk", + initial_input_form=wrap_modify_initial_input_form(_initial_input_form), + target=Target.MODIFY, +) +def activate_iptrunk() -> StepList: + """Move an IP Trunk from a ``PROVISIONING`` state to an ``ACTIVE`` state. + + * Send email notifications to different teams. + * Wait for approval to be given. + * Update the subscription lifecycle state to ``ACTIVE``. + """ + return ( + init + >> store_process_subscription(Target.MODIFY) + >> unsync + >> verify_complete_checklist + >> set_status(SubscriptionLifecycle.ACTIVE) + >> resync + >> done + ) diff --git a/test/fixtures.py b/test/fixtures.py index bc96381b..32bbfdca 100644 --- a/test/fixtures.py +++ b/test/fixtures.py @@ -226,6 +226,7 @@ def iptrunk_subscription_factory(iptrunk_side_subscription_factory, faker): iptrunk_ipv4_network=None, iptrunk_ipv6_network=None, iptrunk_sides=None, + status: SubscriptionLifecycle | None = None, ) -> UUIDstr: product_id = subscriptions.get_product_id_by_name(ProductType.IP_TRUNK) description = description or faker.sentence() @@ -255,6 +256,10 @@ def iptrunk_subscription_factory(iptrunk_side_subscription_factory, faker): iptrunk_subscription, SubscriptionLifecycle.ACTIVE, ) + + if status: + iptrunk_subscription.status = status + iptrunk_subscription.description = description iptrunk_subscription.start_date = start_date iptrunk_subscription.save() diff --git a/test/workflows/iptrunk/test_activate_iptrunk.py b/test/workflows/iptrunk/test_activate_iptrunk.py new file mode 100644 index 00000000..837f340c --- /dev/null +++ b/test/workflows/iptrunk/test_activate_iptrunk.py @@ -0,0 +1,36 @@ +import pytest + +from gso.products import Iptrunk +from test.workflows import ( + assert_complete, + assert_suspended, + extract_state, + resume_workflow, + run_workflow, +) + + +@pytest.mark.workflow() +def test_activate_router_success( + iptrunk_subscription_factory, + faker, +): + # Set up mock return values + product_id = iptrunk_subscription_factory(status="provisioning") + # Sanity check + assert Iptrunk.from_subscription(product_id).status == "provisioning" + + # Run workflow + initial_input_data = [{"subscription_id": product_id}, {}] + result, process_stat, step_log = run_workflow("activate_iptrunk", initial_input_data) + + assert_suspended(result) + result, step_log = resume_workflow(process_stat, step_log, input_data=[{"checklist_url": "http://localhost"}]) + + assert_complete(result) + + state = extract_state(result) + subscription_id = state["subscription_id"] + subscription = Iptrunk.from_subscription(subscription_id) + + assert subscription.status == "active" -- GitLab