diff --git a/inventory_provider/routes/poller.py b/inventory_provider/routes/poller.py index 34d3a81e26332f08561a42a9b37f4e2836bc9365..a0748eb2cc05b24d7fb7b6def65bce99cc1d0b49 100644 --- a/inventory_provider/routes/poller.py +++ b/inventory_provider/routes/poller.py @@ -54,6 +54,12 @@ These endpoints are intended for use by BRIAN. .. autofunction:: inventory_provider.routes.poller.get_service_types +/poller/error-report-interfaces</hostname> +------------------------------------------ + +.. autofunction:: inventory_provider.routes.poller.error_report_interfaces + + support method: _get_dashboards --------------------------------- @@ -209,6 +215,25 @@ INTERFACE_LIST_SCHEMA = { 'items': {'$ref': '#/definitions/interface'} } +ERROR_REPORT_INTERFACE_LIST_SCHEMA = { + '$schema': 'https://json-schema.org/draft-07/schema#', + 'definitions': { + 'interface': { + 'type': 'object', + 'properties': { + 'router': {'type': 'string'}, + 'name': {'type': 'string'}, + 'description': {'type': 'string'}, + 'vendor': {'type': 'string', 'enum': ['juniper', 'nokia']} + }, + 'required': ['router', 'name', 'description', 'vendor'], + 'additionalProperties': False + }, + }, + 'type': 'array', + 'items': {'$ref': '#/definitions/interface'} +} + INTERFACE_SPEED_LIST_SCHEMA = { '$schema': 'https://json-schema.org/draft-07/schema#', @@ -883,6 +908,89 @@ def interfaces(hostname=None): return Response(result, mimetype="application/json") +def load_error_report_interfaces( + config, hostname=None, use_next_redis=False +): + interfaces = _load_interfaces(config, hostname, use_next_redis=use_next_redis) + + def filter_interface(interface: dict): + return all( + ( + "phy" in interface["description"].lower(), + "spare" not in interface["description"].lower(), + "non-operational" not in interface["description"].lower(), + "reserved" not in interface["description"].lower(), + "test" not in interface["description"].lower(), + "dsc." not in interface["name"].lower(), + "fxp" not in interface["name"].lower(), + ) + ) + + def transform_interface(interface: dict): + return { + "router": interface["router"], + "name": interface["name"], + "description": interface["description"], + # TODO: This is a complete hack until we have a proper way to determine + # router vendor + "vendor": "nokia" if interface["router"].startswith("rt0") else "juniper" + } + + return sorted( + map(transform_interface, filter(filter_interface, interfaces)), + key=lambda i: (i["router"], i["name"]), + ) + + +@routes.route("/error-report-interfaces", methods=['GET']) +@routes.route('/error-report-interfaces/<hostname>', methods=['GET']) +@common.require_accepts_json +def error_report_interfaces(hostname=None): + """ + Handler for `/poller/error-report-interfaces` and + `/poller/error-report-interfaces/<hostname>` + which returns information for either all interfaces + or those on the requested hostname. + + The optional `no-lab` parameter omits lab routers + if it's truthiness evaluates to True. + + The response is a list of information for all + interfaces that should be included in the neteng error report + and includes vendor information (either juniper or nokia) + + .. asjson:: + inventory_provider.routes.poller.ERROR_REPORT_INTERFACE_LIST_SCHEMA + + :param hostname: optional, if present should be a router hostname + :return: + """ + + suffix = hostname or "all" + cache_key = f'classifier-cache:error-report-interfaces:{suffix}' + + r = common.get_current_redis() + + result = _ignore_cache_or_retrieve(request, cache_key, r) + + if not result: + interfaces = load_error_report_interfaces( + current_app.config['INVENTORY_PROVIDER_CONFIG'], hostname + ) + result = json.dumps(interfaces).encode('utf-8') + # cache this data for the next call + r.set(cache_key, result) + + if not result or result == b'[]': + return Response( + response='no interfaces found', + status=404, + mimetype='text/plain' + ) + + return Response(result, mimetype="application/json") + + def interface_speed(ifc): """ Return the maximum bits per second expected for the given interface. diff --git a/inventory_provider/tasks/worker.py b/inventory_provider/tasks/worker.py index eafdb92cea8029a970f4205466e7d65dba5d0417..a360124dd6e344324ce5316caf77e21f278598ae 100644 --- a/inventory_provider/tasks/worker.py +++ b/inventory_provider/tasks/worker.py @@ -21,7 +21,7 @@ from ncclient.transport import TransportError from inventory_provider.db import ims_data from inventory_provider.db.ims import IMS -from inventory_provider.routes.poller import load_interfaces_to_poll +from inventory_provider.routes.poller import load_error_report_interfaces, load_interfaces_to_poll from inventory_provider.tasks.app import app from inventory_provider.tasks.common \ import get_next_redis, get_current_redis, \ @@ -1827,6 +1827,7 @@ def final_task(self): _build_snmp_peering_db(update_callback=self.log_info) _build_juniper_peering_db(update_callback=self.log_info) populate_poller_interfaces_cache(warning_callback=self.log_warning) + populate_error_report_interfaces_cache(warning_callback=self.log_warning) collate_netconf_interfaces_all_cache(warning_callback=self.log_warning) latch_db(InventoryTask.config) @@ -1879,6 +1880,50 @@ def populate_poller_interfaces_cache(warning_callback=lambda s: None): r.set(all_cache_key, json.dumps(all_populated_interfaces)) +@log_task_entry_and_exit +def populate_error_report_interfaces_cache(warning_callback=lambda s: None): + cache_ns = 'classifier-cache:error-report-interfaces:' + all_cache_key = cache_ns + 'all' + all_populated_interfaces = None + + r = get_next_redis(InventoryTask.config) + + try: + all_populated_interfaces = load_error_report_interfaces( + InventoryTask.config, use_next_redis=True + ) + + except Exception as e: + warning_callback(f"Failed to retrieve all required data {e}") + logger.exception( + "Failed to retrieve all required data, logging exception") + + if not all_populated_interfaces: + previous_r = get_current_redis(InventoryTask.config) + + try: + warning_callback(f"populating {all_cache_key} from previously cached data") + previous = json.loads(previous_r.get(all_cache_key)) + all_populated_interfaces = sorted( + previous, key=lambda i: (i["router"], i["name"]) + ) + except Exception as e: + warning_callback( + f"Failed to load {all_cache_key} from previously cached data: {e}" + ) + return + + router_interfaces = {} + for ifc in all_populated_interfaces: + interfaces = router_interfaces.setdefault(ifc['router'], []) + interfaces.append(ifc) + + for router, ifcs in router_interfaces.items(): + r.set(cache_ns + router, json.dumps(ifcs)) + + r.set(all_cache_key, json.dumps(all_populated_interfaces)) + + @log_task_entry_and_exit def collate_netconf_interfaces_all_cache(warning_callback=lambda s: None): """ diff --git a/test/conftest.py b/test/conftest.py index 245f1f3b97d78dba94b814833eed2a6fc13c4b6d..c77fc91d9877f8d274bbbf1987d2a64a5430dbfc 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -258,7 +258,9 @@ def mocked_redis(mocker): def client(flask_config_filename, data_config_filename, mocked_redis): os.environ['FLASK_SETTINGS_FILENAME'] = flask_config_filename os.environ['INVENTORY_PROVIDER_CONFIG_FILENAME'] = data_config_filename - with inventory_provider.create_app(setup_logging=False).test_client() as c: + app = inventory_provider.create_app(setup_logging=False) + app.testing = True # Show exceptions instead of generid 500 status + with app.test_client() as c: yield c diff --git a/test/test_general_poller_routes.py b/test/test_general_poller_routes.py index a42700acd659488fea874a41680058e951f13d69..611bd3df301f56bdd57159b143cb183071b74282 100644 --- a/test/test_general_poller_routes.py +++ b/test/test_general_poller_routes.py @@ -466,3 +466,25 @@ def test_gws_config_html(client): # just a sanity check, no validation # ... for now, this isn't an important interface assert response_data.endswith('</html>') + + +def test_get_all_error_report_interfaces(client): + rv = client.get('/poller/error-report-interfaces', headers=DEFAULT_REQUEST_HEADERS) + assert rv.status_code == 200 + assert rv.is_json + response_data = json.loads(rv.data) + jsonschema.validate(response_data, poller.ERROR_REPORT_INTERFACE_LIST_SCHEMA) + response_routers = {ifc['router'] for ifc in response_data} + assert len(response_routers) > 1, 'there should data from be lots of routers' + + +def test_get_single_router_error_report_interfaces(client): + rv = client.get( + '/poller/error-report-interfaces/mx1.ams.nl.geant.net', + headers=DEFAULT_REQUEST_HEADERS, + ) + assert rv.status_code == 200 + assert rv.is_json + response_data = json.loads(rv.data) + jsonschema.validate(response_data, poller.ERROR_REPORT_INTERFACE_LIST_SCHEMA) + assert {ifc['router'] for ifc in response_data} == {"mx1.ams.nl.geant.net"} diff --git a/test/test_worker.py b/test/test_worker.py index 0120d47f9de55b0806afac65d659290ea909538d..066393e45c61e4c0025c830d0a8557148c468bab 100644 --- a/test/test_worker.py +++ b/test/test_worker.py @@ -5,7 +5,7 @@ import jsonschema from lxml import etree from inventory_provider.tasks import common -from inventory_provider.tasks.worker import transform_ims_data, \ +from inventory_provider.tasks.worker import populate_error_report_interfaces_cache, transform_ims_data, \ extract_ims_data, persist_ims_data, \ retrieve_and_persist_neteng_managed_device_list, \ populate_poller_interfaces_cache, refresh_nokia_interface_list @@ -941,3 +941,104 @@ def test_refresh_nokia_interface_list(mocked_redis, data_config): interface = json.loads(r.get(k).decode('utf-8')) interfaces[k] = interface jsonschema.validate(interface, interfaces_schema) + + +def test_populate_error_report_interfaces_cache(mocker, data_config, mocked_redis): + r = common._get_redis(data_config) + + mocker.patch('inventory_provider.tasks.worker.get_next_redis', return_value=r) + all_interfaces = [ + { + "router": "router_a.geant.net", + "name": "interface_a", + "bundle": ["ae_a"], + "bundle-parents": [], + "description": "PHY DESCRIPTION A", + "circuits": [] + }, + { + "router": "router_a.geant.net", + "name": "ae_a", + "bundle": [], + "bundle-parents": [], + "description": "PHY DESCRIPTION B", + "circuits": [] + }, + { + "router": "router_a.geant.net", + "name": "ae_a.123", + "bundle": [], + "bundle-parents": [], + "description": "PHY DESCRIPTION C", + "circuits": [] + }, + { + "router": "rt0.geant.net", + "name": "lag-1.0", + "bundle": ["ae_c"], + "bundle-parents": [], + "description": "PHY DESCRIPTION D", + "circuits": [] + }, + { + "router": "router_a.geant.net", + "name": "ae_a.456", + "bundle": [], + "bundle-parents": [], + "description": "Spare Not included", + "circuits": [] + }, + { + "router": "router_a.geant.net", + "name": "dsc.1", + "bundle": [], + "bundle-parents": [], + "description": "PHY discard", + "circuits": [] + }, + ] + + mocker.patch('inventory_provider.routes.poller._load_interfaces', + return_value=all_interfaces) + mocker.patch( + 'inventory_provider.tasks.worker.InventoryTask.config' + ) + exp_router_a_interfaces = [ + { + "router": "router_a.geant.net", + "name": "ae_a", + "description": "PHY DESCRIPTION B", + "vendor": "juniper" + }, + { + "router": "router_a.geant.net", + "name": "ae_a.123", + "description": "PHY DESCRIPTION C", + "vendor": "juniper" + }, + { + "router": "router_a.geant.net", + "name": "interface_a", + "description": "PHY DESCRIPTION A", + "vendor": "juniper" + }, + ] + exp_nokia_router_interfaces = [ + { + "router": "rt0.geant.net", + "name": "lag-1.0", + "description": "PHY DESCRIPTION D", + "vendor": "nokia" + } + ] + + populate_error_report_interfaces_cache() + + all = r.get("classifier-cache:error-report-interfaces:all").decode("utf-8") + assert json.loads(all) == exp_router_a_interfaces + exp_nokia_router_interfaces + + router_a = r.get("classifier-cache:error-report-interfaces:router_a.geant.net") + assert json.loads(router_a) == exp_router_a_interfaces + + nokia_router = r.get("classifier-cache:error-report-interfaces:rt0.geant.net") + assert json.loads(nokia_router) == exp_nokia_router_interfaces