diff --git a/inventory_provider/routes/poller.py b/inventory_provider/routes/poller.py index 03aea603f9cd88fcb095d98f5289762659db0b13..4ea3052c5eac65b4e2cdb08b36ff1a3bbd86dd8c 100644 --- a/inventory_provider/routes/poller.py +++ b/inventory_provider/routes/poller.py @@ -62,6 +62,7 @@ support method: _get_dashboards """ from enum import Enum, auto +import itertools import json import logging import re @@ -605,6 +606,39 @@ def _load_services(config, hostname=None, use_next_redis=False): return result +def _load_netconf_docs( + config, filter_pattern, use_next_redis=False): + """ + yields dicts like: + { + 'router': router hostname + 'netconf': loaded netconf xml doc + } + + :param config: app config + :param filter_pattern: search filter, including 'netconf:' + :param use_next_redis: use next instead of current redis, if true + :return: yields netconf docs, formatted as above + """ + + m = re.match(r'^(.*netconf:).+', filter_pattern) + # TODO: probably better to not required netconf: to be passed in + assert m # sanity + key_prefix_len = len(m.group(1)) + assert key_prefix_len >= len('netconf:') # sanity + + for doc in common.load_xml_docs( + config_params=config, + key_pattern=filter_pattern, + num_threads=10, + use_next_redis=use_next_redis): + + yield { + 'router': doc['key'][key_prefix_len:], + 'netconf': doc['value'] + } + + def _load_interfaces( config, hostname=None, no_lab=False, use_next_redis=False): """ @@ -617,25 +651,14 @@ def _load_interfaces( """ def _load_docs(key_pattern): - m = re.match(r'^(.*netconf:).+', key_pattern) - assert m # sanity - key_prefix_len = len(m.group(1)) - assert key_prefix_len >= len('netconf:') # sanity + for doc in _load_netconf_docs(config, key_pattern, use_next_redis): - for doc in common.load_xml_docs( - config_params=config, - key_pattern=key_pattern, - num_threads=10, - use_next_redis=use_next_redis): - - router = doc['key'][key_prefix_len:] - - for ifc in juniper.list_interfaces(doc['value']): + for ifc in juniper.list_interfaces(doc['netconf']): if not ifc['description']: continue yield { - 'router': router, + 'router': doc['router'], 'name': ifc['name'], 'bundle': ifc['bundle'], 'bundle-parents': [], @@ -894,18 +917,38 @@ def interface_speeds(hostname=None): return Response(result, mimetype="application/json") -MX1_FRA = 'mx1.fra.de.geant.net' +def _load_community_strings(base_key_pattern): + for doc in _load_netconf_docs( + config=current_app.config['INVENTORY_PROVIDER_CONFIG'], + filter_pattern=base_key_pattern): + community = juniper.snmp_community_string(doc['netconf']) + if not community: + # HACKHACK: vpn source ip isn't in acl + # TODO: remove this when done testing! + community = '0pBiFbD' + if not community: + yield { + 'router': doc['router'], + 'error': f'error extracting community string for {hostname}' + } + else: + yield { + 'router': doc['router'], + 'community': community + } @routes.route('/eumetsat-multicast', methods=['GET', 'POST']) @routes.route('/eumetsat-multicast/<hostname>', methods=['GET', 'POST']) @common.require_accepts_json -def eumetsat_multicast(hostname=MX1_FRA): +def eumetsat_multicast(hostname=None): """ Handler for `/poller/eumetsat-multicast` which returns information about multicast subscriptions on mx1.fra.de.geant.net. - The hostname is optional, with default value mx1.fra.de.geant.net + The hostname parameter is optional. If it is present, only hostnames + matching `hostname*` are returned. If not present, data for all + `mx*` routers is returned. The response is a list of oid/router/community structures that all all subscription octet counters to be polled. @@ -934,41 +977,53 @@ def eumetsat_multicast(hostname=MX1_FRA): f'.{sub["subscription"]}.{sub["endpoint"]}' '.255.255.255.255') - cache_key = f'classifier-cache:poller-eumetsat-multicast:{hostname}' - r = common.get_current_redis() + cache_key = f'classifier-cache:poller-eumetsat-multicast' + if hostname: + cache_key = f'{cache_key}:{hostname}' + result = r.get(cache_key) if result: result = result.decode('utf-8') else: - netconf = r.get(f'netconf:{hostname}') - if not netconf: - return Response( - status=503, - response=f'error loading netconf for {hostname}') - netconf_doc = etree.fromstring(netconf.decode('utf-8')) - community = juniper.snmp_community_string(netconf_doc) - if not community: - # HACKHACK: vpn source ip isn't in acl - # TODO: remove this when done testing! - community = '0pBiFbD' - if not community: + def _multicast_oids(router_info): + + if not router_info['community']: + logger.error(f'this address can''t query {router["routers"]}') + return + + def _rsp_element(sub): + result = { + 'router': router_info['router'], + 'oid': _oid(sub), + 'community': router_info['community'] + } + result.update(sub) + return result + + yield from map(_rsp_element, SUBSCRIPTIONS) + + routers = list(_load_community_strings( + base_key_pattern=f'netconf:{hostname}*' + if hostname else 'netconf:mx*')) + errors = list(filter(lambda x: 'error' in x, routers)) + if errors: return Response( + response=', '.join(errors), status=503, - response=f'error extracting community string for {hostname}') - - def _rsp_element(sub): - result = { - 'router': hostname, - 'oid': _oid(sub), - 'community': community - } - result.update(sub) - return result + mimetype='text/html') - result = [_rsp_element(sub) for sub in SUBSCRIPTIONS] + result = list(map(_multicast_oids, routers)) + result = itertools.chain(*result) + result = list(result) + if not result: + target = hostname or 'any routers!' + return Response( + response=f'no multicast config for {target}', + status=404, + mimetype='text/html') result = json.dumps(result) # cache this data for the next call