diff --git a/mapping_provider/api/map.py b/mapping_provider/api/map.py index 59f4ede7df4fe0809014dc8ffe225ae8ac46b3aa..54fdba9b8b3f03bee869be61c44731966415784c 100644 --- a/mapping_provider/api/map.py +++ b/mapping_provider/api/map.py @@ -2,7 +2,7 @@ from typing import Any import jsonschema import requests -from fastapi import APIRouter +from fastapi import APIRouter, HTTPException from pydantic import BaseModel from mapping_provider import config @@ -128,9 +128,21 @@ def get_equipment() -> EquipmentList: return EquipmentList(equipment=list(map(_make_equipment, equipment_list_obj))) +@router.get("/services") +@router.get("/services/{service_type}") +def get_services(service_type: str | None = None) -> services.ServiceList: + """ + handler for /trunks + """ + return_value = services.build_service_info_list(service_type=service_type) + if not return_value.services: + raise HTTPException(status_code=404, detail=f'unrecognized service type: {service_type}') + return return_value + + @router.get("/trunks") def get_trunks() -> services.ServiceList: """ - handler for /trunks + handler for /trunks, same as /services/IP TRUNK """ - return services.build_service_info_list(service_type='IP TRUNK') + return get_services(service_type='IP TRUNK') diff --git a/mapping_provider/backends/services.py b/mapping_provider/backends/services.py index 7e7f2a3cfae9156db094e8306d3beb51cf3439be..5e0f307c5e828acd692a7fb8aa84fb154aca6384 100644 --- a/mapping_provider/backends/services.py +++ b/mapping_provider/backends/services.py @@ -30,6 +30,7 @@ class Service(BaseModel): name: str type: str pops: list[str] + equipment: list[str] overlays: Overlays @@ -37,8 +38,11 @@ class ServiceList(BaseModel): services: list[Service] -def endpoint_to_pop(endpoint: dict[str, Any], inprov_equipment_dict: dict[str, dict[str, Any]]) -> str: - +def endpoint_equipment(endpoint: dict[str, Any]) -> str: + """ + convert the correlator router hostname or optical equipment name + to the inventory equipment format + """ def _hostname_to_equipment(_hn: str) -> str: m = re.match(r'^(.+)\.geant\.net$', _hn) if not m: @@ -47,21 +51,12 @@ def endpoint_to_pop(endpoint: dict[str, Any], inprov_equipment_dict: dict[str, d return m.group(1).upper() if 'hostname' in endpoint: - eq_name = _hostname_to_equipment(endpoint['hostname']) + return _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 endpoint['equipment'] - pop_name = inprov_equipment_dict[eq_name]['pop'] - assert isinstance(pop_name, str) # mypy noise - return pop_name + # should already be validated + raise AssertionError(f'no equipment or hostname in endpoint: {endpoint}') def _services(service_type: str | None = None) -> Generator[Service]: @@ -92,7 +87,15 @@ def _services(service_type: str | None = None) -> Generator[Service]: 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) + + def _get_equipment_pop(equipment_name: str) -> str: + if equipment_name not in equipment_dict: + # TODO: is this really possible if all data is read from IMS at the same time? + logger.error(f'unknown endpoint equipment: {equipment_name}') + return '?' + _pop_name = equipment_dict[equipment_name]['pop'] + assert isinstance(_pop_name, str) # mypy noise + return _pop_name for _s in scid_current: @@ -102,23 +105,24 @@ 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']))) + equipment = sorted(set(map(endpoint_equipment, _s['endpoints']))) + pops = sorted(set(map(_get_equipment_pop, equipment))) rates = brian_scid_rates.get(_s['scid'], {}) overlays = Overlays( speed = _s['speed'], up = _s['sid'] not in down_service_sids, latest = BitRates( - egress = rates['latest']['egress'], - ingress = rates['latest']['ingress'], + egress = rates.get('latest', {}).get('egress'), + ingress = rates.get('latest', {}).get('ingress'), ), mean = BitRates( - egress = rates['mean']['egress'], - ingress = rates['mean']['ingress'], + egress = rates.get('mean', {}).get('egress'), + ingress = rates.get('mean', {}).get('ingress'), ), max = BitRates( - egress = rates['max']['egress'], - ingress = rates['max']['ingress'], + egress = rates.get('max', {}).get('egress'), + ingress = rates.get('max', {}).get('ingress'), ), ) @@ -128,6 +132,7 @@ def _services(service_type: str | None = None) -> Generator[Service]: name = _s['name'], type = _s['service_type'], pops = pops, + equipment = equipment, overlays = overlays, ) diff --git a/test/test_map_endpoints.py b/test/test_map_endpoints.py index ede29b4544244fb0d75df48718b239ea4c1e8fb7..e3a104fb78b352fe052f048e492df1e98c2458b0 100644 --- a/test/test_map_endpoints.py +++ b/test/test_map_endpoints.py @@ -1,5 +1,6 @@ import re +import pytest import responses from mapping_provider.api.map import EquipmentList, PopList @@ -38,9 +39,58 @@ def test_get_equipment(client): assert equipment_list.equipment, 'test data should not be empty' + + +@responses.activate +@pytest.mark.parametrize('service_type', [ + 'IP PEERING - R&E', + 'GEANT SPECTRUM SERVICE', + 'L3-VPN', + 'OOB IP LINK', + 'GEANT OPEN CROSS CONNECT', + 'GEANT - GBS', + 'GWS - INDIRECT', + 'GEANT IP', + 'POP LAN LINK', + 'IP PEERING - NON R&E (PUBLIC)', + 'IP TRUNK', + 'GEANT PLUS', + 'L2SERVICES', + 'ETHERNET', + 'EUMETSAT TERRESTRIAL', + 'IP PEERING - NON R&E (PRIVATE)', + 'EXPRESS ROUTE', + 'CBL1', + 'GWS - UPSTREAM', + 'GEANT PEERING', + 'SERVER LINK', + 'GEANT MANAGED WAVELENGTH SERVICE', + 'CORPORATE', + 'EUMETSAT GRE']) +def test_get_services(client, service_type): + rv = client.get(f"/map/services/{service_type}") + assert rv.status_code == 200 + service_list = ServiceList.model_validate(rv.json()) + assert service_list.services, 'test data should not be empty' + assert all(s.type == service_type for s in service_list.services) + +def test_get_unknown_service_type(client): + rv = client.get(f"/map/services/BOGUS_SERVICE_TYPE") + assert rv.status_code == 404 + + +def test_get_all_services(client): + rv = client.get("/map/services") + assert rv.status_code == 200 + service_list = ServiceList.model_validate(rv.json()) + assert service_list.services, 'test data should not be empty' + @responses.activate def test_get_trunks(client): rv = client.get("/map/trunks") assert rv.status_code == 200 service_list = ServiceList.model_validate(rv.json()) assert service_list.services, 'test data should not be empty' + assert all(s.type == 'IP TRUNK' for s in service_list.services) + +