From 32ec1310953988bad260a890f252202b715fd9a7 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Fri, 9 Aug 2024 14:11:58 +0200
Subject: [PATCH] Improve TT Number validation.

---
 docs/source/module/utils/types.rst            |  6 +++++
 gso/utils/types.py                            |  9 +++++++
 gso/workflows/iptrunk/create_iptrunk.py       |  8 ++----
 gso/workflows/iptrunk/deploy_twamp.py         |  9 ++-----
 gso/workflows/iptrunk/migrate_iptrunk.py      |  8 ++----
 gso/workflows/iptrunk/modify_isis_metric.py   |  3 ++-
 .../iptrunk/modify_trunk_interface.py         |  8 ++----
 gso/workflows/iptrunk/terminate_iptrunk.py    | 10 +++----
 gso/workflows/router/create_router.py         |  3 ++-
 gso/workflows/router/promote_p_to_pe.py       | 26 +++++--------------
 gso/workflows/router/redeploy_base_config.py  |  3 ++-
 gso/workflows/router/terminate_router.py      |  3 ++-
 gso/workflows/router/update_ibgp_mesh.py      |  3 ++-
 13 files changed, 43 insertions(+), 56 deletions(-)
 create mode 100644 docs/source/module/utils/types.rst
 create mode 100644 gso/utils/types.py

diff --git a/docs/source/module/utils/types.rst b/docs/source/module/utils/types.rst
new file mode 100644
index 00000000..c70c8dd0
--- /dev/null
+++ b/docs/source/module/utils/types.rst
@@ -0,0 +1,6 @@
+``gso.utils.types``
+===================
+
+.. automodule:: gso.utils.types
+   :members:
+   :show-inheritance:
diff --git a/gso/utils/types.py b/gso/utils/types.py
new file mode 100644
index 00000000..3e1b4091
--- /dev/null
+++ b/gso/utils/types.py
@@ -0,0 +1,9 @@
+"""Define custom types for use across the application."""
+
+from typing import Annotated
+
+from pydantic import AfterValidator
+
+from gso.utils.helpers import validate_tt_number
+
+TTNumber = Annotated[str, AfterValidator(validate_tt_number)]
diff --git a/gso/workflows/iptrunk/create_iptrunk.py b/gso/workflows/iptrunk/create_iptrunk.py
index 373dfc03..90a966ef 100644
--- a/gso/workflows/iptrunk/create_iptrunk.py
+++ b/gso/workflows/iptrunk/create_iptrunk.py
@@ -40,9 +40,9 @@ from gso.utils.helpers import (
     validate_interface_name_list,
     validate_iptrunk_unique_interface,
     validate_router_in_netbox,
-    validate_tt_number,
 )
 from gso.utils.shared_enums import Vendor
+from gso.utils.types import TTNumber
 from gso.utils.workflow_steps import prompt_sharepoint_checklist_url
 
 
@@ -58,7 +58,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
     class CreateIptrunkForm(FormPage):
         model_config = ConfigDict(title=product_name)
 
-        tt_number: str
+        tt_number: TTNumber
         partner: ReadOnlyField("GEANT", default_type=str)  # type: ignore[valid-type]
         geant_s_sid: str | None = None
         iptrunk_description: str | None = None
@@ -66,10 +66,6 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
         iptrunk_speed: PhysicalPortCapacity
         iptrunk_number_of_members: int
 
-        @field_validator("tt_number")
-        def validate_tt_number(cls, tt_number: str) -> str:
-            return validate_tt_number(tt_number)
-
     initial_user_input = yield CreateIptrunkForm
     recommended_minimum_links = calculate_recommended_minimum_links(
         initial_user_input.iptrunk_number_of_members, initial_user_input.iptrunk_speed
diff --git a/gso/workflows/iptrunk/deploy_twamp.py b/gso/workflows/iptrunk/deploy_twamp.py
index 92e37fd5..a45b5eca 100644
--- a/gso/workflows/iptrunk/deploy_twamp.py
+++ b/gso/workflows/iptrunk/deploy_twamp.py
@@ -10,11 +10,10 @@ from orchestrator.utils.json import json_dumps
 from orchestrator.workflow import StepList, begin, done, step, workflow
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
-from pydantic import field_validator
 
 from gso.products.product_types.iptrunk import Iptrunk
 from gso.services.lso_client import execute_playbook, lso_interaction
-from gso.utils.helpers import validate_tt_number
+from gso.utils.types import TTNumber
 
 
 def _initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
@@ -26,11 +25,7 @@ def _initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
             f"{trunk.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn} to "
             f"{trunk.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn}"
         )
-        tt_number: str
-
-        @field_validator("tt_number")
-        def validate_tt_number(cls, tt_number: str) -> str:
-            return validate_tt_number(tt_number)
+        tt_number: TTNumber
 
     user_input = yield DeployTWAMPForm
 
diff --git a/gso/workflows/iptrunk/migrate_iptrunk.py b/gso/workflows/iptrunk/migrate_iptrunk.py
index 908fcdbc..be15848a 100644
--- a/gso/workflows/iptrunk/migrate_iptrunk.py
+++ b/gso/workflows/iptrunk/migrate_iptrunk.py
@@ -38,9 +38,9 @@ from gso.utils.helpers import (
     available_lags_choices,
     get_router_vendor,
     validate_interface_name_list,
-    validate_tt_number,
 )
 from gso.utils.shared_enums import Vendor
+from gso.utils.types import TTNumber
 from gso.utils.workflow_steps import set_isis_to_max
 
 
@@ -65,16 +65,12 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     class IPTrunkMigrateForm(FormPage):
         model_config = ConfigDict(title=form_title)
 
-        tt_number: str
+        tt_number: TTNumber
         replace_side: replaced_side_enum  # type: ignore[valid-type]
         warning_label: Label = "Are we moving to a different Site?"
         migrate_to_different_site: bool = False
         restore_isis_metric: bool = True
 
-        @field_validator("tt_number", mode="before")
-        def validate_tt_number(cls, tt_number: str) -> str:
-            return validate_tt_number(tt_number)
-
     migrate_form_input = yield IPTrunkMigrateForm
 
     current_routers = [
diff --git a/gso/workflows/iptrunk/modify_isis_metric.py b/gso/workflows/iptrunk/modify_isis_metric.py
index 2e4a4586..285907b4 100644
--- a/gso/workflows/iptrunk/modify_isis_metric.py
+++ b/gso/workflows/iptrunk/modify_isis_metric.py
@@ -12,6 +12,7 @@ from orchestrator.workflows.utils import wrap_modify_initial_input_form
 
 from gso.products.product_types.iptrunk import Iptrunk
 from gso.services.lso_client import execute_playbook, lso_interaction
+from gso.utils.types import TTNumber
 
 
 def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
@@ -19,7 +20,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     subscription = Iptrunk.from_subscription(subscription_id)
 
     class ModifyIptrunkForm(FormPage):
-        tt_number: str
+        tt_number: TTNumber
         isis_metric: int = subscription.iptrunk.iptrunk_isis_metric
 
     user_input = yield ModifyIptrunkForm
diff --git a/gso/workflows/iptrunk/modify_trunk_interface.py b/gso/workflows/iptrunk/modify_trunk_interface.py
index f5d17d77..394e369a 100644
--- a/gso/workflows/iptrunk/modify_trunk_interface.py
+++ b/gso/workflows/iptrunk/modify_trunk_interface.py
@@ -32,9 +32,9 @@ from gso.utils.helpers import (
     get_router_vendor,
     validate_interface_name_list,
     validate_iptrunk_unique_interface,
-    validate_tt_number,
 )
 from gso.utils.shared_enums import IPv4AddressType, IPv6AddressType, Vendor
+from gso.utils.types import TTNumber
 from gso.workflows.iptrunk.migrate_iptrunk import check_ip_trunk_optical_levels_pre
 from gso.workflows.iptrunk.validate_iptrunk import check_ip_trunk_isis
 
@@ -85,7 +85,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     subscription = Iptrunk.from_subscription(subscription_id)
 
     class ModifyIptrunkForm(FormPage):
-        tt_number: str
+        tt_number: TTNumber
         geant_s_sid: str | None = subscription.iptrunk.geant_s_sid
         iptrunk_description: str | None = subscription.iptrunk.iptrunk_description
         iptrunk_type: IptrunkType = subscription.iptrunk.iptrunk_type
@@ -103,10 +103,6 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
             str(subscription.iptrunk.iptrunk_ipv6_network), default_type=IPv6AddressType
         )
 
-        @field_validator("tt_number")
-        def validate_tt_number(cls, tt_number: str) -> str:
-            return validate_tt_number(tt_number)
-
     initial_user_input = yield ModifyIptrunkForm
 
     recommended_minimum_links = calculate_recommended_minimum_links(
diff --git a/gso/workflows/iptrunk/terminate_iptrunk.py b/gso/workflows/iptrunk/terminate_iptrunk.py
index bee9739a..bb1a6fd9 100644
--- a/gso/workflows/iptrunk/terminate_iptrunk.py
+++ b/gso/workflows/iptrunk/terminate_iptrunk.py
@@ -16,15 +16,15 @@ from orchestrator.workflows.steps import (
     unsync,
 )
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
-from pydantic import field_validator
 
 from gso.products.product_blocks.iptrunk import IptrunkSideBlock
 from gso.products.product_types.iptrunk import Iptrunk
 from gso.services import infoblox
 from gso.services.lso_client import execute_playbook, lso_interaction
 from gso.services.netbox_client import NetboxClient
-from gso.utils.helpers import get_router_vendor, validate_tt_number
+from gso.utils.helpers import get_router_vendor
 from gso.utils.shared_enums import Vendor
+from gso.utils.types import TTNumber
 from gso.utils.workflow_steps import set_isis_to_max
 
 
@@ -40,16 +40,12 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
             )
             info_label_3: Label = "ONLY EXECUTE THIS WORKFLOW WHEN YOU ARE ABSOLUTELY SURE WHAT YOU ARE DOING."
 
-        tt_number: str
+        tt_number: TTNumber
         termination_label: Label = (
             "Please confirm whether configuration should get removed from the A and B sides of the trunk."
         )
         remove_configuration: bool = True
 
-        @field_validator("tt_number")
-        def validate_tt_number(cls, tt_number: str) -> str:
-            return validate_tt_number(tt_number)
-
     user_input = yield TerminateForm
     return user_input.model_dump()
 
diff --git a/gso/workflows/router/create_router.py b/gso/workflows/router/create_router.py
index 8382e227..59dd6210 100644
--- a/gso/workflows/router/create_router.py
+++ b/gso/workflows/router/create_router.py
@@ -25,6 +25,7 @@ from gso.services.sharepoint import SharePointClient
 from gso.settings import load_oss_params
 from gso.utils.helpers import generate_fqdn, iso_from_ipv4
 from gso.utils.shared_enums import PortNumber, Vendor
+from gso.utils.types import TTNumber
 from gso.utils.workflow_steps import (
     deploy_base_config_dry,
     deploy_base_config_real,
@@ -48,7 +49,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
     class CreateRouterForm(FormPage):
         model_config = ConfigDict(title=product_name)
 
-        tt_number: str
+        tt_number: TTNumber
         partner: ReadOnlyField("GEANT", default_type=str)  # type: ignore[valid-type]
         vendor: Vendor
         router_site: _site_selector()  # type: ignore[valid-type]
diff --git a/gso/workflows/router/promote_p_to_pe.py b/gso/workflows/router/promote_p_to_pe.py
index 5b3a4bdb..c8c4f016 100644
--- a/gso/workflows/router/promote_p_to_pe.py
+++ b/gso/workflows/router/promote_p_to_pe.py
@@ -11,15 +11,16 @@ from orchestrator.utils.errors import ProcessFailureError
 from orchestrator.workflow import StepList, begin, conditional, done, inputstep, step, workflow
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
-from pydantic import ConfigDict, field_validator
+from pydantic import ConfigDict
 
 from gso.products.product_blocks.router import RouterRole
 from gso.products.product_types.router import Router
 from gso.services import lso_client
 from gso.services.kentik_client import KentikClient, NewKentikDevice
 from gso.services.lso_client import lso_interaction
-from gso.utils.helpers import generate_inventory_for_active_routers, validate_tt_number
+from gso.utils.helpers import generate_inventory_for_active_routers
 from gso.utils.shared_enums import Vendor
+from gso.utils.types import TTNumber
 
 
 def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
@@ -29,23 +30,11 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     class PromotePToPEForm(FormPage):
         model_config = ConfigDict(title=f"Promote {subscription.router.router_fqdn} to PE router?")
 
-        tt_number: str
-
-        @field_validator("tt_number")
-        def validate_tt_number(cls, tt_number: str) -> str:
-            return validate_tt_number(tt_number)
+        tt_number: TTNumber
 
     user_input = yield PromotePToPEForm
 
-    return user_input.model_dump()
-
-
-@step("Prepare required keys in state")
-def prepare_state(subscription_id: UUIDstr) -> State:
-    """Add required keys to the state for the workflow to run successfully."""
-    router = Router.from_subscription(subscription_id)
-
-    return {"subscription": router}
+    return user_input.model_dump() | {"subscription": subscription}
 
 
 @step("Evacuate the router by setting isis_overload")
@@ -138,8 +127,8 @@ def create_kentik_device(subscription: Router) -> State:
     kentik_site = kentik_client.get_site_by_name(subscription.router.router_site.site_name)
 
     if not kentik_site:
-        msg = f"Site could not be found in Kentik: {subscription.router.router_site.site_name}"
-        raise ProcessFailureError(msg)
+        msg = "Site could not be found in Kentik."
+        raise ProcessFailureError(msg, details=subscription.router.router_site.site_name)
 
     site_tier = subscription.router.router_site.site_tier
     new_device = NewKentikDevice(
@@ -573,7 +562,6 @@ def promote_p_to_pe() -> StepList:
     return (
         begin
         >> store_process_subscription(Target.MODIFY)
-        >> prepare_state
         >> router_is_juniper(done)
         >> router_is_pe(done)
         >> unsync
diff --git a/gso/workflows/router/redeploy_base_config.py b/gso/workflows/router/redeploy_base_config.py
index c1a24c83..b30d02f1 100644
--- a/gso/workflows/router/redeploy_base_config.py
+++ b/gso/workflows/router/redeploy_base_config.py
@@ -10,6 +10,7 @@ from orchestrator.workflows.utils import wrap_modify_initial_input_form
 
 from gso.products.product_types.router import Router
 from gso.services.lso_client import lso_interaction
+from gso.utils.types import TTNumber
 from gso.utils.workflow_steps import deploy_base_config_dry, deploy_base_config_real
 
 
@@ -18,7 +19,7 @@ def _initial_input_form(subscription_id: UUIDstr) -> FormGenerator:
 
     class RedeployBaseConfigForm(FormPage):
         info_label: Label = f"Redeploy base config on {router.router.router_fqdn}?"
-        tt_number: str
+        tt_number: TTNumber
 
     user_input = yield RedeployBaseConfigForm
 
diff --git a/gso/workflows/router/terminate_router.py b/gso/workflows/router/terminate_router.py
index 688e3ecc..07b06925 100644
--- a/gso/workflows/router/terminate_router.py
+++ b/gso/workflows/router/terminate_router.py
@@ -26,6 +26,7 @@ from gso.services.lso_client import execute_playbook, lso_interaction
 from gso.services.netbox_client import NetboxClient
 from gso.utils.helpers import generate_inventory_for_active_routers
 from gso.utils.shared_enums import Vendor
+from gso.utils.types import TTNumber
 
 logger = logging.getLogger(__name__)
 
@@ -42,7 +43,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
             )
             info_label_3: Label = "ONLY EXECUTE THIS WORKFLOW WHEN YOU ARE ABSOLUTELY SURE WHAT YOU ARE DOING."
 
-        tt_number: str
+        tt_number: TTNumber
         termination_label: Label = "Please confirm whether configuration should get removed from the router."
         remove_configuration: bool = True
         update_ibgp_mesh_label: Label = "Please confirm whether the iBGP mesh should get updated."
diff --git a/gso/workflows/router/update_ibgp_mesh.py b/gso/workflows/router/update_ibgp_mesh.py
index 58d20702..8fbb2813 100644
--- a/gso/workflows/router/update_ibgp_mesh.py
+++ b/gso/workflows/router/update_ibgp_mesh.py
@@ -18,6 +18,7 @@ from gso.services import librenms_client, lso_client
 from gso.services.lso_client import lso_interaction
 from gso.services.subscriptions import get_trunks_that_terminate_on_router
 from gso.utils.helpers import SNMPVersion, generate_inventory_for_active_routers
+from gso.utils.types import TTNumber
 
 
 def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
@@ -31,7 +32,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     class AddBGPSessionForm(FormPage):
         model_config = ConfigDict(title=f"Add {subscription.router.router_fqdn} to the iBGP mesh?")
 
-        tt_number: str
+        tt_number: TTNumber
 
         @model_validator(mode="before")
         def router_has_a_trunk(cls, data: Any) -> Any:
-- 
GitLab