diff --git a/mapping_provider/__init__.py b/mapping_provider/__init__.py index aa309f9cfa78136a8ef621a23d83e13d37b4dffa..2898e51fb985552ec45e3fe3396316b75a9d6e65 100644 --- a/mapping_provider/__init__.py +++ b/mapping_provider/__init__.py @@ -1,15 +1,19 @@ -"""Initializes the FastAPI application.""" +""" +Default entry point for the FastAPI application. +""" from fastapi import FastAPI -from mapping_provider.api.common import router as version_router - +from mapping_provider.api import common, map def create_app() -> FastAPI: - """Create a FastAPI application.""" + """ + Creates the FastAPI application instance, with routers attached. + """ app = FastAPI( title="Mapping provider", description="Mapping provider endpoints for GÉANT maps", ) - app.include_router(version_router) + app.include_router(common.router) + app.include_router(map.router, prefix='/map') return app diff --git a/mapping_provider/api/map.py b/mapping_provider/api/map.py new file mode 100644 index 0000000000000000000000000000000000000000..79c64800776e531733cac1065f151d93cb315837 --- /dev/null +++ b/mapping_provider/api/map.py @@ -0,0 +1,210 @@ +from importlib.metadata import version + +from fastapi import APIRouter +import jsonschema +from pydantic import BaseModel +import requests + +router = APIRouter() + + +class Site(BaseModel): + latitude: float + longitude: float + name: str + + @classmethod + def from_inprov_site(cls, site: dict) -> 'Site': + return cls( + latitude=site['latitude'], + longitude=site['longitude'], + name=site['name'] + ) + +class SiteList(BaseModel): + sites: list[Site] + +class Router(BaseModel): + fqdn: str + site: str + + @classmethod + def from_inprov_router(cls, router: dict) -> 'Router': + return cls( + fqdn = router['fqdn'], + site = router['site'] + ) + +class RouterList(BaseModel): + routers: list[Router] + + +class Endpoint(BaseModel): + hostname: str + interface: str + + @classmethod + def from_inprov_endpoint(cls, endpoint: dict) -> 'Endpoint': + return cls( + hostname = endpoint['hostname'], + interface = endpoint['interface'], + ) + +class Overlays(BaseModel): + speed: int + + @classmethod + def from_inprov_overlays(cls, overlays: dict) -> 'Overlays': + return cls( + speed = overlays['speed'], + ) + +class Service(BaseModel): + sid: str + name: str + type: str + endpoints: list[Endpoint] + overlays: Overlays + + @classmethod + def from_inprov_service(cls, service: dict) -> 'Service': + return cls( + sid = service['sid'], + name = service['name'], + type = service['type'], + endpoints = list(map(Endpoint.from_inprov_endpoint, service['endpoints'])), + overlays = Overlays.from_inprov_overlays(service['overlays']), + ) + +class ServiceList(BaseModel): + services: list[Service] + + +INPROV_SITE_LIST_SCHEMA = { + '$schema': 'https://json-schema.org/draft/2020-12/schema', + + 'definitions': { + 'site': { + 'type': 'object', + 'properties': { + 'name': {'type': 'string'}, + 'latitude': {'type': 'number'}, + 'longitude': {'type': 'number'}, + }, + 'required': ['name', 'latitude', 'longitude'], + 'additionalProperties': True, + }, + }, + + 'type': 'array', + 'items': {'$ref': '#/definitions/site'} +} + +INPROV_ROUTER_LIST_SCHEMA = { + '$schema': 'https://json-schema.org/draft/2020-12/schema', + + 'definitions': { + 'router': { + 'type': 'object', + 'properties': { + 'fqdn': {'type': 'string'}, + 'site': {'type': 'string'}, + }, + 'required': ['fqdn', 'site'], + }, + }, + + 'type': 'array', + 'items': {'$ref': '#/definitions/router'} +} + +INPROV_SERVICE_LIST_SCHEMA = { + '$schema': 'https://json-schema.org/draft/2020-12/schema', + + 'definitions': { + 'endpoint': { + 'type': 'object', + 'properties': { + 'hostname': {'type': 'string'}, + 'interface': {'type': 'string'}, + }, + }, + 'service': { + 'type': 'object', + 'properties': { + 'sid': {'type': 'string'}, + 'name': {'type': 'string'}, + 'type': {'type': 'string'}, + 'endpoints': { + 'type': 'array', + 'items': {'$ref': '#/definitions/endpoint'}, + 'minItems': 1, + }, + 'overlays': { + 'type': 'object', 'properties': { + 'speed': {'type': 'number'}, + }, + 'required': ['speed'], + }, + }, + 'required': ['sid', 'name', 'type', 'endpoints', 'overlays'], + }, + }, + + 'type': 'array', + 'items': {'$ref': '#/definitions/service'} +} + +@router.get("/sites") +def get_sites() -> SiteList: + """ + handler for /sites + """ + + # TODO: catch/handle the usual exceptions + + rv = requests.get( + 'https://test-inprov01.geant.org/map/sites', + headers={'Accept': 'application/json'}) + rv.raise_for_status() + site_list_json = rv.json() + jsonschema.validate(site_list_json, INPROV_SITE_LIST_SCHEMA) + + return SiteList(sites=map(Site.from_inprov_site, site_list_json)) + + +@router.get("/routers") +def get_routers() -> RouterList: + """ + handler for /sites + """ + + # TODO: catch/handle the usual exceptions + + rv = requests.get( + 'https://test-inprov01.geant.org/map/routers', + headers={'Accept': 'application/json'}) + rv.raise_for_status() + router_list_json = rv.json() + jsonschema.validate(router_list_json, INPROV_ROUTER_LIST_SCHEMA) + + return RouterList(routers=map(Router.from_inprov_router, router_list_json)) + + +@router.get("/trunks") +def get_trunks() -> ServiceList: + """ + handler for /trunks + """ + + # TODO: catch/handle the usual exceptions + + rv = requests.get( + 'https://test-inprov01.geant.org/map/services/IP TRUNK', + headers={'Accept': 'application/json'}) + rv.raise_for_status() + service_list_json = rv.json() + jsonschema.validate(service_list_json, INPROV_SERVICE_LIST_SCHEMA) + + return ServiceList(services=map(Service.from_inprov_service, service_list_json)) + diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/test/test_map_endpoints.py b/test/test_map_endpoints.py new file mode 100644 index 0000000000000000000000000000000000000000..b8678717fc098a074303239210b01f474fd0d2f1 --- /dev/null +++ b/test/test_map_endpoints.py @@ -0,0 +1,35 @@ +import responses + +from mapping_provider.api.map import SiteList, RouterList, ServiceList + + +# @responses.activate +# def test_inventory_uri_validation(): +# responses.add( +# method=responses.GET, url=re.compile(r".*/version$"), json={"api": "0.9"} +# ) +# assert ( +# classifier.verify_inventory_provider_uri(None, None, "http://a.b.c:9999") +# == "http://a.b.c:9999/" +# ) + + +def test_get_sites(client): + rv = client.get("/map/sites") + assert rv.status_code == 200 + assert rv.json() + SiteList.model_validate(rv.json()) + + +def test_get_routers(client): + rv = client.get("/map/routers") + assert rv.status_code == 200 + assert rv.json() + RouterList.model_validate(rv.json()) + + +def test_get_trunks(client): + rv = client.get("/map/trunks") + assert rv.status_code == 200 + assert rv.json() + ServiceList.model_validate(rv.json())