diff --git a/docs/source/protocol.rst b/docs/source/protocol.rst index dd878449d8d6d2a26178e837baf8e7c49454780c..ccbb59d5edff3d86b11f491d59a10d6b5e7f8be0 100644 --- a/docs/source/protocol.rst +++ b/docs/source/protocol.rst @@ -39,3 +39,5 @@ API modules .. automodule:: inventory_provider.routes.data .. automodule:: inventory_provider.routes.jobs + +.. automodule:: inventory_provider.routes.neteng diff --git a/inventory_provider/__init__.py b/inventory_provider/__init__.py index d350c9ebbcd98a251485f562b3259f9980ae8721..b3ab8f8a1f71ef03c5a0ca0f456de37578e38042 100644 --- a/inventory_provider/__init__.py +++ b/inventory_provider/__init__.py @@ -74,6 +74,9 @@ def create_app(): from inventory_provider.routes import lnetd app.register_blueprint(lnetd.routes, url_prefix='/LnetD') + from inventory_provider.routes import neteng + app.register_blueprint(neteng.routes, url_prefix='/neteng') + 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/db/ims_data.py b/inventory_provider/db/ims_data.py index f0f9ddb516115a1e0435b9b390a933832122c340..e3a59750d9cb1b30336eb95494ebc8b14f3b4b51 100644 --- a/inventory_provider/db/ims_data.py +++ b/inventory_provider/db/ims_data.py @@ -26,6 +26,40 @@ IMS_OPSDB_STATUS_MAP = { STATUSES_TO_IGNORE = \ [InventoryStatus.OUT_OF_SERVICE.value] +NODE_LOCATION_SCHEMA = { + '$schema': 'http://json-schema.org/draft-07/schema#', + 'definitions': { + 'pop-location': { + 'type': 'object', + 'properties': { + 'name': {'type': 'string'}, + 'city': {'type': 'string'}, + 'country': {'type': 'string'}, + 'abbreviation': {'type': 'string'}, + 'longitude': {'type': 'number'}, + 'latitude': {'type': 'number'} + }, + 'required': [ + 'name', + 'city', + 'country', + 'abbreviation', + 'longitude', + 'latitude'], + 'additionalProperties': False + } + }, + + 'type': 'object', + 'properties': { + 'equipment-name': {'type': 'string'}, + 'status': {'type': 'string'}, + 'pop': {'$ref': '#/definitions/pop-location'} + }, + 'required': ['equipment-name', 'status', 'pop'], + 'additionalProperties': False +} + def get_non_monitored_circuit_ids(ds: IMS): # note the id for the relevant field is hard-coded. I didn't want to use @@ -300,6 +334,17 @@ def get_circuit_hierarchy(ds: IMS): def get_node_locations(ds: IMS): + """ + return location info for all Site nodes + + yields dictionaries formatted as: + + .. as_json:: + inventory_provider.db.ims_data.NODE_LOCATION_SCHEMA + + :param ds: + :return: yields dicts as above + """ site_nav_props = [ ims.SITE_PROPERTIES['City'], ims.SITE_PROPERTIES['SiteAliases'], diff --git a/inventory_provider/routes/neteng.py b/inventory_provider/routes/neteng.py new file mode 100644 index 0000000000000000000000000000000000000000..29f8abcd9c963c06ac40fed5063d32f401435a28 --- /dev/null +++ b/inventory_provider/routes/neteng.py @@ -0,0 +1,68 @@ +""" +Neteng Support Endpoints +========================= + +These endpoints are intended for use by neteng tools. + + +.. contents:: :local: + +/neteng/location/equipment-name +--------------------------------- + +.. autofunction:: inventory_provider.routes.neteng.get_location + +""" +import json +import logging +import threading + +from flask import Blueprint, Response, jsonify + +from inventory_provider.routes import common + +routes = Blueprint('neteng-query-routes', __name__) +logger = logging.getLogger(__name__) +_subnet_lookup_semaphore = threading.Semaphore() + + +@routes.after_request +def after_request(resp): + return common.after_request(resp) + + +@routes.route('/location/<equipment>', methods=['GET', 'POST']) +@common.require_accepts_json +def get_location(equipment): + """ + Handler for `/neteng/location/equipment-name` + + This method will pop location information for the IMS node + with name = `equipment-name`. + + 404 is returned if the IMS node name is not known. + Otherwise the return value will be formatted as: + + .. asjson:: + inventory_provider.db.ims_data.NODE_LOCATION_SCHEMA + + :return: as above + """ + + r = common.get_current_redis() + + value = r.get(f'ims:location:{equipment}') + if not value: + return Response( + response='no location information available for "{equipment}"', + status=404, + mimetype='text/html') + + value = json.loads(value.decode('utf-8')) + if not value: + return Response( + response='unexpected empty cached data for "{equipment}"', + status=500, + mimetype='text/html') + + return jsonify(value[0]) diff --git a/test/test_ims_data.py b/test/test_ims_data.py index cd8d7e421eb8750f077bf74a22c8f6b3ee3d019c..7d07aff8e7b0ee6ef3a13801beae0a4cd3cd71a2 100644 --- a/test/test_ims_data.py +++ b/test/test_ims_data.py @@ -1,19 +1,30 @@ import json +import os + +import jsonschema import inventory_provider from inventory_provider.db.ims import InventoryStatus from inventory_provider.db.ims_data import lookup_lg_routers, \ get_node_locations, IMS_OPSDB_STATUS_MAP, \ get_port_id_services, get_port_details, \ - get_circuit_hierarchy + get_circuit_hierarchy, NODE_LOCATION_SCHEMA + + +def _json_test_data(filename): + abs_filename = os.path.join( + os.path.dirname(__file__), + 'data', + filename) + with open(abs_filename) as data: + return json.load(data) def test_get_circuit_hierarchy(mocker): ds = inventory_provider.db.ims.IMS( 'http://dummy_base', 'dummy_username', 'dummy_password') - with open('test/data/ims_circuit_hierarchy_data.json') as data: - se_data = json.load(data) + se_data = _json_test_data('ims_circuit_hierarchy_data.json') mocker.patch.object( inventory_provider.db.ims.IMS, 'get_filtered_entities', @@ -55,8 +66,7 @@ def test_get_circuit_hierarchy(mocker): def test_get_port_details(mocker): def _se(entity, y, step_count): - with open(f'test/data/ims_{entity}_details_data.json') as data: - return json.load(data) + return _json_test_data(f'ims_{entity}_details_data.json') mocker.patch.object( inventory_provider.db.ims.IMS, @@ -105,8 +115,7 @@ def test_get_port_details(mocker): def test_get_port_id_services(mocker): - with open('test/data/ims_port_id_services_data.json') as data: - d = json.load(data) + d = _json_test_data('ims_port_id_services_data.json') mocker.patch.object( inventory_provider.db.ims.IMS, @@ -215,8 +224,8 @@ def test_get_port_id_services(mocker): def test_lookup_lg_routers(mocker): ims = mocker.patch('inventory_provider.db.ims.IMS') - with open('test/data/ims_lg_data.json') as data: - ims.return_value.get_filtered_entities.return_value = json.load(data) + ims.return_value.get_filtered_entities.return_value \ + = _json_test_data('ims_lg_data.json') ims.return_value.get_entity_by_id.return_value = { 'name': 'pop name', 'longitude': 'long', @@ -266,13 +275,16 @@ def test_lookup_lg_routers(mocker): def test_get_node_location(mocker): ims = mocker.patch('inventory_provider.db.ims.IMS') - with open('test/data/ims_nodes_data.json') as data: - resp_data = json.load(data) + resp_data = _json_test_data('ims_nodes_data.json') ims.return_value.get_all_entities.return_value = resp_data ds = inventory_provider.db.ims.IMS( 'dummy_base', 'dummy_username', 'dummy_password') res = list(get_node_locations(ds)) + for name, node in res: + assert isinstance(name, str) + jsonschema.validate(node, NODE_LOCATION_SCHEMA) + assert len(res) == 36 assert res[0] == ('LON3_CX_01', { 'equipment-name': 'LON3_CX_01', diff --git a/test/test_neteng_routes.py b/test/test_neteng_routes.py new file mode 100644 index 0000000000000000000000000000000000000000..1e95322a3be4cd43a83caeea12c2a9f6d35e034e --- /dev/null +++ b/test/test_neteng_routes.py @@ -0,0 +1,27 @@ +import json +import jsonschema +import pytest + +from inventory_provider.db.ims_data import NODE_LOCATION_SCHEMA + + +@pytest.mark.parametrize('equipment_name', [ + 'MX1.AMS.NL', + 'MIL-OLA1', + 'LON02-GRV1' +]) +def test_location(client, mocked_redis, equipment_name): + rv = client.post( + f'/neteng/location/{equipment_name}', + headers={'Accept': ['application/json']}) + assert rv.status_code == 200 + jsonschema.validate( + json.loads(rv.data.decode('utf-8')), + NODE_LOCATION_SCHEMA) + + +def test_location_not_found(client, mocked_redis): + rv = client.post( + '/neteng/location/BOGUS.EQUIPMENT.NAME', + headers={'Accept': ['application/json']}) + assert rv.status_code == 404