diff --git a/inventory_provider/db/opsdb.py b/inventory_provider/db/opsdb.py index f9d9cf433fe6b50a88b8449e009f7fd706f92d26..35fae900deebba4c3194455a172183e3aef3e316 100644 --- a/inventory_provider/db/opsdb.py +++ b/inventory_provider/db/opsdb.py @@ -242,3 +242,127 @@ def get_equipment_location_data(connection): # pragma: no cover crs.execute(equipment_location_query) r = _convert_to_dict(crs) return r + + +def get_coriant_path(connection, equipment_name, card_id, port_number): + + base_query = """ +SELECT + + vcc.absid, + vcc.category, + vcc.circuit_type, + vcc.service_type, + vcc.peering_type, + eq_a.name as equipment_name_a, + eq_b.name as equipment_name_b, + eqc_a.card_id as card_id_a, + eqc_a.card_id as card_id_b, + pop_a.name as pop_name_a, + pop_b.name as pop_name_b, + vcc.port_a as port_a, + vcc.port_b as port_b + + FROM vcircuitconns vcc + LEFT JOIN equipment eq_a ON eq_a.absid = vcc.PTR_equip_a + LEFT JOIN equipment eq_b ON eq_b.absid = vcc.PTR_equip_b + LEFT JOIN equipment_card eqc_a ON eqc_a.absid = vcc.PTR_card_a + LEFT JOIN equipment_card eqc_b ON eqc_b.absid = vcc.PTR_card_b + LEFT JOIN pop pop_a ON pop_a.absid = vcc.PTR_pop_a + LEFT JOIN pop pop_b ON pop_b.absid = vcc.PTR_pop_b + """ + + def _fields2rsp(row): + return { + 'absid': row['absid'], + 'category': row['category'], + 'circuit_type': row['circuit_type'], + 'service_type': row['service_type'], + 'peering_type': row['peering_type'], + 'a': { + 'name': row['equipment_name_a'], + 'card id': row['card_id_a'], + 'port number': row['port_a'], + 'pop': row['pop_name_a'] + }, + 'b': { + 'name': row['equipment_name_b'], + 'card id': row['card_id_b'], + 'port number': row['port_b'], + 'pop': row['pop_name_b'] + } + } + + circuit_query = base_query + """ +WHERE vcc.status = 'Operational' + AND ( + (eq_a.name = %(equipment_name)s + AND eqc_a.card_id = %(card_id)s + AND vcc.port_a = %(port_number)s ) + OR + (eq_b.name = %(equipment_name)s + AND eqc_b.card_id = %(card_id)s + AND vcc.port_b = %(port_number)s )) + +ORDER BY FIELD(vcc.circuit_type, 'path') DESC + """ + + parent_query = base_query + """ +JOIN circuit_glue + ON circuit_glue.PTR_circuit = vcc.absid +WHERE circuit_glue.PTR_component = %s +AND circuit_type = 'Path' + """ + + # for_testing = """ + # SELECT parent.absid, child.absid, + # parent.circuit_type as parent_circuit_type, + # child.circuit_type as child_circuit_type, + # eq_a.name as equipment_name_a, + # eq_b.name as equipment_name_b, + # eqc_a.card_id as card_id_a, + # eqc_a.card_id as card_id_b, + # pop_a.name as pop_name_a, + # pop_b.name as pop_name_b, + # parent.port_a as port_a, + # parent.port_b as port_b, + # child.port_a as child_port_a, + # child.port_b as child_port_b + # + # FROM vcircuitconns parent + # RIGHT JOIN circuit_glue + # ON circuit_glue.PTR_circuit = parent.absid + # JOIN vcircuitconns child + # ON circuit_glue.PTR_component = child.absid + # + # LEFT JOIN equipment eq_a ON eq_a.absid = parent.PTR_equip_a + # LEFT JOIN equipment eq_b ON eq_b.absid = parent.PTR_equip_b + # LEFT JOIN equipment_card eqc_a ON eqc_a.absid = parent.PTR_card_a + # LEFT JOIN equipment_card eqc_b ON eqc_b.absid = parent.PTR_card_b + # LEFT JOIN pop pop_a ON pop_a.absid = parent.PTR_pop_a + # LEFT JOIN pop pop_b ON pop_b.absid = parent.PTR_pop_b + # + # WHERE parent.circuit_type = 'Path' + # AND child.circuit_type <> 'Path' + # AND child.status = 'Operational' + # AND (eq_a.name like 'grv%' or eq_b.name like 'grv%') + # """ + + args = { + 'equipment_name': equipment_name, + 'card_id': card_id, + 'port_number': port_number + } + + with db.cursor(connection) as crs: + crs.execute(circuit_query, args) + r = _convert_to_dict(crs) + if not r: + return None + circuit = r[0] + if circuit['circuit_type'].lower() == 'path': + return _fields2rsp(circuit) + + crs.execute(parent_query, [circuit['absid']]) + r = _convert_to_dict(crs) + return _fields2rsp(r[0]) if r else None diff --git a/inventory_provider/routes/classifier.py b/inventory_provider/routes/classifier.py index 6a604491242dd778ef6a72c456d1654c1cf016ed..1a942124eaed307b2fa2480714c1ff4d84fb56c1 100644 --- a/inventory_provider/routes/classifier.py +++ b/inventory_provider/routes/classifier.py @@ -2,9 +2,10 @@ import ipaddress import json import re -from flask import Blueprint, Response +from flask import Blueprint, Response, current_app from inventory_provider.routes import common +from inventory_provider.db import opsdb, db routes = Blueprint("inventory-data-classifier-support-routes", __name__) @@ -277,3 +278,36 @@ def get_trap_metadata(source_equipment, interface, circuit_id): r.set(cache_key, result.encode('utf-8')) return Response(result, mimetype="application/json") + + +@routes.route("/coriant-info/" + "<equipment_name>/<card_id>/<port_number>", + methods=['GET', 'POST']) +@common.require_accepts_json +def get_coriant_info(equipment_name, card_id, port_number): + r = common.get_redis() + + cache_key = 'classifier-cache:coriant:%s:%s:%s' % ( + equipment_name, card_id, port_number) + result = r.get(cache_key) + + if result: + result = result.decode('utf-8') + else: + config = current_app.config['INVENTORY_PROVIDER_CONFIG'] + with db.connection(config['ops-db']) as cx: + path = opsdb.get_coriant_path( + cx, equipment_name, card_id, port_number) + + if not path: + return Response( + response="no available info for {} {} {}".format( + equipment_name, card_id, port_number), + status=404, + mimetype="text/html") + + result = json.dumps({'path': path}) + # cache this data for the next call + r.set(cache_key, result.encode('utf-8')) + + return Response(result, mimetype="application/json") diff --git a/test/test_classifier_routes.py b/test/test_classifier_routes.py index 2b4e76a52a5610a5179387e8db383aa20010ea6b..cdbdc396d34edd9aad75a33b7358c90ed22c45df 100644 --- a/test/test_classifier_routes.py +++ b/test/test_classifier_routes.py @@ -1,3 +1,4 @@ +import contextlib import json import jsonschema import pytest @@ -271,3 +272,58 @@ def test_peer_not_found(client_with_mocked_data): '/classifier/peer-info/1.2.3.4', headers=DEFAULT_REQUEST_HEADERS) assert rv.status_code == 404 + + +def test_coriant_info(client, mocker): + """ + just check the correct method is called, but mock out all sql access + """ + expected_path = { + 'C': 'bogus connection', + 'E': 'bogus equipment name', + 'CID': 'bogus card id', + 'P': 'bogus card number' + } + + @contextlib.contextmanager + def mocked_connection(ignored): + yield expected_path['C'] + + mocker.patch( + 'inventory_provider.db.db.connection', mocked_connection) + mocker.patch( + 'inventory_provider.db.opsdb.get_coriant_path', + lambda a, b, c, d: {'C': a, 'E': b, 'CID': c, 'P': d}) + + rv = client.get( + '/classifier/coriant-info/{E}/{CID}/{P}'.format(**expected_path), + headers=DEFAULT_REQUEST_HEADERS) + + assert rv.status_code == 200 + assert rv.is_json + response_data = json.loads(rv.data.decode('utf-8')) + + expected_response = {'path': expected_path} + assert response_data == expected_response + + +def test_coriant_info_not_found(client, mocker): + """ + just check the correct method is called, but mock out all sql access + """ + + @contextlib.contextmanager + def mocked_connection(ignored): + yield None + + mocker.patch( + 'inventory_provider.db.db.connection', mocked_connection) + mocker.patch( + 'inventory_provider.db.opsdb.get_coriant_path', + lambda a, b, c, d: None) + + rv = client.get( + '/classifier/coriant-info/aaa/bbb/ccc', + headers=DEFAULT_REQUEST_HEADERS) + + assert rv.status_code == 404 diff --git a/test/test_opsdb_queries.py b/test/test_opsdb_queries.py new file mode 100644 index 0000000000000000000000000000000000000000..5fbada7715f7c801a59c67c57b7c30194cca17e7 --- /dev/null +++ b/test/test_opsdb_queries.py @@ -0,0 +1,48 @@ +import os +import pytest + +from inventory_provider.db import db +from inventory_provider.db import opsdb + + +pytestmark = pytest.mark.skipif( + 'TEST_OPSDB_HOSTNAME' not in os.environ, + reason='TEST_OPSDB_HOSTNAME environment variable not found') +pytestmark = pytest.mark.skipif( + 'TEST_OPSDB_DBNAME' not in os.environ, + reason='TEST_OPSDB_DBNAME environment variable not found') +pytestmark = pytest.mark.skipif( + 'TEST_OPSDB_USERNAME' not in os.environ, + reason='TEST_OPSDB_USERNAME environment variable not found') +pytestmark = pytest.mark.skipif( + 'TEST_OPSDB_PASSWORD' not in os.environ, + reason='TEST_OPSDB_PASSWORD environment variable not found') + + +@pytest.fixture +def db_params(): + return { + 'hostname': os.environ['TEST_OPSDB_HOSTNAME'], + 'dbname': os.environ['TEST_OPSDB_DBNAME'], + 'username': os.environ['TEST_OPSDB_USERNAME'], + 'password': os.environ['TEST_OPSDB_PASSWORD'], + } + + +@pytest.fixture +def connection(db_params): + with db.connection(db_params) as c: + yield c + + +@pytest.mark.parametrize('equipment,card,port', [ + ('grv3.lon.uk.geant.net', '1-1', '3'), + ('grv3.lon.uk.geant.net', '1-1', '5'), + ('grv1.ams.nl.geant.net', '1-1', '1'), + ('grv3.lon.uk.geant.net', '1-1', '1'), +]) +def test_query(connection, equipment, card, port): + circuit = opsdb.get_coriant_path(connection, equipment, card, port) + + from pprint import pprint + pprint(circuit) diff --git a/tox.ini b/tox.ini index 858bdcba2d31ffcf351627300f188d49725d99e6..4eeb11cee311ecf0857ca098950ad07a9ea67fa9 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,7 @@ envlist = py36 [testenv] +passenv = TEST_OPSDB_HOSTNAME TEST_OPSDB_DBNAME TEST_OPSDB_USERNAME TEST_OPSDB_PASSWORD deps = coverage flake8