import ipaddress import json import logging import re 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__) logger = logging.getLogger(__name__) class ClassifierRequestError(Exception): status_code = 500 def __init__(self): super().__init__() self.message = "Unclassified Internal Error" class ClassifierProcessingError(ClassifierRequestError): status_code = 422 def __init__(self, message, status_code=None): super().__init__() self.message = str(message) if status_code is not None: self.status_code = status_code @routes.errorhandler(ClassifierRequestError) def handle_request_error(error): return Response( response=error.message, status=error.status_code) @routes.after_request def after_request(resp): return common.after_request(resp) def related_interfaces(hostname, interface): r = common.get_current_redis() prefix = 'netconf-interfaces:%s:' % hostname for k in r.keys(prefix + 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):] def get_top_level_services(circuit_id, r): tls = [] results = r.get("opsdb:services:children:{}".format(circuit_id)) if results: results = json.loads(results.decode('utf-8')) for c in results: temp_parents = \ get_top_level_services(c['parent_circuit_id'], r) if not temp_parents: tls.append( {'name': c['parent_circuit'], 'status': c['parent_circuit_status'], 'circuit_type': c['parent_circuit_type'].lower()}) tls.extend(temp_parents) return tls @routes.route("/juniper-link-info/<source_equipment>/<path:interface>", methods=['GET', 'POST']) @common.require_accepts_json def get_juniper_link_info(source_equipment, interface): r = common.get_current_redis() cache_key = 'classifier-cache:juniper:%s:%s' % ( source_equipment, interface) result = r.get(cache_key) if result: result = result.decode('utf-8') else: result = {} top_level_services = [] services = r.get( 'opsdb:interface_services:%s:%s' % (source_equipment, interface)) if services: result['services'] = json.loads(services.decode('utf=8')) for s in result['services']: top_level_services.extend(get_top_level_services(s['id'], r)) ifc_info = r.get( 'netconf-interfaces:%s:%s' % (source_equipment, interface)) if ifc_info: result['interface'] = json.loads(ifc_info.decode('utf-8')) else: # warning: this should match the structure returned by # juniper:list_interfaces:_ifc_info result['interface'] = { 'name': interface, 'description': '', 'bundle': [], 'ipv4': [], 'ipv6': [] } def _related_services(): for related in related_interfaces(source_equipment, interface): rs = r.get('opsdb:interface_services:%s:%s' % (source_equipment, related)) all_rs = [] if rs: for s in json.loads(rs.decode('utf-8')): top_level_services.extend( get_top_level_services(s['id'], r)) all_rs.append( { 'name': s['name'], 'status': s['status'], 'circuit_type': s['circuit_type'] }) return all_rs related_services = _related_services() if related_services: top_level_services.extend(related_services) if top_level_services: result['related-services'] = top_level_services result = json.dumps(result) # cache this data for the next call r.set(cache_key, result.encode('utf-8')) return Response(result, mimetype="application/json") def ix_peering_info(peer_info): """ TODO: this is probably the least efficient way of doing this (if it's a problem, pre-compute these lists) :param peer_info: an element from ix_public_peer:address :return: """ result = { 'peer': peer_info, 'group': [], 'router': [] } try: address = ipaddress.ip_address(peer_info['name']) except ValueError: raise ClassifierProcessingError( 'unable to parse %r as an ip address' % address) description = peer_info['description'] assert description is not None # sanity keyword = description.split(' ')[0] # regex needed??? (e.g. tabs???) r = common.get_current_redis() for k in r.keys('ix_public_peer:*'): other = r.get(k.decode('utf-8')).decode('utf-8') other = json.loads(other) if other['router'] == peer_info['router']: result['router'].append(other['name']) assert other['description'] is not None # sanity: as above... if other['description'].startswith(keyword): result['group'].append(other['name']) return result def find_interfaces(address): """ TODO: this is probably the least efficient way of doing this (if it's a problem, pre-compute these lists) :param address: an ipaddress object :return: """ r = common.get_current_redis() for k in r.keys('subnets:*'): k = k.decode('utf-8') m = re.match(r'^subnets:(.*)$', k) assert m, 'sanity failure: redis returned an invalid key name' interface = ipaddress.ip_interface(m.group(1)) if address in interface.network: info = r.get(k).decode('utf-8') info = json.loads(info) for ifc in info: yield ifc def find_interfaces_and_services(address_str): """ :param address_str: an ipaddress object :return: """ try: address = ipaddress.ip_address(address_str) except ValueError: raise ClassifierProcessingError( 'unable to parse %r as an ip address' % address_str) r = common.get_current_redis() for interface in find_interfaces(address): services = r.get( 'opsdb:interface_services:%s:%s' % ( interface['router'], interface['interface name'])) if not services: services = [] else: services = json.loads(services.decode('utf=8')) yield { 'interface': interface, 'services': services } @routes.route("/peer-info/<address>", methods=['GET', 'POST']) @common.require_accepts_json def peer_info(address): # canonicalize the input address first ... try: obj = ipaddress.ip_address(address) address = obj.exploded except ValueError: raise ClassifierProcessingError( 'unable to parse %r as an ip address' % address) r = common.get_current_redis() cache_key = 'classifier-cache:peer:%s' % address result = r.get(cache_key) if result: result = result.decode('utf-8') else: result = {} info = r.get('ix_public_peer:%s' % address) if info: info = info.decode('utf-8') result['ix-public-peer-info'] = ix_peering_info(json.loads(info)) info = r.get('vpn_rr_peer:%s' % address) if info: info = info.decode('utf-8') result['vpn-rr-peer-info'] = json.loads(info) interfaces = list(find_interfaces_and_services(address)) if interfaces: result['interfaces'] = interfaces result = json.dumps(result) # cache this data for the next call r.set(cache_key, result.encode('utf-8')) return Response(result, mimetype="application/json") @routes.route("/infinera-lambda-info/" "<source_equipment>/<interface>/<circuit_id>", methods=['GET', 'POST']) @common.require_accepts_json def get_trap_metadata(source_equipment, interface, circuit_id): interface = interface.replace('-T', '-') cache_key = 'classifier-cache:infinera:%s:%s' % ( source_equipment, interface) r = common.get_current_redis() result = r.get(cache_key) if result: result = result.decode('utf-8') else: result = {} services = r.get( 'opsdb:interface_services:%s:%s' % (source_equipment, interface)) if services: result['services'] = json.loads(services.decode('utf=8')) gl = r.get('opsdb:geant_lambdas:%s' % circuit_id.lower()) if gl: t_gl = json.loads(gl.decode('utf=8')) t_gl['status'] = t_gl['status'].lower() result['geant-lambda'] = t_gl if not result: return Response( response="no available info for {} {}".format( source_equipment, interface), status=404, mimetype="text/html") result = json.dumps(result) # cache this data for the next call r.set(cache_key, result.encode('utf-8')) return Response(result, mimetype="application/json") @routes.route('/coriant-info/<equipment_name>/<path:entity_string>', methods=['GET', 'POST']) @common.require_accepts_json def get_coriant_info(equipment_name, entity_string): r = common.get_current_redis() cache_key = 'classifier-cache:coriant:%s:%s' % ( equipment_name, entity_string) result = r.get(cache_key) # this is just for development to save deleting the cache every time # result = False if result: result = result.decode('utf-8') else: m = re.match(r'^(\d+\-\d+)\.(\d+)', entity_string) if not m: logger.error( 'invalid coriant entity string format: %r' % entity_string) return Response( response="no available info for '{}' '{}'".format( equipment_name, entity_string), status=404, mimetype="text/html") result = { 'equipment name': equipment_name, 'card id': m.group(1), 'port number': m.group(2) } config = current_app.config['INVENTORY_PROVIDER_CONFIG'] with db.connection(config['ops-db']) as cx: path = opsdb.lookup_coriant_path( cx, equipment_name, result['card id'], result['port number']) if path: result['path'] = path top_level_services = get_top_level_services(path['id'], r) if top_level_services: result['related-services'] = top_level_services else: logger.error('no path found for {}:{}'.format( equipment_name, entity_string)) # cache this data for the next call result = json.dumps(result).encode('utf-8') r.set(cache_key, result) return Response(result, mimetype="application/json")