diff --git a/inventory_provider/db/opsdb.py b/inventory_provider/db/opsdb.py index e41748fd1c7472fdc2ac59914ad3c73d3108a55f..07a24e275ca2aa107ddcfc1199fee64292ac4745 100644 --- a/inventory_provider/db/opsdb.py +++ b/inventory_provider/db/opsdb.py @@ -1,5 +1,11 @@ +import logging +import re +from collections import defaultdict + from inventory_provider.db import db +logger = logging.getLogger(__name__) + def _convert_to_dict(crs): return [dict((crs.description[i][0], "" if value is None else value) @@ -109,6 +115,65 @@ WHERE return r +def get_fibre_spans(connection): + + _sql = """ +SELECT c.absid, c.name, +parent.absid parent_absid, parent.name parent_name, +parent.status parent_status, LOWER(parent.circuit_type) parent_type, +pa.name pop_a, pa.abbreviation pop_abbr_a, +ea.name equipment_a, LOWER(ea.type) eq_type_a, +pb.name pop_b, pb.abbreviation pop_abbr_b, +eb.name equipment_b, LOWER(eb.type) eq_type_b +FROM vcircuitconns c +INNER JOIN pop pa ON pa.absid = c.PTR_pop_a +INNER JOIN pop pb ON pb.absid = c.PTR_pop_b +INNER JOIN equipment ea ON ea.absid = c.PTR_equip_a +INNER JOIN equipment eb ON eb.absid = c.PTR_equip_b +INNER JOIN circuit_glue cg ON c.absid = cg.PTR_component +INNER JOIN circuit parent ON parent.absid = cg.PTR_circuit +WHERE +c.is_circuit = 1 AND c.status != 'terminated' AND parent.status != 'terminated' +AND c.circuit_type = 'fibre span' +""" + + ne_details = {} + with db.cursor(connection) as crs: + crs.execute(_sql) + rows = _convert_to_dict(crs) + for row in rows: + if row['parent_type'] != 'fibre route': + logger.debug(f'Wrong Parent Type c: {row["absid"]} ' + f'p: {row["parent_absid"]} {row["parent_type"]}') + continue + ne_pattern = r'.+-(OLA|DTNX)\d+-\d.*' + ne_a_match = re.match(ne_pattern, row['equipment_a']) + ne_b_match = re.match(ne_pattern, row['equipment_b']) + if ne_a_match: + ne_details[f'{row["equipment_a"]}_{row["parent_absid"]}'] = { + 'ne': row['equipment_a'], + 'df_route': row['parent_name'], + 'df_route_id': row['parent_absid'], + 'df_status': row['parent_status'], + 'pop': row['pop_a'], + 'pop_abbreviation': row['pop_abbr_a'], + } + if ne_b_match: + ne_details[f'{row["equipment_b"]}_{row["parent_absid"]}'] = { + 'ne': row['equipment_b'], + 'df_route': row['parent_name'], + 'df_route_id': row['parent_absid'], + 'df_status': row['parent_status'], + 'pop': row['pop_b'], + 'pop_abbreviation': row['pop_abbr_b'] + } + by_ne = defaultdict(lambda: []) + for d in ne_details.values(): + by_ne[d['ne']].append(d) + + yield from by_ne.items() + + def get_circuits(connection): _sql = """ SELECT * diff --git a/inventory_provider/routes/classifier.py b/inventory_provider/routes/classifier.py index f11e22692d08fa313a2fdf0b1a1a74e5507c042e..ba9f31a365bb86cea64771353215471e8726a906 100644 --- a/inventory_provider/routes/classifier.py +++ b/inventory_provider/routes/classifier.py @@ -1,4 +1,5 @@ import ipaddress +import itertools import json import logging import re @@ -412,6 +413,72 @@ def get_trap_metadata(source_equipment, interface, circuit_id): return Response(result, mimetype="application/json") +@routes.route("/infinera-fiberlink-info/<ne_name_str>/<object_name_str>", + methods=['GET', 'POST']) +@common.require_accepts_json +def get_fiberlink_trap_metadata(ne_name_str, object_name_str): + objects = object_name_str.split('_') + shelves = [x.split('-')[0] for x in objects] + p = r'([a-zA-Z\d]+?-(OLA|DTNX)\d+(-\d)?)' + matches = re.findall(p, ne_name_str) + if len(matches) != 2 or len(shelves) != 2: + raise ClassifierProcessingError( + f'unable to parse {ne_name_str} {object_name_str } ' + 'into two elements') + + r = common.get_current_redis() + + # double check that we only need to check the two nodes and not the objects + cache_key = f'classifier-cache:fiberlink:{ne_name_str}:{object_name_str}' + result = r.get(cache_key) + + if result: + result = result.decode('utf-8') + else: + nes_a = f'{matches[0][0]}-{shelves[0]}' + nes_b = f'{matches[1][0]}-{shelves[1]}' + result = [] + df_a = r.get(f'opsdb:ne_fibre_spans:{nes_a}') + df_b = r.get(f'opsdb:ne_fibre_spans:{nes_b}') + if df_a and df_b: + a = json.loads(df_a.decode('utf-8')) + b = json.loads(df_b.decode('utf-8')) + + matches = [x for x in itertools.product(a, b) if + x[0]['df_route_id'] == x[1]['df_route_id']] + if matches: + match = matches[0] + result = { + 'ends': { + 'a': { + 'pop': match[0]['pop'], + 'pop_abbreviation': match[0]['pop_abbreviation'], + }, + 'b': { + 'pop': match[1]['pop'], + 'pop_abbreviation': match[1]['pop_abbreviation'], + }, + }, + 'df_route': { + 'id': match[0]['df_route_id'], + 'name': match[0]['df_route'], + 'status': match[0]['df_status'], + }, + 'related-services': + get_top_level_services(match[0]['df_route_id'], r) + } + result = json.dumps(result) + r.set(cache_key, result) + if not result: + return Response( + response="no available info for " + f"{ne_name_str} {object_name_str}", + status=404, + mimetype="text/html") + + return Response(result, mimetype="application/json") + + @routes.route('/coriant-info/<equipment_name>/<path:entity_string>', methods=['GET', 'POST']) @common.require_accepts_json diff --git a/inventory_provider/routes/testing.py b/inventory_provider/routes/testing.py index 60073eae1eb16aa4ebc146202002a31a74979101..6f9ce50e67bdb58aec172e2276b64fcd883e77f1 100644 --- a/inventory_provider/routes/testing.py +++ b/inventory_provider/routes/testing.py @@ -41,6 +41,12 @@ def update_geant_lambdas(): return Response('OK') +@routes.route("update-fibre-spans", methods=['GET', 'POST']) +def update_fibre_spans(): + worker.update_fibre_spans.delay() + return Response('OK') + + @routes.route("update-service-hierarchy") def update_service_hierarchy(): worker.update_circuit_hierarchy.delay() diff --git a/inventory_provider/tasks/worker.py b/inventory_provider/tasks/worker.py index 9189a9cd0b8552d9604f03030256d44bca9f4e5f..944fc2bb8912a9746078aee2215fcc70954add87 100644 --- a/inventory_provider/tasks/worker.py +++ b/inventory_provider/tasks/worker.py @@ -311,6 +311,26 @@ def update_geant_lambdas(self): rp.execute() +@app.task(base=InventoryTask, bind=True, name='update_fibre_spans') +@log_task_entry_and_exit +def update_fibre_spans(self): + r = get_next_redis(InventoryTask.config) + rp = r.pipeline() + # scan with bigger batches, to mitigate network latency effects + for key in r.scan_iter('opsdb:ne_fibre_spans:*', count=1000): + rp.delete(key) + rp.execute() + + with db.connection(InventoryTask.config["ops-db"]) as cx: + rp = r.pipeline() + for ne, fs in opsdb.get_fibre_spans(cx): + + rp.set( + f'opsdb:ne_fibre_spans:{ne}', + json.dumps(fs)) + rp.execute() + + @app.task(base=InventoryTask, bind=True, name='update_neteng_managed_device_list') @log_task_entry_and_exit diff --git a/test/test_classifier_routes.py b/test/test_classifier_routes.py index caee5904d1e17a9f09d3b0f681c05a19c36d31d2..aaef9369e7914ae3032073bdc4389e5ceff4cb5f 100644 --- a/test/test_classifier_routes.py +++ b/test/test_classifier_routes.py @@ -461,3 +461,69 @@ def test_coriant_info_not_found(client, mocker): headers=DEFAULT_REQUEST_HEADERS) assert rv.status_code == 404 + + +def test_infinera_unparseable_fiberlink(client): + rv = client.get( + '/classifier/infinera-fiberlink-info/unparseableentitystring/aaa', + headers=DEFAULT_REQUEST_HEADERS) + assert rv.status_code == 422 + + rv = client.get( + '/classifier/infinera-fiberlink-info/XXX-OLA1-XXX02-DTNX10-1/aaa', + headers=DEFAULT_REQUEST_HEADERS) + assert rv.status_code == 422 + + +def test_infinera_fiberlink_not_found(client): + rv = client.get( + '/classifier/infinera-fiberlink-info/' + 'XXX-OLA1-XXX02-DTNX10-1/1-A-2-L1_3-A-2-L1', + headers=DEFAULT_REQUEST_HEADERS) + assert rv.status_code == 404 + + +def test_infinera_fiberlink(client, mocker): + # mocker.patch('inventory_provider.routes.classifier.json.dumps') + mocked_redis = mocker.patch( + 'inventory_provider.routes.classifier.common.get_current_redis') + mg = mocked_redis.return_value.get + mg.side_effect = [ + False, + b"[{\"ne\": \"XXX-OLA1-1\", \"df_route\": \"end1-end2-dfroute\", \"df_route_id\": 1234, \"df_status\": \"Operational\", \"pop\": \"POP 1\", \"pop_abbreviation\": \"p1\"}]", # noqa + b"[{\"ne\": \"XXX02-DTNX10-1-3\", \"df_route\": \"end1-end2-dfroute\", \"df_route_id\": 1234, \"df_status\": \"Operational\", \"pop\": \"POP 2\", \"pop_abbreviation\": \"p2\"}]", # noqa + ] + mocked_tls = mocker.patch( + 'inventory_provider.routes.classifier.get_top_level_services') + mocked_tls.return_value = {'a': 'A'} + rv = client.get( + '/classifier/infinera-fiberlink-info/' + 'XXX-OLA1-XXX02-DTNX10-1/1-A-2-L1_3-A-2-L1', + headers=DEFAULT_REQUEST_HEADERS) + mg.assert_any_call( + 'classifier-cache:fiberlink:XXX-OLA1-XXX02-DTNX10-1:1-A-2-L1_3-A-2-L1') + mg.assert_any_call('opsdb:ne_fibre_spans:XXX-OLA1-1') + mg.assert_any_call('opsdb:ne_fibre_spans:XXX02-DTNX10-1-3') + mocked_tls.assert_called_with(1234, mocker.ANY) + + expected = { + 'ends': { + 'a': { + 'pop': 'POP 1', + 'pop_abbreviation': 'p1', + }, + 'b': { + 'pop': 'POP 2', + 'pop_abbreviation': 'p2', + }, + }, + 'df_route': { + 'id': 1234, + 'name': 'end1-end2-dfroute', + 'status': 'Operational', + }, + 'related-services': {'a': 'A'} + } + + assert rv.status_code == 200 + assert rv.get_data(as_text=True) == json.dumps(expected)