diff --git a/changelog b/changelog index 4b04fcdd101c47623ff16b64290838c77b334d73..d4e390378b9398fce00ceaa70ebaf18c64c1283c 100644 --- a/changelog +++ b/changelog @@ -22,4 +22,5 @@ 0.16<working>: flatten redis storage structure poller api classifier metadata api - + read snmp community string from netconf + derive active router list from junosspace diff --git a/inventory_provider/juniper.py b/inventory_provider/juniper.py index 8dca8768b1e0ec76a1fc9968a5fa5ef85eddc70e..680648625818dee03abe9dcb11f204df55a2e59d 100644 --- a/inventory_provider/juniper.py +++ b/inventory_provider/juniper.py @@ -1,8 +1,10 @@ import logging import re +import ipaddress from jnpr.junos import Device from lxml import etree +import netifaces import requests from requests.auth import HTTPBasicAuth @@ -314,3 +316,44 @@ def load_routers_from_junosspace(config): "name": name, "hostname": hostname } + + +def local_interfaces( + type=netifaces.AF_INET, + omit_link_local=True, + omit_loopback=True): + """ + generator yielding IPv4Interface or IPv6Interface objects, + depending on the value of type + :param type: hopefully AF_INET or AF_INET6 + :param omit_link_local: skip v6 fe80* addresses if true + :param omit_loopback: skip lo* interfaces if true + :return: + """ + for n in netifaces.interfaces(): + if omit_loopback and re.match(r'^lo\d+', n): + continue + am = netifaces.ifaddresses(n) + for a in am.get(type, []): + if omit_link_local and a['addr'].startswith('fe80:'): + continue + m = re.match(r'^(.+?)(%.*)?$', a['addr']) + assert m + addr = m.group(1) + m = re.match(r'.*/(\d+)$', a['netmask']) + if m: + mask = m.group(1) + else: + mask = a['netmask'] + yield ipaddress.ip_interface('%s/%s' % (addr, mask)) + + +def snmp_community_string(netconf_config): + my_addressess = list([i.ip for i in local_interfaces()]) + for community in netconf_config.xpath('//configuration/snmp/community'): + for subnet in community.xpath('./clients/name/text()'): + allowed_network = ipaddress.ip_network(subnet, strict=False) + for me in my_addressess: + if me in allowed_network: + return community.xpath('./name/text()')[0] + return None diff --git a/inventory_provider/tasks/worker.py b/inventory_provider/tasks/worker.py index 5cc2be20438d1a8173828f2501f2b137dcc33a7f..685cf38174ddd71b8e42eb1e18520e4595c83fd2 100644 --- a/inventory_provider/tasks/worker.py +++ b/inventory_provider/tasks/worker.py @@ -215,6 +215,53 @@ def update_junosspace_device_list(): task_logger.debug('<<< update_junosspace_device_list') +def load_netconf_data(hostname): + """ + this method should only be called from a task + + :param hostname: + :return: + """ + r = get_redis(InventoryTask.config) + netconf = r.get('netconf:' + hostname) + if not netconf: + return None + return etree.fromstring(netconf.decode('utf-8')) + + +def clear_cached_classifier_responses(hostname): + task_logger = logging.getLogger(constants.TASK_LOGGER_NAME) + task_logger.debug( + 'removing cached classifier responses for %r' % hostname) + r = get_redis(InventoryTask.config) + for k in r.keys('classifier:cache:%s:*' % hostname): + r.delete(k) + + +@app.task() +def update_router_config(hostname): + task_logger = logging.getLogger(constants.TASK_LOGGER_NAME) + task_logger.debug('>>> update_router_config') + + netconf_refresh_config.apply(hostname) + + netconf_doc = load_netconf_data(hostname) + if not netconf_doc: + task_logger.error('no netconf data available for %r' % hostname) + else: + community = juniper.snmp_community_string(netconf_doc) + if not community: + task_logger.error( + 'error extracting community string for %r' % hostname) + else: + snmp_refresh_interfaces.apply(hostname, community) + + # TODO: move this out of else? (i.e. clear even if netconf fails?) + clear_cached_classifier_responses(hostname) + + task_logger.debug('<<< update_router_config') + + def _derive_router_hostnames(config): r = get_redis(config) junosspace_equipment = set() @@ -261,12 +308,6 @@ def start_refresh_cache_all(config): for hostname in _derive_router_hostnames(config): task_logger.debug( 'queueing router refresh jobs for %r' % hostname) - - # TODO: !!!! extract community string from netconf data - task_logger.error( - 'TODO: !!!! extract community string from netconf data') - subtasks.append(netconf_refresh_config.s(hostname)) - # TODO: these should be synchronous, and then cleanup classifier cache - subtasks.append(snmp_refresh_interfaces.s(hostname, '0pBiFbD')) + subtasks.append(update_router_config.s(hostname)) return group(subtasks).apply_async() diff --git a/requirements.txt b/requirements.txt index ff5a3796d6da2c9e606022c04fc5806a5f353eb5..815ac1d5e52e62e71ff23f0a037d4e30be705bb3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,7 @@ celery junos-eznc lxml requests +netifaces pytest pytest-mock diff --git a/setup.py b/setup.py index cacec6f53c463e17d73d2a10471fa2a9f217daff..0989421fee7a8fe127d25d1176c7bce8b54abfe4 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,8 @@ setup( 'celery', 'junos-eznc', 'lxml', - 'requests' + 'requests', + 'netifaces' ], include_package_data=True, ) diff --git a/test/per_router/test_juniper_data.py b/test/per_router/test_juniper_data.py index 32b31990b7c33690af3c27288b20b306061942d4..7847e322edd15ba2e85e87836a6a37ffc1322467 100644 --- a/test/per_router/test_juniper_data.py +++ b/test/per_router/test_juniper_data.py @@ -1,3 +1,4 @@ +import ast import os import jsonschema @@ -98,3 +99,22 @@ def test_bgp_list(netconf_doc): routes = list(juniper.list_bgp_routes(netconf_doc)) jsonschema.validate(routes, schema) + + +NETIFACES_TEST_DATA_STRING = """{ + 'lo0': {2: [{'addr': '127.0.0.1', 'netmask': '255.0.0.0', 'peer': '127.0.0.1'}], + 30: [{'addr': '::1', 'netmask': 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128', 'peer': '::1', 'flags': 0}, + {'addr': 'fe80::1%lo0', 'netmask': 'ffff:ffff:ffff:ffff::/64', 'flags': 0}]}, + 'eth0': {18: [{'addr': '78:4f:43:76:73:ba'}], + 2: [{'addr': '83.97.92.239', 'netmask': '255.255.252.0', 'broadcast': '83.97.95.255'}], + 30: [{'addr': 'fe80::250:56ff:fea1:8340', 'netmask': 'ffff:ffff:ffff:ffff::/64', 'flags': 1024}, + {'addr': '2001:798:3::104', 'netmask': 'ffff:ffff:ffff:ffff::/64', 'flags': 1088}]} +}""" # noqa E501 + +NETIFACES_TEST_DATA = ast.literal_eval(NETIFACES_TEST_DATA_STRING) + + +def test_snmp_community_string(mocker, netconf_doc): + mocker.patch('netifaces.interfaces', lambda: NETIFACES_TEST_DATA.keys()) + mocker.patch('netifaces.ifaddresses', lambda n: NETIFACES_TEST_DATA[n]) + assert juniper.snmp_community_string(netconf_doc) == '0pBiFbD' diff --git a/test/test_juniper_data_global.py b/test/test_juniper_data_global.py new file mode 100644 index 0000000000000000000000000000000000000000..74454f003e531ee2070a99c52cd3860997978355 --- /dev/null +++ b/test/test_juniper_data_global.py @@ -0,0 +1,62 @@ +import ast +import netifaces +import ipaddress + +from inventory_provider import juniper + +NETIFACES_TEST_DATA_STRING = """{ + 'lo0': {2: [{'addr': '127.0.0.1', 'netmask': '255.0.0.0', 'peer': '127.0.0.1'}], + 30: [{'addr': '::1', 'netmask': 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128', 'peer': '::1', 'flags': 0}, + {'addr': 'fe80::1%lo0', 'netmask': 'ffff:ffff:ffff:ffff::/64', 'flags': 0}]}, + 'gif0': {}, + 'stf0': {}, + 'XHC20': {}, + 'XHC4': {}, + 'XHC3': {}, + 'en3': {18: [{'addr': 'b6:00:24:b9:f0:01'}]}, + 'en8': {18: [{'addr': 'b6:00:24:b9:f0:00'}]}, + 'en4': {18: [{'addr': 'b6:00:24:b9:f0:05'}]}, + 'en9': {18: [{'addr': 'b6:00:24:b9:f0:04'}]}, + 'en0': {18: [{'addr': '78:4f:43:76:73:ba'}], + 2: [{'addr': '195.169.24.149', 'netmask': '255.255.255.128', 'broadcast': '195.169.24.255'}], + 30: [{'addr': 'fe80::1c97:ec77:3f32:cdfe%en0', 'netmask': 'ffff:ffff:ffff:ffff::/64', 'flags': 1024}, + {'addr': '2001:610:9d8:4:4d7:f763:9815:e78d', 'netmask': 'ffff:ffff:ffff:ffff::/64', 'flags': 1088}, + {'addr': '2001:610:9d8:4:492e:61b6:2c92:c387', 'netmask': 'ffff:ffff:ffff:ffff::/64', 'flags': 192}]}, + 'p2p0': {18: [{'addr': '0a:4f:43:76:73:ba'}]}, + 'awdl0': {18: [{'addr': '8e:87:e3:bb:92:1f'}], + 30: [{'addr': 'fe80::8c87:e3ff:febb:921f%awdl0', 'netmask': 'ffff:ffff:ffff:ffff::/64', 'flags': 0}]}, + 'bridge0': {18: [{'addr': 'b6:00:24:b9:f0:01'}]}, + 'utun0': {30: [{'addr': 'fe80::8328:d0ef:52b4:d379%utun0', 'netmask': 'ffff:ffff:ffff:ffff::/64', 'flags': 0}]}, + 'utun1': {30: [{'addr': 'fe80::5a75:c789:2fa0:6ee4%utun1', 'netmask': 'ffff:ffff:ffff:ffff::/64', 'flags': 0}]}, + 'XHC0': {}, + 'XHC1': {}, + 'XHC2': {}, + 'en21': {18: [{'addr': '64:4b:f0:10:23:25'}], + 2: [{'addr': '195.169.24.170', 'netmask': '255.255.255.128', 'broadcast': '195.169.24.255'}], + 30: [{'addr': 'fe80::41c:798c:3fff:f8c9%en21', 'netmask': 'ffff:ffff:ffff:ffff::/64', 'flags': 1024}, + {'addr': '2001:610:9d8:4:c1e:4402:e7cf:547f', 'netmask': 'ffff:ffff:ffff:ffff::/64', 'flags': 1088}, + {'addr': '2001:610:9d8:4:911c:954d:d4e2:baef', 'netmask': 'ffff:ffff:ffff:ffff::/64', 'flags': 192}]}, + 'en5': {18: [{'addr': 'ac:de:48:00:11:22'}], + 30: [{'addr': 'fe80::aede:48ff:fe00:1122%en5', 'netmask': 'ffff:ffff:ffff:ffff::/64', 'flags': 0}]}, + 'en18': {18: [{'addr': 'ca:3c:85:86:34:2a'}], 2: [{'addr': '169.254.184.83', 'netmask': '255.255.0.0'}]}, +}""" # noqa: E501 + +NETIFACES_TEST_DATA = ast.literal_eval(NETIFACES_TEST_DATA_STRING) + + +def test_local_v4_interfaces(mocker): + mocker.patch('netifaces.interfaces', lambda: NETIFACES_TEST_DATA.keys()) + mocker.patch('netifaces.ifaddresses', lambda n: NETIFACES_TEST_DATA[n]) + addresses = list(juniper.local_interfaces()) + assert len(addresses) == 3 + for a in addresses: + assert isinstance(a, ipaddress.IPv4Interface) + + +def test_local_v6_interfaces(mocker): + mocker.patch('netifaces.interfaces', lambda: NETIFACES_TEST_DATA.keys()) + mocker.patch('netifaces.ifaddresses', lambda n: NETIFACES_TEST_DATA[n]) + addresses = list(juniper.local_interfaces(netifaces.AF_INET6)) + assert len(addresses) == 4 + for a in addresses: + assert isinstance(a, ipaddress.IPv6Interface)