diff --git a/gso/api/v1/network.py b/gso/api/v1/network.py index b92a135e63ff1a4fcee89e987b3c568fccc28d7f..62983f77f0b2287cb366ba2cb062babc2e972d05 100644 --- a/gso/api/v1/network.py +++ b/gso/api/v1/network.py @@ -10,10 +10,11 @@ from orchestrator.security import authorize from orchestrator.services.subscriptions import build_extended_domain_model from starlette import status -from gso.products.product_blocks.iptrunk import IptrunkType, PhysicalPortCapacity +from gso.products.product_blocks.iptrunk import IptrunkType from gso.products.product_blocks.router import RouterRole -from gso.products.product_blocks.site import LatitudeCoordinate, LongitudeCoordinate from gso.services.subscriptions import get_active_iptrunk_subscriptions +from gso.types.coordinates import LatitudeCoordinate, LongitudeCoordinate +from gso.types.interfaces import PhysicalPortCapacity from gso.utils.shared_enums import Vendor router = APIRouter(prefix="/networks", tags=["Network"], dependencies=[Depends(authorize)]) diff --git a/gso/cli/imports.py b/gso/cli/imports.py index 406fa85d1dbc0e11d58187909a63dc5f6285285c..eddb8dd9c6949aa5828e602f49e8db34bf0d224d 100644 --- a/gso/cli/imports.py +++ b/gso/cli/imports.py @@ -18,7 +18,7 @@ from sqlalchemy.exc import SQLAlchemyError from gso.db.models import PartnerTable from gso.products import ProductType -from gso.products.product_blocks.iptrunk import IptrunkType, PhysicalPortCapacity +from gso.products.product_blocks.iptrunk import IptrunkType from gso.products.product_blocks.router import RouterRole from gso.services.partners import ( PartnerNotFoundError, @@ -31,7 +31,8 @@ from gso.services.subscriptions import ( get_active_subscriptions_by_field_and_value, get_subscriptions, ) -from gso.utils.helpers import BaseSiteValidatorModel, LAGMember +from gso.types.base_site import BaseSiteValidatorModel +from gso.types.interfaces import LAGMember, LAGMemberList, PhysicalPortCapacity from gso.utils.shared_enums import IPv4AddressType, IPv6AddressType, PortNumber, Vendor app: typer.Typer = typer.Typer() @@ -115,11 +116,11 @@ class IptrunkImportModel(BaseModel): side_a_node_id: str side_a_ae_iface: str side_a_ae_geant_a_sid: str | None - side_a_ae_members: list[LAGMember] + side_a_ae_members: LAGMemberList[LAGMember] side_b_node_id: str side_b_ae_iface: str side_b_ae_geant_a_sid: str | None - side_b_ae_members: list[LAGMember] + side_b_ae_members: LAGMemberList[LAGMember] iptrunk_ipv4_network: ipaddress.IPv4Network iptrunk_ipv6_network: ipaddress.IPv6Network @@ -150,15 +151,6 @@ class IptrunkImportModel(BaseModel): return value - @field_validator("side_a_ae_members", "side_b_ae_members") - def check_side_uniqueness(cls, value: list[str]) -> list[str]: - """:term:`LAG` members must be unique.""" - if len(value) != len(set(value)): - msg = "Items must be unique" - raise ValueError(msg) - - return value - @model_validator(mode="after") def check_members(self) -> Self: """Amount of :term:`LAG` members has to match on side A and B, and meet the minimum requirement.""" diff --git a/gso/products/product_blocks/iptrunk.py b/gso/products/product_blocks/iptrunk.py index 88ff75cb2afad1960c4bc4f03953903c23040745..1ff4fe4652d6fea6f1a80491976ff3063978f82e 100644 --- a/gso/products/product_blocks/iptrunk.py +++ b/gso/products/product_blocks/iptrunk.py @@ -15,18 +15,7 @@ from gso.products.product_blocks.router import ( RouterBlockInactive, RouterBlockProvisioning, ) - - -class PhysicalPortCapacity(strEnum): - """Physical port capacity enumerator. - - An enumerator that has the different possible capacities of ports that are available to use in subscriptions. - """ - - ONE_GIGABIT_PER_SECOND = "1G" - TEN_GIGABIT_PER_SECOND = "10G" - HUNDRED_GIGABIT_PER_SECOND = "100G" - FOUR_HUNDRED_GIGABIT_PER_SECOND = "400G" +from gso.types.interfaces import LAGMemberList, PhysicalPortCapacity class IptrunkType(strEnum): @@ -36,9 +25,6 @@ class IptrunkType(strEnum): LEASED = "Leased" -LAGMemberList = Annotated[ - list[T], AfterValidator(validate_unique_list), Len(min_length=0), Doc("A list of :term:`LAG` member interfaces.") -] IptrunkSides = Annotated[ list[T], AfterValidator(validate_unique_list), diff --git a/gso/products/product_blocks/lan_switch_interconnect.py b/gso/products/product_blocks/lan_switch_interconnect.py index a9b1c77366662e24499f936f9762d515b882b934..544da1e757c279ca3c16f2e52489dba2624e68aa 100644 --- a/gso/products/product_blocks/lan_switch_interconnect.py +++ b/gso/products/product_blocks/lan_switch_interconnect.py @@ -3,9 +3,9 @@ from orchestrator.domain.base import ProductBlockModel from orchestrator.types import SubscriptionLifecycle, strEnum -from gso.products.product_blocks.iptrunk import LAGMemberList from gso.products.product_blocks.router import RouterBlock, RouterBlockInactive, RouterBlockProvisioning from gso.products.product_blocks.switch import SwitchBlock, SwitchBlockInactive, SwitchBlockProvisioning +from gso.types.interfaces import LAGMemberList class LanSwitchInterconnectAddressSpace(strEnum): diff --git a/gso/products/product_blocks/site.py b/gso/products/product_blocks/site.py index be7d086adc021992ede2048b6ef4a843c1793755..a0e21a584ddc6afed3377ee6d52bde2e6b665bd7 100644 --- a/gso/products/product_blocks/site.py +++ b/gso/products/product_blocks/site.py @@ -1,18 +1,9 @@ """The product block that describes a site subscription.""" -import re -from typing import Annotated - from orchestrator.domain.base import ProductBlockModel from orchestrator.types import SubscriptionLifecycle, strEnum -from pydantic import AfterValidator -from typing_extensions import Doc - -MAX_LONGITUDE = 180 -MIN_LONGITUDE = -180 -MAX_LATITUDE = 90 -MIN_LATITUDE = -90 +from gso.types.coordinates import LatitudeCoordinate, LongitudeCoordinate class SiteTier(strEnum): @@ -28,56 +19,6 @@ class SiteTier(strEnum): TIER4 = 4 -def validate_latitude(v: str) -> str: - """Validate a latitude coordinate.""" - msg = "Invalid latitude coordinate. Valid examples: '40.7128', '-74.0060', '90', '-90', '0'." - regex = re.compile(r"^-?([1-8]?\d(\.\d+)?|90(\.0+)?)$") - if not regex.match(str(v)): - raise ValueError(msg) - - float_v = float(v) - if float_v > MAX_LATITUDE or float_v < MIN_LATITUDE: - raise ValueError(msg) - - return v - - -def validate_longitude(v: str) -> str: - """Validate a longitude coordinate.""" - regex = re.compile(r"^-?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$") - msg = "Invalid longitude coordinate. Valid examples: '40.7128', '-74.0060', '180', '-180', '0'." - if not regex.match(v): - raise ValueError(msg) - - float_v = float(v) - if float_v > MAX_LONGITUDE or float_v < MIN_LONGITUDE: - raise ValueError(msg) - - return v - - -LatitudeCoordinate = Annotated[ - str, - AfterValidator(validate_latitude), - Doc( - "A latitude coordinate, modeled as a string. " - "The coordinate must match the format conforming to the latitude range of -90 to +90 degrees. " - "It can be a floating-point number or an integer. Valid examples: 40.7128, -74.0060, 90, -90, 0." - ), -] - -LongitudeCoordinate = Annotated[ - str, - AfterValidator(validate_longitude), - Doc( - "A longitude coordinate, modeled as a string. " - "The coordinate must match the format conforming to the longitude " - "range of -180 to +180 degrees. It can be a floating-point number or an integer. " - "Valid examples: 40.7128, -74.0060, 180, -180, 0." - ), -] - - class SiteBlockInactive( ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], diff --git a/gso/services/librenms_client.py b/gso/services/librenms_client.py index 2e04a866e3f85b539f22031b3f878b2149823d0f..4d8e4b197f647aadf829648a01e056cf8b199068 100644 --- a/gso/services/librenms_client.py +++ b/gso/services/librenms_client.py @@ -10,7 +10,7 @@ from requests import HTTPError, Response from requests.adapters import HTTPAdapter from gso.settings import load_oss_params -from gso.utils.helpers import SNMPVersion +from gso.types.snmp import SNMPVersion logger = logging.getLogger(__name__) diff --git a/gso/services/subscriptions.py b/gso/services/subscriptions.py index 25ed84e0593b2963656167737f17b4697ef2b0df..eed969a729c2da5ef943ec72c79dc94b65d60674 100644 --- a/gso/services/subscriptions.py +++ b/gso/services/subscriptions.py @@ -69,20 +69,6 @@ def get_subscriptions( return [dict(zip(includes, result, strict=True)) for result in results] -def get_active_site_subscriptions(includes: list[str] | None = None) -> list[SubscriptionType]: - """Retrieve active subscriptions specifically for sites. - - :param includes: The fields to be included in the returned Subscription objects. - :type includes: list[str] - - :return: A list of Subscription objects for sites. - :rtype: list[Subscription] - """ - return get_subscriptions( - product_types=[ProductType.SITE], lifecycles=[SubscriptionLifecycle.ACTIVE], includes=includes - ) - - def get_router_subscriptions( includes: list[str] | None = None, lifecycles: list[SubscriptionLifecycle] | None = None ) -> list[SubscriptionType]: @@ -122,6 +108,20 @@ def get_provisioning_router_subscriptions(includes: list[str] | None = None) -> ) +def get_active_switch_subscriptions(includes: list[str] | None = None) -> list[SubscriptionType]: + """Retrieve active subscriptions specifically for switches. + + :param includes: The fields to be included in the returned Subscription objects. + :type includes: list[str] + + :return: A list of Subscription objects for switches. + :rtype: list[Subscription] + """ + return get_subscriptions( + product_types=[ProductType.SWITCH], lifecycles=[SubscriptionLifecycle.ACTIVE], includes=includes + ) + + def get_active_iptrunk_subscriptions(includes: list[str] | None = None) -> list[SubscriptionType]: """Retrieve active subscriptions specifically for IP trunks. @@ -233,6 +233,18 @@ def get_active_insync_subscriptions() -> list[SubscriptionTable]: ) +def get_active_site_subscriptions(includes: list[str] | None = None) -> list[SubscriptionType]: + """Retrieve active subscriptions specifically for sites. + + :param includes: The fields to be included in the returned Subscription objects. + :type includes: list[str] + + :return: A list of Subscription objects for sites. + :rtype: list[Subscription] + """ + return get_subscriptions(product_types=["Site"], lifecycles=[SubscriptionLifecycle.ACTIVE], includes=includes) # type: ignore[list-item] + + def get_site_by_name(site_name: str) -> Site: """Get a site by its name. diff --git a/gso/translations/en-GB.json b/gso/translations/en-GB.json index 108a28522a74747461352dbd5133ba5ccf63f3ec..f1ede053f813aefef202d92ffd31b6d48c3908a5 100644 --- a/gso/translations/en-GB.json +++ b/gso/translations/en-GB.json @@ -32,16 +32,18 @@ "migrate_to_different_site": "Migrating to a different Site", "remove_configuration": "Remove configuration from the router", "clean_up_ipam": "Clean up related entries in IPAM", - "restore_isis_metric": "Restore ISIS metric to original value" + "restore_isis_metric": "Restore ISIS metric to original value", + "confirm_info": "Please verify this form looks correct." } }, "workflow": { "activate_iptrunk": "Activate IP Trunk", - "activate_router": "Activate router", - "confirm_info": "Please verify this form looks correct.", + "activate_router": "Activate Router", + "activate_switch": "Activate Switch", "create_iptrunk": "Create IP Trunk", "create_router": "Create Router", "create_site": "Create Site", + "create_switch": "Create Switch", "deploy_twamp": "Deploy TWAMP", "migrate_iptrunk": "Migrate IP Trunk", "modify_isis_metric": "Modify the ISIS metric", @@ -52,6 +54,7 @@ "terminate_iptrunk": "Terminate IP Trunk", "terminate_router": "Terminate Router", "terminate_site": "Terminate Site", + "terminate_switch": "Terminate Switch", "redeploy_base_config": "Redeploy base config", "update_ibgp_mesh": "Update iBGP mesh", "create_imported_site": "NOT FOR HUMANS -- Import existing site", @@ -67,7 +70,8 @@ "import_super_pop_switch": "NOT FOR HUMANS -- Finalize import into a Super PoP switch", "import_opengear": "NOT FOR HUMANS -- Finalize import into an OpenGear", "validate_iptrunk": "Validate IP Trunk configuration", - "validate_router": "Validate router configuration", + "validate_router": "Validate Router configuration", + "validate_switch": "Validate Switch configuration", "task_validate_geant_products": "Validation task for GEANT products", "task_send_email_notifications": "Send email notifications for failed tasks", "task_create_partners": "Create partner task", diff --git a/gso/types/__init__.py b/gso/types/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c3d1994f0c0c918fc35c2aabd757c67488065aa9 --- /dev/null +++ b/gso/types/__init__.py @@ -0,0 +1 @@ +"""Define custom types for use across the application.""" diff --git a/gso/types/base_site.py b/gso/types/base_site.py new file mode 100644 index 0000000000000000000000000000000000000000..1c29c1417aaeb2797c2f4007fd03e64cfa5e81b2 --- /dev/null +++ b/gso/types/base_site.py @@ -0,0 +1,59 @@ +"""A base site type for validation purposes that can be extended elsewhere.""" + +from pydantic import BaseModel, field_validator +from pydantic_core.core_schema import ValidationInfo + +from gso.products.product_blocks.site import SiteTier +from gso.types.coordinates import LatitudeCoordinate, LongitudeCoordinate +from gso.types.country_code import validate_country_code +from gso.types.ip_address import validate_ipv4_or_ipv6 +from gso.types.site_name import validate_site_name +from gso.types.unique_field import validate_field_is_unique + + +class BaseSiteValidatorModel(BaseModel): + """A base site validator model extended by create site and by import site.""" + + site_bgp_community_id: int + site_internal_id: int + site_tier: SiteTier + site_ts_address: str + site_country_code: str + site_name: str + site_city: str + site_country: str + site_latitude: LatitudeCoordinate + site_longitude: LongitudeCoordinate + partner: str + + @field_validator("site_ts_address") + def validate_ts_address(cls, site_ts_address: str) -> str: + """Validate that a terminal server address is valid.""" + validate_ipv4_or_ipv6(site_ts_address) + return site_ts_address + + @field_validator("site_country_code") + def country_code_must_exist(cls, country_code: str) -> str: + """Validate that the country code exists.""" + validate_country_code(country_code) + return country_code + + @field_validator("site_ts_address", "site_internal_id", "site_bgp_community_id", "site_name") + def field_must_be_unique(cls, value: str | int, info: ValidationInfo) -> str | int: + """Validate that a field is unique.""" + if not info.field_name: + msg = "Field name must be provided." + raise ValueError(msg) + + validate_field_is_unique(info.field_name, value) + + return value + + @field_validator("site_name") + def site_name_must_be_valid(cls, site_name: str) -> str: + """Validate the site name. + + The site name must consist of three uppercase letters, followed by an optional single digit. + """ + validate_site_name(site_name) + return site_name diff --git a/gso/types/coordinates.py b/gso/types/coordinates.py new file mode 100644 index 0000000000000000000000000000000000000000..91f1188fbe9196a564af4ab9f34a8d2aedeafb16 --- /dev/null +++ b/gso/types/coordinates.py @@ -0,0 +1,61 @@ +"""Custom coordinate types for latitude and longitude.""" + +import re +from typing import Annotated + +from pydantic import AfterValidator +from typing_extensions import Doc + +MAX_LONGITUDE = 180 +MIN_LONGITUDE = -180 +MAX_LATITUDE = 90 +MIN_LATITUDE = -90 + + +def validate_latitude(v: str) -> str: + """Validate a latitude coordinate.""" + msg = "Invalid latitude coordinate. Valid examples: '40.7128', '-74.0060', '90', '-90', '0'." + regex = re.compile(r"^-?([1-8]?\d(\.\d+)?|90(\.0+)?)$") + if not regex.match(str(v)): + raise ValueError(msg) + + float_v = float(v) + if float_v > MAX_LATITUDE or float_v < MIN_LATITUDE: + raise ValueError(msg) + + return v + + +def validate_longitude(v: str) -> str: + """Validate a longitude coordinate.""" + regex = re.compile(r"^-?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$") + msg = "Invalid longitude coordinate. Valid examples: '40.7128', '-74.0060', '180', '-180', '0'." + if not regex.match(v): + raise ValueError(msg) + + float_v = float(v) + if float_v > MAX_LONGITUDE or float_v < MIN_LONGITUDE: + raise ValueError(msg) + + return v + + +LatitudeCoordinate = Annotated[ + str, + AfterValidator(validate_latitude), + Doc( + "A latitude coordinate, modeled as a string. " + "The coordinate must match the format conforming to the latitude range of -90 to +90 degrees. " + "It can be a floating-point number or an integer. Valid examples: 40.7128, -74.0060, 90, -90, 0." + ), +] +LongitudeCoordinate = Annotated[ + str, + AfterValidator(validate_longitude), + Doc( + "A longitude coordinate, modeled as a string. " + "The coordinate must match the format conforming to the longitude " + "range of -180 to +180 degrees. It can be a floating-point number or an integer. " + "Valid examples: 40.7128, -74.0060, 180, -180, 0." + ), +] diff --git a/gso/types/country_code.py b/gso/types/country_code.py new file mode 100644 index 0000000000000000000000000000000000000000..4f03392845e34dde7299cda8a3135ded67041def --- /dev/null +++ b/gso/types/country_code.py @@ -0,0 +1,15 @@ +"""Country codes.""" + +import pycountry + + +def validate_country_code(country_code: str) -> str: + """Validate that a country code is valid.""" + # Check for the UK code before attempting to look it up since it's known as "GB" in the pycountry database. + if country_code != "UK": + try: + pycountry.countries.lookup(country_code) + except LookupError as e: + msg = "Invalid or non-existent country code, it must be in ISO 3166-1 alpha-2 format." + raise ValueError(msg) from e + return country_code diff --git a/gso/types/interfaces.py b/gso/types/interfaces.py new file mode 100644 index 0000000000000000000000000000000000000000..15c91167de822fee116600b71bf0b05dedfd87ae --- /dev/null +++ b/gso/types/interfaces.py @@ -0,0 +1,102 @@ +"""Custom types for interfaces, both physical and logical.""" + +import re +from typing import Annotated + +from annotated_types import Len +from orchestrator.domain.base import T +from pydantic import AfterValidator, BaseModel +from pydantic_forms.types import strEnum +from pydantic_forms.validators import validate_unique_list +from typing_extensions import Doc + + +class LAGMember(BaseModel): + """A :term:`LAG` member interface that consists of a name and description.""" + + interface_name: str + interface_description: str | None = None + + def __hash__(self) -> int: + """Calculate the hash based on the interface name and description, so that uniqueness can be determined.""" + return hash(self.interface_name) + + +def validate_interface_names_are_unique(interfaces: list[LAGMember]) -> list[LAGMember]: + """Verify if interfaces are unique. + + Raises a :class:`ValueError` if the interfaces are not unique. + + :param interfaces: The list of interfaces. + :type interfaces: list[:class:`utils.types.LAGMember`] + + :return: The list of interfaces + :rtype: list[:class:`utils.types.LAGMember`] + """ + interface_names = [member.interface_name for member in interfaces] + if len(interface_names) != len(set(interface_names)): + msg = "Interfaces must be unique." + raise ValueError(msg) + return interfaces + + +def validate_juniper_phy_interface_name(interface_name: str) -> str: + """Validate that the provided interface name matches the expected pattern. + + The expected pattern for the interface name is one of 'ge', 'et', 'xe' followed by a dash '-', + then a number between 0 and 19, a forward slash '/', another number between 0 and 99, + another forward slash '/', and ends with a number between 0 and 99. + For example: 'xe-1/0/0'. This only applies to Juniper-brand hardware. + + :param str interface_name: Interface name to validate. + + :return str: The interface name if match was successful, otherwise it will throw a ValueError exception. + """ + pattern = re.compile(r"^(ge|et|xe)-1?[0-9]/[0-9]{1,2}/[0-9]{1,2}$") + if not bool(pattern.match(interface_name)): + error_msg = ( + f"Invalid interface name. The interface name should be of format: xe-1/0/0. " f"Got: {interface_name}" + ) + raise ValueError(error_msg) + return interface_name + + +def validate_juniper_ae_interface_name(interface_name: str) -> str: + """Validate that the provided interface name matches the expected pattern for a :term:`LAG` interface. + + Interface names must match the pattern 'ae' followed by a one- or two-digit number. + """ + juniper_lag_re = re.compile("^ae\\d{1,2}$") + if not juniper_lag_re.match(interface_name): + msg = "Invalid LAG name, please try again." + raise ValueError(msg) + return interface_name + + +JuniperPhyInterface = Annotated[str, AfterValidator(validate_juniper_phy_interface_name)] +JuniperAEInterface = Annotated[str, AfterValidator(validate_juniper_ae_interface_name)] +LAGMemberList = Annotated[ + list[T], + AfterValidator(validate_unique_list), + AfterValidator(validate_interface_names_are_unique), + Len(min_length=0), + Doc("A list of :term:`LAG` member interfaces."), +] + + +class JuniperLAGMember(LAGMember): + """A Juniper-specific :term:`LAG` member interface.""" + + interface_name: JuniperPhyInterface + + +class PhysicalPortCapacity(strEnum): + """Physical port capacity enumerator. + + An enumerator that has the different possible capacities of ports that are available to use in subscriptions. + """ + + ONE_GIGABIT_PER_SECOND = "1G" + TEN_GIGABIT_PER_SECOND = "10G" + HUNDRED_GIGABIT_PER_SECOND = "100G" + FOUR_HUNDRED_GIGABIT_PER_SECOND = "400G" diff --git a/gso/types/ip_address.py b/gso/types/ip_address.py new file mode 100644 index 0000000000000000000000000000000000000000..793ebb4f1a7aefd66c85d4f307de983255edee31 --- /dev/null +++ b/gso/types/ip_address.py @@ -0,0 +1,14 @@ +"""IP addresses.""" + +import ipaddress + + +def validate_ipv4_or_ipv6(value: str) -> str: + """Validate that a value is a valid IPv4 or IPv6 address.""" + try: + ipaddress.ip_address(value) + except ValueError as e: + msg = "Enter a valid IPv4 or IPv6 address." + raise ValueError(msg) from e + else: + return value diff --git a/gso/types/netbox_router.py b/gso/types/netbox_router.py new file mode 100644 index 0000000000000000000000000000000000000000..a5845a479b605f98ba1faaa6f4a3c6e461bbf8d3 --- /dev/null +++ b/gso/types/netbox_router.py @@ -0,0 +1,27 @@ +"""A router that must be present in Netbox.""" + +from pydantic_forms.types import UUIDstr + +from gso.products.product_types.router import Router +from gso.services.netbox_client import NetboxClient +from gso.utils.shared_enums import Vendor + + +def validate_router_in_netbox(subscription_id: UUIDstr) -> UUIDstr: + """Verify if a device exists in Netbox. + + Raises a :class:`ValueError` if the device is not found. + + :param subscription_id: The :term:`UUID` of the router subscription. + :type subscription_id: :class:`UUIDstr` + + :return: The :term:`UUID` of the router subscription. + :rtype: :class:`UUIDstr` + """ + router_type = Router.from_subscription(subscription_id) + if router_type.router.vendor == Vendor.NOKIA: + device = NetboxClient().get_device_by_name(router_type.router.router_fqdn) + if not device: + msg = "The selected router does not exist in Netbox." + raise ValueError(msg) + return subscription_id diff --git a/gso/types/site_name.py b/gso/types/site_name.py new file mode 100644 index 0000000000000000000000000000000000000000..69a46b1a43646bb50389f47a4bb5dc0ace1f170b --- /dev/null +++ b/gso/types/site_name.py @@ -0,0 +1,17 @@ +"""Type for the name of a site.""" + +import re + + +def validate_site_name(site_name: str) -> None: + """Validate the site name. + + The site name must consist of three uppercase letters, optionally followed by a single digit. + """ + pattern = re.compile(r"^[A-Z]{3}[0-9]?$") + if not pattern.match(site_name): + msg = ( + "Enter a valid site name. It must consist of three uppercase letters (A-Z), followed by an optional single " + f"digit (0-9). Received: {site_name}" + ) + raise ValueError(msg) diff --git a/gso/types/snmp.py b/gso/types/snmp.py new file mode 100644 index 0000000000000000000000000000000000000000..03581cf970c036db37ea901d8b159d25fc480136 --- /dev/null +++ b/gso/types/snmp.py @@ -0,0 +1,10 @@ +"""An enumerator of SNMP version numbers.""" + +from enum import StrEnum + + +class SNMPVersion(StrEnum): + """An enumerator for the two relevant versions of :term:`SNMP`: v2c and 3.""" + + V2C = "v2c" + V3 = "v3" diff --git a/gso/types/tt_number.py b/gso/types/tt_number.py new file mode 100644 index 0000000000000000000000000000000000000000..a9e1d05c6ad9563047764d56461afe846b99be88 --- /dev/null +++ b/gso/types/tt_number.py @@ -0,0 +1,29 @@ +"""A Trouble Ticket number.""" + +import re +from typing import Annotated + +from pydantic import AfterValidator + + +def validate_tt_number(tt_number: str) -> str: + """Validate a string to match a specific pattern. + + This method checks if the input string starts with 'TT#' and is followed by exactly 16 digits. + + :param str tt_number: The TT number as string to validate + + :return str: The TT number string if TT number match was successful, otherwise it will raise a ValueError. + """ + pattern = r"^TT#\d{16}$" + if not bool(re.match(pattern, tt_number)): + err_msg = ( + f"The given TT number: {tt_number} is not valid. " + f" A valid TT number starts with 'TT#' followed by 16 digits." + ) + raise ValueError(err_msg) + + return tt_number + + +TTNumber = Annotated[str, AfterValidator(validate_tt_number)] diff --git a/gso/types/unique_field.py b/gso/types/unique_field.py new file mode 100644 index 0000000000000000000000000000000000000000..1b47dcb0db386d09dc58e2c48119854c6fb4adb3 --- /dev/null +++ b/gso/types/unique_field.py @@ -0,0 +1,10 @@ +"""An input field that must be unique in the database.""" + +from gso.services import subscriptions + + +def validate_field_is_unique(field_name: str, value: str | int) -> None: + """Validate that a site field is unique.""" + if len(subscriptions.get_active_subscriptions_by_field_and_value(field_name, str(value))) > 0: + msg = f"{field_name} must be unique" + raise ValueError(msg) diff --git a/gso/utils/helpers.py b/gso/utils/helpers.py index 13016c76f13028db17e8397ebff7ce23455a997a..f1063cecd97209247ecd950a49de95bf46f5c0e0 100644 --- a/gso/utils/helpers.py +++ b/gso/utils/helpers.py @@ -1,42 +1,21 @@ """Helper methods that are used across :term:`GSO`.""" -import ipaddress import re -from enum import StrEnum +from typing import TYPE_CHECKING from uuid import UUID -import pycountry -from orchestrator.types import UUIDstr -from pydantic import BaseModel, field_validator -from pydantic_core.core_schema import ValidationInfo from pydantic_forms.validators import Choice from gso import settings -from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock, PhysicalPortCapacity from gso.products.product_blocks.router import RouterRole -from gso.products.product_blocks.site import LatitudeCoordinate, LongitudeCoordinate, SiteTier from gso.products.product_types.router import Router +from gso.services import subscriptions from gso.services.netbox_client import NetboxClient -from gso.services.subscriptions import get_active_router_subscriptions, get_active_subscriptions_by_field_and_value +from gso.types.interfaces import PhysicalPortCapacity from gso.utils.shared_enums import IPv4AddressType, Vendor - -class LAGMember(BaseModel): - """A :term:`LAG` member interface that consists of a name and description.""" - - interface_name: str - interface_description: str | None = None - - def __hash__(self) -> int: - """Calculate the hash based on the interface name and description, so that uniqueness can be determined.""" - return hash((self.interface_name, self.interface_description)) - - -class SNMPVersion(StrEnum): - """An enumerator for the two relevant versions of :term:`SNMP`: v2c and 3.""" - - V2C = "v2c" - V3 = "v3" +if TYPE_CHECKING: + from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock def available_interfaces_choices(router_id: UUID, speed: str) -> Choice | None: @@ -57,7 +36,7 @@ def available_interfaces_choices(router_id: UUID, speed: str) -> Choice | None: def available_interfaces_choices_including_current_members( router_id: UUID, speed: str, - interfaces: list[IptrunkInterfaceBlock], + interfaces: list["IptrunkInterfaceBlock"], ) -> Choice | None: """Return a list of available interfaces for a given router and speed including the current members. @@ -119,184 +98,6 @@ def iso_from_ipv4(ipv4_address: IPv4AddressType) -> str: return f"49.51e5.0001.{re_split}.00" -def validate_router_in_netbox(subscription_id: UUIDstr) -> UUIDstr: - """Verify if a device exists in Netbox. - - Raises a :class:`ValueError` if the device is not found. - - :param subscription_id: The :term:`UUID` of the router subscription. - :type subscription_id: :class:`UUIDstr` - - :return: The :term:`UUID` of the router subscription. - :rtype: :class:`UUIDstr` - """ - router_type = Router.from_subscription(subscription_id) - if router_type.router.vendor == Vendor.NOKIA: - device = NetboxClient().get_device_by_name(router_type.router.router_fqdn) - if not device: - msg = "The selected router does not exist in Netbox." - raise ValueError(msg) - return subscription_id - - -def validate_iptrunk_unique_interface(interfaces: list[LAGMember]) -> list[LAGMember]: - """Verify if the interfaces are unique. - - Raises a :class:`ValueError` if the interfaces are not unique. - - :param interfaces: The list of interfaces. - :type interfaces: list[:class:`LAGMember`] - - :return: The list of interfaces - :rtype: list[:class:`LAGMember`] - """ - interface_names = [member.interface_name for member in interfaces] - if len(interface_names) != len(set(interface_names)): - msg = "Interfaces must be unique." - raise ValueError(msg) - return interfaces - - -def validate_site_fields_is_unique(field_name: str, value: str | int) -> None: - """Validate that a site field is unique.""" - if len(get_active_subscriptions_by_field_and_value(field_name, str(value))) > 0: - msg = f"{field_name} must be unique" - raise ValueError(msg) - - -def validate_ipv4_or_ipv6(value: str) -> str: - """Validate that a value is a valid IPv4 or IPv6 address.""" - try: - ipaddress.ip_address(value) - except ValueError as e: - msg = "Enter a valid IPv4 or IPv6 address." - raise ValueError(msg) from e - else: - return value - - -def validate_country_code(country_code: str) -> str: - """Validate that a country code is valid.""" - # Check for the UK code before attempting to look it up since it's known as "GB" in the pycountry database. - if country_code != "UK": - try: - pycountry.countries.lookup(country_code) - except LookupError as e: - msg = "Invalid or non-existent country code, it must be in ISO 3166-1 alpha-2 format." - raise ValueError(msg) from e - return country_code - - -def validate_site_name(site_name: str) -> None: - """Validate the site name. - - The site name must consist of three uppercase letters, optionally followed by a single digit. - """ - pattern = re.compile(r"^[A-Z]{3}[0-9]?$") - if not pattern.match(site_name): - msg = ( - "Enter a valid site name. It must consist of three uppercase letters (A-Z), followed by an optional single " - f"digit (0-9). Received: {site_name}" - ) - raise ValueError(msg) - - -class BaseSiteValidatorModel(BaseModel): - """A base site validator model extended by create site and by import site.""" - - site_bgp_community_id: int - site_internal_id: int - site_tier: SiteTier - site_ts_address: str - site_country_code: str - site_name: str - site_city: str - site_country: str - site_latitude: LatitudeCoordinate - site_longitude: LongitudeCoordinate - partner: str - - @field_validator("site_ts_address") - def validate_ts_address(cls, site_ts_address: str) -> str: - """Validate that a terminal server address is valid.""" - validate_ipv4_or_ipv6(site_ts_address) - return site_ts_address - - @field_validator("site_country_code") - def country_code_must_exist(cls, country_code: str) -> str: - """Validate that the country code exists.""" - validate_country_code(country_code) - return country_code - - @field_validator("site_ts_address", "site_internal_id", "site_bgp_community_id", "site_name") - def field_must_be_unique(cls, value: str | int, info: ValidationInfo) -> str | int: - """Validate that a field is unique.""" - if not info.field_name: - msg = "Field name must be provided." - raise ValueError(msg) - - validate_site_fields_is_unique(info.field_name, value) - - return value - - @field_validator("site_name") - def site_name_must_be_valid(cls, site_name: str) -> str: - """Validate the site name. - - The site name must consist of three uppercase letters, followed by an optional single digit. - """ - validate_site_name(site_name) - return site_name - - -def validate_interface_name_list(interface_name_list: list, vendor: str) -> list: - """Validate that the provided interface name matches the expected pattern. - - The expected pattern for the interface name is one of 'ge', 'et', 'xe' followed by a dash '-', - then a number between 0 and 19, a forward slash '/', another number between 0 and 99, - another forward slash '/', and ends with a number between 0 and 99. - For example: 'xe-1/0/0'. - - :param list interface_name_list: List of interface names to validate. - :param str vendor: Router vendor to check interface names - - :return list: The list of interface names if all match was successful, otherwise it will throw a ValueError - exception. - """ - # For Nokia nothing to do - if vendor == Vendor.NOKIA: - return interface_name_list - pattern = re.compile(r"^(ge|et|xe)-1?[0-9]/[0-9]{1,2}/[0-9]{1,2}$") - for interface in interface_name_list: - if not bool(pattern.match(interface.interface_name)): - error_msg = ( - f"Invalid interface name. The interface name should be of format: xe-1/0/0. " - f"Got: [{interface.interface_name}]" - ) - raise ValueError(error_msg) - return interface_name_list - - -def validate_tt_number(tt_number: str) -> str: - """Validate a string to match a specific pattern. - - This method checks if the input string starts with 'TT#' and is followed by exactly 16 digits. - - :param str tt_number: The TT number as string to validate - - :return str: The TT number string if TT number match was successful, otherwise it will raise a ValueError. - """ - pattern = r"^TT#\d{16}$" - if not bool(re.match(pattern, tt_number)): - err_msg = ( - f"The given TT number: {tt_number} is not valid. " - f" A valid TT number starts with 'TT#' followed by 16 digits." - ) - raise ValueError(err_msg) - - return tt_number - - def generate_fqdn(hostname: str, site_name: str, country_code: str) -> str: """Generate an :term:`FQDN` from a hostname, site name, and a country code.""" oss = settings.load_oss_params() @@ -318,7 +119,9 @@ def generate_inventory_for_active_routers( :return: A dictionary representing the inventory of active routers. :rtype: dict[str, Any] """ - all_routers = [Router.from_subscription(r["subscription_id"]) for r in get_active_router_subscriptions()] + all_routers = [ + Router.from_subscription(r["subscription_id"]) for r in subscriptions.get_active_router_subscriptions() + ] exclude_routers = exclude_routers or [] return { @@ -351,3 +154,33 @@ def calculate_recommended_minimum_links(iptrunk_number_of_members: int, iptrunk_ if iptrunk_speed == PhysicalPortCapacity.FOUR_HUNDRED_GIGABIT_PER_SECOND: return iptrunk_number_of_members - 1 return iptrunk_number_of_members + + +def active_site_selector() -> Choice: + """Generate a dropdown selector for choosing an active site in an input form.""" + site_subscriptions = { + str(site["subscription_id"]): site["description"] + for site in subscriptions.get_active_site_subscriptions(includes=["subscription_id", "description"]) + } + + return Choice("Select a site", zip(site_subscriptions.keys(), site_subscriptions.items(), strict=True)) # type: ignore[arg-type] + + +def active_router_selector() -> Choice: + """Generate a dropdown selector for choosing an active Router in an input form.""" + router_subscriptions = { + str(router["subscription_id"]): router["description"] + for router in subscriptions.get_active_router_subscriptions(includes=["subscription_id", "description"]) + } + + return Choice("Select a router", zip(router_subscriptions.keys(), router_subscriptions.items(), strict=True)) # type: ignore[arg-type] + + +def active_switch_selector() -> Choice: + """Generate a dropdown selector for choosing an active Switch in an input form.""" + switch_subscriptions = { + str(switch["subscription_id"]): switch["description"] + for switch in subscriptions.get_active_switch_subscriptions(includes=["subscription_id", "description"]) + } + + return Choice("Select a switch", zip(switch_subscriptions.keys(), switch_subscriptions.items(), strict=True)) # type: ignore[arg-type] diff --git a/gso/utils/types.py b/gso/utils/types.py deleted file mode 100644 index 3e1b4091b127d9a572c12c7fad462dc4887de9f7..0000000000000000000000000000000000000000 --- a/gso/utils/types.py +++ /dev/null @@ -1,9 +0,0 @@ -"""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_imported_iptrunk.py b/gso/workflows/iptrunk/create_imported_iptrunk.py index 9b0e6b87a8a095073875721e79d72c9e03baa66d..18f76a7f93d30e7bc082bb2696ac9daf8448ce86 100644 --- a/gso/workflows/iptrunk/create_imported_iptrunk.py +++ b/gso/workflows/iptrunk/create_imported_iptrunk.py @@ -6,7 +6,6 @@ from uuid import uuid4 from orchestrator import workflow from orchestrator.forms import FormPage -from orchestrator.forms.validators import Choice from orchestrator.targets import Target from orchestrator.types import FormGenerator, State, SubscriptionLifecycle from orchestrator.workflow import StepList, begin, done, step @@ -15,30 +14,17 @@ from pydantic import AfterValidator, ConfigDict from pydantic_forms.validators import validate_unique_list from gso.products import ProductName -from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlockInactive, IptrunkType, PhysicalPortCapacity +from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlockInactive, IptrunkType from gso.products.product_types.iptrunk import ImportedIptrunkInactive from gso.products.product_types.router import Router from gso.services import subscriptions from gso.services.partners import get_partner_by_name -from gso.utils.helpers import LAGMember - - -def _generate_routers() -> dict[str, str]: - """Generate a dictionary of router IDs and descriptions.""" - routers = {} - for subscription in subscriptions.get_active_router_subscriptions(includes=["subscription_id", "description"]): - routers[str(subscription["subscription_id"])] = subscription["description"] - - return routers - - -LAGMemberList = Annotated[list[LAGMember], AfterValidator(validate_unique_list)] +from gso.types.interfaces import LAGMember, LAGMemberList, PhysicalPortCapacity +from gso.utils.helpers import active_router_selector def initial_input_form_generator() -> FormGenerator: """Take all information passed to this workflow by the :term:`API` endpoint that was called.""" - routers = _generate_routers() - router_enum = Choice("Select a router", zip(routers.keys(), routers.items(), strict=True)) # type: ignore[arg-type] class CreateIptrunkForm(FormPage): model_config = ConfigDict(title="Import Iptrunk") @@ -51,15 +37,15 @@ def initial_input_form_generator() -> FormGenerator: iptrunk_minimum_links: int iptrunk_isis_metric: int - side_a_node_id: router_enum # type: ignore[valid-type] + side_a_node_id: active_router_selector() # type: ignore[valid-type] side_a_ae_iface: str side_a_ae_geant_a_sid: str | None = None - side_a_ae_members: LAGMemberList + side_a_ae_members: Annotated[list[LAGMember], AfterValidator(validate_unique_list)] - side_b_node_id: router_enum # type: ignore[valid-type] + side_b_node_id: active_router_selector() # type: ignore[valid-type] side_b_ae_iface: str side_b_ae_geant_a_sid: str | None = None - side_b_ae_members: LAGMemberList + side_b_ae_members: Annotated[list[LAGMember], AfterValidator(validate_unique_list)] iptrunk_ipv4_network: ipaddress.IPv4Network iptrunk_ipv6_network: ipaddress.IPv6Network @@ -94,11 +80,11 @@ def initialize_subscription( side_a_node_id: str, side_a_ae_iface: str, side_a_ae_geant_a_sid: str | None, - side_a_ae_members: list[dict], + side_a_ae_members: LAGMemberList, side_b_node_id: str, side_b_ae_iface: str, side_b_ae_geant_a_sid: str | None, - side_b_ae_members: list[dict], + side_b_ae_members: LAGMemberList, ) -> State: """Take all input from the user, and store it in the database.""" subscription.iptrunk.geant_s_sid = geant_s_sid diff --git a/gso/workflows/iptrunk/create_iptrunk.py b/gso/workflows/iptrunk/create_iptrunk.py index 246d0639974fd90a32b08f3600d90231c0707400..aba759808ff59cd8886a2b01cff715fe95421f38 100644 --- a/gso/workflows/iptrunk/create_iptrunk.py +++ b/gso/workflows/iptrunk/create_iptrunk.py @@ -16,15 +16,14 @@ from orchestrator.workflow import StepList, begin, conditional, done, step, step from orchestrator.workflows.steps import resync, set_status, store_process_subscription from orchestrator.workflows.utils import wrap_create_initial_input_form from ping3 import ping -from pydantic import AfterValidator, ConfigDict, field_validator -from pydantic_forms.validators import ReadOnlyField, validate_unique_list +from pydantic import ConfigDict, field_validator +from pydantic_forms.validators import ReadOnlyField from pynetbox.models.dcim import Interfaces from gso.products.product_blocks.iptrunk import ( IptrunkInterfaceBlockInactive, IptrunkSideBlockInactive, IptrunkType, - PhysicalPortCapacity, ) from gso.products.product_types.iptrunk import Iptrunk, IptrunkInactive, IptrunkProvisioning from gso.products.product_types.router import Router @@ -35,29 +34,27 @@ from gso.services.partners import get_partner_by_name from gso.services.sharepoint import SharePointClient from gso.services.subscriptions import get_non_terminated_iptrunk_subscriptions from gso.settings import load_oss_params +from gso.types.interfaces import JuniperLAGMember, LAGMember, LAGMemberList, PhysicalPortCapacity +from gso.types.netbox_router import validate_router_in_netbox +from gso.types.tt_number import TTNumber from gso.utils.helpers import ( - LAGMember, available_interfaces_choices, available_lags_choices, calculate_recommended_minimum_links, get_router_vendor, - validate_interface_name_list, - validate_iptrunk_unique_interface, - validate_router_in_netbox, ) from gso.utils.shared_enums import Vendor -from gso.utils.types import TTNumber from gso.utils.workflow_steps import prompt_sharepoint_checklist_url def initial_input_form_generator(product_name: str) -> FormGenerator: """Gather input from the user in three steps. General information, and information on both sides of the trunk.""" - routers = {} - for router in subscriptions.get_active_router_subscriptions( - includes=["subscription_id", "description"] - ) + subscriptions.get_provisioning_router_subscriptions(includes=["subscription_id", "description"]): - # Add both provisioning and active routers, since trunks are required for promoting a router to active. - routers[str(router["subscription_id"])] = router["description"] + # Add both provisioning and active routers, since trunks are required for promoting a router to active. + routers = { + str(router["subscription_id"]): router["description"] + for router in subscriptions.get_active_router_subscriptions(includes=["subscription_id", "description"]) + + subscriptions.get_provisioning_router_subscriptions(includes=["subscription_id", "description"]) + } class CreateIptrunkForm(FormPage): model_config = ConfigDict(title=product_name) @@ -97,8 +94,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: router_a_fqdn = Router.from_subscription(router_a).router.router_fqdn juniper_ae_members = Annotated[ - list[LAGMember], - AfterValidator(validate_unique_list), + LAGMemberList[JuniperLAGMember], Len( min_length=initial_user_input.iptrunk_number_of_members, max_length=initial_user_input.iptrunk_number_of_members, @@ -114,8 +110,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: ) ae_members_side_a_type = Annotated[ - list[NokiaLAGMemberA], - AfterValidator(validate_unique_list), + LAGMemberList[NokiaLAGMemberA], Len( min_length=initial_user_input.iptrunk_number_of_members, max_length=initial_user_input.iptrunk_number_of_members, @@ -125,19 +120,12 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: ae_members_side_a_type = juniper_ae_members # type: ignore[assignment, misc] class CreateIptrunkSideAForm(FormPage): - model_config = ConfigDict(title=f"Provide subscription details for side A of the trunk.({router_a_fqdn})") + model_config = ConfigDict(title=f"Provide subscription details for side A of the trunk. ({router_a_fqdn})") side_a_ae_iface: available_lags_choices(router_a) or str # type: ignore[valid-type] side_a_ae_geant_a_sid: str | None side_a_ae_members: ae_members_side_a_type - @field_validator("side_a_ae_members") - def validate_side_a_ae_members(cls, side_a_ae_members: list[LAGMember]) -> list[LAGMember]: - validate_iptrunk_unique_interface(side_a_ae_members) - vendor = get_router_vendor(router_a) - validate_interface_name_list(side_a_ae_members, vendor) - return side_a_ae_members - user_input_side_a = yield CreateIptrunkSideAForm # Remove the selected router for side A, to prevent any loops routers.pop(str(router_a)) @@ -165,8 +153,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: ) ae_members_side_b = Annotated[ - list[NokiaLAGMemberB], - AfterValidator(validate_unique_list), + LAGMemberList[NokiaLAGMemberB], Len( min_length=len(user_input_side_a.side_a_ae_members), max_length=len(user_input_side_a.side_a_ae_members) ), @@ -175,19 +162,12 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: ae_members_side_b = juniper_ae_members # type: ignore[assignment, misc] class CreateIptrunkSideBForm(FormPage): - model_config = ConfigDict(title=f"Provide subscription details for side B of the trunk.({router_b_fqdn})") + model_config = ConfigDict(title=f"Provide subscription details for side B of the trunk. ({router_b_fqdn})") side_b_ae_iface: available_lags_choices(router_b) or str # type: ignore[valid-type] side_b_ae_geant_a_sid: str | None side_b_ae_members: ae_members_side_b - @field_validator("side_b_ae_members") - def validate_side_b_ae_members(cls, side_b_ae_members: list[LAGMember]) -> list[LAGMember]: - validate_iptrunk_unique_interface(side_b_ae_members) - vendor = get_router_vendor(router_b) - validate_interface_name_list(side_b_ae_members, vendor) - return side_b_ae_members - user_input_side_b = yield CreateIptrunkSideBForm return ( diff --git a/gso/workflows/iptrunk/deploy_twamp.py b/gso/workflows/iptrunk/deploy_twamp.py index a45b5eca61144577c5dbf58a251b0b1ca7c76f6d..feb7100a46360f5c9fb14ef28f98b6bb4cb165ac 100644 --- a/gso/workflows/iptrunk/deploy_twamp.py +++ b/gso/workflows/iptrunk/deploy_twamp.py @@ -13,7 +13,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 +from gso.types.tt_number import TTNumber def _initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: diff --git a/gso/workflows/iptrunk/migrate_iptrunk.py b/gso/workflows/iptrunk/migrate_iptrunk.py index 73fb344e169a0bb26d3ef1eba18b84f0e0367531..0ed107a6fb9aab243887a6af3fdf88a355e7b887 100644 --- a/gso/workflows/iptrunk/migrate_iptrunk.py +++ b/gso/workflows/iptrunk/migrate_iptrunk.py @@ -6,7 +6,6 @@ configured to run from A to C. B is then no longer associated with this IP trunk import copy import json -import re from typing import Annotated from uuid import uuid4 @@ -21,8 +20,8 @@ from orchestrator.utils.json import json_dumps from orchestrator.workflow import StepList, begin, conditional, done, inputstep from orchestrator.workflows.steps import resync, store_process_subscription, unsync from orchestrator.workflows.utils import wrap_modify_initial_input_form -from pydantic import AfterValidator, ConfigDict, field_validator -from pydantic_forms.validators import ReadOnlyField, validate_unique_list +from pydantic import ConfigDict +from pydantic_forms.validators import ReadOnlyField from pynetbox.models.dcim import Interfaces from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock, IptrunkType @@ -34,15 +33,14 @@ from gso.services.netbox_client import NetboxClient from gso.services.sharepoint import SharePointClient from gso.services.subscriptions import get_active_router_subscriptions from gso.settings import load_oss_params +from gso.types.interfaces import JuniperAEInterface, JuniperLAGMember, LAGMember, LAGMemberList +from gso.types.tt_number import TTNumber from gso.utils.helpers import ( - LAGMember, available_interfaces_choices, available_lags_choices, get_router_vendor, - validate_interface_name_list, ) from gso.utils.shared_enums import Vendor -from gso.utils.types import TTNumber from gso.utils.workflow_steps import prompt_sharepoint_checklist_url, set_isis_to_max @@ -103,7 +101,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: new_side_iptrunk_router_input = yield NewSideIPTrunkRouterForm new_router = new_side_iptrunk_router_input.new_node - side_a_ae_iface = available_lags_choices(new_router) or str + side_a_ae_iface = available_lags_choices(new_router) or JuniperAEInterface new_side_is_nokia = get_router_vendor(new_router) == Vendor.NOKIA if new_side_is_nokia: @@ -115,8 +113,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: ) ae_members = Annotated[ - list[NokiaLAGMember], - AfterValidator(validate_unique_list), + LAGMemberList[NokiaLAGMember], Len( min_length=len(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members), max_length=len(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members), @@ -124,8 +121,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: ] else: ae_members = Annotated[ # type: ignore[assignment, misc] - list[LAGMember], - AfterValidator(validate_unique_list), + LAGMemberList[JuniperLAGMember], Len( min_length=len(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members), max_length=len(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members), @@ -150,23 +146,9 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: model_config = ConfigDict(title=form_title) new_lag_interface: side_a_ae_iface # type: ignore[valid-type] - existing_lag_interface: ReadOnlyField(existing_lag_ae_members, default_type=list[LAGMember]) # type: ignore[valid-type] + existing_lag_interface: ReadOnlyField(existing_lag_ae_members, default_type=LAGMemberList[LAGMember]) # type: ignore[valid-type] new_lag_member_interfaces: ae_members - @field_validator("new_lag_interface") - def lag_interface_proper_name(cls, new_lag_interface: str) -> str: - if get_router_vendor(new_router) == Vendor.JUNIPER: - juniper_lag_re = re.compile("^ae\\d{1,2}$") - if not juniper_lag_re.match(new_lag_interface): - msg = "Invalid LAG name, please try again." - raise ValueError(msg) - return new_lag_interface - - @field_validator("new_lag_member_interfaces") - def is_interface_names_valid_juniper(cls, new_lag_member_interfaces: list[LAGMember]) -> list[LAGMember]: - vendor = get_router_vendor(new_router) - return validate_interface_name_list(new_lag_member_interfaces, vendor) - new_side_input = yield NewSideIPTrunkForm return ( migrate_form_input.model_dump() diff --git a/gso/workflows/iptrunk/modify_isis_metric.py b/gso/workflows/iptrunk/modify_isis_metric.py index 285907b45508249794bb8c5fd486b62ed0b4dac6..493f0eb98c5db0b5edc1d3b20269ef50019d4432 100644 --- a/gso/workflows/iptrunk/modify_isis_metric.py +++ b/gso/workflows/iptrunk/modify_isis_metric.py @@ -12,7 +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 +from gso.types.tt_number import TTNumber def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: diff --git a/gso/workflows/iptrunk/modify_trunk_interface.py b/gso/workflows/iptrunk/modify_trunk_interface.py index 394e369a88d1f64481750080410ffcb8f4335064..6ec7d59a83ef272dc63837d8d01b59c81fac07b2 100644 --- a/gso/workflows/iptrunk/modify_trunk_interface.py +++ b/gso/workflows/iptrunk/modify_trunk_interface.py @@ -1,7 +1,7 @@ """A modification workflow that updates the :term:`LAG` interfaces that are part of an existing IP trunk.""" import json -from typing import Annotated, TypeVar +from typing import Annotated from uuid import UUID, uuid4 from annotated_types import Len @@ -12,38 +12,33 @@ from orchestrator.utils.json import json_dumps from orchestrator.workflow import StepList, begin, conditional, 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 AfterValidator, ConfigDict, field_validator -from pydantic_forms.validators import Label, ReadOnlyField, validate_unique_list +from pydantic import ConfigDict +from pydantic_forms.validators import Label, ReadOnlyField from gso.products.product_blocks.iptrunk import ( IptrunkInterfaceBlock, IptrunkSideBlock, IptrunkType, - PhysicalPortCapacity, ) from gso.products.product_types.iptrunk import Iptrunk from gso.services.lso_client import execute_playbook, lso_interaction from gso.services.netbox_client import NetboxClient +from gso.types.interfaces import JuniperLAGMember, LAGMember, LAGMemberList, PhysicalPortCapacity +from gso.types.tt_number import TTNumber from gso.utils.helpers import ( - LAGMember, available_interfaces_choices, available_interfaces_choices_including_current_members, calculate_recommended_minimum_links, get_router_vendor, - validate_interface_name_list, - validate_iptrunk_unique_interface, ) 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 -T = TypeVar("T", bound=LAGMember) - def initialize_ae_members( subscription: Iptrunk, initial_user_input: dict, side_index: int -) -> Annotated[list[LAGMember], ""]: +) -> type[LAGMemberList[LAGMember]]: """Initialize the list of AE members.""" router = subscription.iptrunk.iptrunk_sides[side_index].iptrunk_side_node router_vendor = get_router_vendor(router.owner_subscription_id) @@ -67,17 +62,15 @@ def initialize_ae_members( ) ) - return Annotated[ - list[NokiaLAGMember], - AfterValidator(validate_unique_list), + return Annotated[ # type: ignore[return-value] + LAGMemberList[NokiaLAGMember], Len(min_length=iptrunk_number_of_members, max_length=iptrunk_number_of_members), - ] # type: ignore[return-value] + ] - return Annotated[ - list[LAGMember], - AfterValidator(validate_unique_list), + return Annotated[ # type: ignore[return-value] + LAGMemberList[JuniperLAGMember], Len(min_length=iptrunk_number_of_members, max_length=iptrunk_number_of_members), - ] # type: ignore[return-value] + ] def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: @@ -132,15 +125,6 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: else [] ) - @field_validator("side_a_ae_members") - def validate_iptrunk_unique_interface_side_a(cls, side_a_ae_members: list[LAGMember]) -> list[LAGMember]: - return validate_iptrunk_unique_interface(side_a_ae_members) - - @field_validator("side_a_ae_members") - def validate_interface_name_members(cls, side_a_ae_members: list[LAGMember]) -> list[LAGMember]: - vendor = subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.vendor - return validate_interface_name_list(side_a_ae_members, vendor) - user_input_side_a = yield ModifyIptrunkSideAForm ae_members_side_b = initialize_ae_members(subscription, initial_user_input.model_dump(), 1) @@ -158,15 +142,6 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: else [] ) - @field_validator("side_b_ae_members") - def validate_iptrunk_unique_interface_side_b(cls, side_b_ae_members: list[LAGMember]) -> list[LAGMember]: - return validate_iptrunk_unique_interface(side_b_ae_members) - - @field_validator("side_b_ae_members") - def validate_interface_name_members(cls, side_b_ae_members: list[LAGMember]) -> list[LAGMember]: - vendor = subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.vendor - return validate_interface_name_list(side_b_ae_members, vendor) - user_input_side_b = yield ModifyIptrunkSideBForm return ( diff --git a/gso/workflows/iptrunk/terminate_iptrunk.py b/gso/workflows/iptrunk/terminate_iptrunk.py index bb1a6fd90b3d9a5e3b9aa9bb633db58cc2eb1cd4..fec1c48bb59142d51b6a65f5a1bcfada4d69ee28 100644 --- a/gso/workflows/iptrunk/terminate_iptrunk.py +++ b/gso/workflows/iptrunk/terminate_iptrunk.py @@ -22,9 +22,9 @@ 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.types.tt_number import TTNumber 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 diff --git a/gso/workflows/router/create_router.py b/gso/workflows/router/create_router.py index 59dd6210578d9556fdf79ecacf7df0b5518fdbd5..68c55cf0909e9781864b14348cc5e47ccd6a3bf0 100644 --- a/gso/workflows/router/create_router.py +++ b/gso/workflows/router/create_router.py @@ -23,9 +23,9 @@ from gso.services.netbox_client import NetboxClient from gso.services.partners import get_partner_by_name from gso.services.sharepoint import SharePointClient from gso.settings import load_oss_params +from gso.types.tt_number import TTNumber 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, diff --git a/gso/workflows/router/promote_p_to_pe.py b/gso/workflows/router/promote_p_to_pe.py index 29a110c63589701f239715a030f87567fde6d17f..f294bd99468942094016687e93937c093788b074 100644 --- a/gso/workflows/router/promote_p_to_pe.py +++ b/gso/workflows/router/promote_p_to_pe.py @@ -21,9 +21,9 @@ from gso.services import lso_client from gso.services.kentik_client import KentikClient, NewKentikDevice from gso.services.lso_client import lso_interaction from gso.services.subscriptions import get_all_active_sites +from gso.types.tt_number import TTNumber 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: diff --git a/gso/workflows/router/redeploy_base_config.py b/gso/workflows/router/redeploy_base_config.py index b30d02f16b6bb197a20257f9016eabb6dad0d8fa..eef8b6e74fe3db1c5d5d292fc498a1872955c622 100644 --- a/gso/workflows/router/redeploy_base_config.py +++ b/gso/workflows/router/redeploy_base_config.py @@ -10,7 +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.types.tt_number import TTNumber from gso.utils.workflow_steps import deploy_base_config_dry, deploy_base_config_real diff --git a/gso/workflows/router/terminate_router.py b/gso/workflows/router/terminate_router.py index 5b0c6ef546c9cbb08d93c5cd277f8e31e71f57c1..60895ab33d7fc0c40a5d8df58db7d6a543c3361d 100644 --- a/gso/workflows/router/terminate_router.py +++ b/gso/workflows/router/terminate_router.py @@ -28,9 +28,9 @@ from gso.services.librenms_client import LibreNMSClient from gso.services.lso_client import execute_playbook, lso_interaction from gso.services.netbox_client import NetboxClient from gso.settings import load_oss_params +from gso.types.tt_number import TTNumber 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__) diff --git a/gso/workflows/router/update_ibgp_mesh.py b/gso/workflows/router/update_ibgp_mesh.py index 8fbb2813c94cff8443647a033c146e93f34b7ca4..f8f17a4829060c6654782a342e0380415e01336c 100644 --- a/gso/workflows/router/update_ibgp_mesh.py +++ b/gso/workflows/router/update_ibgp_mesh.py @@ -17,8 +17,9 @@ from gso.products.product_types.router import Router 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 +from gso.types.snmp import SNMPVersion +from gso.types.tt_number import TTNumber +from gso.utils.helpers import generate_inventory_for_active_routers def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: diff --git a/gso/workflows/site/create_imported_site.py b/gso/workflows/site/create_imported_site.py index 1fd9d59d7aeddbb04faac6131a0cc68533a7b1d4..1e149ab89733ad093f894cb76556949c677cc10d 100644 --- a/gso/workflows/site/create_imported_site.py +++ b/gso/workflows/site/create_imported_site.py @@ -10,11 +10,12 @@ from orchestrator.workflows.steps import resync, set_status, store_process_subsc from pydantic import ConfigDict from gso.products import ProductName -from gso.products.product_blocks.site import LatitudeCoordinate, LongitudeCoordinate, SiteTier +from gso.products.product_blocks.site import SiteTier from gso.products.product_types.site import ImportedSiteInactive from gso.services import subscriptions from gso.services.partners import get_partner_by_name -from gso.utils.helpers import BaseSiteValidatorModel +from gso.types.base_site import BaseSiteValidatorModel +from gso.types.coordinates import LatitudeCoordinate, LongitudeCoordinate @step("Create subscription") diff --git a/gso/workflows/site/create_site.py b/gso/workflows/site/create_site.py index d2e99d510e678a7f7e654de4cbee1a162868ed2f..0858b372843828aa0944e712673b2cf11cbe3a10 100644 --- a/gso/workflows/site/create_site.py +++ b/gso/workflows/site/create_site.py @@ -10,10 +10,10 @@ from pydantic import ConfigDict from pydantic_forms.validators import ReadOnlyField from gso.products.product_blocks import site as site_pb -from gso.products.product_blocks.site import LatitudeCoordinate, LongitudeCoordinate from gso.products.product_types import site from gso.services.partners import get_partner_by_name -from gso.utils.helpers import BaseSiteValidatorModel +from gso.types.base_site import BaseSiteValidatorModel +from gso.types.coordinates import LatitudeCoordinate, LongitudeCoordinate def initial_input_form_generator(product_name: str) -> FormGenerator: diff --git a/gso/workflows/site/modify_site.py b/gso/workflows/site/modify_site.py index 0fb2b50d3b54432138b2c15addfffddc39de4e66..aac144356899f123c387db2db0694831ca9dea63 100644 --- a/gso/workflows/site/modify_site.py +++ b/gso/workflows/site/modify_site.py @@ -15,9 +15,11 @@ from pydantic import ConfigDict, field_validator from pydantic_core.core_schema import ValidationInfo from pydantic_forms.validators import ReadOnlyField -from gso.products.product_blocks.site import LatitudeCoordinate, LongitudeCoordinate, SiteTier +from gso.products.product_blocks.site import SiteTier from gso.products.product_types.site import Site -from gso.utils.helpers import validate_ipv4_or_ipv6, validate_site_fields_is_unique +from gso.types.coordinates import LatitudeCoordinate, LongitudeCoordinate +from gso.types.ip_address import validate_ipv4_or_ipv6 +from gso.types.unique_field import validate_field_is_unique def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: @@ -47,7 +49,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: if value and value == getattr(subscription.site, info.field_name): return value - validate_site_fields_is_unique(info.field_name, value) + validate_field_is_unique(info.field_name, value) return value diff --git a/test/cli/test_imports.py b/test/cli/test_imports.py index 8933627b82b06a35ca17e5a2a4417dc9cd9859c6..89933fc8de3f9b4860f60d387867dedc2ed99371 100644 --- a/test/cli/test_imports.py +++ b/test/cli/test_imports.py @@ -13,9 +13,10 @@ from gso.cli.imports import ( import_super_pop_switches, ) from gso.products import Router, Site -from gso.products.product_blocks.iptrunk import IptrunkType, PhysicalPortCapacity +from gso.products.product_blocks.iptrunk import IptrunkType from gso.products.product_blocks.router import RouterRole from gso.products.product_blocks.site import SiteTier +from gso.types.interfaces import PhysicalPortCapacity from gso.utils.helpers import iso_from_ipv4 from gso.utils.shared_enums import Vendor @@ -327,11 +328,11 @@ def test_import_iptrunk_non_unique_members_side_a_and_b(mock_start_process, mock assert "Validation error: 2 validation errors for IptrunkImportModel" in captured_output assert ( """side_a_ae_members - Value error, Items must be unique [type=value_error, input_value=[{'interface_name':""" + List must be unique [type=unique_list, input_value=[{'interface_name':""" ) in captured_output assert ( """side_b_ae_members - Value error, Items must be unique [type=value_error, input_value=[{'interface_name':""" + List must be unique [type=unique_list, input_value=[{'interface_name':""" ) in captured_output assert mock_start_process.call_count == 0 diff --git a/test/conftest.py b/test/conftest.py index 0c4e36e4f52d60d8270493b9dd234ffb080ba507..8cfea9d28eacfd24c6397c514245024cb15b5f6d 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -34,7 +34,7 @@ from urllib3_mock import Responses from gso.main import init_gso_app from gso.services.partners import PartnerSchema, create_partner -from gso.utils.helpers import LAGMember +from gso.types.interfaces import LAGMember, LAGMemberList from test.fixtures import ( # noqa: F401 iptrunk_side_subscription_factory, iptrunk_subscription_factory, @@ -107,7 +107,7 @@ class FakerProvider(BaseProvider): def network_interface(self) -> str: return self.generator.numerify("ge-@#/@#/@#") - def link_members_juniper(self) -> list[LAGMember]: + def link_members_juniper(self) -> LAGMemberList[LAGMember]: iface_amount = self.generator.random_int(min=2, max=5) interface_names = [f"{prefix}{i}" for prefix in ["xe-1/0/", "ge-3/0/", "xe-2/1/"] for i in range(iface_amount)] return [ @@ -115,7 +115,7 @@ class FakerProvider(BaseProvider): for interface_name in interface_names ] - def link_members_nokia(self) -> list[LAGMember]: + def link_members_nokia(self) -> LAGMemberList[LAGMember]: iface_amount = self.generator.random_int(min=2, max=5) return [ LAGMember(interface_name=f"Interface{i}", interface_description=self.generator.sentence()) diff --git a/test/fixtures.py b/test/fixtures.py index 84481eda9c9d816e438414636cd335ecb2ef1b63..e642c4128113669cad29693e61c7506d3eafc8fc 100644 --- a/test/fixtures.py +++ b/test/fixtures.py @@ -26,7 +26,6 @@ from gso.products.product_blocks.iptrunk import ( IptrunkInterfaceBlock, IptrunkSideBlock, IptrunkType, - PhysicalPortCapacity, ) from gso.products.product_blocks.router import RouterRole from gso.products.product_blocks.site import SiteTier @@ -37,6 +36,7 @@ from gso.products.product_types.router import ImportedRouterInactive, Router, Ro from gso.products.product_types.site import ImportedSiteInactive, Site, SiteInactive from gso.products.product_types.super_pop_switch import ImportedSuperPopSwitchInactive, SuperPopSwitchInactive from gso.services import subscriptions +from gso.types.interfaces import PhysicalPortCapacity from gso.utils.shared_enums import Vendor from test.workflows import WorkflowInstanceForTests diff --git a/test/schemas/test_types.py b/test/schemas/test_types.py index a968084f06e2674828b6a59df58484e1fd965851..33c3f20937232b75b92c9db5813462b5f0a5c57c 100644 --- a/test/schemas/test_types.py +++ b/test/schemas/test_types.py @@ -1,7 +1,7 @@ import pytest from pydantic import BaseModel, ValidationError -from gso.products.product_blocks.site import LatitudeCoordinate, LongitudeCoordinate +from gso.types.coordinates import LatitudeCoordinate, LongitudeCoordinate class LatitudeModel(BaseModel): diff --git a/test/services/test_librenms_client.py b/test/services/test_librenms_client.py index e28eaef74742d59d8b443ba0fad1d323f5b26254..1bd14b9b65c06dcc8d1ad3785b575b11de3bfc54 100644 --- a/test/services/test_librenms_client.py +++ b/test/services/test_librenms_client.py @@ -5,7 +5,7 @@ import pytest from requests import HTTPError from gso.services.librenms_client import LibreNMSClient -from gso.utils.helpers import SNMPVersion +from gso.types.snmp import SNMPVersion @pytest.fixture() diff --git a/test/utils/test_helpers.py b/test/utils/test_helpers.py index dc7854eaa3287b4b98132a8243bf2068636a684a..5f9476096ea070d3c8856e623e3dc1301f001941 100644 --- a/test/utils/test_helpers.py +++ b/test/utils/test_helpers.py @@ -6,10 +6,10 @@ from orchestrator.types import SubscriptionLifecycle from gso.products import Router from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock from gso.products.product_blocks.router import RouterRole +from gso.types.tt_number import validate_tt_number from gso.utils.helpers import ( available_interfaces_choices_including_current_members, generate_inventory_for_active_routers, - validate_tt_number, ) from gso.utils.shared_enums import Vendor diff --git a/test/workflows/iptrunk/test_create_imported_iptrunk.py b/test/workflows/iptrunk/test_create_imported_iptrunk.py index c08ddbe643a122e0ed710cadffdddea70d78add3..263c4f161d99985b48da258d1660925e58c24157 100644 --- a/test/workflows/iptrunk/test_create_imported_iptrunk.py +++ b/test/workflows/iptrunk/test_create_imported_iptrunk.py @@ -2,8 +2,9 @@ import pytest from orchestrator.types import SubscriptionLifecycle from gso.products import ProductName -from gso.products.product_blocks.iptrunk import IptrunkType, PhysicalPortCapacity +from gso.products.product_blocks.iptrunk import IptrunkType from gso.products.product_types.iptrunk import ImportedIptrunk +from gso.types.interfaces import PhysicalPortCapacity from test.workflows import ( assert_complete, extract_state, diff --git a/test/workflows/iptrunk/test_create_iptrunk.py b/test/workflows/iptrunk/test_create_iptrunk.py index 117444b7a33b5725df7558c2a3f7cd67b1cd00f1..cd33943ce75887824d73ac2a810cef2a75edfa04 100644 --- a/test/workflows/iptrunk/test_create_iptrunk.py +++ b/test/workflows/iptrunk/test_create_iptrunk.py @@ -5,8 +5,9 @@ import pytest from infoblox_client.objects import HostRecord from gso.products import Iptrunk, ProductName -from gso.products.product_blocks.iptrunk import IptrunkType, PhysicalPortCapacity +from gso.products.product_blocks.iptrunk import IptrunkType from gso.services.subscriptions import get_product_id_by_name +from gso.types.interfaces import PhysicalPortCapacity from gso.utils.shared_enums import Vendor from test import USER_CONFIRM_EMPTY_FORM from test.services.conftest import MockedNetboxClient, MockedSharePointClient diff --git a/test/workflows/iptrunk/test_modify_trunk_interface.py b/test/workflows/iptrunk/test_modify_trunk_interface.py index 1383ea2edc1380613900ae62b0024b4622ac6d57..b784d262a75533e5e46945763c76c06e391cd269 100644 --- a/test/workflows/iptrunk/test_modify_trunk_interface.py +++ b/test/workflows/iptrunk/test_modify_trunk_interface.py @@ -3,7 +3,8 @@ from unittest.mock import patch import pytest from gso.products import Iptrunk -from gso.products.product_blocks.iptrunk import IptrunkType, PhysicalPortCapacity +from gso.products.product_blocks.iptrunk import IptrunkType +from gso.types.interfaces import LAGMemberList, PhysicalPortCapacity from gso.utils.shared_enums import Vendor from test.conftest import UseJuniperSide from test.workflows import ( @@ -172,7 +173,7 @@ def test_iptrunk_modify_trunk_interface_success( assert subscription.iptrunk.iptrunk_minimum_links == input_form_iptrunk_data[1]["iptrunk_number_of_members"] - 1 assert subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_geant_a_sid == new_side_a_sid - def _find_interface_by_name(interfaces: list[dict[str, str]], name: str): + def _find_interface_by_name(interfaces: LAGMemberList, name: str): for interface in interfaces: if interface.interface_name == name: return interface