diff --git a/gso/services/provisioning_proxy.py b/gso/services/provisioning_proxy.py index c76900e109a93586f3c262322db734c32dfe7cae..4c90ea7b894fb8b58c0d08604e8197aa2d6b770b 100644 --- a/gso/services/provisioning_proxy.py +++ b/gso/services/provisioning_proxy.py @@ -19,12 +19,11 @@ from pydantic import validator from gso import settings from gso.products.product_types.iptrunk import Iptrunk, IptrunkProvisioning -from gso.products.product_types.router import RouterProvisioning +from gso.products.product_types.router import Router, RouterProvisioning logger = logging.getLogger(__name__) -"""{class}`logging.Logger` instance.""" DEFAULT_LABEL = "Provisioning proxy is running. Please come back later for the results." -"""The default label displayed when the provisioning proxy is running.""" +"""The default label displayed when the provisioning proxy is running, in case no custom label is provided.""" class CUDOperation(strEnum): @@ -159,6 +158,52 @@ def deprovision_ip_trunk(subscription: Iptrunk, process_id: UUIDstr, dry_run: bo _send_request("ip_trunk", parameters, process_id, CUDOperation.DELETE) +def migrate_ip_trunk( + subscription: Iptrunk, + new_node: Router, + new_lag_interface: str, + new_lag_member_interfaces: list[str], + replace_index: int, + process_id: UUIDstr, + verb: str, + dry_run: bool = True, +) -> None: + """Migrate an IP trunk service using {term}`LSO`. + + :param subscription: The subscription object that's to be migrated. + :type subscription: {class}`Iptrunk` + :param new_node: The new node that is being migrated to + :type new_node: {class}`Router` + :param new_lag_interface: The name of the new aggregated Ethernet interface + :type new_lag_interface: str + :param new_lag_member_interfaces: The new list of interfaces that are part of the LAG + :type new_lag_member_interfaces: list[str] + :param replace_index: The index of the side that is going to be replaced as part of the existing trunk, + can be `0` or `1`. + :type replace_index: int + :param process_id: The related process ID, used for callback. + :type process_id: UUIDstr + :param verb: The verb that is passed to the executed playbook + :type verb: str + :param dry_run: A boolean indicating whether this should be a dry run or not, defaults to `True`. + :type dry_run: bool + :rtype: None + """ + parameters = { + "subscription": json.loads(json_dumps(subscription)), + "new_side": { + "new_node": json.loads(json_dumps(new_node)), + "new_lag_interface": new_lag_interface, + "new_lag_member_interfaces": new_lag_member_interfaces, + "replace_index": replace_index, + }, + "verb": verb, + "dry_run": dry_run, + } + + _send_request("ip_trunk/migrate", parameters, process_id, CUDOperation.POST) + + @inputstep("Await provisioning proxy results", assignee=Assignee("SYSTEM")) def _await_pp_results(subscription: SubscriptionModel, label_text: str = DEFAULT_LABEL) -> FormGenerator: """Input step that forces the workflow to go into a `SUSPENDED` state. diff --git a/gso/workflows/iptrunk/__init__.py b/gso/workflows/iptrunk/__init__.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ee4b7535aa2257e514bb023df427d9ae5fb80fd0 100644 --- a/gso/workflows/iptrunk/__init__.py +++ b/gso/workflows/iptrunk/__init__.py @@ -0,0 +1,24 @@ +from logging import getLogger + +from orchestrator import step +from orchestrator.types import State +from products import Iptrunk + +logger = getLogger(__name__) + + +@step("Set ISIS metric to 9000") +def set_isis_to_9000(subscription: Iptrunk) -> State: + old_isis_metric = subscription.iptrunk.iptrunk_isis_metric + subscription.iptrunk.iptrunk_isis_metric = 90000 + logger.warning("ISIS metric is only updated in the subscription, not in the real world.") + + return {"subscription": subscription, "old_isis_metric": old_isis_metric} + + +@step("Restore ISIS metric to original value") +def restore_isis_metric(subscription: Iptrunk, old_isis_metric: int) -> State: + subscription.iptrunk.iptrunk_isis_metric = old_isis_metric + logger.warning("ISIS metric is only updated in the subscription, not in the real world.") + + return {"subscription": subscription} diff --git a/gso/workflows/iptrunk/create_iptrunk.py b/gso/workflows/iptrunk/create_iptrunk.py index 147377a3501a8656f8cb50e6c649e44bf1566ced..0bc9d4aedf5e4b36dabf0faf132b22cb75a0bb6a 100644 --- a/gso/workflows/iptrunk/create_iptrunk.py +++ b/gso/workflows/iptrunk/create_iptrunk.py @@ -1,5 +1,3 @@ -from uuid import UUID - from orchestrator.db.models import ProductTable, SubscriptionTable from orchestrator.forms import FormPage from orchestrator.forms.validators import Choice, UniqueConstrainedList @@ -10,7 +8,7 @@ from orchestrator.workflows.steps import resync, set_status, store_process_subsc from orchestrator.workflows.utils import wrap_create_initial_input_form from gso.products.product_blocks import PhyPortCapacity -from gso.products.product_blocks.iptrunk import IptrunkType, IptrunkSideBlock +from gso.products.product_blocks.iptrunk import IptrunkType from gso.products.product_types.iptrunk import IptrunkInactive, IptrunkProvisioning from gso.products.product_types.router import Router from gso.services import ipam, provisioning_proxy, subscriptions diff --git a/gso/workflows/iptrunk/migrate_iptrunk.py b/gso/workflows/iptrunk/migrate_iptrunk.py index c1a86049d947f2b7430a42e975311b11d2e3ddd6..ab61c1cebfd31e38739d3d29d5b60dc67c411af4 100644 --- a/gso/workflows/iptrunk/migrate_iptrunk.py +++ b/gso/workflows/iptrunk/migrate_iptrunk.py @@ -1,18 +1,24 @@ import re +from logging import getLogger from typing import NoReturn from orchestrator import step, workflow +from orchestrator.config.assignee import Assignee from orchestrator.db import ProductTable, SubscriptionTable from orchestrator.forms import FormPage -from orchestrator.forms.validators import Choice, UniqueConstrainedList +from orchestrator.forms.validators import Choice, Label, UniqueConstrainedList from orchestrator.targets import Target from orchestrator.types import FormGenerator, State, UUIDstr -from orchestrator.workflow import StepList, done, init +from orchestrator.workflow import StepList, done, init, inputstep from orchestrator.workflows.steps import resync, store_process_subscription, unsync from orchestrator.workflows.utils import wrap_modify_initial_input_form +from products import Iptrunk, Router from pydantic import validator +from services import provisioning_proxy +from services.provisioning_proxy import pp_interaction +from workflows.iptrunk import restore_isis_metric, set_isis_to_9000 -from products import Iptrunk +logger = getLogger(__name__) def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: @@ -35,15 +41,10 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: routers[str(router_id)] = router_description NewRouterEnum = Choice("Select a new router", zip(routers.keys(), routers.items())) # type: ignore - ReplacedSide = Choice( - "Select the side of the IP trunk to be replaced", - [ # type: ignore - (str(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.subscription.subscription_id), - subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.subscription.description), - (str(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.subscription.subscription_id), - subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.subscription.description), - ], - ) + sides_dict = { + side.subscription.subscription_id: side.subscription.description for side in subscription.iptrunk.iptrunk_sides + } + ReplacedSide = Choice("Select the side of the IP trunk to be replaced", zip(sides_dict.keys(), sides_dict.items())) class LagMemberList(UniqueConstrainedList[str]): min_items = len(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members) @@ -74,12 +75,285 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: user_input = yield ModifyIptrunkForm - return user_input.dict() + def _find_updated_side_of_trunk(trunk: Iptrunk, new_side: str) -> int: + sides = trunk.iptrunk.iptrunk_sides + if str(sides[0].iptrunk_side_node.subscription.subscription_id) == new_side: + return 0 + elif str(sides[1].iptrunk_side_node.subscription.subscription_id) == new_side: + return 1 + raise ValueError("Invalid Router id provided to be replaced!") + + replace_index = _find_updated_side_of_trunk(subscription, user_input["replace_side"]) + + return user_input.dict() | {"replace_index": replace_index} + + +@inputstep("Wait for confirmation", assignee=Assignee.SYSTEM) +def confirm_continue() -> State: + class ProvisioningResultPage(FormPage): + class Config: + title = "Please confirm before continuing" + + info_label: Label = ( # type: ignore + "ISIS metric has been set to 9000, please confirm to continue the workflow when ready." + ) + + yield ProvisioningResultPage + + return {} + + +@step("[DRY RUN] Disable configuration on old router") +def disable_old_config_dry( + subscription: Iptrunk, + new_node: Router, + new_lag_interface: str, + new_lag_member_interfaces: list[str], + replace_index: int, + process_id: UUIDstr, +) -> State: + provisioning_proxy.migrate_ip_trunk( + subscription, + new_node, + new_lag_interface, + new_lag_member_interfaces, + replace_index, + process_id, + "VERB NEEDS TO BE UPDATED", + ) + + return { + "subscription": subscription, + "label_text": "[DRY RUN] Migrating old trunk interface, please refresh to get the results of the playbook.", + } + + +@step("[REAL] Disable configuration on old router") +def disable_old_config_real( + subscription: Iptrunk, + new_node: Router, + new_lag_interface: str, + new_lag_member_interfaces: list[str], + replace_index: int, + process_id: UUIDstr, +) -> State: + provisioning_proxy.migrate_ip_trunk( + subscription, + new_node, + new_lag_interface, + new_lag_member_interfaces, + replace_index, + process_id, + "VERB NEEDS TO BE UPDATED", + False, + ) + + logger.warning("Playbook verb is not yet properly set.") + + return { + "subscription": subscription, + "label_text": "Migrating old trunk interface, please refresh to get the results of the playbook.", + } + + +@step("[DRY RUN] Deploy configuration on new router") +def deploy_new_config_dry( + subscription: Iptrunk, + new_node: Router, + new_lag_interface: str, + new_lag_member_interfaces: list[str], + replace_index: int, + process_id: UUIDstr, +) -> State: + provisioning_proxy.migrate_ip_trunk( + subscription, + new_node, + new_lag_interface, + new_lag_member_interfaces, + replace_index, + process_id, + "VERB NEEDS TO BE UPDATED", + ) + + logger.warning("Playbook verb is not yet properly set.") + + return { + "subscription": subscription, + "label_text": "[DRY RUN] Deploying new trunk interface, please refresh to get the results of the playbook.", + } + + +@step("Deploy configuration on new router") +def deploy_new_config_real( + subscription: Iptrunk, + new_node: Router, + new_lag_interface: str, + new_lag_member_interfaces: list[str], + replace_index: int, + process_id: UUIDstr, +) -> State: + provisioning_proxy.migrate_ip_trunk( + subscription, + new_node, + new_lag_interface, + new_lag_member_interfaces, + replace_index, + process_id, + "VERB NEEDS TO BE UPDATED", + False, + ) + + logger.warning("Playbook verb is not yet properly set.") + + return { + "subscription": subscription, + "label_text": "Deploying new trunk interface, please refresh to get the results of the playbook.", + } + + +@step("Run interface checks") +def run_interface_checks( + subscription: Iptrunk, + new_node: Router, + new_lag_interface: str, + new_lag_member_interfaces: list[str], + replace_index: int, + process_id: UUIDstr, +) -> State: + provisioning_proxy.migrate_ip_trunk( + subscription, + new_node, + new_lag_interface, + new_lag_member_interfaces, + replace_index, + process_id, + "MIGRATION_INTERFACE_CHECK", + ) + + logger.warning("Playbook verb is not yet properly set.") + + return { + "subscription": subscription, + "label_text": "Running checks on the new trunk interface, please refresh to get the results of the playbook.", + } + + +@step("Deploy configuration on new router") +def deploy_new_isis( + subscription: Iptrunk, + new_node: Router, + new_lag_interface: str, + new_lag_member_interfaces: list[str], + replace_index: int, + process_id: UUIDstr, +) -> State: + provisioning_proxy.migrate_ip_trunk( + subscription, + new_node, + new_lag_interface, + new_lag_member_interfaces, + replace_index, + process_id, + "VERB NEEDS TO BE UPDATED", + False, + ) + + logger.warning("Playbook verb is not yet properly set.") + + return { + "subscription": subscription, + "label_text": "Updating new ISIS metric, please refresh to get the results of the playbook.", + } + + +@step("Check ISIS metric") +def check_isis(subscription: Iptrunk, process_id: UUIDstr) -> State: + provisioning_proxy.check_ip_trunk(subscription, process_id, "VERB NEEDS TO BE UPDATED") + + logger.warning("Playbook verb is not yet properly set.") + + return { + "subscription": subscription, + "label_text": "Checking ISIS functionality, please refresh to get the results of the playbook.", + } + + +@step("[DRY RUN] Delete configuration on old router") +def delete_old_config_dry( + subscription: Iptrunk, + new_node: Router, + new_lag_interface: str, + new_lag_member_interfaces: list[str], + replace_index: int, + process_id: UUIDstr, +) -> State: + provisioning_proxy.migrate_ip_trunk( + subscription, + new_node, + new_lag_interface, + new_lag_member_interfaces, + replace_index, + process_id, + "VERB NEEDS TO BE UPDATED", + ) + + logger.warning("Playbook verb is not yet properly set.") + + return { + "subscription": subscription, + "label_text": "[DRY RUN] Removing configuration from old router," + "please refresh to get the results of the playbook.", + } + + +@step("Delete configuration on old router") +def delete_old_config_real( + subscription: Iptrunk, + new_node: Router, + new_lag_interface: str, + new_lag_member_interfaces: list[str], + replace_index: int, + process_id: UUIDstr, +) -> State: + provisioning_proxy.migrate_ip_trunk( + subscription, + new_node, + new_lag_interface, + new_lag_member_interfaces, + replace_index, + process_id, + "VERB NEEDS TO BE UPDATED", + False, + ) + + logger.warning("Playbook verb is not yet properly set.") + + return { + "subscription": subscription, + "label_text": "Removing configuration from old router, please refresh to get the results of the playbook.", + } + + +@step("Update IPAM") +def update_ipam(subscription: Iptrunk) -> State: + pass + + return {"subscription": subscription} + +@step("Update subscription model") +def update_subscription_model( + subscription: Iptrunk, + replace_index: int, + new_node: UUIDstr, + new_lag_interface: str, + new_lag_member_interfaces: list[str], +) -> State: + subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_node = Router.from_subscription(new_node).router + subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_iface = new_lag_interface + subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_members = new_lag_member_interfaces -@step("Hilversum Stinks") -def temp_test_step(subscription: Iptrunk) -> State: - return {"test": subscription} + return {"subscription": subscription} @workflow( @@ -89,14 +363,25 @@ def temp_test_step(subscription: Iptrunk) -> State: ) def migrate_iptrunk() -> StepList: return ( - init - >> store_process_subscription(Target.MODIFY) - >> unsync - >> temp_test_step - # >> set ISIS to 9000 - # >> wait confirm - # >> disable config - # >> - >> resync - >> done + init + >> store_process_subscription(Target.MODIFY) + >> unsync + >> set_isis_to_9000 + >> confirm_continue + >> pp_interaction(disable_old_config_dry, 3) + >> pp_interaction(disable_old_config_real, 3) + >> pp_interaction(deploy_new_config_dry, 3) + >> pp_interaction(deploy_new_config_real, 3) + >> confirm_continue + >> pp_interaction(run_interface_checks, 3) + >> pp_interaction(deploy_new_isis, 3) + >> pp_interaction(check_isis, 3) + >> confirm_continue + >> restore_isis_metric + >> pp_interaction(delete_old_config_dry, 3) + >> pp_interaction(delete_old_config_real, 3) + >> update_ipam + >> update_subscription_model + >> resync + >> done ) diff --git a/gso/workflows/iptrunk/terminate_iptrunk.py b/gso/workflows/iptrunk/terminate_iptrunk.py index 97659572218f5e11b6a59f16aa1239d1283d809d..4572bb650bd93d628d329844d9d816411daeb3a1 100644 --- a/gso/workflows/iptrunk/terminate_iptrunk.py +++ b/gso/workflows/iptrunk/terminate_iptrunk.py @@ -7,6 +7,7 @@ from orchestrator.types import FormGenerator, State, SubscriptionLifecycle, UUID from orchestrator.workflow import StepList, conditional, done, init, step, workflow from orchestrator.workflows.steps import resync, set_status, store_process_subscription, unsync from orchestrator.workflows.utils import wrap_modify_initial_input_form +from workflows.iptrunk import set_isis_to_9000 from gso.products.product_types.iptrunk import Iptrunk from gso.services import ipam, provisioning_proxy @@ -27,22 +28,13 @@ def initial_input_form_generator() -> FormGenerator: return user_input.dict() -@step("Set iptrunk ISIS metric to 9000") -def update_isis_metric(subscription: Iptrunk) -> State: - subscription.iptrunk.iptrunk_isis_metric = 9000 - - return {"subscription": subscription} - - @step("Drain traffic from trunk") def drain_traffic_from_ip_trunk(subscription: Iptrunk, process_id: UUIDstr) -> State: provisioning_proxy.provision_ip_trunk(subscription, process_id, "isis_interface", False) return { "subscription": subscription, - "label_text": "This is setting the ISIS metric of the trunk to 9000" - "trunk. " - "Press refresh to get the results\n" - "When traffic is drained, confirm to continue", + "label_text": "This is setting the ISIS metric of the trunk to 9000. Press refresh to get the results." + "When traffic is drained, confirm to continue.", } @@ -94,7 +86,7 @@ def terminate_iptrunk() -> StepList: run_ipam_steps = conditional(lambda state: state.get("clean_up_ipam", True)) config_steps = ( - StepList([update_isis_metric]) + StepList([set_isis_to_9000]) >> pp_interaction(drain_traffic_from_ip_trunk, 3) >> pp_interaction(deprovision_ip_trunk_dry, 3) >> pp_interaction(deprovision_ip_trunk_real, 3)