diff --git a/Changelog.md b/Changelog.md index 85f13623c2e8e4d948132baad83021a952152c22..ef5ee4915b713335fe0a96b5976c267cee77bd25 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## [0.117] - 2024-04-16 +- adding monitored id check for third party + +## [0.116] - 2024-04-15 +- adding noc and planned noc details for third party circuit + ## [0.115] - 2024-04-12 - adding related service for circuit hierarchy diff --git a/inventory_provider/routes/poller.py b/inventory_provider/routes/poller.py index 34d3a81e26332f08561a42a9b37f4e2836bc9365..4fc27222f6630cc471eafa8c72b45a01db765ecd 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#', @@ -479,10 +504,10 @@ def _get_dashboards(interface): if re.match(r'(SRV_GLOBAL|LAG|PHY) INFRASTRUCTURE BACKBONE', description): yield BRIAN_DASHBOARDS.INFRASTRUCTURE_BACKBONE if router == 'mx1.lon.uk.geant.net' \ - and re.match(r'^ae12(\.\d+|$)$', ifc_name): + and re.match(r'^ae12\.\d+$', ifc_name): yield BRIAN_DASHBOARDS.CAE1 if router == 'rt1.mar.fr.geant.net' \ - and re.match(r'^ae12(\.\d+|$)$', ifc_name): + and re.match(r'^ae12\.\d+$', ifc_name): yield BRIAN_DASHBOARDS.IC1 if re.match(r'PHY UPSTREAM\s', description): yield BRIAN_DASHBOARDS.GWS_PHY_UPSTREAM @@ -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..627985b15c32f4ee9c333e301943abf3ceb5bddf 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, \ @@ -1493,6 +1493,7 @@ def transform_ims_data(data): 'locations': data['locations'], 'site_locations': data['site_locations'], 'lg_routers': data['lg_routers'], + 'circuit_ids_to_monitor': data['circuit_ids_to_monitor'], } @@ -1506,6 +1507,7 @@ def persist_ims_data(data, use_current=False): node_pair_services = data['node_pair_services'] sid_services = data['sid_services'] pop_nodes = data['pop_nodes'] + circuit_ids_to_monitor = data['circuit_ids_to_monitor'] def _get_sites(): # de-dupe the sites (by abbreviation) @@ -1578,7 +1580,7 @@ def persist_ims_data(data, use_current=False): populate_poller_cache(interface_services, r) populate_mic_cache(interface_services, r) - populate_mic_with_third_party_data(interface_services, hierarchy, r) + populate_mic_with_third_party_data(interface_services, hierarchy, circuit_ids_to_monitor, r) for service_type, services in services_by_type.items(): for v in services.values(): @@ -1613,7 +1615,7 @@ def persist_ims_data(data, use_current=False): rp.execute() -def populate_mic_with_third_party_data(interface_services, hierarchy, r): +def populate_mic_with_third_party_data(interface_services, hierarchy, circuit_ids_to_monitor, r): cache_key = "mic:impact:third-party-data" third_party_data = defaultdict(lambda: defaultdict(dict)) third_party_interface_data = defaultdict(lambda: defaultdict(dict)) @@ -1629,7 +1631,8 @@ def populate_mic_with_third_party_data(interface_services, hierarchy, r): return s def get_formatted_third_party_rs(_circuit_data): - if _circuit_data and _circuit_data['status'] == 'operational' and _circuit_data['circuit-type'] == 'service': + if _circuit_data and _circuit_data['status'] == 'operational' and \ + _circuit_data['id'] in circuit_ids_to_monitor and _circuit_data['circuit-type'] == 'service': return { 'id': _circuit_data['id'], 'name': _circuit_data['name'] + ' (' + _circuit_data['sid'] + ')', @@ -1660,7 +1663,7 @@ def populate_mic_with_third_party_data(interface_services, hierarchy, r): related_services_info['related_services'].append({ 'id': formatted_rs['id'], 'name': name, - 'service_type': formatted_rs['service_type'] + 'service_type': formatted_rs['service_type'], }) for contact in formatted_rs['contacts']: related_services_info['contacts'].add(contact) @@ -1827,6 +1830,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 +1883,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/setup.py b/setup.py index a668b097593320381568bb87d77cf0f2af973a9c..e70ce07badf0b4bc5b6995b7e0029aaea86bbb47 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name='inventory-provider', - version="0.116", + version="0.117", author='GEANT', author_email='swd@geant.org', description='Dashboard inventory provider', 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..86064e7aba058162a1d756a987cad4bb6a8ba36e 100644 --- a/test/test_general_poller_routes.py +++ b/test/test_general_poller_routes.py @@ -318,24 +318,19 @@ def test_interface_dashboard_mapping(description, expected_dashboards): assert set(d.name for d in dashboards) == set(expected_dashboards) -def test__CAE_1_dashboards(): - interface = { - 'router': 'mx1.lon.uk.geant.net', - 'name': 'ae12.123', - 'description': '' - } +@pytest.mark.parametrize( + "router, interface, exp_dashboards", + [ + ("mx1.lon.uk.geant.net", "ae12.123", ["CAE1"]), + ("mx1.lon.uk.geant.net", "ae12", []), # POL1-704 + ("rt1.mar.fr.geant.net", "ae12.123", ["IC1"]), # POL1_703 + ("rt1.mar.fr.geant.net", "ae12", []), # POL1-704 + ], +) +def test_ae12_aggregate_dashboards(router, interface, exp_dashboards): + interface = {"router": router, "name": interface, "description": ""} dashboards = poller._get_dashboards(interface) - assert set(d.name for d in dashboards) == {"CAE1"} - - -def test_POL1_703_IC1_dashboards(): - interface = { - 'router': 'rt1.mar.fr.geant.net', - 'name': 'ae12.123', - 'description': '' - } - dashboards = poller._get_dashboards(interface) - assert set(d.name for d in dashboards) == {"IC1"} + assert [d.name for d in dashboards] == exp_dashboards @pytest.mark.parametrize('interface,customers,dashboard_info', [ @@ -466,3 +461,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..50a4f2e03035770d62bd2ed3042075b3db64f79c 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 @@ -595,7 +595,8 @@ def test_persist_ims_data(mocker, data_config, mocked_redis): }, "sid_services": {"SID-001": [{"k1": "data"}, {"k1": "data"}]}, "services_by_type": {}, - "geant_nodes": [] + "geant_nodes": [], + "circuit_ids_to_monitor": ["123", "456", "id1", "sub_circuit_1"] } for k in r.keys("ims:*"): r.delete(k) @@ -941,3 +942,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