diff --git a/mapping_provider/api/map.py b/mapping_provider/api/map.py index a022f77c09e7a5db28bf5b168858fcc88c070c67..b42956bc1315927f3cecd4209b6074028423d702 100644 --- a/mapping_provider/api/map.py +++ b/mapping_provider/api/map.py @@ -11,69 +11,64 @@ from mapping_provider.backends import services router = APIRouter() - -class Site(BaseModel): - latitude: float - longitude: float +class Pop(BaseModel): + latitude: float | None + longitude: float | None name: str + abbreviation: str + city: str + country: str - @classmethod - def from_inprov_site(cls, site: dict[str, Any]) -> 'Site': - return cls( - latitude=site['latitude'], - longitude=site['longitude'], - name=site['name'] - ) -class SiteList(BaseModel): - sites: list[Site] +class PopList(BaseModel): + pops: list[Pop] -class Router(BaseModel): - fqdn: str - site: str - @classmethod - def from_inprov_router(cls, router: dict[str, Any]) -> 'Router': - return cls( - fqdn = router['fqdn'], - site = router['site'] - ) +class Equipment(BaseModel): + name: str + pop: str + status: str -class RouterList(BaseModel): - routers: list[Router] +class EquipmentList(BaseModel): + equipment: list[Equipment] -INPROV_SITE_LIST_SCHEMA = { + +INPROV_POP_LIST_SCHEMA = { '$schema': 'https://json-schema.org/draft/2020-12/schema', 'definitions': { - 'site': { + 'pop': { 'type': 'object', 'properties': { 'name': {'type': 'string'}, - 'latitude': {'type': 'number'}, - 'longitude': {'type': 'number'}, + 'abbreviation': {'type': 'string'}, + 'city': {'type': 'string'}, + 'country': {'type': 'string'}, + 'latitude': {'type': ['number', 'null']}, + 'longitude': {'type': ['number', 'null']}, }, - 'required': ['name', 'latitude', 'longitude'], + 'required': ['name', 'abbreviation', 'city', 'country', 'latitude', 'longitude'], 'additionalProperties': True, }, }, 'type': 'array', - 'items': {'$ref': '#/definitions/site'} + 'items': {'$ref': '#/definitions/pop'} } -INPROV_ROUTER_LIST_SCHEMA = { +INPROV_EQUIPMENT_LIST_SCHEMA = { '$schema': 'https://json-schema.org/draft/2020-12/schema', 'definitions': { 'router': { 'type': 'object', 'properties': { - 'fqdn': {'type': 'string'}, - 'site': {'type': 'string'}, + 'name': {'type': 'string'}, + 'pop': {'type': 'string'}, + 'status': {'type': 'string'}, }, - 'required': ['fqdn', 'site'], + 'required': ['name', 'pop', 'status'], }, }, @@ -81,81 +76,94 @@ INPROV_ROUTER_LIST_SCHEMA = { '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'} -} +# 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'} +# } INPROV_API_URL_TODO = 'https://test-inprov01.geant.org' -@router.get("/sites") -def get_sites() -> SiteList: + +@router.get("/pops") +def get_pops() -> PopList: """ - handler for /sites + handler for /pops """ # TODO: catch/handle the usual exceptions app_params = config.load() rv = requests.get( - f'{app_params.inventory}/map/sites', + f'{app_params.inventory}/map/pops', headers={'Accept': 'application/json'}) rv.raise_for_status() - site_list_json = rv.json() - jsonschema.validate(site_list_json, INPROV_SITE_LIST_SCHEMA) - - rsp_sites = map(Site.from_inprov_site, site_list_json) - return SiteList(sites=list(rsp_sites)) + pop_list_obj = rv.json() + jsonschema.validate(pop_list_obj, INPROV_POP_LIST_SCHEMA) + + def _make_pop(pop_dict: dict[str, Any]) -> Pop: + return Pop( + latitude=pop_dict['latitude'], + longitude=pop_dict['longitude'], + name=pop_dict['name'], + abbreviation=pop_dict['abbreviation'], + city=pop_dict['city'], + country=pop_dict['country'], + ) + return PopList(pops=map(_make_pop, pop_list_obj)) -@router.get("/routers") -def get_routers() -> RouterList: +@router.get("/equipment") +def get_equipment() -> EquipmentList: """ - handler for /sites + handler for /equipment """ - # TODO: catch/handle the usual exceptions app_params = config.load() rv = requests.get( - f'{app_params.inventory}/map/routers', + f'{app_params.inventory}/map/equipment', headers={'Accept': 'application/json'}) rv.raise_for_status() - router_list_json = rv.json() - jsonschema.validate(router_list_json, INPROV_ROUTER_LIST_SCHEMA) - - rsp_routers = map(Router.from_inprov_router, router_list_json) - return RouterList(routers=list(rsp_routers)) + equipment_list_obj = rv.json() + jsonschema.validate(equipment_list_obj, INPROV_EQUIPMENT_LIST_SCHEMA) + + def _make_equipment(equipment_dict: dict[str, Any]) -> Equipment: + return Equipment( + name=equipment_dict['name'], + pop=equipment_dict['pop'], + status=equipment_dict['status'], + ) + return EquipmentList(equipment=map(_make_equipment, equipment_list_obj)) @router.get("/trunks") diff --git a/mapping_provider/backends/inventory.py b/mapping_provider/backends/inventory.py index 383f0b698495d8b3eac2fefc4dbbcd1cb38f7787..6409cfaa3bcb6cbeffc30822c77df090f77e4c7c 100644 --- a/mapping_provider/backends/inventory.py +++ b/mapping_provider/backends/inventory.py @@ -25,24 +25,22 @@ REPORTING_SCID_CURRENT_CACHE_SCHEMA = { 'type': 'object', 'properties': { 'hostname': {'type': 'string'}, - # 'interface': {'type': 'string'}, + 'interface': {'type': 'string'}, # 'addresses': { # 'type': 'array', # 'items': {'type': 'string'} # } }, - 'required': ['hostname'] - # 'required': ['hostname', 'interface'] + 'required': ['hostname', 'interface'] }, 'lambda_interface': { 'type': 'object', 'properties': { 'equipment': {'type': 'string'}, - # 'port': {'type': 'string'}, + 'port': {'type': 'string'}, }, - 'required': ['equipment'] - # 'required': ['equipment', 'port'] + 'required': ['equipment', 'port'] }, 'service': { diff --git a/mapping_provider/backends/services.py b/mapping_provider/backends/services.py index 05c835d5d322a160889804b9d653607b808b0a58..124d9676d824ed6c57c3a09384dcd48421fc5d0e 100644 --- a/mapping_provider/backends/services.py +++ b/mapping_provider/backends/services.py @@ -1,5 +1,8 @@ +import functools import logging from collections.abc import Generator +import re +from typing import Any from pydantic import BaseModel @@ -8,11 +11,6 @@ from . import brian, cache, correlator, inventory logger = logging.getLogger(__name__) -class Endpoint(BaseModel): - equipment: str - interface: str - - class BitRates(BaseModel): ingress: float | None egress: float | None @@ -31,13 +29,39 @@ class Service(BaseModel): scid: str name: str type: str - endpoints: list[Endpoint] + pops: list[str] overlays: Overlays class ServiceList(BaseModel): services: list[Service] +def endpoint_to_pop(endpoint: dict[str, Any], inprov_equipment_dict: list[dict[str, str]]) -> str: + + def _hostname_to_equipment(_hn): + m = re.match(r'^(.+)\.geant\.net$', _hn) + if not m: + logger.error(f'unexpected hostname pattern: {_hn}') + return '?' + return m.group(1).upper() + + if 'hostname' in endpoint: + eq_name = _hostname_to_equipment(endpoint['hostname']) + elif 'equipment' in endpoint: + eq_name = endpoint['equipment'] + else: + # should already be validated + raise AssertionError(f'no equipment or hostname in endpoint: {endpoint}') + + if eq_name not in inprov_equipment_dict: + # TODO: is this really possible if all data is read from IMS at the same time? + logger.error(f'unknown endpoint equipment: {eq_name}') + return '?' + + return inprov_equipment_dict[eq_name]['pop'] + + + def _services(service_type: str | None = None) -> Generator[Service]: """ @@ -52,6 +76,7 @@ def _services(service_type: str | None = None) -> Generator[Service]: # poller_interfaces = cache.get(inventory.INPROV_POLLER_INTERFACES_CACHE_FILENAME) correlator_state = cache.get(correlator.CACHED_CORRELATOR_STATE_FILENAME) brian_rates = cache.get(brian.CACHED_BRIAN_SCID_RATES_FILENAME) + equipment_list = cache.get(inventory.INPROV_EQUIPMENT_CACHE_FILENAME) except FileNotFoundError: logger.exception('not enough data available to build the service list') return @@ -68,6 +93,9 @@ def _services(service_type: str | None = None) -> Generator[Service]: down_service_sids = set(_get_down_correlator_services()) brian_scid_rates = {r['scid']: r['values'] for r in brian_rates} + equipment_dict = {_x['name']: _x for _x in equipment_list} + _endpoint_to_pop = functools.partial(endpoint_to_pop, inprov_equipment_dict=equipment_dict) + for _s in scid_current: if _s['status'] != 'operational': @@ -76,6 +104,8 @@ def _services(service_type: str | None = None) -> Generator[Service]: if service_type and _s['service_type'] != service_type: continue + pops = sorted(set(map(_endpoint_to_pop, _s['endpoints']))) + rates = brian_scid_rates.get(_s['scid'], {}) overlays = Overlays( speed = _s['speed'], @@ -94,18 +124,12 @@ def _services(service_type: str | None = None) -> Generator[Service]: ), ) - endpoints = [] - for _e in _s['endpoints']: - equipment = _e['hostname'] if 'hostname' in _e else _e['equipment'] - interface = _e['interface'] if 'interface' in _e else _e['port'] - endpoints.append(Endpoint(equipment=equipment, interface=interface)) - yield Service( sid = _s['sid'], scid = _s['scid'], name = _s['name'], type = _s['service_type'], - endpoints = endpoints, + pops = pops, overlays = overlays, ) diff --git a/test/conftest.py b/test/conftest.py index 0f1fa35aa78b63917e1c6f2f0ee7e866383a020e..6e7892ae85a98d26af90f34f5bf9ae9ec80a1ef2 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -58,9 +58,10 @@ def client(dummy_config_filename): # there's no rmq in the test config data, so cache won't be initialized cache.init(tmp_dir) - cache.set(inventory.INPROV_MAP_SERVICES_CACHE_FILENAME, load_test_data('inprov-services.json')) + # cache.set(inventory.INPROV_MAP_SERVICES_CACHE_FILENAME, load_test_data('inprov-services.json')) cache.set(inventory.REPORTING_SCID_CURRENT_CACHE_FILENAME, load_test_data('scid-current.json')) - cache.set(inventory.INPROV_POLLER_INTERFACES_CACHE_FILENAME, load_test_data('poller-interfaces.json')) + cache.set(inventory.INPROV_EQUIPMENT_CACHE_FILENAME, load_test_data('inprov-equipment.json')) + # cache.set(inventory.INPROV_POLLER_INTERFACES_CACHE_FILENAME, load_test_data('poller-interfaces.json')) cache.set(correlator.CACHED_CORRELATOR_STATE_FILENAME, load_test_data('correlator-state.json')) cache.set(brian.CACHED_BRIAN_SCID_RATES_FILENAME, load_test_data('brian-scid-rates.json')) diff --git a/test/test_inventory.py b/test/test_inventory.py index 48f05e3051a6be80f540d503025af31272d77f9e..ee205c7f6ef00b58b813ec0861d8001502a9ac62 100644 --- a/test/test_inventory.py +++ b/test/test_inventory.py @@ -36,8 +36,8 @@ def test_inventory_service_download(): cache.init(tmp_dir) inventory._load_all_inventory( - inventory_base_uri='https://dummy-hostname.dummy.domain', - reporting_base_uri='https://another-dummy-hostname.dummy.domain') + inventory_base_uri=inventory_base_uri, + reporting_base_uri=reporting_base_uri) # assert os.path.exists(os.path.join(tmp_dir, services.POLLER_INTERFACES_CACHE_FILENAME)) diff --git a/test/test_map_endpoints.py b/test/test_map_endpoints.py index f71a56a5429397cf03a40ac7397af23fe51306b9..a8bd3956560da3c652a231daf3924e00ce8a3808 100644 --- a/test/test_map_endpoints.py +++ b/test/test_map_endpoints.py @@ -2,40 +2,41 @@ import re import responses -from mapping_provider.api.map import RouterList, SiteList +from mapping_provider.api.map import EquipmentList, PopList from mapping_provider.backends.services import ServiceList from .common import load_test_data @responses.activate -def test_get_sites(client): +def test_get_pops(client): responses.add( method=responses.GET, - url=re.compile(r'.*/map/sites$'), - json=load_test_data('inprov-sites.json') + url=re.compile(r'.*/map/pops$'), + json=load_test_data('inprov-pops.json') ) - rv = client.get("/map/sites") + rv = client.get("/map/pops") assert rv.status_code == 200 - site_list = SiteList.model_validate(rv.json()) - assert site_list.sites, 'test data should not be empty' + pop_list = PopList.model_validate(rv.json()) + assert pop_list.pops, 'test data should not be empty' @responses.activate -def test_get_routers(client): +def test_get_equipment(client): responses.add( method=responses.GET, - url=re.compile(r'.*/map/routers$'), - json=load_test_data('inprov-routers.json') + url=re.compile(r'.*/map/equipment$'), + json=load_test_data('inprov-equipment.json') ) - rv = client.get("/map/routers") + rv = client.get("/map/equipment") assert rv.status_code == 200 - router_list = RouterList.model_validate(rv.json()) - assert router_list.routers, 'test data should not be empty' + equipment_list = EquipmentList.model_validate(rv.json()) + assert equipment_list.equipment, 'test data should not be empty' + @responses.activate def test_get_trunks(client):