Skip to content
Snippets Groups Projects
migrate_iptrunk.py 13.5 KiB
Newer Older
from typing import NoReturn, Optional

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, Label, UniqueConstrainedList
from orchestrator.targets import Target
from orchestrator.types import FormGenerator, State, UUIDstr
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 pydantic import validator

from gso.products.product_types.iptrunk import Iptrunk
from gso.products.product_types.router import Router
Neda Moeini's avatar
Neda Moeini committed
from gso.services import provisioning_proxy
from gso.services.provisioning_proxy import pp_interaction
from gso.workflows.iptrunk.utils import set_isis_to_90000

def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
    subscription = Iptrunk.from_subscription(subscription_id)
    sides_dict = {
        str(side.iptrunk_side_node.subscription.subscription_id): side.iptrunk_side_node.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())  # type: ignore
    )

    class OldSideIptrunkForm(FormPage):
        class Config:
            title = (
                f"Subscription {subscription.iptrunk.geant_s_sid} from "
                f"{subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn}"
                f" to "
                f"{subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn}"
            )

        replace_side: ReplacedSide  # type: ignore
        warning_label: Label = "Are we moving to a different Site?"  # type: ignore
        migrate_to_different_site: Optional[bool] = False

    old_side_input = yield OldSideIptrunkForm

    routers = {}
    for router_id, router_description in (
        SubscriptionTable.query.join(ProductTable)
        .filter(
            ProductTable.product_type == "Router",
            SubscriptionTable.status == "active",
        )
        .with_entities(SubscriptionTable.subscription_id, SubscriptionTable.description)
        .all()
        if router_id not in [
            subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.subscription.subscription_id,
            subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.subscription.subscription_id,
            current_router = Router.from_subscription(router_id)
            old_side_site_id = Router.from_subscription(old_side_input.replace_side).router.router_site
            if (
                not old_side_input.migrate_to_different_site
                and current_router.router.router_site.subscription.subscription_id != old_side_site_id
            ):
                continue
            routers[str(router_id)] = router_description

    NewRouterEnum = Choice("Select a new router", zip(routers.keys(), routers.items()))  # type: ignore

    class LagMemberList(UniqueConstrainedList[str]):
        min_items = len(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members)
        max_items = len(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members)
    class NewSideIptrunkForm(FormPage):
            title = (
                f"Subscription {subscription.iptrunk.geant_s_sid} from "
                f"{subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn} to "
                f"{subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn}"

        new_node: NewRouterEnum  # type: ignore
        new_lag_interface: str
        new_lag_member_interfaces: LagMemberList
        @validator("new_lag_interface", allow_reuse=True, pre=True, always=True)
        def lag_interface_proper_name(cls, new_lag_name: str) -> str | NoReturn:
            nokia_lag_re = re.compile("^lag-\\d+$")
            juniper_lag_re = re.compile("^ae\\d{1,2}$")

            if nokia_lag_re.match(new_lag_name) or juniper_lag_re.match(new_lag_name):
                return new_lag_name

            raise ValueError("Invalid LAG name, please try again.")

    new_side_input = yield NewSideIptrunkForm
    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:  # noqa: RET505
            return 1
        raise ValueError("Invalid Router id provided to be replaced!")

    replace_index = _find_updated_side_of_trunk(subscription, old_side_input.replace_side)
    return old_side_input.dict() | new_side_input.dict() | {"replace_index": replace_index}


@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,
        tt_number,
        "deactivate",
        "deactivate",
        "label_text": "[DRY RUN] Disable config on old node, 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,
        tt_number,
        "deactivate",
        "deactivate",
        "label_text": "Disable config on the old node, 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,
        tt_number,
        "deploy",
        "trunk_interface",
    )

    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,
        tt_number,
        "deploy",
        "trunk_interface",
        False,
    )

    logger.warning("Playbook verb is not yet properly set.")

    return {
        "subscription": subscription,
        "label_text": "[COMMIT] Deploying new trunk interface, please refresh to get the results of the playbook.",
@inputstep("Wait for confirmation", assignee=Assignee.SYSTEM)
def confirm_continue_move_fiber() -> FormGenerator:
    class ProvisioningResultPage(FormPage):
        class Config:
            title = "Please confirm before continuing"
        info_label: Label = (
            "New Trunk interface has been deployed, wait for the physical connection to be moved"  # type: ignore
        )
    yield ProvisioningResultPage

# Interface checks go here


@step("Deploy ISIS 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,
        tt_number,
        "deploy",
        "isis_interface",
        False,
    )

    logger.warning("Playbook verb is not yet properly set.")

    return {
        "subscription": subscription,
        "label_text": "Deploy ISIS config on the new router, please refresh to get the results of the playbook.",
# Leaving it like this as a placeholder
# @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.",
#    }
@inputstep("Wait for confirmation", assignee=Assignee.SYSTEM)
def confirm_continue_restore_isis() -> FormGenerator:
    class ProvisioningResultPage(FormPage):
        class Config:
            title = "Please confirm before continuing"

        info_label: Label = (
            "ISIS config has been deployed, confirm if you want to restore the old metric"  # type: ignore
        )

    yield ProvisioningResultPage

    return {}


@step("Restore ISIS metric to original value")
def restore_isis_metric(subscription: Iptrunk, process_id: UUIDstr, tt_number: str, old_isis_metric: int) -> State:
    subscription.iptrunk.iptrunk_isis_metric = old_isis_metric
    provisioning_proxy.provision_ip_trunk(subscription, process_id, tt_number, "isis_interface", False)

    return {"subscription": subscription}


@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,
        tt_number,
        "delete",
        "delete",
    )

    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,
        tt_number,
        "delete",
        "delete",
        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
    return {"subscription": subscription}


@workflow(
    "Migrate an IP Trunk",
    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
    target=Target.MODIFY,
)
def migrate_iptrunk() -> StepList:
        init
        >> store_process_subscription(Target.MODIFY)
        >> unsync
        >> pp_interaction(set_isis_to_90000, 3)
        >> 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_move_fiber
        >> pp_interaction(deploy_new_isis, 3)
        >> confirm_continue_restore_isis
        >> pp_interaction(restore_isis_metric, 3)
        >> pp_interaction(delete_old_config_dry, 3)
        >> pp_interaction(delete_old_config_real, 3)
        >> update_ipam
        >> update_subscription_model
        >> resync
        >> done