diff --git a/gso/products/product_blocks/site.py b/gso/products/product_blocks/site.py index 176d8e88af482fd7e557f38f0ef19e7cb6b5fda0..d519f8b8b549e96823570d4398bd5a3abb6d66b2 100644 --- a/gso/products/product_blocks/site.py +++ b/gso/products/product_blocks/site.py @@ -1,9 +1,10 @@ """The product block that describes a site subscription.""" - +import re from typing import Optional from orchestrator.domain.base import ProductBlockModel from orchestrator.types import SubscriptionLifecycle, strEnum +from pydantic import ConstrainedStr class SiteTier(strEnum): @@ -19,6 +20,14 @@ class SiteTier(strEnum): TIER4 = 4 +class SnmpCoordinate(ConstrainedStr): + """An SNMP coordinate, modeled as a constrained string. + + The coordinate must match the format of `1.35`, `-12.3456`, etc. + """ + regex = re.compile(r"^-?\d{1,2}\.\d+$") + + class SiteBlockInactive(ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="SiteBlock"): """A site that's currently inactive, see {class}`SiteBlock`.""" @@ -26,8 +35,8 @@ class SiteBlockInactive(ProductBlockModel, lifecycle=[SubscriptionLifecycle.INIT site_city: Optional[str] = None site_country: Optional[str] = None site_country_code: Optional[str] = None - site_latitude: Optional[float] = None - site_longitude: Optional[float] = None + site_latitude: Optional[SnmpCoordinate] = None + site_longitude: Optional[SnmpCoordinate] = None site_internal_id: Optional[int] = None site_bgp_community_id: Optional[int] = None site_tier: Optional[SiteTier] = None @@ -41,8 +50,8 @@ class SiteBlockProvisioning(SiteBlockInactive, lifecycle=[SubscriptionLifecycle. site_city: Optional[str] = None site_country: Optional[str] = None site_country_code: Optional[str] = None - site_latitude: Optional[float] = None - site_longitude: Optional[float] = None + site_latitude: Optional[SnmpCoordinate] = None + site_longitude: Optional[SnmpCoordinate] = None site_internal_id: Optional[int] = None site_bgp_community_id: Optional[int] = None site_tier: Optional[SiteTier] = None @@ -62,9 +71,9 @@ class SiteBlock(SiteBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]) site_country_code: str """The code of the corresponding country. This is also used for the {term}`FQDN`, following the example given for the site name, the country code would end up in the Y position.""" - site_latitude: float + site_latitude: SnmpCoordinate """The latitude of the site, used for {term}`SNMP` purposes.""" - site_longitude: float + site_longitude: SnmpCoordinate """Similar to the latitude, the longitude of a site.""" site_internal_id: int """The internal ID used within GÉANT to denote a site.""" diff --git a/gso/workflows/site/create_site.py b/gso/workflows/site/create_site.py index b756971c5e61d113be6e81c23cb465c218ae8969..94237b300528c23cb91512c3ae573fa7168f1b65 100644 --- a/gso/workflows/site/create_site.py +++ b/gso/workflows/site/create_site.py @@ -1,9 +1,13 @@ +import ipaddress + from orchestrator.forms import FormPage from orchestrator.targets import Target from orchestrator.types import FormGenerator, State, SubscriptionLifecycle, UUIDstr from orchestrator.workflow import StepList, done, init, step, workflow from orchestrator.workflows.steps import resync, set_status, store_process_subscription from orchestrator.workflows.utils import wrap_create_initial_input_form +from pydantic import validator +import pycountry from gso.products.product_blocks import site as site_pb from gso.products.product_types import site @@ -20,13 +24,48 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: site_city: str site_country: str site_country_code: str - site_latitude: float - site_longitude: float + site_latitude: str + site_longitude: str site_bgp_community_id: int site_internal_id: int site_tier: site_pb.SiteTier site_ts_address: str + @validator("site_country_code", allow_reuse=True) + def country_code_must_exist(cls, country_code): + try: + _ = pycountry.countries.lookup(country_code) + # Lookup succeeded, the country code is valid. + return country_code + except LookupError: + # Lookup failed, the country code is not valid. + raise ValueError("Invalid or non-existent country code, it must be in ISO 3166-1 alpha-2 format.") + + @validator("site_latitude", allow_reuse=True) + def latitude_must_be_valid(cls, latitude): + if -90 <= float(latitude) <= 90: + # Check whether the value is a valid degree of latitude. + return latitude + else: + raise ValueError("Entered latitude is not a valid value, must be between -90.0° and 90.0°.") + + @validator("site_longitude", allow_reuse=True) + def longitude_must_be_valid(cls, longitude): + if -180 <= float(longitude) <= 180: + # Check whether the value is a valid degree of longitude. + return longitude + else: + raise ValueError("Entered longitude is not a valid value, must be between -180.0° and 180.0°.") + + @validator("site_ts_address", allow_reuse=True) + def ts_address_must_be_valid(cls, ts_address): + try: + ipaddress.ip_address(ts_address) + # The address is valid + return ts_address + except ValueError: + raise ValueError("Enter a valid IPv4 or v6 address.") + user_input = yield CreateSiteForm return user_input.dict() diff --git a/setup.py b/setup.py index c005de0cf4a05cff6a950383bc2f80542a726de6..97e32111f2347c5bffdd9d3da9485013a1de115a 100644 --- a/setup.py +++ b/setup.py @@ -12,5 +12,6 @@ setup( "orchestrator-core==1.2.2", "pydantic", "requests", + "pycountry", ], )