diff --git a/Changelog.md b/Changelog.md index 31e6c80a0033635261027312ca20fc96147da482..853920da13387fff19262ec1e1296afe7d4688f7 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. +## [0.86] - 2022-03-22 +- POL1-552: neteng pop api +- POL1-571: poller/interfaces cache bug fix + +## [0.85] - 2022-03-15 +- POL1-569: return all services by default for BRIAN, not only monitored + ## [0.84] - 2022-03-07 - DBOARD3-536: added /ping endpoint diff --git a/inventory_provider/db/ims_data.py b/inventory_provider/db/ims_data.py index c644d13c5d63fe88709b7f8630affdd8baffb496..ec9717ffec56b98c807fd7266a105deb0d87008b 100644 --- a/inventory_provider/db/ims_data.py +++ b/inventory_provider/db/ims_data.py @@ -26,28 +26,35 @@ IMS_OPSDB_STATUS_MAP = { STATUSES_TO_IGNORE = \ [InventoryStatus.OUT_OF_SERVICE.value] +_POP_LOCATION_SCHEMA_STRUCT = { + '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 +} + +POP_LOCATION_SCHEMA = { + '$schema': 'http://json-schema.org/draft-07/schema#', + **_POP_LOCATION_SCHEMA_STRUCT +} + 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 - } + 'pop-location': _POP_LOCATION_SCHEMA_STRUCT }, 'type': 'object', diff --git a/inventory_provider/routes/common.py b/inventory_provider/routes/common.py index 6dda30525fb971c0c46987d62e17846710ef985e..c74e5dfea1e2c0ff926db8764004f2b132727e91 100644 --- a/inventory_provider/routes/common.py +++ b/inventory_provider/routes/common.py @@ -137,7 +137,8 @@ def after_request(response): return response -def _redis_client_proc(key_queue, value_queue, config_params, doc_type): +def _redis_client_proc( + key_queue, value_queue, config_params, doc_type, use_next_redis=False): """ create a local redis connection with the current db index, lookup the values of the keys that come from key_queue @@ -165,7 +166,10 @@ def _redis_client_proc(key_queue, value_queue, config_params, doc_type): return etree.XML(value) try: - r = tasks_common.get_current_redis(config_params) + if use_next_redis: + r = tasks_common.get_next_redis(config_params) + else: + r = tasks_common.get_current_redis(config_params) while True: key = key_queue.get() @@ -215,7 +219,7 @@ def _load_redis_docs( q = queue.Queue() t = threading.Thread( target=_redis_client_proc, - args=[q, response_queue, config_params, doc_type]) + args=[q, response_queue, config_params, doc_type, use_next_redis]) t.start() threads.append({'thread': t, 'queue': q}) diff --git a/inventory_provider/routes/jobs.py b/inventory_provider/routes/jobs.py index 44e7e8ec9fb7eee700b6de2a4b1ed086bc5a7ee9..1f76e4309ef0f436b6771434b064a82764687ee5 100644 --- a/inventory_provider/routes/jobs.py +++ b/inventory_provider/routes/jobs.py @@ -87,22 +87,6 @@ INDIVIDUAL_TASK_STATUS_RESPONSE_SCHEMA = { } -# INDIVIDUAL_TASK_STATUS_RESPONSE_SCHEMA = { -# "$schema": "http://json-schema.org/draft-07/schema#", -# "type": "object", -# "properties": { -# "id": {"type": "string"}, -# "status": {"type": "string"}, -# "exception": {"type": "boolean"}, -# "ready": {"type": "boolean"}, -# "success": {"type": "boolean"}, -# "result": {"type": "object"} -# }, -# "required": ["id", "status", "exception", "ready", "success"], -# "additionalProperties": False -# } - - @routes.after_request def after_request(resp): return common.after_request(resp) diff --git a/inventory_provider/routes/neteng.py b/inventory_provider/routes/neteng.py index 29f8abcd9c963c06ac40fed5063d32f401435a28..dce7b2dc432e7caa6df58345441b14517603aa36 100644 --- a/inventory_provider/routes/neteng.py +++ b/inventory_provider/routes/neteng.py @@ -12,9 +12,20 @@ These endpoints are intended for use by neteng tools. .. autofunction:: inventory_provider.routes.neteng.get_location +/neteng/pops +--------------------------------- + +.. autofunction:: inventory_provider.routes.neteng.get_pop_names + +/neteng/pop/name +--------------------------------- + +.. autofunction:: inventory_provider.routes.neteng.get_pop_location + """ import json import logging +import re import threading from flask import Blueprint, Response, jsonify @@ -25,6 +36,12 @@ routes = Blueprint('neteng-query-routes', __name__) logger = logging.getLogger(__name__) _subnet_lookup_semaphore = threading.Semaphore() +STRING_LIST_SCHEMA = { + '$schema': 'http://json-schema.org/draft-07/schema#', + 'type': 'array', + 'items': {'type': 'string'} +} + @routes.after_request def after_request(resp): @@ -35,9 +52,9 @@ def after_request(resp): @common.require_accepts_json def get_location(equipment): """ - Handler for `/neteng/location/equipment-name` + Handler for `/neteng/location/<equipment-name>` - This method will pop location information for the IMS node + This method will return pop location information for the IMS node with name = `equipment-name`. 404 is returned if the IMS node name is not known. @@ -54,15 +71,78 @@ def get_location(equipment): value = r.get(f'ims:location:{equipment}') if not value: return Response( - response='no location information available for "{equipment}"', + response=f'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}"', + response=f'unexpected empty cached data for "{equipment}"', status=500, mimetype='text/html') return jsonify(value[0]) + + +@routes.route('/pops', methods=['GET', 'POST']) +@common.require_accepts_json +def get_pop_names(): + """ + Handler for `/neteng/pops` + + This method will return a list of defined pop + abbreviations. Elements from this list can be used + with `/neteng/pop`. + + .. asjson:: + inventory_provider.routes.neteng.STRING_LIST_SCHEMA + + :return: as above + """ + + def _pops(): + r = common.get_current_redis() + for k in r.scan_iter('ims:pop:*', count=1000): + k = k.decode('utf-8') + m = re.match('^ims:pop:(.+)$', k) + yield m.group(1) + + return jsonify(sorted(list(_pops()))) + + +@routes.route('/pop/<abbreviation>', methods=['GET', 'POST']) +@common.require_accepts_json +def get_pop_location(abbreviation): + """ + Handler for `/neteng/pop/<name>` + + This method will return location information for the POP + with abbreviation = `abbreviation` in IMS. + + 404 is returned if the POP name is not known. + Otherwise the return value will be formatted as: + + .. asjson:: + inventory_provider.db.ims_data.POP_LOCATION_SCHEMA + + :return: as above + """ + + r = common.get_current_redis() + + value = r.get(f'ims:pop:{abbreviation}') + if not value: + return Response( + response=f'no location information available for "{abbreviation}"', + status=404, + mimetype='text/html') + + value = json.loads(value.decode('utf-8')) + if not value: + return Response( + response=f'unexpected empty cached data for "{abbreviation}"', + status=500, + mimetype='text/html') + + return jsonify(value) diff --git a/inventory_provider/routes/poller.py b/inventory_provider/routes/poller.py index 97145a72eb74d86cf3b0e2c2536f006480a26741..525564e86ab84ae0cb9e54544bf380a9ad7aa48a 100644 --- a/inventory_provider/routes/poller.py +++ b/inventory_provider/routes/poller.py @@ -72,6 +72,7 @@ from flask import Blueprint, Response, current_app, request, jsonify 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.tasks import common as tasks_common from inventory_provider.routes.classifier import get_ims_equipment_name, \ get_ims_interface from inventory_provider.routes.common import _ignore_cache_or_retrieve @@ -524,20 +525,6 @@ def _get_dashboard_data(ifc): } -def _add_dashboards(interfaces): - """ - generator that dashboards to each interfaces. - - :param interfaces: result of _load_interfaces - :return: generator with `dashboards` populated in each element - """ - - for ifc in interfaces: - dashboards = _get_dashboards(ifc) - ifc['dashboards'] = sorted([d.name for d in dashboards]) - yield _get_dashboard_data(ifc) - - def _load_interface_bundles(config, hostname=None, use_next_redis=False): result = dict() @@ -568,8 +555,8 @@ def _load_interface_bundles(config, hostname=None, use_next_redis=False): def _load_services(config, hostname=None, use_next_redis=False): - # if hostname: - # hostname = get_ims_equipment_name(hostname) + if hostname: + hostname = get_ims_equipment_name(hostname) result = dict() key_pattern = f'ims:interface_services:{hostname}:*' \ @@ -654,6 +641,9 @@ def _load_interfaces( :return: """ def _load_docs(key_pattern): + # print('') + # logger.debug(f'docs Key: {key_pattern}') + # print('') for doc in _load_netconf_docs(config, key_pattern, use_next_redis): @@ -673,6 +663,7 @@ def _load_interfaces( base_key_pattern = f'netconf:{hostname}*' if hostname else 'netconf:*' yield from _load_docs(base_key_pattern) if not no_lab: + logger.debug('lab') yield from _load_docs(f'lab:{base_key_pattern}') @@ -695,78 +686,42 @@ def _add_bundle_parents(interfaces, hostname=None): yield ifc -def _add_circuits(interfaces, hostname=None): - """ - generator that adds service info to each interface. - - :param interfaces: result of _load_interfaces - :param hostname: hostname or None for all - :return: generator with 'circuits' populated in each element, if present - """ - - if hostname: - hostname = get_ims_equipment_name(hostname) - services = _load_services( - current_app.config['INVENTORY_PROVIDER_CONFIG'], hostname=hostname) - for ifc in interfaces: - router_services = services.get( - get_ims_equipment_name(ifc['router']), None) - if router_services: - ifc['circuits'] = router_services.get( - get_ims_interface(ifc['name']), [] - ) - - yield ifc - - -def _add_snmp_indexes(interfaces, hostname=None): - """ - generator that adds snmp info to each interface, if available - - :param interfaces: result of _load_interfaces - :param hostname: hostname or None for all - :return: generator with 'snmp-index' optionally added to each element - """ - snmp_indexes = common.load_snmp_indexes( - current_app.config['INVENTORY_PROVIDER_CONFIG'], hostname) - for ifc in interfaces: - router_snmp = snmp_indexes.get(ifc['router'], None) - if router_snmp and ifc['name'] in router_snmp: - ifc['snmp-index'] = router_snmp[ifc['name']]['index'] - # TODO: uncomment this when it won't break poller-admin-service - # not urgent ... it looks empirically like all logical-system - # interfaces are repeated for both communities - # ifc['snmp-community'] = router_snmp[ifc['name']]['community'] - yield ifc - - -def _load_interfaces_to_poll(hostname=None): - """ - prepares the result of a call to /interfaces - - :param hostname: hostname or None for all - :return: generator yielding interface elements - """ - - no_lab = common.get_bool_request_arg('no-lab', False) - basic_interfaces = _load_interfaces( - current_app.config['INVENTORY_PROVIDER_CONFIG'], - hostname, - no_lab=no_lab) - # basic_interfaces = list(basic_interfaces) - with_bundles = _add_bundle_parents(basic_interfaces, hostname) - with_circuits = _add_circuits(with_bundles, hostname) - # with_circuits = list(with_circuits) - with_snmp = _add_snmp_indexes(with_circuits, hostname) - # with_snmp = list(with_snmp) - - # only return interfaces that can be polled - def _has_snmp_index(ifc): - return 'snmp-index' in ifc - - to_poll = filter(_has_snmp_index, with_snmp) - - return _add_dashboards(to_poll) +def load_interfaces_to_poll( + config, hostname=None, no_lab=False, use_next_redis=False): + basic_interfaces = \ + list(_load_interfaces(config, hostname, no_lab, use_next_redis)) + bundles = _load_interface_bundles(config, hostname, use_next_redis) + services = _load_services(config, hostname, use_next_redis) + snmp_indexes = common.load_snmp_indexes(config, hostname, use_next_redis) + + def _get_populated_interfaces(all_interfaces): + if use_next_redis: + r = tasks_common.get_next_redis(config) + else: + r = tasks_common.get_current_redis(config) + for ifc in all_interfaces: + router_snmp = snmp_indexes.get(ifc['router'], None) + if router_snmp and ifc['name'] in router_snmp: + ifc['snmp-index'] = router_snmp[ifc['name']]['index'] + + router_bundle = bundles.get(ifc['router'], None) + if router_bundle: + base_ifc = ifc['name'].split('.')[0] + ifc['bundle-parents'] = router_bundle.get(base_ifc, []) + + router_services = services.get( + get_ims_equipment_name(ifc['router'], r), None) + if router_services: + ifc['circuits'] = router_services.get( + get_ims_interface(ifc['name']), [] + ) + + dashboards = _get_dashboards(ifc) + ifc['dashboards'] = sorted([d.name for d in dashboards]) + yield _get_dashboard_data(ifc) + else: + continue + return _get_populated_interfaces(basic_interfaces) @routes.route("/interfaces", methods=['GET', 'POST']) @@ -808,7 +763,8 @@ def interfaces(hostname=None): result = _ignore_cache_or_retrieve(request, cache_key, r) if not result: - result = list(_load_interfaces_to_poll(hostname)) + result = list(load_interfaces_to_poll( + current_app.config['INVENTORY_PROVIDER_CONFIG'], hostname, no_lab)) if not result: return Response( response='no interfaces found', @@ -1394,9 +1350,9 @@ def gws_direct_config(): :return: """ - format = request.args.get('format', default='json', type=str) - format = format.lower() - if format not in ('html', 'json'): + wanted = request.args.get('format', default='json', type=str) + wanted = wanted.lower() + if wanted not in ('html', 'json'): return Response( response='format must be one of: html, json', status=400, @@ -1420,7 +1376,7 @@ def gws_direct_config(): 'info': ifc.get('info', '') } - if format == 'json': + if wanted == 'json': if not request.accept_mimetypes.accept_json: return Response( response="response will be json", diff --git a/inventory_provider/snmp.py b/inventory_provider/snmp.py index 6e974f2822ed5ad4bef26c5d8a698907a1e6a12c..d53eb6265dd25dabaafb4568a8fb6cb566534b36 100644 --- a/inventory_provider/snmp.py +++ b/inventory_provider/snmp.py @@ -72,13 +72,13 @@ def walk(agent_hostname, community, base_oid): # pragma: no cover :return: """ - mibBuilder = builder.MibBuilder() + mib_builder = builder.MibBuilder() # mibViewController = view.MibViewController(mibBuilder) compiler.addMibCompiler( - mibBuilder, + mib_builder, sources=['http://mibs.snmplabs.com/asn1/@mib@']) # Pre-load MIB modules we expect to work with - mibBuilder.loadModules( + mib_builder.loadModules( 'SNMPv2-MIB', 'SNMP-COMMUNITY-MIB', 'RFC1213-MIB') @@ -86,10 +86,10 @@ def walk(agent_hostname, community, base_oid): # pragma: no cover logger.debug("walking %s: %s" % (agent_hostname, base_oid)) try: - for (engineErrorIndication, - pduErrorIndication, - errorIndex, - varBinds) in nextCmd( + for (engine_error_indication, + pdu_error_indication, + error_index, + var_binds) in nextCmd( SnmpEngine(), CommunityData(community), UdpTransportTarget((agent_hostname, 161)), @@ -101,17 +101,17 @@ def walk(agent_hostname, community, base_oid): # pragma: no cover # cf. http://snmplabs.com/ # pysnmp/examples/hlapi/asyncore/sync/contents.html - if engineErrorIndication: + if engine_error_indication: raise SNMPWalkError( f'snmp response engine error indication: ' - f'{str(engineErrorIndication)} - {agent_hostname}') - if pduErrorIndication: + f'{str(engine_error_indication)} - {agent_hostname}') + if pdu_error_indication: raise SNMPWalkError( 'snmp response pdu error %r at %r' % ( - pduErrorIndication, - errorIndex - and varBinds[int(errorIndex) - 1][0] or '?')) - if errorIndex != 0: + pdu_error_indication, + error_index + and var_binds[int(error_index) - 1][0] or '?')) + if error_index != 0: raise SNMPWalkError( 'sanity failure: errorIndex != 0, ' 'but no error indication') @@ -120,7 +120,7 @@ def walk(agent_hostname, community, base_oid): # pragma: no cover # rfc1902.ObjectType(rfc1902.ObjectIdentity(x[0]),x[1]) # .resolveWithMib(mibViewController) # for x in varBinds] - for oid, val in varBinds: + for oid, val in var_binds: result = { "oid": _canonify_oid(oid), "value": _cast_snmp_value(val) diff --git a/inventory_provider/static/interfaces.html b/inventory_provider/static/interfaces.html deleted file mode 100644 index e4c59af9ef7a17d4d536de2eb833e69d291a8b0e..0000000000000000000000000000000000000000 --- a/inventory_provider/static/interfaces.html +++ /dev/null @@ -1,31 +0,0 @@ -<!doctype html> -<html ng-app="inventoryApp" lang="en"> - <head> - <title>interfaces</title> - <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.7.5/angular.min.js"></script> - <script src="interfaces.js"></script> - <link rel="stylesheet" href="style.css"> - </head> - <body> - - <div ng-controller="interfaces"> - <div class="column"> - <p><strong>interfaces</strong></p> - <ul> - <li ng-repeat="i in interfaces">{{i.router}}:{{i.name}} - <ul> - <li>{{i.description}}</li> - <li ng-repeat="v4 in i.ipv4">v4: {{v4}}</li> - <li ng-repeat="v6 in i.ipv6">v6: {{v6}}</li> - </ul> - </li> - </ul> - <!--div class="raw">{{interfaces}}</div--> - </div> - - <div> - STATUS: {{status}} - </div> - </div> - </body> -</html> \ No newline at end of file diff --git a/inventory_provider/static/interfaces.js b/inventory_provider/static/interfaces.js deleted file mode 100644 index 63ab9939fdf58148bd49631002ff40552d966c28..0000000000000000000000000000000000000000 --- a/inventory_provider/static/interfaces.js +++ /dev/null @@ -1,53 +0,0 @@ -var myApp = angular.module('inventoryApp', []); - -myApp.controller('interfaces', function($scope, $http) { - - $scope.routers = []; - $scope.router = ''; - - $scope.interfaces = []; - $scope.interface = ''; - - $scope.status = 'not yet loaded'; - - $http({ - method: 'GET', - url: window.location.origin + "/data/interfaces" - }).then( - function(rsp) {$scope.interfaces = rsp.data;}, - function(rsp) {$scope.routers = ['error'];} - ); - - /* - $scope.update_interfaces = function() { - - $http({ - method: 'GET', - url: window.location.origin + "/data/interfaces/" + $scope.router - }).then( - function(rsp) { - $scope.interfaces = rsp.data.map(function(x){ - return x.name - }); - }, - function(rsp) {$scope.interfaces = ['error'];} - ); - } - - $scope.update_status = function() { - - $http({ - method: 'GET', - url: window.location.origin - + "/alarmsdb/interface-status?equipment=" - + $scope.router - + "&interface=" - + $scope.interface - }).then( - function(rsp) {$scope.status = rsp.data.status;}, - function(rsp) {$scope.interfaces = 'query error';} - ); - } - */ - -}); \ No newline at end of file diff --git a/inventory_provider/static/juniper.html b/inventory_provider/static/juniper.html deleted file mode 100644 index 212276b8004a62fba92d844e654f50e1b1338e32..0000000000000000000000000000000000000000 --- a/inventory_provider/static/juniper.html +++ /dev/null @@ -1,56 +0,0 @@ -<!doctype html> -<html ng-app="inventoryApp" lang="en"> - <head> - <title>juniper</title> - <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.7.5/angular.min.js"></script> - <script src="juniper.js"></script> - <link rel="stylesheet" href="style.css"> - </head> - <body> - - <div ng-controller="juniper"> - <h2>Interfaces</h2> - <div> - <select - ng-options="r for r in routers" - ng-change="update_interface()" - ng-model="router"></select> - </div> - - <div class="column"> - <p><strong>interfaces</strong></p> - <ul> - <li ng-repeat="i in interfaces">{{i.name}} - <ul> - <li>{{i.description}}</li> - <li ng-repeat="v4 in i.ipv4">v4: {{v4}}</li> - <li ng-repeat="v6 in i.ipv6">v6: {{v6}}</li> - </ul> - </li> - </ul> - <div class="raw">{{interfaces}}</div> - </div> - <div class="column"> - <p><strong>bgp</strong></p> - <ul> - <li ng-repeat="p in bgp">{{p.description}} - <ul> - <li>local as: {{p.as.local}}</li> - <li>peer as: {{p.as.peer}}</li> - </ul> - </li> - </ul> - <div class="raw">{{bgp}}</div> - </div> - <div class="column"> - <p><strong>snmp</strong></p> - <ul> - <li ng-repeat="i in snmp">{{i.name}} - <ul><li>index: {{i.index}}</li></ul> - </li> - </ul> - <div class="raw">{{snmp}}</div> - </div> - </div> - </body> -</html> \ No newline at end of file diff --git a/inventory_provider/static/juniper.js b/inventory_provider/static/juniper.js deleted file mode 100644 index 035a0b442dc07c63ebc427cc4523585b21bbd085..0000000000000000000000000000000000000000 --- a/inventory_provider/static/juniper.js +++ /dev/null @@ -1,48 +0,0 @@ -var myApp = angular.module('inventoryApp', []); - -myApp.controller('juniper', function($scope, $http) { - - $scope.routers = []; - $scope.router = ''; - - $scope.interfaces = 'not yet loaded'; - $scope.bgp = 'not yet loaded'; - $scope.snmp = 'not yet loaded'; - - $http({ - method: 'GET', - url: window.location.origin + "/data/routers" - }).then( - function(rsp) {$scope.routers = rsp.data;}, - function(rsp) {$scope.routers = ['error'];} - ); - - $scope.update_interface = function() { - - $http({ - method: 'GET', - url: window.location.origin + "/data/interfaces/" + $scope.router - }).then( - function(rsp) {$scope.interfaces = rsp.data;}, - function(rsp) {$scope.interfaces = 'error';} - ); - - $http({ - method: 'GET', - url: window.location.origin + "/data/bgp/" + $scope.router - }).then( - function(rsp) {$scope.bgp = rsp.data;}, - function(rsp) {$scope.bgp = 'error';} - ); - - $http({ - method: 'GET', - url: window.location.origin + "/data/snmp/" + $scope.router - }).then( - function(rsp) {$scope.snmp = rsp.data;}, - function(rsp) {$scope.snmp = 'error';} - ); - - } - -}); \ No newline at end of file diff --git a/inventory_provider/static/style.css b/inventory_provider/static/style.css deleted file mode 100644 index b234cbe2d3b0a09e2a4a2efef909588cd0ddedc1..0000000000000000000000000000000000000000 --- a/inventory_provider/static/style.css +++ /dev/null @@ -1,17 +0,0 @@ -.column { - float: left; - width: 100%%; -} - -/* Clear floats after the columns */ -.row:after { - content: ""; - display: table; - clear: both; -} - -.raw { - font-style: italic; - font-size: 10px; - font-family: Courier -} \ No newline at end of file diff --git a/inventory_provider/tasks/worker.py b/inventory_provider/tasks/worker.py index e06c7feb7a5b8ad1701d7e64416044ff8ef9db46..c6057747dfb24b55f1f938d92ed680a68762768f 100644 --- a/inventory_provider/tasks/worker.py +++ b/inventory_provider/tasks/worker.py @@ -17,12 +17,7 @@ from lxml import etree from inventory_provider.db import ims_data from inventory_provider.db.ims import IMS -from inventory_provider.routes.classifier import get_ims_interface, \ - get_ims_equipment_name -from inventory_provider.routes.common import load_snmp_indexes -from inventory_provider.routes.poller import _load_interfaces, \ - _load_interface_bundles, _get_dashboard_data, _get_dashboards, \ - _load_services +from inventory_provider.routes.poller import load_interfaces_to_poll from inventory_provider.tasks.app import app from inventory_provider.tasks.common \ import get_next_redis, get_current_redis, \ @@ -521,7 +516,8 @@ def retrieve_and_persist_neteng_managed_device_list( 'No equipment retrieved from previous list') except Exception as e: warning_callback(str(e)) - update_latch_status(pending=False, failure=True) + update_latch_status( + InventoryTask.config, pending=False, failure=True) raise e try: @@ -530,7 +526,7 @@ def retrieve_and_persist_neteng_managed_device_list( info_callback(f'saved {len(netdash_equipment)} managed routers') except Exception as e: warning_callback(str(e)) - update_latch_status(pending=False, failure=True) + update_latch_status(InventoryTask.config, pending=False, failure=True) raise e return netdash_equipment @@ -1075,12 +1071,23 @@ def persist_ims_data(data, use_current=False): node_pair_services = data['node_pair_services'] sid_services = data['sid_services'] + def _get_pops(): + # de-dupe the sites (by abbreviation) + pops = { + equip['pop']['abbreviation']: equip['pop'] + for equip in locations.values() + if equip['pop']['abbreviation']} + return pops.values() + if use_current: r = get_current_redis(InventoryTask.config) + r.delete('ims:sid_services') + # only need to delete the individual keys if it's just an IMS update # rather than a complete update (the db will have been flushed) for key_pattern in [ + 'ims:pop:*', 'ims:location:*', 'ims:lg:*', 'ims:circuit_hierarchy:*', @@ -1089,8 +1096,6 @@ def persist_ims_data(data, use_current=False): 'ims:gws_indirect:*', 'ims:node_pair_services:*' ]: - - r.delete('ims:sid_services') rp = r.pipeline() for k in r.scan_iter(key_pattern, count=1000): rp.delete(k) @@ -1101,6 +1106,8 @@ def persist_ims_data(data, use_current=False): rp = r.pipeline() for h, d in locations.items(): rp.set(f'ims:location:{h}', json.dumps([d])) + for pop in _get_pops(): + rp.set(f'ims:pop:{pop["abbreviation"]}', json.dumps(pop)) rp.execute() rp = r.pipeline() for router in lg_routers: @@ -1191,47 +1198,9 @@ def populate_poller_interfaces_cache(warning_callback=lambda s: None): lab_keys_pattern = 'lab:netconf-interfaces-hosts:*' lab_equipment = [h.decode('utf-8')[len(lab_keys_pattern) - 1:] for h in r.keys(lab_keys_pattern)] - standard_interfaces = _load_interfaces( - InventoryTask.config, - no_lab=False, - use_next_redis=True) - - bundles = _load_interface_bundles( - InventoryTask.config, - use_next_redis=True - ) - snmp_indexes = load_snmp_indexes( - InventoryTask.config, use_next_redis=True) - - services = _load_services(InventoryTask.config, use_next_redis=True) - - def _get_populated_interfaces(interfaces): - - for ifc in interfaces: - router_snmp = snmp_indexes.get(ifc['router'], None) - if router_snmp and ifc['name'] in router_snmp: - ifc['snmp-index'] = router_snmp[ifc['name']]['index'] - - router_bundle = bundles.get(ifc['router'], None) - if router_bundle: - base_ifc = ifc['name'].split('.')[0] - ifc['bundle-parents'] = router_bundle.get(base_ifc, []) - - router_services = services.get( - get_ims_equipment_name(ifc['router'], r), None) - if router_services: - ifc['circuits'] = router_services.get( - get_ims_interface(ifc['name']), [] - ) - - dashboards = _get_dashboards(ifc) - ifc['dashboards'] = sorted([d.name for d in dashboards]) - yield _get_dashboard_data(ifc) - else: - continue - all_populated_interfaces = \ - list(_get_populated_interfaces(standard_interfaces)) + all_populated_interfaces = list( + load_interfaces_to_poll(InventoryTask.config, use_next_redis=True)) non_lab_populated_interfaces = [x for x in all_populated_interfaces if x['router'] not in lab_equipment] diff --git a/setup.py b/setup.py index 402d60e24a134bdd44bce44e1b3ca57fc24ade95..9508ab83c38fddb17b8ce8db5c5fdcb6925aae00 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name='inventory-provider', - version="0.85", + version="0.86", author='GEANT', author_email='swd@geant.org', description='Dashboard inventory provider', diff --git a/test/data/router-info.json b/test/data/router-info.json index 11cf50f26a517892c3177aaebce341a8dd8207d0..213f34b48238c27b9d813ccddb8c914280e64405 100644 Binary files a/test/data/router-info.json and b/test/data/router-info.json differ diff --git a/test/test_general_poller_routes.py b/test/test_general_poller_routes.py index f97ca9552bd7f60fb9c9932d0d450a515428378b..4a40cb4f4ad971370700b45c5ae51faddb286917 100644 --- a/test/test_general_poller_routes.py +++ b/test/test_general_poller_routes.py @@ -155,10 +155,13 @@ def test_gws_indirect_snmp(client): response_data = json.loads(rv.data.decode('utf-8')) jsonschema.validate(response_data, poller.SERVICES_LIST_SCHEMA) - assert response_data # test data is non-empty - # all access services should have snmp info - assert all('snmp' in s for s in response_data) - assert all('counters' in s['snmp'] for s in response_data) + assert response_data # sanity: test data is non-empty + # all operational access services should have snmp info + operational = list(filter( + lambda s: s['status'] == 'operational', response_data)) + assert operational # sanity: test data contains operational services + assert all('snmp' in s for s in operational) + assert all('counters' in s['snmp'] for s in operational) def test_get_services_default(client): @@ -201,9 +204,12 @@ def test_services_snmp(client, service_type): response_data = json.loads(rv.data.decode('utf-8')) jsonschema.validate(response_data, poller.SERVICES_LIST_SCHEMA) - assert response_data # test data is non-empty - # all access services should have snmp info - assert all('snmp' in s for s in response_data) + assert response_data # sanity: test data is non-empty + # operational services should have snmp info + operational = list(filter( + lambda s: s['status'] == 'operational', response_data)) + assert operational # sanity: test data contains operational services + assert all('snmp' in s for s in operational) @pytest.mark.parametrize('uri_type,expected_type', [ diff --git a/test/test_neteng_routes.py b/test/test_neteng_routes.py index 1e95322a3be4cd43a83caeea12c2a9f6d35e034e..7b23b5a2510650e0c8a96b6a9a74168926ad9396 100644 --- a/test/test_neteng_routes.py +++ b/test/test_neteng_routes.py @@ -2,7 +2,9 @@ import json import jsonschema import pytest -from inventory_provider.db.ims_data import NODE_LOCATION_SCHEMA +from inventory_provider.db.ims_data \ + import NODE_LOCATION_SCHEMA, POP_LOCATION_SCHEMA +from inventory_provider.routes.neteng import STRING_LIST_SCHEMA @pytest.mark.parametrize('equipment_name', [ @@ -25,3 +27,35 @@ def test_location_not_found(client, mocked_redis): '/neteng/location/BOGUS.EQUIPMENT.NAME', headers={'Accept': ['application/json']}) assert rv.status_code == 404 + + +def test_get_pops(client, mocked_redis): + rv = client.get( + '/neteng/pops', + headers={'Accept': ['application/json']}) + assert rv.status_code == 200 + jsonschema.validate( + json.loads(rv.data.decode('utf-8')), + STRING_LIST_SCHEMA) + + +@pytest.mark.parametrize('pop_name', [ + 'AMS', 'LON', 'LON2', 'ORB', 'ORBE' +]) +def test_pop_location(client, mocked_redis, pop_name): + rv = client.post( + f'/neteng/pop/{pop_name}', + headers={'Accept': ['application/json']}) + assert rv.status_code == 200 + jsonschema.validate( + json.loads(rv.data.decode('utf-8')), + POP_LOCATION_SCHEMA) + s = json.loads(rv.data.decode('utf-8')) + print(s) + + +def test_pop_not_found(client, mocked_redis): + rv = client.post( + '/neteng/pop/BOGUS.POP.ABBREV', + headers={'Accept': ['application/json']}) + assert rv.status_code == 404 diff --git a/test/test_worker.py b/test/test_worker.py index 032768c53599c125efb946f019a4e7b5c7201d03..2ff0ce69d32d2bc8da47ea5eb728489565c3a9f5 100644 --- a/test/test_worker.py +++ b/test/test_worker.py @@ -392,7 +392,10 @@ def test_persist_ims_data(mocker, data_config, mocked_redis): return_value=r) data = { - "locations": {"loc_a": "LOC A", "loc_b": "LOC B"}, + "locations": { + "loc_a": {'pop': {'name': "LOC A", 'abbreviation': 'aaa'}}, + "loc_b": {'pop': {'name': "LOC B", 'abbreviation': 'bbb'}}, + }, "lg_routers": [ {"equipment name": "lg_eq1"}, {"equipment name": "lg_eq2"} ], @@ -484,6 +487,11 @@ def test_retrieve_and_persist_neteng_managed_device_list( def test_populate_poller_interfaces_cache( mocker, data_config, mocked_redis): r = common._get_redis(data_config) + + mocker.patch('inventory_provider.tasks.common.get_next_redis', + return_value=r) + mocker.patch('inventory_provider.tasks.worker.get_next_redis', + return_value=r) all_interfaces = [ { "router": "router_a.geant.net", @@ -638,19 +646,17 @@ def test_populate_poller_interfaces_cache( r.set("lab:netconf-interfaces-hosts:lab_router_a.geant.net", "dummy") r.set("lab:netconf-interfaces-hosts:lab_router_b.geant.net", "dummy") - mocker.patch('inventory_provider.tasks.worker._load_interfaces', + mocker.patch('inventory_provider.routes.poller._load_interfaces', side_effect=[all_interfaces, ]) - mocker.patch('inventory_provider.tasks.worker._load_interface_bundles', + mocker.patch('inventory_provider.routes.poller._load_interface_bundles', return_value=bundles) - mocker.patch('inventory_provider.tasks.worker.load_snmp_indexes', + mocker.patch('inventory_provider.routes.common.load_snmp_indexes', return_value=snmp_indexes) - mocker.patch('inventory_provider.tasks.worker._load_services', + mocker.patch('inventory_provider.routes.poller._load_services', return_value=services) mocker.patch( 'inventory_provider.tasks.worker.InventoryTask.config' ) - mocker.patch('inventory_provider.tasks.worker.get_next_redis', - return_value=r) populate_poller_interfaces_cache() assert r.exists("classifier-cache:poller-interfaces:all:no_lab")