diff --git a/inventory_provider/routes/poller.py b/inventory_provider/routes/poller.py index 8497b2cc310e778f08c892b871cd0b928591c44b..ad87d6866e6f017371e2efcbca720cb52543af5b 100644 --- a/inventory_provider/routes/poller.py +++ b/inventory_provider/routes/poller.py @@ -82,3 +82,23 @@ def poller_interface_oids(hostname): result.append(ifc_data) return jsonify(result) + + +@routes.route('/services/<category>', methods=['GET', 'POST']) +@common.require_accepts_json +def service_category_interfaces(category): + + result = [] + + r = common.get_current_redis() + for k in r.scan_iter(f'interface-services:{category.lower()}:*'): + ifc = r.get(k.decode('utf-8')) + result.append(json.loads(ifc.decode('utf-8'))) + + if not result: + return Response( + response=f'no info available for service category {category}', + status=404, + mimetype="text/html") + + return jsonify(result) diff --git a/inventory_provider/tasks/worker.py b/inventory_provider/tasks/worker.py index 33709d1f4d0ee3b219f32b99287b726f25ce8d37..e39f3d12f6ec6c6eaa6e22eab371925199db1972 100644 --- a/inventory_provider/tasks/worker.py +++ b/inventory_provider/tasks/worker.py @@ -560,6 +560,7 @@ def refresh_finalizer(self, pending_task_ids_json): _wait_for_tasks(task_ids, update_callback=_update) _build_subnet_db(update_callback=_update) + _build_interface_services(update_callback=_update) except (jsonschema.ValidationError, json.JSONDecodeError, @@ -573,6 +574,54 @@ def refresh_finalizer(self, pending_task_ids_json): logger.debug('<<< refresh_finalizer') +def _build_interface_services(update_callback=lambda s: None): + logger.debug('>>> _build_interface_services') + + r = get_next_redis(InventoryTask.config) + + def _interfaces(): + for k in r.scan_iter('netconf-interfaces:*'): + k = k.decode('utf-8') + (_, router_name, ifc_name) = k.split(':') + + info = r.get(k).decode('utf-8') + info = json.loads(info) + + assert ifc_name == info['name'] + yield { + 'router': router_name, + 'interface': info['name'], + 'description': info['description'] + } + + def _classify(ifc): + if ifc['description'].startswith('SRV_MDVPN'): + return 'mdvpn' + if 'LHCONE' in ifc['description']: + return 'lhcone' + return None + + r = get_next_redis(InventoryTask.config) + rp = r.pipeline() + + update_callback('loading all known interfaces') + interfaces = list(_interfaces()) + update_callback(f'loaded {len(interfaces)} interfaces, ' + 'saving by service category') + + for ifc in interfaces: + service_type = _classify(ifc) + if not service_type: + continue + rp.set( + f'interface-services:{service_type}' + f':{ifc["router"]}:{ifc["interface"]}', + json.dumps(ifc)) + + rp.execute() + logger.debug('<<< _build_interface_services') + + def _build_subnet_db(update_callback=lambda s: None): r = get_next_redis(InventoryTask.config) diff --git a/test/test_general_poller_routes.py b/test/test_general_poller_routes.py new file mode 100644 index 0000000000000000000000000000000000000000..0e88d1e660b919b4da39c4f07820b5c8dac8a3bd --- /dev/null +++ b/test/test_general_poller_routes.py @@ -0,0 +1,54 @@ +import json + +import jsonschema +import pytest + +from inventory_provider.tasks import worker + +DEFAULT_REQUEST_HEADERS = { + "Content-type": "application/json", + "Accept": ["application/json"] +} + + +INTERFACE_LIST_SCHEMA = { + '$schema': 'http://json-schema.org/draft-07/schema#', + + 'definitions': { + 'ifc-info': { + 'type': 'object', + 'properties': { + 'description': {'type': 'string'}, + 'router': {'type': 'string'}, + 'interface': {'type': 'string'} + }, + 'required': ['router', 'interface', 'description'], + 'additionalProperties': False + }, + }, + + 'type': 'array', + 'items': {'$ref': '#/definitions/ifc-info'} +} + + +@pytest.mark.parametrize('category', ['mdvpn', 'lhcone', 'MDVpn', 'LHCONE']) +def test_service_category(client, mocked_worker_module, category): + worker._build_interface_services() + rv = client.get( + f'/poller/services/{category}', + 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, INTERFACE_LIST_SCHEMA) + assert response_data, 'expected a non-empty list' + + +@pytest.mark.parametrize('category', ['mdvpn ', ' mdvpn', 'mdvpn1', 'aaa']) +def test_service_category_not_found(client, mocked_worker_module, category): + worker._build_interface_services() + rv = client.get( + f'/poller/services/{category}', + headers=DEFAULT_REQUEST_HEADERS) + assert rv.status_code == 404 diff --git a/test/test_worker_utils.py b/test/test_worker_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f94d2dab2d488d68a0b9375bf7ca74670ec97e86 --- /dev/null +++ b/test/test_worker_utils.py @@ -0,0 +1,106 @@ +""" +tests of a few worker utilities +""" +import json +import re + +import jsonschema + +from inventory_provider.tasks import worker +from inventory_provider.tasks import common + + +def backend_db(): + return common._get_redis({ + 'redis': { + 'hostname': None, + 'port': None + }, + 'redis-databases': [0, 7] + }).db + + +def test_build_interface_services(mocked_worker_module): + """ + checks that valid interface service objects are created + :param mocked_worker_module: fixture + :return: + """ + ifc_schema = { + '$schema': 'http://json-schema.org/draft-07/schema#', + + 'type': 'object', + 'properties': { + 'description': {'type': 'string'}, + 'router': {'type': 'string'}, + 'interface': {'type': 'string'} + }, + 'required': ['router', 'interface', 'description'], + 'additionalProperties': False + } + + db = backend_db() # also forces initialization + worker._build_interface_services() + + seen_types = set() + for k, v in db.items(): + if not k.startswith('interface-services:'): + continue + + (_, type, router, ifc_name) = k.split(':') + + ifc_info = json.loads(v) + jsonschema.validate(json.loads(v), ifc_schema) + + assert ifc_info['router'] == router + assert ifc_info['interface'] == ifc_name + + seen_types.add(type) + + assert type in ('mdvpn', 'lhcone') + + expected_seen_types = set(['mdvpn', 'lhcone']) + assert seen_types == expected_seen_types + + +def test_build_subnet_db(mocked_worker_module): + """ + checks that valid reverse subnet objects are created + :param mocked_worker_module: fixture + :return: + """ + + address_schema = { + '$schema': 'http://json-schema.org/draft-07/schema#', + + 'type': 'object', + 'properties': { + 'name': {'type': 'string'}, + 'interface address': {'type': 'string'}, + 'interface name': {'type': 'string'}, + 'router': {'type': 'string'} + }, + 'required': ['name', 'interface address', 'interface name', 'router'], + 'additionalProperties': False + } + + db = backend_db() # also forces initialization + worker._build_subnet_db() + + found_record = False + for key, value in db.items(): + + if not key.startswith('reverse_interface_addresses:'): + continue + + found_record = True + + m = re.match('^reverse_interface_addresses:(.+)', key) + assert m + address = m.group(1) + + value = json.loads(value) + jsonschema.validate(value, address_schema) + assert value['name'] == address + + assert found_record