diff --git a/inventory_provider/__init__.py b/inventory_provider/__init__.py index 3155d6cce97b98ca09e7942da6806f23292a2181..fb6aa000e7510036de79c6f532c68192a4f62a1f 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 4cd55dfe6fb4089cb72a0b3ebed1c07d0c7eddb3..bbf6f098e71c4a5e4e62f9f4857e2e9709c06214 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 0000000000000000000000000000000000000000..f75a65cac16ef5cd3b6e6fef4f5ba0dd8c37c218 --- /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 9ccdb3409777c363a8d4d39c091b759390c97a59..1650505335e9375901646e7967094d6e90ce038b 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 0000000000000000000000000000000000000000..7b6943f59cdb5b9338e0dfa63de610035aaaa763 --- /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