diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index bb7a2a69ca2c76900be5d138f6f3db610528538b..b98edde4222ab7f001fba8b36dfcc750d1821a8e 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -3,6 +3,9 @@ Glossary of terms .. glossary:: + API + Application Programming Interface + BGP Border Gateway Protocol: a path vector routing protocol described in `RFC 4271 <https://datatracker.ietf.org/doc/html/rfc4271>`_. diff --git a/docs/source/module/products/index.rst b/docs/source/module/products/index.rst index c68072cc071e570f268408b67e656b63a1ed4bf6..415b1c48357cd59d9a59fbd372be22cf11ac7b2c 100644 --- a/docs/source/module/products/index.rst +++ b/docs/source/module/products/index.rst @@ -1,6 +1,10 @@ ``gso.products`` ================ +.. automodule:: gso.products + :members: + :show-inheritance: + Subpackages ----------- diff --git a/gso/api/v1/imports.py b/gso/api/v1/imports.py index 242deb5ed47df089ebcf1c41b21a70f56dc4c0af..6f1cf4964c5f7d79fb31eb3d1170dd98d04e5a4e 100644 --- a/gso/api/v1/imports.py +++ b/gso/api/v1/imports.py @@ -1,17 +1,126 @@ +import ipaddress from typing import Any from uuid import UUID from fastapi import Depends, HTTPException, status from fastapi.routing import APIRouter from orchestrator.security import opa_security_default -from orchestrator.services import processes, subscriptions +from orchestrator.services import processes +from orchestrator.services import subscriptions as wfo_subscriptions +from pydantic import BaseModel, root_validator, validator from sqlalchemy.exc import MultipleResultsFound -from gso.utils.types.imports import ImportResponseModel, IptrunkImportModel, RouterImportModel, SiteImportModel +from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity +from gso.products.product_blocks.router import RouterRole, RouterVendor +from gso.products.product_blocks.site import SiteTier +from gso.services import subscriptions +from gso.services.crm import CustomerNotFoundError, get_customer_by_name +from gso.workflows.iptrunk.utils import LAGMember router = APIRouter(prefix="/imports", tags=["Imports"], dependencies=[Depends(opa_security_default)]) +class ImportResponseModel(BaseModel): + pid: UUID + detail: str + + +class SiteImportModel(BaseModel): + site_name: str + site_city: str + site_country: str + site_country_code: str + site_latitude: float + site_longitude: float + site_bgp_community_id: int + site_internal_id: int + site_tier: SiteTier + site_ts_address: str + customer: str + + +class RouterImportModel(BaseModel): + customer: str + router_site: str + hostname: str + ts_port: int + router_vendor: RouterVendor + router_role: RouterRole + is_ias_connected: bool + router_lo_ipv4_address: ipaddress.IPv4Address + router_lo_ipv6_address: ipaddress.IPv6Address + router_lo_iso_address: str + router_si_ipv4_network: ipaddress.IPv4Network | None = None + router_ias_lt_ipv4_network: ipaddress.IPv4Network | None = None + router_ias_lt_ipv6_network: ipaddress.IPv6Network | None = None + + +class IptrunkImportModel(BaseModel): + customer: str + geant_s_sid: str + iptrunk_type: IptrunkType + iptrunk_description: str + iptrunk_speed: PhyPortCapacity + iptrunk_minimum_links: int + side_a_node_id: str + side_a_ae_iface: str + side_a_ae_geant_a_sid: str + side_a_ae_members: list[LAGMember] + side_b_node_id: str + side_b_ae_iface: str + side_b_ae_geant_a_sid: str + side_b_ae_members: list[LAGMember] + + iptrunk_ipv4_network: ipaddress.IPv4Network + iptrunk_ipv6_network: ipaddress.IPv6Network + + @classmethod + def _get_active_routers(cls) -> set[str]: + return { + str(router_id) for router_id in subscriptions.get_active_router_subscriptions(fields=["subscription_id"]) + } + + @validator("customer") + def check_if_customer_exists(cls, value: str) -> str: + try: + get_customer_by_name(value) + except CustomerNotFoundError: + raise ValueError(f"Customer {value} not found") + + return value + + @validator("side_a_node_id", "side_b_node_id") + def check_if_router_side_is_available(cls, value: str) -> str: + if value not in cls._get_active_routers(): + raise ValueError(f"Router {value} not found") + + return value + + @validator("side_a_ae_members", "side_b_ae_members") + def check_side_uniqueness(cls, value: list[str]) -> list[str]: + if len(value) != len(set(value)): + raise ValueError("Items must be unique") + + return value + + @root_validator + def check_members(cls, values: dict[str, Any]) -> dict[str, Any]: + min_links = values["iptrunk_minimum_links"] + side_a_members = values.get("side_a_ae_members", []) + side_b_members = values.get("side_b_ae_members", []) + + len_a = len(side_a_members) + len_b = len(side_b_members) + + if len_a < min_links: + raise ValueError(f"Side A members should be at least {min_links} (iptrunk_minimum_links)") + + if len_a != len_b: + raise ValueError("Mismatch between Side A and B members") + + return values + + def _start_process(process_name: str, data: dict) -> UUID: """Start a process and handle common exceptions.""" @@ -42,7 +151,7 @@ def import_site(site: SiteImportModel) -> dict[str, Any]: :raises HTTPException: If the site already exists or if there's an error in the process. """ try: - subscription = subscriptions.retrieve_subscription_by_subscription_instance_value( + subscription = wfo_subscriptions.retrieve_subscription_by_subscription_instance_value( resource_type="site_name", value=site.site_name, sub_status=("provisioning", "active") ) if subscription: diff --git a/gso/products/__init__.py b/gso/products/__init__.py index c48474d7e46da845761cd3b940f499f3d57164f8..e6a8c06f8850748b36233be63006a8fa9709d946 100644 --- a/gso/products/__init__.py +++ b/gso/products/__init__.py @@ -1,4 +1,9 @@ -"""Module that updates the domain model of :term:`GSO`. Should contain all types of subscriptions.""" +"""Module that updates the domain model of :term:`GSO`. Should contain all types of subscriptions. + +.. warning:: + Whenever a new product type is added, this should be reflected in the :py:class:`gso.products.ProductType` + enumerator. +""" from orchestrator.domain import SUBSCRIPTION_MODEL_REGISTRY from pydantic_forms.types import strEnum @@ -15,8 +20,8 @@ class ProductType(strEnum): SUBSCRIPTION_MODEL_REGISTRY.update( { - ProductType.SITE.value: Site, - ProductType.ROUTER.value: Router, - ProductType.IP_TRUNK.value: Iptrunk, + "Site": Site, + "Router": Router, + "IP trunk": Iptrunk, } ) diff --git a/gso/products/product_blocks/iptrunk.py b/gso/products/product_blocks/iptrunk.py index 4f0a4ed5cb60cf78edcdaddacb40ae7e9db5e0e7..7fab56c0d20be1d1adb9e9b65ffebd8049caa1ff 100644 --- a/gso/products/product_blocks/iptrunk.py +++ b/gso/products/product_blocks/iptrunk.py @@ -8,7 +8,18 @@ from orchestrator.forms.validators import UniqueConstrainedList from orchestrator.types import SubscriptionLifecycle, strEnum from gso.products.product_blocks.router import RouterBlock, RouterBlockInactive, RouterBlockProvisioning -from gso.utils.types.phy_port import PhyPortCapacity + + +class PhyPortCapacity(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" class IptrunkType(strEnum): diff --git a/gso/products/product_blocks/router.py b/gso/products/product_blocks/router.py index e5bdc7d8eb6559dd00edd4ddbf04d617143a7333..a8a820448a1b7388b903b0be69b7da9d4c17d660 100644 --- a/gso/products/product_blocks/router.py +++ b/gso/products/product_blocks/router.py @@ -3,9 +3,9 @@ import ipaddress from orchestrator.domain.base import ProductBlockModel from orchestrator.types import SubscriptionLifecycle, strEnum +from pydantic import ConstrainedInt from gso.products.product_blocks.site import SiteBlock, SiteBlockInactive, SiteBlockProvisioning -from gso.utils.types.ip_port import PortNumber class RouterVendor(strEnum): @@ -23,6 +23,16 @@ class RouterRole(strEnum): AMT = "amt" +class PortNumber(ConstrainedInt): + """Constrained integer for valid port numbers. + + The range from 49152 to 65535 is marked as ephemeral, and can therefore not be selected for permanent allocation. + """ + + gt = 0 + le = 49151 + + class RouterBlockInactive( ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="RouterBlock" ): diff --git a/gso/products/product_blocks/site.py b/gso/products/product_blocks/site.py index 683636e15d7f89a38a7343185c61be6ca9fdc541..739e1a0a91629b6dc2e427bfbd6ccd93daa8bb63 100644 --- a/gso/products/product_blocks/site.py +++ b/gso/products/product_blocks/site.py @@ -1,8 +1,10 @@ """The product block that describes a site subscription.""" +import re +from typing import Union + from orchestrator.domain.base import ProductBlockModel from orchestrator.types import SubscriptionLifecycle, strEnum - -from gso.utils.types.snmp import LatitudeCoordinate, LongitudeCoordinate +from pydantic import ConstrainedStr class SiteTier(strEnum): @@ -18,6 +20,42 @@ class SiteTier(strEnum): TIER4 = 4 +class LatitudeCoordinate(ConstrainedStr): + """A latitude coordinate, modeled as a constrained 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 + """ + + regex = re.compile(r"^-?([1-8]?\d(\.\d+)?|90(\.0+)?)$") + + @classmethod + def validate(cls, value: Union[str]) -> Union[str]: + if not cls.regex.match(value): + raise ValueError("Invalid latitude coordinate. Valid examples: '40.7128', '-74.0060', '90', '-90', '0'.") + + return value + + +class LongitudeCoordinate(ConstrainedStr): + """A longitude coordinate, modeled as a constrained 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 + """ + + regex = re.compile(r"^-?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$") + + @classmethod + def validate(cls, value: Union[str]) -> Union[str]: + if not cls.regex.match(value): + raise ValueError("Invalid longitude coordinate. Valid examples: '40.7128', '-74.0060', '180', '-180'") + + return value + + class SiteBlockInactive(ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="SiteBlock"): """A site that's currently inactive, see :class:`SiteBlock`.""" diff --git a/gso/utils/functions.py b/gso/utils/helpers.py similarity index 100% rename from gso/utils/functions.py rename to gso/utils/helpers.py diff --git a/gso/utils/types/__init__.py b/gso/utils/types/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/gso/utils/types/imports.py b/gso/utils/types/imports.py deleted file mode 100644 index 7c13292cd67732cab563571635873156ba8aec08..0000000000000000000000000000000000000000 --- a/gso/utils/types/imports.py +++ /dev/null @@ -1,114 +0,0 @@ -import ipaddress -from typing import Any -from uuid import UUID - -from pydantic import BaseModel, root_validator, validator - -from gso.products.product_blocks.iptrunk import IptrunkType -from gso.products.product_blocks.router import RouterRole, RouterVendor -from gso.products.product_blocks.site import SiteTier -from gso.services import subscriptions -from gso.services.crm import CustomerNotFoundError, get_customer_by_name -from gso.utils.types.phy_port import PhyPortCapacity -from gso.workflows.iptrunk.utils import LAGMember - - -class ImportResponseModel(BaseModel): - pid: UUID - detail: str - - -class SiteImportModel(BaseModel): - site_name: str - site_city: str - site_country: str - site_country_code: str - site_latitude: float - site_longitude: float - site_bgp_community_id: int - site_internal_id: int - site_tier: SiteTier - site_ts_address: str - customer: str - - -class RouterImportModel(BaseModel): - customer: str - router_site: str - hostname: str - ts_port: int - router_vendor: RouterVendor - router_role: RouterRole - is_ias_connected: bool - router_lo_ipv4_address: ipaddress.IPv4Address - router_lo_ipv6_address: ipaddress.IPv6Address - router_lo_iso_address: str - router_si_ipv4_network: ipaddress.IPv4Network | None = None - router_ias_lt_ipv4_network: ipaddress.IPv4Network | None = None - router_ias_lt_ipv6_network: ipaddress.IPv6Network | None = None - - -class IptrunkImportModel(BaseModel): - customer: str - geant_s_sid: str - iptrunk_type: IptrunkType - iptrunk_description: str - iptrunk_speed: PhyPortCapacity - iptrunk_minimum_links: int - side_a_node_id: str - side_a_ae_iface: str - side_a_ae_geant_a_sid: str - side_a_ae_members: list[LAGMember] - side_b_node_id: str - side_b_ae_iface: str - side_b_ae_geant_a_sid: str - side_b_ae_members: list[LAGMember] - - iptrunk_ipv4_network: ipaddress.IPv4Network - iptrunk_ipv6_network: ipaddress.IPv6Network - - @classmethod - def _get_active_routers(cls) -> set[str]: - return { - str(router_id) for router_id in subscriptions.get_active_router_subscriptions(fields=["subscription_id"]) - } - - @validator("customer") - def check_if_customer_exists(cls, value: str) -> str: - try: - get_customer_by_name(value) - except CustomerNotFoundError: - raise ValueError(f"Customer {value} not found") - - return value - - @validator("side_a_node_id", "side_b_node_id") - def check_if_router_side_is_available(cls, value: str) -> str: - if value not in cls._get_active_routers(): - raise ValueError("Router not found") - - return value - - @validator("side_a_ae_members", "side_b_ae_members") - def check_side_uniqueness(cls, value: list[str]) -> list[str]: - if len(value) != len(set(value)): - raise ValueError("Items must be unique") - - return value - - @root_validator - def check_members(cls, values: dict[str, Any]) -> dict[str, Any]: - min_links = values["iptrunk_minimum_links"] - side_a_members = values.get("side_a_ae_members", {}) - side_b_members = values.get("side_b_ae_members", {}) - - len_a = len(side_a_members) - len_b = len(side_b_members) - - if len_a < min_links: - raise ValueError(f"Side A members should be at least {min_links} (iptrunk_minimum_links)") - - if len_a != len_b: - raise ValueError("Mismatch between Side A and B members") - - return values diff --git a/gso/utils/types/ip_port.py b/gso/utils/types/ip_port.py deleted file mode 100644 index 81e080618557b4431b1c58df1c9f9c172d79078f..0000000000000000000000000000000000000000 --- a/gso/utils/types/ip_port.py +++ /dev/null @@ -1,11 +0,0 @@ -from pydantic import ConstrainedInt - - -class PortNumber(ConstrainedInt): - """Constrained integer for valid port numbers. - - The range from 49152 to 65535 is marked as ephemeral, and can therefore not be selected for permanent allocation. - """ - - gt = 0 - le = 49151 diff --git a/gso/utils/types/phy_port.py b/gso/utils/types/phy_port.py deleted file mode 100644 index 0d55eecec1116ec25e7a5c5ef842ed7a0f62c286..0000000000000000000000000000000000000000 --- a/gso/utils/types/phy_port.py +++ /dev/null @@ -1,13 +0,0 @@ -from orchestrator.types import strEnum - - -class PhyPortCapacity(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/utils/types/snmp.py b/gso/utils/types/snmp.py deleted file mode 100644 index 114e573611b457a6adf3360517c3bd599021e08b..0000000000000000000000000000000000000000 --- a/gso/utils/types/snmp.py +++ /dev/null @@ -1,40 +0,0 @@ -import re -from typing import Union - -from pydantic import ConstrainedStr - - -class LatitudeCoordinate(ConstrainedStr): - """A latitude coordinate, modeled as a constrained 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 - """ - - regex = re.compile(r"^-?([1-8]?\d(\.\d+)?|90(\.0+)?)$") - - @classmethod - def validate(cls, value: Union[str]) -> Union[str]: - if not cls.regex.match(value): - raise ValueError("Invalid latitude coordinate. Valid examples: '40.7128', '-74.0060', '90', '-90', '0'.") - - return value - - -class LongitudeCoordinate(ConstrainedStr): - """A longitude coordinate, modeled as a constrained 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 - """ - - regex = re.compile(r"^-?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$") - - @classmethod - def validate(cls, value: Union[str]) -> Union[str]: - if not cls.regex.match(value): - raise ValueError("Invalid longitude coordinate. Valid examples: '40.7128', '-74.0060', '180', '-180'") - - return value diff --git a/gso/workflows/iptrunk/create_iptrunk.py b/gso/workflows/iptrunk/create_iptrunk.py index 8501fc8b325bc89bf10a70170cb2dda1a0e9a3a0..f1ee2cce08adec12a375b0c0ca39233d26dfd5ce 100644 --- a/gso/workflows/iptrunk/create_iptrunk.py +++ b/gso/workflows/iptrunk/create_iptrunk.py @@ -10,7 +10,7 @@ from orchestrator.workflows.utils import wrap_create_initial_input_form from pydantic import validator from pynetbox.models.dcim import Interfaces -from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlockInactive, IptrunkType +from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlockInactive, IptrunkType, PhyPortCapacity from gso.products.product_blocks.router import RouterVendor from gso.products.product_types.iptrunk import IptrunkInactive, IptrunkProvisioning from gso.products.product_types.router import Router diff --git a/gso/workflows/iptrunk/modify_trunk_interface.py b/gso/workflows/iptrunk/modify_trunk_interface.py index 0b2b37f4d061e9b41cd8ec7b3c77d91f315c135c..9e402bbe01270563847124c949b06386a5602ea6 100644 --- a/gso/workflows/iptrunk/modify_trunk_interface.py +++ b/gso/workflows/iptrunk/modify_trunk_interface.py @@ -9,11 +9,10 @@ from orchestrator.workflow import StepList, done, init, step, workflow from orchestrator.workflows.steps import resync, store_process_subscription, unsync from orchestrator.workflows.utils import wrap_modify_initial_input_form -from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock, IptrunkType +from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock, IptrunkType, PhyPortCapacity from gso.products.product_types.iptrunk import Iptrunk from gso.services import provisioning_proxy from gso.services.provisioning_proxy import pp_interaction -from gso.utils.types.phy_port import PhyPortCapacity from gso.workflows.iptrunk.utils import LAGMember diff --git a/gso/workflows/router/create_router.py b/gso/workflows/router/create_router.py index 48011b04df66e0f96697658310b25c305abc4adf..d559d245b0c051920d65f9415b870db6b285a1d8 100644 --- a/gso/workflows/router/create_router.py +++ b/gso/workflows/router/create_router.py @@ -11,15 +11,14 @@ from orchestrator.workflows.steps import resync, set_status, store_process_subsc from orchestrator.workflows.utils import wrap_create_initial_input_form from pydantic import validator -from gso.products.product_blocks.router import RouterRole, RouterVendor, generate_fqdn +from gso.products.product_blocks.router import PortNumber, RouterRole, RouterVendor, generate_fqdn from gso.products.product_types.router import RouterInactive, RouterProvisioning from gso.products.product_types.site import Site from gso.services import infoblox, provisioning_proxy, subscriptions from gso.services.crm import customer_selector from gso.services.netbox_client import NetboxClient from gso.services.provisioning_proxy import pp_interaction -from gso.utils.functions import iso_from_ipv4 -from gso.utils.types.ip_port import PortNumber +from gso.utils.helpers import iso_from_ipv4 def _site_selector() -> Choice: diff --git a/gso/workflows/site/create_site.py b/gso/workflows/site/create_site.py index c00856efd2c9f5fdc194e274ae7624089d22756f..429b0d7e489be69a980050c6b4698cddd1a1dd10 100644 --- a/gso/workflows/site/create_site.py +++ b/gso/workflows/site/create_site.py @@ -11,9 +11,9 @@ from orchestrator.workflows.utils import wrap_create_initial_input_form from pydantic import validator 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.crm import customer_selector -from gso.utils.types.snmp import LatitudeCoordinate, LongitudeCoordinate def initial_input_form_generator(product_name: str) -> FormGenerator: # noqa: C901 diff --git a/gso/workflows/tasks/import_iptrunk.py b/gso/workflows/tasks/import_iptrunk.py index 8203a905ad899812c5bf3ff5887281e082f27fb6..fe58ea11041b5573ae427795b18039e04b5d683c 100644 --- a/gso/workflows/tasks/import_iptrunk.py +++ b/gso/workflows/tasks/import_iptrunk.py @@ -9,11 +9,10 @@ from orchestrator.workflow import StepList, done, init, step from orchestrator.workflows.steps import resync, set_status, store_process_subscription from gso.products import ProductType -from gso.products.product_blocks.iptrunk import IptrunkType +from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity from gso.products.product_types.iptrunk import IptrunkInactive, IptrunkProvisioning from gso.services import subscriptions from gso.services.crm import get_customer_by_name -from gso.utils.types.phy_port import PhyPortCapacity from gso.workflows.iptrunk.create_iptrunk import initialize_subscription from gso.workflows.iptrunk.utils import LAGMember diff --git a/gso/workflows/tasks/import_router.py b/gso/workflows/tasks/import_router.py index 272f2750119d20a06e6d00abdf8d5fcc8d80cb98..d7984b9264af68bed7815126bee445d29062daea 100644 --- a/gso/workflows/tasks/import_router.py +++ b/gso/workflows/tasks/import_router.py @@ -10,13 +10,12 @@ from orchestrator.workflows.steps import resync, set_status, store_process_subsc from gso.products import ProductType from gso.products.product_blocks import router as router_pb -from gso.products.product_blocks.router import RouterRole, RouterVendor +from gso.products.product_blocks.router import PortNumber, RouterRole, RouterVendor from gso.products.product_types import router from gso.products.product_types.router import RouterInactive from gso.products.product_types.site import Site from gso.services import subscriptions from gso.services.crm import get_customer_by_name -from gso.utils.types.ip_port import PortNumber def _get_site_by_name(site_name: str) -> Site: diff --git a/test/fixtures.py b/test/fixtures.py index b7e95eb17be6da247939c449897581627f010d98..ec2b2bd56179f2fa6e54dfe69bfa78d21a408c08 100644 --- a/test/fixtures.py +++ b/test/fixtures.py @@ -6,14 +6,13 @@ from orchestrator.domain import SubscriptionModel from orchestrator.types import SubscriptionLifecycle, UUIDstr from gso.products import ProductType -from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock, IptrunkSideBlock, IptrunkType +from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock, IptrunkSideBlock, IptrunkType, PhyPortCapacity from gso.products.product_blocks.router import RouterRole, RouterVendor from gso.products.product_blocks.site import SiteTier from gso.products.product_types.iptrunk import IptrunkInactive from gso.products.product_types.router import Router, RouterInactive from gso.products.product_types.site import Site, SiteInactive from gso.services import subscriptions -from gso.utils.types.phy_port import PhyPortCapacity CUSTOMER_ID: UUIDstr = "2f47f65a-0911-e511-80d0-005056956c1a" diff --git a/test/imports/test_imports.py b/test/imports/test_imports.py index b2bc58b61c8c91ce0a1608cca496823c68d61ae1..3cdfa3ed1d5f87abd72521198220a58687b7960d 100644 --- a/test/imports/test_imports.py +++ b/test/imports/test_imports.py @@ -5,10 +5,10 @@ import pytest from orchestrator.db import SubscriptionTable from orchestrator.services import subscriptions -from gso.products.product_blocks.iptrunk import IptrunkType +from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity from gso.products.product_blocks.router import RouterRole, RouterVendor from gso.products.product_blocks.site import SiteTier -from gso.utils.types.phy_port import PhyPortCapacity +from gso.utils.helpers import iso_from_ipv4 SITE_IMPORT_ENDPOINT = "/api/v1/imports/sites" ROUTER_IMPORT_ENDPOINT = "/api/v1/imports/routers" @@ -21,20 +21,20 @@ def iptrunk_data(router_subscription_factory, faker): router_side_b = router_subscription_factory() return { "customer": "GÉANT", - "geant_s_sid": faker.pystr(), + "geant_s_sid": faker.geant_sid(), "iptrunk_type": IptrunkType.DARK_FIBER, "iptrunk_description": faker.sentence(), "iptrunk_speed": PhyPortCapacity.HUNDRED_GIGABIT_PER_SECOND, "iptrunk_minimum_links": 5, "side_a_node_id": router_side_a, - "side_a_ae_iface": faker.pystr(), - "side_a_ae_geant_a_sid": faker.pystr(), + "side_a_ae_iface": faker.network_interface(), + "side_a_ae_geant_a_sid": faker.geant_sid(), "side_a_ae_members": [ {"interface_name": faker.network_interface(), "interface_description": faker.sentence()} for _ in range(5) ], "side_b_node_id": router_side_b, - "side_b_ae_iface": faker.pystr(), - "side_b_ae_geant_a_sid": faker.pystr(), + "side_b_ae_iface": faker.network_interface(), + "side_b_ae_geant_a_sid": faker.geant_sid(), "side_b_ae_members": [ {"interface_name": faker.network_interface(), "interface_description": faker.sentence()} for _ in range(5) ], @@ -45,23 +45,23 @@ def iptrunk_data(router_subscription_factory, faker): @pytest.fixture def mock_routers(iptrunk_data): - first_call = [iptrunk_data["side_a_node_id"], iptrunk_data["side_b_node_id"], str(uuid4())] - side_effects = [ - first_call, - first_call, - [ - (iptrunk_data["side_a_node_id"], "side_a_node_id description"), - (iptrunk_data["side_b_node_id"], "side_b_node_id description"), - (str(uuid4()), "random description"), - ], - ] with patch("gso.services.subscriptions.get_active_router_subscriptions") as mock_get_active_router_subscriptions: - mock_get_active_router_subscriptions.side_effect = side_effects + + def _active_router_subscriptions(*args, **kwargs): + if kwargs["fields"] == ["subscription_id", "description"]: + return [ + (iptrunk_data["side_a_node_id"], "side_a_node_id description"), + (iptrunk_data["side_b_node_id"], "side_b_node_id description"), + (str(uuid4()), "random description"), + ] + return [iptrunk_data["side_a_node_id"], iptrunk_data["side_b_node_id"], str(uuid4())] + + mock_get_active_router_subscriptions.side_effect = _active_router_subscriptions yield mock_get_active_router_subscriptions @patch("gso.api.v1.imports._start_process") -def test_import_iptrunk_successful_with_mocked_process(mock_start_process, test_client, iptrunk_data, mock_routers): +def test_import_iptrunk_successful_with_mocked_process(mock_start_process, test_client, mock_routers, iptrunk_data): mock_start_process.return_value = "123e4567-e89b-12d3-a456-426655440000" response = test_client.post(IPTRUNK_IMPORT_API_URL, json=iptrunk_data) @@ -72,7 +72,7 @@ def test_import_iptrunk_successful_with_mocked_process(mock_start_process, test_ @pytest.fixture def site_data(faker): return { - "site_name": faker.name(), + "site_name": faker.domain_word(), "site_city": faker.city(), "site_country": faker.country(), "site_country_code": faker.country_code(), @@ -88,6 +88,7 @@ def site_data(faker): @pytest.fixture def router_data(faker, site_data): + mock_ipv4 = faker.ipv4() return { "hostname": "127.0.0.1", "router_role": RouterRole.PE, @@ -96,9 +97,9 @@ def router_data(faker, site_data): "ts_port": 1234, "customer": "GÉANT", "is_ias_connected": True, - "router_lo_ipv4_address": faker.ipv4(), + "router_lo_ipv4_address": mock_ipv4, "router_lo_ipv6_address": faker.ipv6(), - "router_lo_iso_address": "TestAddress", + "router_lo_iso_address": iso_from_ipv4(mock_ipv4), } @@ -169,7 +170,7 @@ def test_import_router_endpoint_with_invalid_data(test_client, site_data, router assert response["detail"][1]["msg"] == "value is not a valid IPv6 address" -def test_import_iptrunk_successful_with_real_process(test_client, iptrunk_data, mock_routers): +def test_import_iptrunk_successful_with_real_process(test_client, mock_routers, iptrunk_data): response = test_client.post(IPTRUNK_IMPORT_API_URL, json=iptrunk_data) assert response.status_code == 201 @@ -184,7 +185,7 @@ def test_import_iptrunk_successful_with_real_process(test_client, iptrunk_data, @patch("gso.api.v1.imports._start_process") -def test_import_iptrunk_invalid_customer(mock_start_process, test_client, iptrunk_data, mock_routers): +def test_import_iptrunk_invalid_customer(mock_start_process, test_client, mock_routers, iptrunk_data): iptrunk_data["customer"] = "not_existing_customer" mock_start_process.return_value = "123e4567-e89b-12d3-a456-426655440000" response = test_client.post(IPTRUNK_IMPORT_API_URL, json=iptrunk_data) @@ -205,32 +206,28 @@ def test_import_iptrunk_invalid_router_id_side_a_and_b(mock_start_process, test_ assert response.status_code == 422 assert response.json() == { "detail": [ - {"loc": ["body", "side_a_node_id"], "msg": "Router not found", "type": "value_error"}, - {"loc": ["body", "side_b_node_id"], "msg": "Router not found", "type": "value_error"}, + { + "loc": ["body", "side_a_node_id"], + "msg": f"Router {iptrunk_data['side_a_node_id']} not found", + "type": "value_error", + }, + { + "loc": ["body", "side_b_node_id"], + "msg": f"Router {iptrunk_data['side_b_node_id']} not found", + "type": "value_error", + }, ] } @patch("gso.api.v1.imports._start_process") -def test_import_iptrunk_non_unique_members_side_a(mock_start_process, test_client, iptrunk_data, mock_routers, faker): +def test_import_iptrunk_non_unique_members_side_a(mock_start_process, test_client, mock_routers, iptrunk_data, faker): mock_start_process.return_value = "123e4567-e89b-12d3-a456-426655440000" repeat_interface_a = {"interface_name": faker.network_interface(), "interface_description": faker.sentence()} repeat_interface_b = {"interface_name": faker.network_interface(), "interface_description": faker.sentence()} - iptrunk_data["side_a_ae_members"] = [ - repeat_interface_a, - repeat_interface_a, - repeat_interface_a, - repeat_interface_a, - repeat_interface_a, - ] - iptrunk_data["side_b_ae_members"] = [ - repeat_interface_b, - repeat_interface_a, - repeat_interface_a, - repeat_interface_b, - repeat_interface_b, - ] + iptrunk_data["side_a_ae_members"] = [repeat_interface_a for _ in range(5)] + iptrunk_data["side_b_ae_members"] = [repeat_interface_b for _ in range(5)] response = test_client.post(IPTRUNK_IMPORT_API_URL, json=iptrunk_data) @@ -250,7 +247,7 @@ def test_import_iptrunk_non_unique_members_side_a(mock_start_process, test_clien @patch("gso.api.v1.imports._start_process") def test_import_iptrunk_fails_on_side_a_member_count_mismatch( - mock_start_process, test_client, iptrunk_data, mock_routers + mock_start_process, test_client, mock_routers, iptrunk_data ): mock_start_process.return_value = "123e4567-e89b-12d3-a456-426655440000" diff --git a/test/schemas/test_types.py b/test/schemas/test_types.py index b8f6eef13c84c74416ed829320a6cfb41edf4514..e5d757dbb84550df27f05949caf2e1ad78c7a2c8 100644 --- a/test/schemas/test_types.py +++ b/test/schemas/test_types.py @@ -1,6 +1,6 @@ import pytest -from gso.utils.types.snmp import LatitudeCoordinate, LongitudeCoordinate +from gso.products.product_blocks.site import LatitudeCoordinate, LongitudeCoordinate @pytest.mark.parametrize( diff --git a/test/workflows/iptrunk/test_create_iptrunk.py b/test/workflows/iptrunk/test_create_iptrunk.py index cff8bed98c5c10ca3c79b918ec3ed5cca6720668..479fbb91c48d0943efb91209a13ff93dcfe14498 100644 --- a/test/workflows/iptrunk/test_create_iptrunk.py +++ b/test/workflows/iptrunk/test_create_iptrunk.py @@ -4,10 +4,9 @@ from unittest.mock import patch import pytest from gso.products import Iptrunk, ProductType -from gso.products.product_blocks.iptrunk import IptrunkType +from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity from gso.services.crm import customer_selector, get_customer_by_name from gso.services.subscriptions import get_product_id_by_name -from gso.utils.types.phy_port import PhyPortCapacity from test.workflows import ( assert_aborted, assert_complete, diff --git a/test/workflows/iptrunk/test_modify_trunk_interface.py b/test/workflows/iptrunk/test_modify_trunk_interface.py index 4160f954de4f1ed113f7e03424ce41ceb1efc116..8f39c4d7f784be6c3beea80c3741f7d856271c20 100644 --- a/test/workflows/iptrunk/test_modify_trunk_interface.py +++ b/test/workflows/iptrunk/test_modify_trunk_interface.py @@ -3,8 +3,7 @@ from unittest.mock import patch import pytest from gso.products import Iptrunk -from gso.products.product_blocks.iptrunk import IptrunkType -from gso.utils.types.phy_port import PhyPortCapacity +from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity from test.workflows import ( assert_complete, assert_suspended,