From 8ad5794b60d43e1b052fa6bd35d52158690f453e Mon Sep 17 00:00:00 2001 From: Mircea Ulinic Date: Tue, 23 Feb 2016 12:44:08 +0000 Subject: [PATCH] Proxy Minion for network devices using Napalm library --- doc/ref/modules/all/index.rst | 1 + .../all/salt.modules.napalm_network.rst | 6 + doc/ref/proxy/all/index.rst | 1 + doc/ref/proxy/all/salt.proxy.napalm.rst | 6 + salt/modules/napalm_network.py | 316 ++++++++++++++++++ salt/proxy/napalm.py | 171 ++++++++++ 6 files changed, 501 insertions(+) create mode 100644 doc/ref/modules/all/salt.modules.napalm_network.rst create mode 100644 doc/ref/proxy/all/salt.proxy.napalm.rst create mode 100644 salt/modules/napalm_network.py create mode 100644 salt/proxy/napalm.py diff --git a/doc/ref/modules/all/index.rst b/doc/ref/modules/all/index.rst index 8f70ce696555..1c226f020c17 100644 --- a/doc/ref/modules/all/index.rst +++ b/doc/ref/modules/all/index.rst @@ -198,6 +198,7 @@ Full list of builtin execution modules nacl nagios nagios_rpc + napalm_network netaddress netbsd_sysctl netbsdservice diff --git a/doc/ref/modules/all/salt.modules.napalm_network.rst b/doc/ref/modules/all/salt.modules.napalm_network.rst new file mode 100644 index 000000000000..5bd81ccd190a --- /dev/null +++ b/doc/ref/modules/all/salt.modules.napalm_network.rst @@ -0,0 +1,6 @@ +salt.modules.napalm_network module +=============================== + +.. automodule:: salt.modules.napalm_network + :members: + :undoc-members: diff --git a/doc/ref/proxy/all/index.rst b/doc/ref/proxy/all/index.rst index 4ca6727ecfe2..cc0281240e2c 100644 --- a/doc/ref/proxy/all/index.rst +++ b/doc/ref/proxy/all/index.rst @@ -15,6 +15,7 @@ Full list of builtin proxy modules fx2 junos marathon + napalm phillips_hue rest_sample ssh_sample diff --git a/doc/ref/proxy/all/salt.proxy.napalm.rst b/doc/ref/proxy/all/salt.proxy.napalm.rst new file mode 100644 index 000000000000..33e5a8b7d02f --- /dev/null +++ b/doc/ref/proxy/all/salt.proxy.napalm.rst @@ -0,0 +1,6 @@ +================ +salt.proxy.napalm +================ + +.. automodule:: salt.proxy.napalm + :members: diff --git a/salt/modules/napalm_network.py b/salt/modules/napalm_network.py new file mode 100644 index 000000000000..685540b0ede5 --- /dev/null +++ b/salt/modules/napalm_network.py @@ -0,0 +1,316 @@ +# -*- coding: utf-8 -*- +''' +Basic functions from Napalm library +''' + +from __future__ import absolute_import + +import logging +log = logging.getLogger(__name__) + +# ------------------------------------------------------------------------ +# module properties +# ------------------------------------------------------------------------ + +__virtualname__ = 'net' +__proxyenabled__ = ['napalm'] +# uses NAPALM-based proxy to interact with network devices + +# ------------------------------------------------------------------------ +# property functions +# ------------------------------------------------------------------------ + + +def __virtual__(): + return True + +# ------------------------------------------------------------------------ +# helper functions -- will not be exported +# ------------------------------------------------------------------------ + + +def _filter_list(input_list, search_key, search_value): + + output_list = list() + + for dictionary in input_list: + if dictionary.get(search_key) == search_value: + output_list.append(dictionary) + + return output_list + + +def _filter_dict(input_dict, search_key, search_value): + + output_dict = dict() + + for key, key_list in input_dict.iteritems(): + key_list_filtered = _filter_list(key_list, search_key, search_value) + if key_list_filtered: + output_dict[key] = key_list_filtered + + return output_dict + +# ------------------------------------------------------------------------ +# callable functions +# ------------------------------------------------------------------------ + + +def ping(): + ''' + is the device alive ? + + CLI example: + + .. code-block:: bash + + salt myminion net.ping + ''' + + return { + 'out': __proxy__['napalm.ping']() + } + + +def cli(*commands): + + """ + NAPALM returns a dictionary with the output of all commands passed as arguments: + + CLI example: + + .. code-block:: bash + + salt myminion net.cli "show version" "show route 8.8.8.8" + + :param commands: list of raw commands to execute on device + + Example: + { + u'show version and haiku' : u'''Hostname: re0.edge01.arn01 + Model: mx480 + Junos: 13.3R6.5 + Help me, Obi-Wan + I just saw Episode Two + You're my only hope + ''', + u'show chassis fan' : u'''Item Status RPM Measurement + Top Rear Fan OK 3840 Spinning at intermediate-speed + Bottom Rear Fan OK 3840 Spinning at intermediate-speed + Top Middle Fan OK 3900 Spinning at intermediate-speed + Bottom Middle Fan OK 3840 Spinning at intermediate-speed + Top Front Fan OK 3810 Spinning at intermediate-speed + Bottom Front Fan OK 3840 Spinning at intermediate-speed + ''' + } + """ + + return __proxy__['napalm.call']( + 'cli', + **{ + 'commands': list(commands) + } + ) + # thus we can display the output as is + # in case of errors, they'll be catched in the proxy + + +def arp(interface='', ipaddr='', macaddr=''): + + """ + NAPALM returns a list of dictionaries with details of the ARP entries: + [{INTERFACE, MAC, IP, AGE}] + + CLI example: + + .. code-block:: bash + + salt myminion net.arp + salt myminion net.arp macaddr='5c:5e:ab:da:3c:f0' + + :param interface: interface name to filter on + :param ipaddr: IP address to filter on + :param macaddr: MAC address to filter on + + Example output: + [ + { + 'interface' : 'MgmtEth0/RSP0/CPU0/0', + 'mac' : '5c:5e:ab:da:3c:f0', + 'ip' : '172.17.17.1', + 'age' : 1454496274.84 + }, + { + 'interface': 'MgmtEth0/RSP0/CPU0/0', + 'mac' : '66:0e:94:96:e0:ff', + 'ip' : '172.17.17.2', + 'age' : 1435641582.49 + } + ] + """ + + proxy_output = __proxy__['napalm.call']( + 'get_arp_table', + **{ + } + ) + + if not proxy_output.get('result'): + return proxy_output + + arp_table = proxy_output.get('out') + + if interface: + arp_table = _filter_list(arp_table, 'interface', interface) + + if ipaddr: + arp_table = _filter_list(arp_table, 'ip', ipaddr) + + if macaddr: + arp_table = _filter_list(arp_table, 'mac', macaddr) + + proxy_output.update({ + 'out': arp_table + }) + + return proxy_output + + +def ipaddrs(): + ''' + Returns IP addresses on the device + + CLI example: + + .. code-block:: bash + + salt myminion net.ipaddrs + ''' + return __proxy__['napalm.call']( + 'get_interfaces_ip', + **{ + } + ) + + +def lldp(interface=''): + + """ + returns LLDP neighbors + + CLI example: + + .. code-block:: bash + + salt myminion net.lldp + salt myminion net.lldp interface='TenGigE0/0/0/8' + + :param interface: interface name to filter on + + Example output: + { + 'TenGigE0/0/0/8': [ + { + 'parent_interface': u'Bundle-Ether8', + 'interface_description': u'TenGigE0/0/0/8', + 'remote_chassis_id': u'8c60.4f69.e96c', + 'remote_system_name': u'switch', + 'remote_port': u'Eth2/2/1', + 'remote_port_description': u'Ethernet2/2/1', + 'remote_system_description': u'''Cisco Nexus Operating System (NX-OS) Software 7.1(0)N1(1a) + TAC support: http://www.cisco.com/tac + Copyright (c) 2002-2015, Cisco Systems, Inc. All rights reserved.''', + 'remote_system_capab': u'B, R', + 'remote_system_enable_capab': u'B' + } + ] + } + """ + + proxy_output = __proxy__['napalm.call']( + 'get_lldp_neighbors_detail', + **{ + } + ) + + if not proxy_output.get('result'): + return proxy_output + + lldp_neighbors = proxy_output.get('out') + + if interface: + lldp_neighbors = {interface: lldp_neighbors.get(interface)} + + proxy_output.update({ + 'out': lldp_neighbors + }) + + return proxy_output + + +def mac(address='', interface='', vlan=0): + + """ + returns device MAC address table + + CLI example: + + .. code-block:: bash + + salt myminion net.mac + salt myminion net.mac vlan=10 + + :param address: MAC address to filter on + :param interface: interface name to filter on + :param vlan: vlan identifier + + Example output: + [ + { + 'mac' : '00:1c:58:29:4a:71', + 'interface' : 'xe-3/0/2', + 'static' : False, + 'active' : True, + 'moves' : 1, + 'vlan' : 10, + 'last_move' : 1454417742.58 + }, + { + 'mac' : '8c:60:4f:58:e1:c1', + 'interface' : 'xe-1/0/1', + 'static' : False, + 'active' : True, + 'moves' : 2, + 'vlan' : 42, + 'last_move' : 1453191948.11 + } + ] + + """ + + proxy_output = __proxy__['napalm.call']( + 'get_mac_address_table', + **{ + } + ) + + if not proxy_output.get('result'): + # if negative, leave the output unchanged + return proxy_output + + mac_address_table = proxy_output.get('out') + + if vlan and isinstance(int, vlan): + mac_address_table = {vlan: mac_address_table.get(vlan)} + + if address: + mac_address_table = _filter_dict(mac_address_table, 'mac', address) + + if interface: + mac_address_table = _filter_dict(mac_address_table, 'interface', interface) + + proxy_output.update({ + 'out': mac_address_table + }) + + return proxy_output diff --git a/salt/proxy/napalm.py b/salt/proxy/napalm.py new file mode 100644 index 000000000000..c0745f47e14b --- /dev/null +++ b/salt/proxy/napalm.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- + +""" +THis module allows Salt interact with network devices via NAPALM library +(https://github.com/napalm-automation/napalm) +""" + +from __future__ import absolute_import + +import logging +log = logging.getLogger(__file__) + +from napalm import get_network_driver + +# ------------------------------------------------------------------------ +# proxy properties +# ------------------------------------------------------------------------ + +__proxyenabled__ = ['napalm'] +# proxy name + +# ------------------------------------------------------------------------ +# global variables +# ------------------------------------------------------------------------ + +NETWORK_DEVICE = {} + +# ------------------------------------------------------------------------ +# property functions +# ------------------------------------------------------------------------ + + +def __virtual__(): + return True + +# ------------------------------------------------------------------------ +# helper functions -- will not be exported +# ------------------------------------------------------------------------ + +# ------------------------------------------------------------------------ +# Salt specific proxy functions +# ------------------------------------------------------------------------ + + +def init(opts): + ''' + Perform any needed setup. + ''' + NETWORK_DEVICE['HOSTNAME'] = opts.get('proxy', {}).get('host') + NETWORK_DEVICE['USERNAME'] = opts.get('proxy', {}).get('username') + NETWORK_DEVICE['PASSWORD'] = opts.get('proxy', {}).get('passwd') + NETWORK_DEVICE['DRIVER_NAME'] = opts.get('proxy', {}).get('driver') + + NETWORK_DEVICE['UP'] = False + + _driver_ = get_network_driver(NETWORK_DEVICE.get('DRIVER_NAME')) + # get driver object form NAPALM + + optional_args = { + 'config_lock': False # to avoid locking config DB + } + + try: + NETWORK_DEVICE['DRIVER'] = _driver_( + NETWORK_DEVICE.get('HOSTNAME', ''), + NETWORK_DEVICE.get('USERNAME', ''), + NETWORK_DEVICE.get('PASSWORD', ''), + optional_args=optional_args + ) + NETWORK_DEVICE.get('DRIVER').open() + # no exception raised here, means connection established + NETWORK_DEVICE['UP'] = True + except Exception as error: + log.error( + "Cannot connect to {hostname} as {username}. Please check error: {error}".format( + hostname=NETWORK_DEVICE.get('HOSTNAME', ''), + username=NETWORK_DEVICE.get('USERNAME', ''), + error=error + ) + ) + + return True + + +def ping(): + ''' + is the device responding ? + ''' + return NETWORK_DEVICE['UP'] + + +def shutdown(opts): + ''' + use napalm close() + ''' + try: + if not NETWORK_DEVICE.get('UP', False): + raise Exception('not connected!') + NETWORK_DEVICE.get('DRIVER').close() + except Exception as error: + log.error( + 'Cannot close connection with {hostname}! Please check error: {error}'.format( + hostname=NETWORK_DEVICE.get('HOSTNAME', '[unknown hostname]'), + error=error + ) + ) + + return True + +# ------------------------------------------------------------------------ +# Callable functions +# ------------------------------------------------------------------------ + + +def call(method, **params): + + """ + This function calls methods from the NAPALM driver object. + Available methods: + + ============================== ===== ===== ====== ======= ====== ====== ===== ========= + _ EOS JunOS IOS-XR FortiOS IBM NXOS IOS Pluribus + ============================== ===== ===== ====== ======= ====== ====== ===== ========= + **cli** |yes| |yes| |yes| |no| |no| |yes| |yes| |yes| + **get_facts** |yes| |yes| |yes| |yes| |no| |yes| |yes| |yes| + **get_interfaces** |yes| |yes| |yes| |yes| |no| |yes| |yes| |yes| + **get_lldp_neighbors** |yes| |yes| |yes| |yes| |no| |no| |yes| |yes| + **get_lldp_neighbors_detail** |yes| |yes| |yes| |no| |no| |yes| |no| |yes| + **get_bgp_neighbors** |yes| |yes| |yes| |yes| |no| |no| |yes| |no| + **get_bgp_neighbors_detail** |yes| |yes| |no| |no| |no| |no| |no| |no| + **get_bgp_config** |yes| |yes| |yes| |no| |no| |no| |no| |no| + **get_environment** |yes| |yes| |yes| |yes| |no| |no| |yes| |no| + **get_mac_address_table** |yes| |yes| |yes| |no| |no| |yes| |no| |yes| + **get_arp_table** |yes| |yes| |yes| |no| |no| |yes| |no| |no| + **get_snmp_information** |no| |no| |no| |no| |no| |no| |no| |yes| + **get_ntp_peers** |yes| |yes| |yes| |no| |no| |yes| |no| |yes| + **get_interfaces_ip** |yes| |yes| |yes| |no| |no| |yes| |yes| |no| + ============================== ===== ===== ====== ======= ====== ====== ===== ========= + + For example:: + + call('cli', **{ + 'commands': [ + "show version", + "show chassis fan" + ] + }) + + """ + + try: + if not NETWORK_DEVICE.get('UP', False): + raise Exception('not connected') + # if connected will try to execute desired command + return { + 'out': getattr(NETWORK_DEVICE.get('DRIVER'), method)(**params), + 'result': True, + 'comment': '' + } + except Exception as error: + # either not connected + # either unable to execute the command + return { + 'out': {}, + 'result': False, + 'comment': 'Cannot execute "{method}" on {device}. Reason: {error}!'.format( + device=NETWORK_DEVICE.get('HOSTNAME', '[unspecified hostname]'), + method=method, + error=error + ) + }