diff --git a/inventory_provider/routes/msr.py b/inventory_provider/routes/msr.py index cdd3cc64315961f71ef4a458e6a58db5d5c662d4..c42944535dd02552337940efd8e98f878df634c1 100644 --- a/inventory_provider/routes/msr.py +++ b/inventory_provider/routes/msr.py @@ -882,6 +882,127 @@ def get_peering_services(): return Response(response, mimetype="application/json") +@functools.cache +def _load_all_interfaces(): + """ + loads all ip interfaces in the network and returns as a dict + of dicts: + hostname -> interface name -> interface info + + :return: dict of dicts + """ + # dict of dicts: + # peering_info[hostname][interface_name] = dict of ifc details + result = defaultdict(dict) + + host_if_extraction_re = re.compile(r'^netconf-interfaces:(.+?):') + for doc in common.load_json_docs( + config_params=current_app.config['INVENTORY_PROVIDER_CONFIG'], + key_pattern='netconf-interfaces:*', + num_threads=20): + matches = host_if_extraction_re.match(doc['key']) + if matches: + hostname = matches[1] + interface_name = doc['value']['name'] + result[hostname][interface_name] = doc['value'] + + return result + + +@functools.cache +def _load_redundant_access_peers(): + """ + load all peers that should be considered + redundant for access services + + that is, all peers for services like NREN-APx + + :return: dict of [peer address] -> [remote asn] + """ + r = common.get_current_redis() + result = {} + + # cf. REPORTING-312: limited to eGEANT group, + # but this can be expanded here in future + redundant_access_groups = ['eGEANT'] + for g in redundant_access_groups: + doc = r.get(f'juniper-peerings:group:{g}') + for peer in json.loads(doc.decode('utf-8')): + result[peer['address']] = peer['remote-asn'] + + return result + + +def _ip_endpoint_extractor(endpoint_details: dict): + """ + special-purpose method used only by _endpoint_extractor + + operates on a dictionary formatted as in worker.transform_ims_data + (cf sid_services) + + WARNING: assumes hostname is a router in _load_all_interfaces, + asserts if not + + :param endpoint_details: dict formatted as in worker.transform_ims_data + :return: dict formatted + as SYSTEM_CORRELATION_SERVICES_LIST_SCHEMA.ip-endpoint + """ + hostname = ims_equipment_to_hostname(endpoint_details['equipment']) + interface = endpoint_details['port'].lower() + + ip_endpoint = { + 'hostname': hostname, + 'interface': interface, + } + + all_interfaces = _load_all_interfaces() + # sanity: should have already been checked + assert hostname in all_interfaces + + host_info = all_interfaces[hostname] + interface_info = host_info.get(interface, {}) + + addresses = {} + ipv4 = interface_info.get('ipv4') + ipv6 = interface_info.get('ipv6') + if ipv4: + addresses['v4'] = ipv4[0] + if ipv6: + addresses['v6'] = ipv6[0] + if addresses: + ip_endpoint['addresses'] = addresses + + return ip_endpoint + + +def _endpoint_extractor(endpoint_details: Dict): + """ + special-purpose method used only by get_system_correlation_services + + operates on a dictionary formatted as in worker.transform_ims_data + (cf sid_services) + + WARNING: assumes hostname is a router in _load_all_interfaces, + asserts if not + + :param endpoint_details: dict formatted as in worker.transform_ims_data + :return: dict formatted as one element of the array + SYSTEM_CORRELATION_SERVICES_LIST_SCHEMA.endpoints + """ + if not endpoint_details['geant_equipment']: + return + potential_hostname = ims_equipment_to_hostname( + endpoint_details['equipment']) + all_routers = _load_all_interfaces().keys() + if potential_hostname in all_routers: + return _ip_endpoint_extractor(endpoint_details) + else: + return { + 'equipment': endpoint_details['equipment'], + 'port': endpoint_details['port'] + } + + @routes.route('/services', methods=['GET', 'POST']) @common.require_accepts_json def get_system_correlation_services(): @@ -901,87 +1022,27 @@ def get_system_correlation_services(): :return: """ + def _get_redundancy_asn(endpoints): + # endpoints should be a list formatted as + # SYSTEM_CORRELATION_SERVICES_LIST_SCHEMA.endpoints + + redundant_peerings = _load_redundant_access_peers() + for ep in endpoints: + addresses = ep.get('addresses', {}) + for ifc_address in addresses.values(): + for p, asn in redundant_peerings.items(): + peer = ipaddress.ip_address(p) + ifc = ipaddress.ip_interface(ifc_address) + if peer in ifc.network: + return asn + return None + cache_key = 'classifier-cache:msr:services' r = common.get_current_redis() response = _ignore_cache_or_retrieve(request, cache_key, r) if not response: - # dict of dicts: - # peering_info[hostname][interface_name] = dict of ifc details - peering_info = defaultdict(dict) - - host_if_extraction_re = re.compile(r'^netconf-interfaces:(.+?):') - for doc in common.load_json_docs( - config_params=current_app.config['INVENTORY_PROVIDER_CONFIG'], - key_pattern='netconf-interfaces:*', - num_threads=20): - matches = host_if_extraction_re.match(doc['key']) - if matches: - peering_info[matches[1]][doc['value']['name']] = doc['value'] - - def _load_redundant_peers(): - # REPORTING-312: limited to eGEANT group, - # but this can be expanded here in future - redundant_access_groups = ['eGEANT'] - for g in redundant_access_groups: - doc = r.get(f'juniper-peerings:group:{g}') - for peer in json.loads(doc.decode('utf-8')): - yield peer['address'], peer['remote-asn'] - - # dict of peers & asn's: [peer address] -> asn - redundant_access_group_peers = dict(_load_redundant_peers()) - - def _ip_endpoint_extractor(endpoint_details: Dict): - hostname = ims_equipment_to_hostname(endpoint_details['equipment']) - interface = endpoint_details['port'].lower() - - ip_endpoint = { - 'hostname': hostname, - 'interface': interface, - } - host_info = peering_info[hostname] - interface_info = host_info.get(interface, {}) - - addresses = {} - ipv4 = interface_info.get('ipv4') - ipv6 = interface_info.get('ipv6') - if ipv4: - addresses['v4'] = ipv4[0] - if ipv6: - addresses['v6'] = ipv6[0] - if addresses: - ip_endpoint['addresses'] = addresses - - return ip_endpoint - - def _optical_endpoint_extractor(endpoint_details: Dict): - return { - 'equipment': endpoint_details['equipment'], - 'port': endpoint_details['port'] - } - - def _endpoint_extractor(endpoint_details: Dict): - if not endpoint_details['geant_equipment']: - return - potential_hostname = ims_equipment_to_hostname( - endpoint_details['equipment']) - if potential_hostname in peering_info.keys(): - return _ip_endpoint_extractor(endpoint_details) - else: - return _optical_endpoint_extractor(endpoint_details) - - def _get_redundancy_asn(service): - for ep in service['endpoints']: - addresses = ep.get('addresses', {}) - for ifc_address in addresses.values(): - for p, asn in redundant_access_group_peers.items(): - peer = ipaddress.ip_address(p) - ifc = ipaddress.ip_interface(ifc_address) - if peer in ifc.network: - return asn - return None - sid_services = json.loads(r.get('ims:sid_services').decode('utf-8')) response = [] @@ -1003,7 +1064,7 @@ def get_system_correlation_services(): service_info['endpoints'].append(endpoint) if service_info.get('endpoints'): - asn = _get_redundancy_asn(service_info) + asn = _get_redundancy_asn(service_info['endpoints']) if asn: service_info['redundant_asn'] = asn response.append(service_info) @@ -1013,7 +1074,7 @@ def get_system_correlation_services(): if response: response = json.dumps(response, indent=2) - r.set(cache_key, response.encode('utf-8')) + # r.set(cache_key, response.encode('utf-8')) if not response: return Response(