From ac051c7a2d4967f99ba5724db6e765a565ae6b37 Mon Sep 17 00:00:00 2001 From: Erik Reid <erik.reid@geant.org> Date: Tue, 20 May 2025 13:10:30 +0200 Subject: [PATCH] first version of /map/services --- inventory_provider/__init__.py | 3 + inventory_provider/routes/__init__.py | 1 + inventory_provider/routes/map.py | 135 ++++++++++++++++++++++++++ inventory_provider/routes/msr.py | 64 +++++++----- test/test_map_routes.py | 20 ++++ 5 files changed, 197 insertions(+), 26 deletions(-) create mode 100644 inventory_provider/routes/map.py create mode 100644 test/test_map_routes.py diff --git a/inventory_provider/__init__.py b/inventory_provider/__init__.py index 3155d6cc..fb6aa000 100644 --- a/inventory_provider/__init__.py +++ b/inventory_provider/__init__.py @@ -93,6 +93,9 @@ def create_app(setup_logging=True): from inventory_provider.routes import state_checker app.register_blueprint(state_checker.routes, url_prefix='/state-checker') + from inventory_provider.routes import map + app.register_blueprint(map.routes, url_prefix='/map') + if app.config.get('ENABLE_TESTING_ROUTES', False): from inventory_provider.routes import testing app.register_blueprint(testing.routes, url_prefix='/testing') diff --git a/inventory_provider/routes/__init__.py b/inventory_provider/routes/__init__.py index 4cd55dfe..bbf6f098 100644 --- a/inventory_provider/routes/__init__.py +++ b/inventory_provider/routes/__init__.py @@ -36,4 +36,5 @@ and www.json.org for more details. .. automodule:: inventory_provider.routes.neteng +.. automodule:: inventory_provider.routes.map """ diff --git a/inventory_provider/routes/map.py b/inventory_provider/routes/map.py new file mode 100644 index 00000000..f75a65ca --- /dev/null +++ b/inventory_provider/routes/map.py @@ -0,0 +1,135 @@ +""" +mapping support endpoints +========================= + +These endpoints are intended for use by the mapping-provider + +.. contents:: :local: + +/map/sites +--------------------------------- + +.. autofunction:: inventory_provider.routes.map.sites + + +/map/routers +--------------------------------- + +.. autofunction:: inventory_provider.routes.map.routers + + +/map/services/<service-type> +--------------------------------- + +.. autofunction:: inventory_provider.routes.map.services + +""" +import json +import logging + +from flask import Blueprint, Response, current_app, request, jsonify + +from inventory_provider.routes import common +from inventory_provider.routes.classifier import get_ims_equipment_name, \ + get_ims_interface +from inventory_provider.routes import msr + +logger = logging.getLogger(__name__) +routes = Blueprint('map-support-routes', __name__) + + +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': ['sid', 'name', 'type', 'endpoints', 'overlays'], + 'additionalProperties': False + }, + }, + + 'type': 'array', + 'items': {'$ref': '#/definitions/service'} +} + + +@routes.route("/services", methods=['GET']) +@routes.route('/services/<service_type>', methods=['GET']) +@common.require_accepts_json +def services(service_type=None): + """ + Handler for `/map/services` and + `/map/services/<service_type>` + which returns information for either all services + or those of a specific service type. + + This endpoint is used by the mapping-provider to get + static information about all operationalservices. + + .. asjson:: + inventory_provider.routes.map.SERVICE_LIST_SCHEMA + + :param service_type: optional, if present should be a router hostname + :return: + """ + + cache_key_all_services = "classifier-cache:map:services" + + r = common.get_current_redis() + all_map_service_info = common._ignore_cache_or_retrieve(request, cache_key_all_services, r) + if not all_map_service_info: + + def _operational(s): + return s["status"].lower() == "operational" + + def _reformat_service(s): + return { + "name": s["name"], + "endpoints": s["endpoints"], + "sid": s["sid"], + "type": s["service_type"], + "overlays": { + "speed": s["speed"], + } + } + + msr_services = filter(_operational, msr.load_all_msr_services()) + all_map_service_info = map(_reformat_service, msr_services) + all_map_service_info = list(all_map_service_info) + r.set(cache_key_all_services, json.dumps(all_map_service_info)) + + if service_type: + result = [s for s in all_map_service_info if s["type"] == service_type] + else: + result = all_map_service_info + + if not result: + message = f'no {service_type} services found' \ + if service_type else 'no services found' + return Response( + response=message, + status=404, + mimetype='text/html') + + return jsonify(result) diff --git a/inventory_provider/routes/msr.py b/inventory_provider/routes/msr.py index 9ccdb340..16505053 100644 --- a/inventory_provider/routes/msr.py +++ b/inventory_provider/routes/msr.py @@ -108,7 +108,7 @@ import threading from collections import defaultdict from typing import Dict -from flask import Blueprint, Response, request, current_app +from flask import Blueprint, Response, request, current_app, jsonify import jsonschema from inventory_provider.routes import common @@ -1061,24 +1061,12 @@ def _endpoint_extractor(all_interfaces: Dict, endpoint_details: Dict): 'port': endpoint_details['port'] } - -@routes.route('/services', methods=['GET']) -@common.require_accepts_json -def get_system_correlation_services(): +def load_all_msr_services(): """ - Handler for `/msr/services` + utility method used to construct the response for /msr/services, + as well as other endpoints that construct similar responses - This method returns all known services with with information required - by the reporting tool stack. - - cf. https://jira.software.geant.org/browse/POL1-530 - - The response will be formatted as follows: - - .. asjson:: - inventory_provider.routes.msr.SYSTEM_CORRELATION_SERVICES_LIST_SCHEMA - - :return: + :return: List of all reportable services """ def _get_redundancy_asn(endpoints): @@ -1099,14 +1087,14 @@ def get_system_correlation_services(): cache_key = 'classifier-cache:msr:services' r = common.get_current_redis() - response = _ignore_cache_or_retrieve(request, cache_key, r) - if not response: + all_msr_services = _ignore_cache_or_retrieve(request, cache_key, r) + if not all_msr_services: all_interfaces = _load_all_interfaces() sid_services = json.loads(r.get('ims:sid_services').decode('utf-8')) - response = [] + all_msr_services = [] for sid, details in sid_services.items(): service_info = {'endpoints': []} for d in details: @@ -1128,14 +1116,38 @@ def get_system_correlation_services(): asn = _get_redundancy_asn(service_info['endpoints']) if asn: service_info['redundant_asn'] = asn - response.append(service_info) + all_msr_services.append(service_info) + # sanity jsonschema.validate( - response, SYSTEM_CORRELATION_SERVICES_LIST_SCHEMA) + all_msr_services, SYSTEM_CORRELATION_SERVICES_LIST_SCHEMA) - if response: - response = json.dumps(response, indent=2) - # r.set(cache_key, response.encode('utf-8')) + if all_msr_services: + # expected to always be non-empty + r.set(cache_key, json.dumps(all_msr_services).encode('utf-8')) + + return all_msr_services + +@routes.route('/services', methods=['GET']) +@common.require_accepts_json +def get_system_correlation_services(): + """ + Handler for `/msr/services` + + This method returns all known services with with information required + by the reporting tool stack. + + cf. https://jira.software.geant.org/browse/POL1-530 + + The response will be formatted as follows: + + .. asjson:: + inventory_provider.routes.msr.SYSTEM_CORRELATION_SERVICES_LIST_SCHEMA + + :return: + """ + + response = load_all_msr_services() if not response: return Response( @@ -1143,7 +1155,7 @@ def get_system_correlation_services(): status=404, mimetype="text/html") - return Response(response, mimetype="application/json") + return jsonify(response) @routes.route('/bgp', methods=['GET']) diff --git a/test/test_map_routes.py b/test/test_map_routes.py new file mode 100644 index 00000000..7b6943f5 --- /dev/null +++ b/test/test_map_routes.py @@ -0,0 +1,20 @@ +import ipaddress +import json +import jsonschema + +import pytest + +from inventory_provider.routes.map import SERVICE_LIST_SCHEMA + +DEFAULT_REQUEST_HEADERS = {'Accept': ['application/json']} + + +def test_get_all_services(client): + rv = client.get( + '/map/services', + headers=DEFAULT_REQUEST_HEADERS) + assert rv.status_code == 200 + assert rv.is_json + response_data = json.loads(rv.data.decode('utf-8')) + jsonschema.validate(response_data, SERVICE_LIST_SCHEMA) + assert response_data # test data is non-empty -- GitLab