diff --git a/inventory_provider/routes/poller.py b/inventory_provider/routes/poller.py index d812b0a8b1ebe34254a50752ddf615a6115db25f..1939e5d4b83e561169b046f2dd10c0d6be0bb2b2 100644 --- a/inventory_provider/routes/poller.py +++ b/inventory_provider/routes/poller.py @@ -36,12 +36,18 @@ These endpoints are intended for use by BRIAN. .. autofunction:: inventory_provider.routes.poller.gws_indirect -/poller/services</type> +/poller/services</service-type> --------------------------------- .. autofunction:: inventory_provider.routes.poller.get_services +/poller/service-types +--------------------------------- + +.. autofunction:: inventory_provider.routes.poller.get_service_types + + """ import json import logging @@ -52,6 +58,7 @@ from lxml import etree from inventory_provider import juniper from inventory_provider.routes import common +from inventory_provider.tasks.common import ims_sorted_service_type_key from inventory_provider.routes.classifier import get_ims_equipment_name, \ get_ims_interface from inventory_provider.routes.common import _ignore_cache_or_retrieve @@ -245,6 +252,13 @@ SERVICES_LIST_SCHEMA = { } +STRING_LIST_SCHEMA = { + '$schema': 'http://json-schema.org/draft-07/schema#', + 'type': 'array', + 'items': {'type': 'string'} +} + + @routes.after_request def after_request(resp): return common.after_request(resp) @@ -706,25 +720,22 @@ def gws_direct(): return Response(result, mimetype="application/json") -@routes.route('/services', methods=['GET', 'POST']) -@routes.route('/services/<service_type>', methods=['GET', 'POST']) -@common.require_accepts_json -def get_services(service_type=None): +def _get_services_internal(service_type=None): """ - Handler for `/poller/services/service_type`. + Performs the lookup and caching done for calls to + `/poller/services</service-type>` + + This is a separate private utility so that it can be called by + :meth:`inventory_provider.routes.poller.get_services` + and :meth:`inventory_provider.routes.poller.get_service_types` The response will be formatted according to the following schema: .. asjson:: inventory_provider.routes.poller.SERVICES_LIST_SCHEMA - A few possible values for `service_type` are (currently): gws_internal, - geant_ip, geant_lambda, l3_vpn, md_vpn_proxy (there are more) - - If the endpoint is called with no `service_type`, - all services are returned - - :return: + :param service_type: a service type, or None to return all + :return: service list, json-serialized to a string """ def _services(): key_pattern = f'ims:services:{service_type}:*' \ @@ -765,18 +776,51 @@ def get_services(service_type=None): result = list(result) 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 None # cache this data for the next call result = json.dumps(result) - redis.set(cache_key, result.encode('utf-8')) + redis.set(cache_key, result) + + return result + +@routes.route('/services', methods=['GET', 'POST']) +@routes.route('/services/<service_type>', methods=['GET', 'POST']) +@common.require_accepts_json +def get_services(service_type=None): + """ + Handler for `/poller/services</service-type>` + + Use `/poller/service-types` for possible values of `service-type`. + + If the endpoint is called with no `service-type`, + all services are returned. + + The response will be formatted according to the following schema: - return Response(result, mimetype='application/json') + .. asjson:: + inventory_provider.routes.poller.SERVICES_LIST_SCHEMA + + A few possible values for `service_type` are (currently): gws_internal, + geant_ip, geant_lambda, l3_vpn, md_vpn_proxy (there are more) + + If the endpoint is called with no `service_type`, + all services are returned + + :return: + """ + + services_json_str = _get_services_internal(service_type) + + if not services_json_str: + message = f'no {service_type} services found' \ + if service_type else 'no services found' + return Response( + response=message, + status=404, + mimetype='text/html') + + return Response(services_json_str, mimetype='application/json') @routes.route("/gws/indirect", methods=['GET', 'POST']) @@ -791,3 +835,45 @@ def gws_indirect(): :return: """ return get_services(service_type='gws_indirect') + + +@routes.route('/service-types', methods=['GET', 'POST']) +@common.require_accepts_json +def get_service_types(): + """ + Handler for `/poller/service-types` + + This method returns a list of all values of `service_type` that + can be used with `/poller/services/service-type` to return + a non-empty list of services. + + The response will be formatted according to the following schema: + + .. asjson:: + inventory_provider.routes.poller.STRING_LIST_SCHEMA + + """ + cache_key = f'classifier-cache:poller:service-types' + + redis = common.get_current_redis() + service_types = _ignore_cache_or_retrieve(request, cache_key, redis) + + if not service_types: + all_services = json.loads(_get_services_internal()) + service_types = { + ims_sorted_service_type_key(s['type']) + for s in all_services + } + + if not service_types: + return Response( + response='no service types found', + status=404, + mimetype='text/html') + + # cache this data for the next call + service_types = sorted(list(service_types)) + service_types = json.dumps(service_types) + redis.set(cache_key, service_types) + + return Response(service_types, mimetype='application/json') diff --git a/inventory_provider/tasks/common.py b/inventory_provider/tasks/common.py index 427d51d8a18e4128f3dd3b174274e65fa17f5f8f..f7942d2f4e28826b4723dd9b7e92521d7c6815de 100644 --- a/inventory_provider/tasks/common.py +++ b/inventory_provider/tasks/common.py @@ -1,5 +1,6 @@ import json import logging +import re import time import jsonschema @@ -218,3 +219,23 @@ def get_next_redis(config): 'derived next id: {}'.format(next_id)) return _get_redis(config, next_id) + + +def ims_sorted_service_type_key(service_type): + """ + special-purpose function used for mapping IMS service type strings + to the key used in redis ('ims:services:*') + + this method is only used by + :meth:`inventory_provider:tasks:worker.update_circuit_hierarchy_and_port_id_services + and :meth:`inventory_provider:routes.poller.get_service_types + + :param service_type: IMS-formatted service_type string + :return: ims sorted service type redis key + """ # noqa: E501 + + service_type = re.sub( + r'[^a-zA-Z0-9]+', '_', service_type.lower()) + # prettification ... e.g. no trailing _ for 'MD-VPN (NATIVE)' + service_type = re.sub('_+$', '', service_type) + return re.sub('^_+', '', service_type) diff --git a/inventory_provider/tasks/worker.py b/inventory_provider/tasks/worker.py index 58bccc8252ffda083a0243fa4def71477e6f7200..4d7e33c506390db0a5b76b129dabe06d02156ffd 100644 --- a/inventory_provider/tasks/worker.py +++ b/inventory_provider/tasks/worker.py @@ -29,7 +29,8 @@ from inventory_provider.db.ims import IMS from inventory_provider.tasks.app import app from inventory_provider.tasks.common \ import get_next_redis, get_current_redis, \ - latch_db, get_latch, set_latch, update_latch_status + latch_db, get_latch, set_latch, update_latch_status, \ + ims_sorted_service_type_key from inventory_provider.tasks import monitor from inventory_provider import config from inventory_provider import environment @@ -762,14 +763,8 @@ def update_circuit_hierarchy_and_port_id_services(self, use_current=False): circ['calculated-speed'] = _get_speed(circ['id']) _format_service(circ) - service_type_key = re.sub( - r'[^a-zA-Z0-9]+', '_', circ['service_type'].lower()) - # prettification ... e.g. no trailing _ for 'MD-VPN (NATIVE)' - service_type_key = re.sub('_+$', '', service_type_key) - service_type_key = re.sub('^_+', '', service_type_key) - type_services = services_by_type.setdefault( - service_type_key, dict()) + ims_sorted_service_type_key(circ['service_type']), dict()) type_services[circ['id']] = circ interface_services[k].extend(circuits) diff --git a/test/test_general_poller_routes.py b/test/test_general_poller_routes.py index b2179230221b5d5b2cc2c4fbeec8196512016b12..f9359a367183ab093ab47a5e8fdef7280399663b 100644 --- a/test/test_general_poller_routes.py +++ b/test/test_general_poller_routes.py @@ -112,3 +112,16 @@ def test_all_services_by_type(client, uri_type, expected_type): assert response_data # test data is non-empty assert all(s['type'] == expected_type for s in response_data) + + +def test_get_service_types(client): + rv = client.get( + f'/poller/service-types', + 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, poller.STRING_LIST_SCHEMA) + + assert response_data # test data is non-empty +