Skip to content
Snippets Groups Projects
Commit 7c8cf514 authored by Karel van Klink's avatar Karel van Klink :smiley_cat:
Browse files

add validators to router creation workflow, also making IAS connection optional

parent 893ef4c1
No related branches found
No related tags found
1 merge request!64new IP trunk migration
...@@ -4,6 +4,7 @@ from typing import Optional ...@@ -4,6 +4,7 @@ from typing import Optional
from orchestrator.domain.base import ProductBlockModel from orchestrator.domain.base import ProductBlockModel
from orchestrator.types import SubscriptionLifecycle, strEnum from orchestrator.types import SubscriptionLifecycle, strEnum
from pydantic import ConstrainedInt
from gso.products.product_blocks.site import SiteBlock, SiteBlockInactive, SiteBlockProvisioning from gso.products.product_blocks.site import SiteBlock, SiteBlockInactive, SiteBlockProvisioning
...@@ -28,13 +29,25 @@ class RouterRole(strEnum): ...@@ -28,13 +29,25 @@ class RouterRole(strEnum):
"""AMT router.""" """AMT router."""
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
"""The lower bound of the valid port number range."""
le = 49151
"""As mentioned earlier, the ephemeral port range should not be chosen, and is therefore not available."""
class RouterBlockInactive( class RouterBlockInactive(
ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="RouterBlock" ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="RouterBlock"
): ):
"""A router that's being currently inactive. See {class}`RouterBlock`.""" """A router that's being currently inactive. See {class}`RouterBlock`."""
router_fqdn: Optional[str] = None router_fqdn: Optional[str] = None
router_ts_port: Optional[int] = None router_ts_port: Optional[PortNumber] = None
router_access_via_ts: Optional[bool] = None router_access_via_ts: Optional[bool] = None
router_lo_ipv4_address: Optional[ipaddress.IPv4Address] = None router_lo_ipv4_address: Optional[ipaddress.IPv4Address] = None
router_lo_ipv6_address: Optional[ipaddress.IPv6Address] = None router_lo_ipv6_address: Optional[ipaddress.IPv6Address] = None
...@@ -52,7 +65,7 @@ class RouterBlockProvisioning(RouterBlockInactive, lifecycle=[SubscriptionLifecy ...@@ -52,7 +65,7 @@ class RouterBlockProvisioning(RouterBlockInactive, lifecycle=[SubscriptionLifecy
"""A router that's being provisioned. See {class}`RouterBlock`.""" """A router that's being provisioned. See {class}`RouterBlock`."""
router_fqdn: str router_fqdn: str
router_ts_port: int router_ts_port: PortNumber
router_access_via_ts: Optional[bool] = None router_access_via_ts: Optional[bool] = None
router_lo_ipv4_address: Optional[ipaddress.IPv4Address] = None router_lo_ipv4_address: Optional[ipaddress.IPv4Address] = None
router_lo_ipv6_address: Optional[ipaddress.IPv6Address] = None router_lo_ipv6_address: Optional[ipaddress.IPv6Address] = None
...@@ -71,9 +84,8 @@ class RouterBlock(RouterBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTI ...@@ -71,9 +84,8 @@ class RouterBlock(RouterBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTI
router_fqdn: str router_fqdn: str
"""{term}`FQDN` of a router.""" """{term}`FQDN` of a router."""
router_ts_port: int router_ts_port: PortNumber
"""The port of the terminal server that this router is connected to. Used for the same reason as mentioned """The port of the terminal server that this router is connected to. Used to provide out of band access."""
previously."""
router_access_via_ts: bool router_access_via_ts: bool
"""Whether this router should be accessed through the terminal server, or through its loopback address.""" """Whether this router should be accessed through the terminal server, or through its loopback address."""
router_lo_ipv4_address: ipaddress.IPv4Address router_lo_ipv4_address: ipaddress.IPv4Address
......
...@@ -25,6 +25,7 @@ class SnmpCoordinate(ConstrainedStr): ...@@ -25,6 +25,7 @@ class SnmpCoordinate(ConstrainedStr):
The coordinate must match the format of `1.35`, `-12.3456`, etc. The coordinate must match the format of `1.35`, `-12.3456`, etc.
""" """
regex = re.compile(r"^-?\d{1,2}\.\d+$") regex = re.compile(r"^-?\d{1,2}\.\d+$")
......
import ipaddress
import re import re
from typing import Optional
# noinspection PyProtectedMember # noinspection PyProtectedMember
from orchestrator.forms import FormPage from orchestrator.forms import FormPage
...@@ -9,6 +9,7 @@ from orchestrator.types import FormGenerator, State, SubscriptionLifecycle, UUID ...@@ -9,6 +9,7 @@ from orchestrator.types import FormGenerator, State, SubscriptionLifecycle, UUID
from orchestrator.workflow import StepList, done, init, step, workflow from orchestrator.workflow import StepList, done, init, step, workflow
from orchestrator.workflows.steps import resync, set_status, store_process_subscription from orchestrator.workflows.steps import resync, set_status, store_process_subscription
from orchestrator.workflows.utils import wrap_create_initial_input_form from orchestrator.workflows.utils import wrap_create_initial_input_form
from products.product_blocks.router import PortNumber
from gso.products.product_blocks import router as router_pb from gso.products.product_blocks import router as router_pb
from gso.products.product_types import router from gso.products.product_types import router
...@@ -16,10 +17,10 @@ from gso.products.product_types.router import RouterInactive, RouterProvisioning ...@@ -16,10 +17,10 @@ from gso.products.product_types.router import RouterInactive, RouterProvisioning
from gso.products.product_types.site import Site from gso.products.product_types.site import Site
from gso.services import ipam, provisioning_proxy, subscriptions from gso.services import ipam, provisioning_proxy, subscriptions
from gso.services.provisioning_proxy import pp_interaction from gso.services.provisioning_proxy import pp_interaction
from gso.workflows.utils import customer_selector from gso.workflows.utils import customer_selector, iso_from_ipv4
def site_selector() -> Choice: def _site_selector() -> Choice:
site_subscriptions = {} site_subscriptions = {}
for site_id, site_description in subscriptions.get_active_site_subscriptions( for site_id, site_description in subscriptions.get_active_site_subscriptions(
fields=["subscription_id", "description"] fields=["subscription_id", "description"]
...@@ -36,12 +37,12 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: ...@@ -36,12 +37,12 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
title = product_name title = product_name
customer: customer_selector() # type: ignore customer: customer_selector() # type: ignore
router_site: site_selector() # type: ignore router_site: _site_selector() # type: ignore
hostname: str hostname: str
ts_port: int ts_port: PortNumber
router_vendor: router_pb.RouterVendor router_vendor: router_pb.RouterVendor
router_role: router_pb.RouterRole router_role: router_pb.RouterRole
is_ias_connected: bool is_ias_connected: Optional[bool]
user_input = yield CreateRouterForm user_input = yield CreateRouterForm
...@@ -58,13 +59,6 @@ def create_subscription(product: UUIDstr, customer: UUIDstr) -> State: ...@@ -58,13 +59,6 @@ def create_subscription(product: UUIDstr, customer: UUIDstr) -> State:
} }
def iso_from_ipv4(ipv4_address: ipaddress.IPv4Address) -> str:
padded_octets = [f"{x:>03}" for x in str(ipv4_address).split(".")]
joined_octets = "".join(padded_octets)
re_split = ".".join(re.findall("....", joined_octets))
return ".".join(["49.51e5.0001", re_split, "00"])
@step("Get information from IPAM") @step("Get information from IPAM")
def get_info_from_ipam(subscription: RouterProvisioning, is_ias_connected: bool) -> State: def get_info_from_ipam(subscription: RouterProvisioning, is_ias_connected: bool) -> State:
lo0_alias = re.sub(".geant.net", "", subscription.router.router_fqdn) lo0_alias = re.sub(".geant.net", "", subscription.router.router_fqdn)
...@@ -93,7 +87,7 @@ def get_info_from_ipam(subscription: RouterProvisioning, is_ias_connected: bool) ...@@ -93,7 +87,7 @@ def get_info_from_ipam(subscription: RouterProvisioning, is_ias_connected: bool)
def initialize_subscription( def initialize_subscription(
subscription: router.RouterInactive, subscription: router.RouterInactive,
hostname: str, hostname: str,
ts_port: int, ts_port: PortNumber,
router_vendor: router_pb.RouterVendor, router_vendor: router_pb.RouterVendor,
router_site: str, router_site: str,
router_role: router_pb.RouterRole, router_role: router_pb.RouterRole,
......
import ipaddress import ipaddress
from typing import NoReturn
import pycountry
from orchestrator.forms import FormPage from orchestrator.forms import FormPage
from orchestrator.targets import Target from orchestrator.targets import Target
from orchestrator.types import FormGenerator, State, SubscriptionLifecycle, UUIDstr from orchestrator.types import FormGenerator, State, SubscriptionLifecycle, UUIDstr
from orchestrator.workflow import StepList, done, init, step, workflow from orchestrator.workflow import StepList, done, init, step, workflow
from orchestrator.workflows.steps import resync, set_status, store_process_subscription from orchestrator.workflows.steps import resync, set_status, store_process_subscription
from orchestrator.workflows.utils import wrap_create_initial_input_form from orchestrator.workflows.utils import wrap_create_initial_input_form
from products.product_blocks.site import SnmpCoordinate
from pydantic import validator from pydantic import validator
import pycountry
from gso.products.product_blocks import site as site_pb from gso.products.product_blocks import site as site_pb
from gso.products.product_types import site from gso.products.product_types import site
...@@ -32,7 +34,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: ...@@ -32,7 +34,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
site_ts_address: str site_ts_address: str
@validator("site_country_code", allow_reuse=True) @validator("site_country_code", allow_reuse=True)
def country_code_must_exist(cls, country_code): def country_code_must_exist(cls, country_code: str) -> str | NoReturn:
try: try:
_ = pycountry.countries.lookup(country_code) _ = pycountry.countries.lookup(country_code)
# Lookup succeeded, the country code is valid. # Lookup succeeded, the country code is valid.
...@@ -42,23 +44,23 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: ...@@ -42,23 +44,23 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
raise ValueError("Invalid or non-existent country code, it must be in ISO 3166-1 alpha-2 format.") raise ValueError("Invalid or non-existent country code, it must be in ISO 3166-1 alpha-2 format.")
@validator("site_latitude", allow_reuse=True) @validator("site_latitude", allow_reuse=True)
def latitude_must_be_valid(cls, latitude): def latitude_must_be_valid(cls, latitude: str) -> str | NoReturn:
if -90 <= float(latitude) <= 90: if -90 <= float(latitude) <= 90:
# Check whether the value is a valid degree of latitude. # Check whether the value is a valid degree of latitude.
return latitude return latitude
else:
raise ValueError("Entered latitude is not a valid value, must be between -90.0° and 90.0°.") raise ValueError("Entered latitude is not a valid value, must be between -90.0° and 90.0°.")
@validator("site_longitude", allow_reuse=True) @validator("site_longitude", allow_reuse=True)
def longitude_must_be_valid(cls, longitude): def longitude_must_be_valid(cls, longitude: str) -> str | NoReturn:
if -180 <= float(longitude) <= 180: if -180 <= float(longitude) <= 180:
# Check whether the value is a valid degree of longitude. # Check whether the value is a valid degree of longitude.
return longitude return longitude
else:
raise ValueError("Entered longitude is not a valid value, must be between -180.0° and 180.0°.") raise ValueError("Entered longitude is not a valid value, must be between -180.0° and 180.0°.")
@validator("site_ts_address", allow_reuse=True) @validator("site_ts_address", allow_reuse=True)
def ts_address_must_be_valid(cls, ts_address): def ts_address_must_be_valid(cls, ts_address: str) -> str | NoReturn:
try: try:
ipaddress.ip_address(ts_address) ipaddress.ip_address(ts_address)
# The address is valid # The address is valid
...@@ -88,8 +90,8 @@ def initialize_subscription( ...@@ -88,8 +90,8 @@ def initialize_subscription(
site_city: str, site_city: str,
site_country: str, site_country: str,
site_country_code: str, site_country_code: str,
site_latitude: float, site_latitude: SnmpCoordinate,
site_longitude: float, site_longitude: SnmpCoordinate,
site_bgp_community_id: int, site_bgp_community_id: int,
site_internal_id: int, site_internal_id: int,
site_ts_address: str, site_ts_address: str,
......
import re
from ipaddress import IPv4Address
from orchestrator.forms.validators import Choice from orchestrator.forms.validators import Choice
from gso.services.crm import all_customers from gso.services.crm import all_customers
...@@ -9,3 +12,15 @@ def customer_selector() -> Choice: ...@@ -9,3 +12,15 @@ def customer_selector() -> Choice:
customers[customer["id"]] = customer["name"] customers[customer["id"]] = customer["name"]
return Choice("Select a customer", zip(customers.keys(), customers.items())) # type: ignore return Choice("Select a customer", zip(customers.keys(), customers.items())) # type: ignore
def iso_from_ipv4(ipv4_address: IPv4Address) -> str:
"""Calculate an ISO address, based on an IPv4 address.
:param IPv4Address ipv4_address: The address that is to be converted
:returns: An ISO-formatted address.
"""
padded_octets = [f"{x:>03}" for x in str(ipv4_address).split(".")]
joined_octets = "".join(padded_octets)
re_split = ".".join(re.findall("....", joined_octets))
return ".".join(["49.51e5.0001", re_split, "00"])
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment