diff --git a/inventory_provider/routes/classifier.py b/inventory_provider/routes/classifier.py index b2e96b6522f9ca56bbb2015633178f087863fd0f..2c9f7f415224944f5df175290a8d941275de0540 100644 --- a/inventory_provider/routes/classifier.py +++ b/inventory_provider/routes/classifier.py @@ -1,5 +1,6 @@ import ipaddress import json +import re from flask import Blueprint, Response @@ -33,6 +34,22 @@ def handle_request_error(error): status=error.status_code) +def base_interface_name(interface): + m = re.match(r'(.*?)(\.\d+)?$', interface) + assert m # sanity: anything should match + return m.group(1) + + +def related_interfaces(hostname, interface): + r = common.get_redis() + prefix = 'netconf-interfaces:%s:' % hostname + for k in r.keys(prefix + base_interface_name(interface) + '*'): + k = k.decode('utf-8') + assert k.startswith(prefix) # sanity + assert len(k) > len(prefix) # sanity (contains at least an interface) + yield k[len(prefix):] + + @routes.route("/trap-metadata/<source_equipment>/<path:interface>", methods=['GET', 'POST']) @common.require_accepts_json @@ -57,6 +74,18 @@ def get_trap_metadata(source_equipment, interface): if ifc_info: result['interface'] = json.loads(ifc_info.decode('utf-8')) + def _related_services(): + for related in related_interfaces(source_equipment, interface): + rs = r.get('opsdb:interface_services:%s:%s' + % (source_equipment, related)) + if rs: + for s in json.loads(rs.decode('utf-8')): + yield s + + related_services = list(_related_services()) + if related_services: + result['related-services'] = related_services + if not result: return Response( response="no available info for {} {}".format( diff --git a/test/test_classifier_routes.py b/test/test_classifier_routes.py index 308a725762c796fb715ddaadb7644f736dfb69b7..e799c11c9b68cdda37ffb44a57bba899ece2b473 100644 --- a/test/test_classifier_routes.py +++ b/test/test_classifier_routes.py @@ -7,19 +7,123 @@ DEFAULT_REQUEST_HEADERS = { "Accept": ["application/json"] } +JUNIPER_LINK_METADATA = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + + "definitions": { + "ip-address": { + "type": "string", + "oneOf": [ + {"pattern": r'^(\d+\.){3}\d+$'}, + {"pattern": r'^([a-f\d]{4}:){7}[a-f\d]{4}$'} + ] + }, + "ipv4-interface-address": { + "type": "string", + "pattern": r'^(\d+\.){3}\d+/\d+$' + }, + "ipv6-interface-address": { + "type": "string", + "pattern": r'^[a-f\d:]+/\d+$' + }, + "interface-info": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "description": {"type": "string"}, + "ipv4": { + "type": "array", + "items": {"$ref": "#/definitions/ipv4-interface-address"} + }, + "ipv6": { + "type": "array", + "items": {"$ref": "#/definitions/ipv6-interface-address"} + } + }, + "required": ["name", "description", "ipv4", "ipv6"], + "additionalProperties": False + }, + "service-info": { + "type": "object", + "properties": { + "id": {"type": "integer"}, + "name": {"type": "string"}, + "status": { + "type": "string", + "enum": ["operational", "installed", "planned", "ordered"] + }, + "circuit_type": { + "type": "string", + "enum": ["path", "service", "l2circuit"] + }, + "service_type": {"type": "string"}, + "project": {"type": "string"}, + "equipment": {"type": "string"}, + "other_end_equipment": {"type": "string"}, + "port": {"type": "string"}, + "logical_unit": { + "oneOf": [ + {"type": "integer"}, + {"type": "string", "maxLength": 0} + ] + }, + "other_end_logical_unit": { + "oneOf": [ + {"type": "integer"}, + {"type": "string", "maxLength": 0} + ] + }, + "manufacturer": { + "type": "string", + "enum": ["juniper", "coriant", "infinera", + "cisco", "hewlett packard", + "corsa", "graham smith uk ltd", + "unknown", ""] + }, + "card_id": {"type": "string"}, + "other_end_card_id": {"type": "string"}, + "interface_name": {"type": "string"}, + "other_end_interface_name": {"type": "string"} + }, + # TODO: modify service-info so that "" entries are just omitted + # (... rather than requiring 'oneOf') + # TODO: put 'other_end_*' params in a sub dictionary + "required": [ + "id", "name", "status", + "circuit_type", "service_type", + "project", "port", "manufacturer", + "equipment", "logical_unit", "card_id", "interface_name" + ], + "additionalProperties": False + } + }, + + "type": "object", + "properties": { + "services": { + "type": "array", + "items": {"$ref": "#/definitions/service-info"} + }, + "interface": {"$ref": "#/definitions/interface-info"}, + "related-services": { + "type": "array", + "items": {"$ref": "#/definitions/service-info"} + } + }, + "required": ["interface"], + "additionalProperties": False +} + def test_trap_metadata(client_with_mocked_data): - response_schema = { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object" - } rv = client_with_mocked_data.get( '/classifier/trap-metadata/mx1.ams.nl.geant.net/ae15.1500', headers=DEFAULT_REQUEST_HEADERS) assert rv.status_code == 200 assert rv.is_json response_data = json.loads(rv.data.decode('utf-8')) - jsonschema.validate(response_data, response_schema) + jsonschema.validate(response_data, JUNIPER_LINK_METADATA) VPN_RR_PEER_INFO_KEYS = {'vpn-rr-peer-info'} diff --git a/test/test_classifier_utilities.py b/test/test_classifier_utilities.py new file mode 100644 index 0000000000000000000000000000000000000000..fae79a43f88c6385ac9fd0c4f0aa5cc594383c9f --- /dev/null +++ b/test/test_classifier_utilities.py @@ -0,0 +1,21 @@ +import pytest +from inventory_provider.routes import classifier + +@pytest.mark.parametrize('interface_name,base_name', [ + ('ae0', 'ae0'), + ('ae0.0', 'ae0'), + ('ae1.0', 'ae1'), + ('ae10.2603', 'ae10'), + ('et-3/1/2', 'et-3/1/2'), + ('et-3/1/2.100', 'et-3/1/2'), + ('xe-2/1/0', 'xe-2/1/0'), + ('xe-2/1/0.933', 'xe-2/1/0'), + + # degenerate cases ... check expected regex behavior + ('xe-2/1/0.933.933', 'xe-2/1/0.933'), + (' sss.333.aaa ', ' sss.333.aaa '), + (' sss.333.aaa .999', ' sss.333.aaa ') +] +) +def test_base_interface_name(interface_name, base_name): + assert classifier.base_interface_name(interface_name) == base_name