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/services/crm.py b/gso/services/crm.py
index c5b4f139da9feccdeb8feac3b60e196d170e0605..f1aa73a09eaf94b4698dbbb1b2aed189544b7541 100644
--- a/gso/services/crm.py
+++ b/gso/services/crm.py
@@ -5,7 +5,7 @@ def all_customers() -> list[dict]:
return [
{
"id": "8f0df561-ce9d-4d9c-89a8-7953d3ffc961",
- "name": "Geant",
+ "name": "GÉANT",
},
]
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",
],
)