Add nxos_api Proxy and Execution modules

This commit is contained in:
Mircea Ulinic 2018-07-12 08:51:58 +00:00
parent d4d6a786be
commit f0e5b4b5dd
3 changed files with 817 additions and 0 deletions

464
salt/modules/nxos_api.py Normal file
View File

@ -0,0 +1,464 @@
# -*- coding: utf-8 -*-
'''
Execution module to manage Cisco Nexus Switches (NX-OS) over the NX-API
.. versionadded:: Fluorine
Execution module used to interface the interaction with a remote or local Nexus
switch whether we're running in a Proxy Minion or regular Minion (or regular
Minion running directly on the Nexus switch).
:codeauthor: Mircea Ulinic <ping@mirceaulinic.net>
:maturity: new
:platform: any
.. note::
To be able to use this module you need to enable to NX-API on your switch,
by executing ``feature nxapi`` in configuration mode.
Configuration example:
.. code-block:: bash
switch# conf t
switch(config)# feature nxapi
To check that NX-API is properly enabled, execute ``show nxapi``.
Output example:
.. code-block:: bash
switch# show nxapi
nxapi enabled
HTTPS Listen on port 443
.. note::
NX-API requires modern NXOS distributions, typically at least 7.0 depending
on the hardware. Due to reliability reasons it is recommended to run the
most recent version.
Check https://www.cisco.com/c/en/us/td/docs/switches/datacenter/nexus7000/sw/programmability/guide/b_Cisco_Nexus_7000_Series_NX-OS_Programmability_Guide/b_Cisco_Nexus_7000_Series_NX-OS_Programmability_Guide_chapter_0101.html
for more details.
Usage
-----
This module can equally be used via the :mod:`nxos_api<salt.proxy.nxos_api>`
Proxy module or directly from an arbitrary (Proxy) Minion that is running on a
machine having access to the network device API. Given that there are no
external dependencies, this module can very well used when using the regular
Salt Minion directly installed on the switch.
When running outside of the :mod:`nxos_api Proxy<salt.proxy.nxos_api>`
(i.e., from another Proxy Minion type, or regular Minion), the NX-API connection
arguments can be either specified from the CLI when executing the command, or
in a configuration block under the ``nxos_api`` key in the configuration opts
(i.e., (Proxy) Minion configuration file), or Pillar. The module supports these
simultaneously. These fields are the exact same supported by the ``nxos_api``
Proxy Module:
transport: ``https``
Specifies the type of connection transport to use. Valid values for the
connection are ``http``, and ``https``.
host: ``localhost``
The IP address or DNS host name of the connection device.
username: ``admin``
The username to pass to the device to authenticate the NX-API connection.
password
The password to pass to the device to authenticate the NX-API connection.
port
The TCP port of the endpoint for the NX-API connection. If this keyword is
not specified, the default value is automatically determined by the
transport type (``80`` for ``http``, or ``443`` for ``https``).
timeout: ``60``
Time in seconds to wait for the device to respond. Default: 60 seconds.
verify: ``True``
Either a boolean, in which case it controls whether we verify the NX-API
TLS certificate, or a string, in which case it must be a path to a CA bundle
to use. Defaults to ``True``.
When there is no certificate configuration on the device and this option is
set as ``True`` (default), the commands will fail with the following error:
``SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:581)``.
In this case, you either need to configure a proper certificate on the
device (*recommended*), or bypass the checks setting this argument as ``False``
with all the security risks considered.
Check https://www.cisco.com/c/en/us/td/docs/switches/datacenter/nexus3000/sw/programmability/6_x/b_Cisco_Nexus_3000_Series_NX-OS_Programmability_Guide/b_Cisco_Nexus_3000_Series_NX-OS_Programmability_Guide_chapter_01.html
to see how to properly configure the certificate.
Example (when not running in a ``nxos_api`` Proxy Minion):
.. code-block:: yaml
nxos_api:
username: test
password: test
In case the ``username`` and ``password`` are the same on any device you are
targeting, the block above (besides other parameters specific to your
environment you might need) should suffice to be able to execute commands from
outside a ``nxos_api`` Proxy, e.g.:
.. code-block:: bash
salt-call --local nxos_api.show 'show lldp neighbors' raw_text
# The command above is available when running in a regular Minion where Salt is installed
salt '*' nxos_api.show 'show version' raw_text=False
.. note::
Remember that the above applies only when not running in a ``nxos_api`` Proxy
Minion. If you want to use the :mod:`nxos_api Proxy<salt.proxy.nxos_api>`,
please follow the documentation notes for a proper setup.
'''
from __future__ import absolute_import, print_function, unicode_literals
# Import python stdlib
import logging
import difflib
# Import Salt libs
from salt.ext import six
from salt.exceptions import CommandExecutionError
from salt.exceptions import SaltException
# -----------------------------------------------------------------------------
# execution module properties
# -----------------------------------------------------------------------------
__proxyenabled__ = ['*']
# Any Proxy Minion should be able to execute these
__virtualname__ = 'nxos_api'
# The Execution Module will be identified as ``nxos_api``
# The ``nxos`` namespace is already taken, used for SSH-based connections.
# -----------------------------------------------------------------------------
# globals
# -----------------------------------------------------------------------------
log = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
# propery functions
# -----------------------------------------------------------------------------
def __virtual__():
'''
This module does not have external dependencies, hence it is widely
available.
'''
# No extra requirements, uses Salt native modules.
return __virtualname__
# -----------------------------------------------------------------------------
# helper functions
# -----------------------------------------------------------------------------
def _cli_command(commands,
method='cli',
**kwargs):
'''
Execute a list of CLI commands.
'''
if not isinstance(commands, (list, tuple)):
commands = [commands]
rpc_responses = rpc(commands,
method=method,
**kwargs)
txt_responses = []
for rpc_reponse in rpc_responses:
error = rpc_reponse.get('error')
if error:
cmd = rpc_reponse.get('command')
if 'data' in error:
msg = 'The command "{cmd}" raised the error "{err}".'.format(cmd=cmd, err=error['data']['msg'])
raise SaltException(msg)
else:
msg = 'Invalid command: "{cmd}".'.format(cmd=cmd)
raise SaltException(msg)
txt_responses.append(rpc_reponse['result'])
return txt_responses
# -----------------------------------------------------------------------------
# callable functions
# -----------------------------------------------------------------------------
def rpc(commands,
method='cli',
**kwargs):
'''
Execute an arbitrary RPC request via the Nexus API.
commands
The commands to be executed.
method: ``cli``
The type of the response, i.e., raw text (``cli_ascii``) or structured
document (``cli``). Defaults to ``cli`` (structured data).
transport: ``https``
Specifies the type of connection transport to use. Valid values for the
connection are ``http``, and ``https``.
host: ``localhost``
The IP address or DNS host name of the connection device.
username: ``admin``
The username to pass to the device to authenticate the NX-API connection.
password
The password to pass to the device to authenticate the NX-API connection.
port
The TCP port of the endpoint for the NX-API connection. If this keyword is
not specified, the default value is automatically determined by the
transport type (``80`` for ``http``, or ``443`` for ``https``).
timeout: ``60``
Time in seconds to wait for the device to respond. Default: 60 seconds.
verify: ``True``
Either a boolean, in which case it controls whether we verify the NX-API
TLS certificate, or a string, in which case it must be a path to a CA bundle
to use. Defaults to ``True``.
CLI Example:
.. code-block:: bash
salt-call --local nxps_api.rpc 'show version'
'''
nxos_api_kwargs = __salt__['config.get']('nxos_api', {})
nxos_api_kwargs.update(**kwargs)
if 'nxos_api.rpc' in __proxy__ and __salt__['config.get']('proxy:proxytype') == 'nxos_api':
# If the nxos_api.rpc Proxy function is available and currently running
# in a nxos_api Proxy Minion
return __proxy__['nxos_api.rpc'](commands, method=method, **nxos_api_kwargs)
nxos_api_kwargs = __salt__['config.get']('nxos_api', {})
nxos_api_kwargs.update(**kwargs)
return __utils__['nxos_api.rpc'](commands, method=method, **nxos_api_kwargs)
def show(commands,
raw_text=True,
**kwargs):
'''
Execute one or more show (non-configuration) commands.
commands
The commands to be executed.
raw_text: ``True``
Whether to return raw text or structured data.
transport: ``https``
Specifies the type of connection transport to use. Valid values for the
connection are ``http``, and ``https``.
host: ``localhost``
The IP address or DNS host name of the connection device.
username: ``admin``
The username to pass to the device to authenticate the NX-API connection.
password
The password to pass to the device to authenticate the NX-API connection.
port
The TCP port of the endpoint for the NX-API connection. If this keyword is
not specified, the default value is automatically determined by the
transport type (``80`` for ``http``, or ``443`` for ``https``).
timeout: ``60``
Time in seconds to wait for the device to respond. Default: 60 seconds.
verify: ``True``
Either a boolean, in which case it controls whether we verify the NX-API
TLS certificate, or a string, in which case it must be a path to a CA bundle
to use. Defaults to ``True``.
CLI Example:
.. code-block:: bash
salt-call --local nxos_api.show 'show version'
salt '*' nxos_api.show 'show bgp sessions' 'show processes' raw_text=False
salt 'regular-minion' nxos_api.show 'show interfaces' host=sw01.example.com username=test password=test
'''
ret = []
if raw_text:
method = 'cli_ascii'
key = 'msg'
else:
method = 'cli'
key = 'body'
response_list = _cli_command(commands,
method=method,
**kwargs)
ret = [response[key] for response in response_list if response]
return ret
def config(commands=None,
config_file=None,
template_engine='jinja',
source_hash=None,
source_hash_name=None,
user=None,
group=None,
mode=None,
attrs=None,
context=None,
defaults=None,
skip_verify=False,
saltenv='base',
**kwargs):
'''
Configures the Nexus switch with the specified commands.
This method is used to send configuration commands to the switch. It
will take either a string or a list and prepend the necessary commands
to put the session into config mode.
.. warning::
All the commands will be applied directly into the running-config.
config_file
The source file with the configuration commands to be sent to the
device.
The file can also be a template that can be rendered using the template
engine of choice.
This can be specified using the absolute path to the file, or using one
of the following URL schemes:
- ``salt://``, to fetch the file from the Salt fileserver.
- ``http://`` or ``https://``
- ``ftp://``
- ``s3://``
- ``swift://``
commands
The commands to send to the switch in config mode. If the commands
argument is a string it will be cast to a list.
The list of commands will also be prepended with the necessary commands
to put the session in config mode.
.. note::
This argument is ignored when ``config_file`` is specified.
template_engine: ``jinja``
The template engine to use when rendering the source file. Default:
``jinja``. To simply fetch the file without attempting to render, set
this argument to ``None``.
source_hash
The hash of the ``config_file``
source_hash_name
When ``source_hash`` refers to a remote file, this specifies the
filename to look for in that file.
user
Owner of the file.
group
Group owner of the file.
mode
Permissions of the file.
attrs
Attributes of the file.
context
Variables to add to the template context.
defaults
Default values of the context_dict.
skip_verify: ``False``
If ``True``, hash verification of remote file sources (``http://``,
``https://``, ``ftp://``, etc.) will be skipped, and the ``source_hash``
argument will be ignored.
transport: ``https``
Specifies the type of connection transport to use. Valid values for the
connection are ``http``, and ``https``.
host: ``localhost``
The IP address or DNS host name of the connection device.
username: ``admin``
The username to pass to the device to authenticate the NX-API connection.
password
The password to pass to the device to authenticate the NX-API connection.
port
The TCP port of the endpoint for the NX-API connection. If this keyword is
not specified, the default value is automatically determined by the
transport type (``80`` for ``http``, or ``443`` for ``https``).
timeout: ``60``
Time in seconds to wait for the device to respond. Default: 60 seconds.
verify: ``True``
Either a boolean, in which case it controls whether we verify the NX-API
TLS certificate, or a string, in which case it must be a path to a CA bundle
to use. Defaults to ``True``.
CLI Example:
.. code-block:: bash
salt '*' nxos_api.config commands="['spanning-tree mode mstp']"
salt '*' nxos_api.config config_file=salt://config.txt
salt '*' nxos_api.config config_file=https://bit.ly/2LGLcDy context="{'servers': ['1.2.3.4']}"
'''
initial_config = show('show running-config', **kwargs)[0]
if config_file:
if template_engine:
file_str = __salt__['template.render'](source=config_file,
template_engine=template_engine,
source_hash=source_hash,
source_hash_name=source_hash_name,
user=user,
group=group,
mode=mode,
attrs=attrs,
saltenv=saltenv,
context=context,
defaults=defaults,
skip_verify=skip_verify)
else:
# If no template engine wanted, simply fetch the source file
file_str = __salt__['cp.get_file_str'](config_file,
saltenv=saltenv)
if file_str is False:
raise CommandExecutionError('Source file {} not found'.format(config_file))
commands = file_str.splitlines()
if isinstance(commands, (six.string_types, six.text_type)):
commands = [commands]
ret = _cli_command(commands, **kwargs)
current_config = show('show running-config', **kwargs)[0]
diff = difflib.unified_diff(initial_config.splitlines(1)[4:], current_config.splitlines(1)[4:])
return ''.join([x.replace('\r', '') for x in diff])

209
salt/proxy/nxos_api.py Normal file
View File

@ -0,0 +1,209 @@
# -*- coding: utf-8 -*-
'''
Proxy Minion to manage Cisco Nexus Switches (NX-OS) over the NX-API
.. versionadded:: Fluorine
Proxy module for managing Cisco Nexus switches via the NX-API.
:codeauthor: Mircea Ulinic <ping@mirceaulinic.net>
:maturity: new
:platform: any
Usage
=====
.. note::
To be able to use this module you need to enable to NX-API on your switch,
by executing ``feature nxapi`` in configuration mode.
Configuration example:
.. code-block:: bash
switch# conf t
switch(config)# feature nxapi
To check that NX-API is properly enabled, execute ``show nxapi``.
Output example:
.. code-block:: bash
switch# show nxapi
nxapi enabled
HTTPS Listen on port 443
.. note::
NX-API requires modern NXOS distributions, typically at least 7.0 depending
on the hardware. Due to reliability reasons it is recommended to run the
most recent version.
Check https://www.cisco.com/c/en/us/td/docs/switches/datacenter/nexus7000/sw/programmability/guide/b_Cisco_Nexus_7000_Series_NX-OS_Programmability_Guide/b_Cisco_Nexus_7000_Series_NX-OS_Programmability_Guide_chapter_0101.html
for more details.
Pillar
------
The ``nxos_api`` proxy configuration requires the following parameters in order
to connect to the network switch:
transport: ``https``
Specifies the type of connection transport to use. Valid values for the
connection are ``http``, and ``https``.
host: ``localhost``
The IP address or DNS host name of the connection device.
username: ``admin``
The username to pass to the device to authenticate the NX-API connection.
password
The password to pass to the device to authenticate the NX-API connection.
port
The TCP port of the endpoint for the NX-API connection. If this keyword is
not specified, the default value is automatically determined by the
transport type (``80`` for ``http``, or ``443`` for ``https``).
timeout: ``60``
Time in seconds to wait for the device to respond. Default: 60 seconds.
verify: ``True``
Either a boolean, in which case it controls whether we verify the NX-API
TLS certificate, or a string, in which case it must be a path to a CA bundle
to use. Defaults to ``True``.
When there is no certificate configuration on the device and this option is
set as ``True`` (default), the commands will fail with the following error:
``SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:581)``.
In this case, you either need to configure a proper certificate on the
device (*recommended*), or bypass the checks setting this argument as ``False``
with all the security risks considered.
Check https://www.cisco.com/c/en/us/td/docs/switches/datacenter/nexus3000/sw/programmability/6_x/b_Cisco_Nexus_3000_Series_NX-OS_Programmability_Guide/b_Cisco_Nexus_3000_Series_NX-OS_Programmability_Guide_chapter_01.html
to see how to properly configure the certificate.
All the arguments may be optional, depending on your setup.
Proxy Pillar Example
--------------------
.. code-block:: yaml
proxy:
proxytype: nxos_api
host: switch1.example.com
username: example
password: example
'''
from __future__ import absolute_import, print_function, unicode_literals
# Import python stdlib
import copy
import logging
# Import Salt modules
from salt.exceptions import SaltException
# -----------------------------------------------------------------------------
# proxy properties
# -----------------------------------------------------------------------------
__proxyenabled__ = ['nxos_api']
# proxy name
# -----------------------------------------------------------------------------
# globals
# -----------------------------------------------------------------------------
__virtualname__ = 'nxos_api'
log = logging.getLogger(__name__)
nxos_device = {}
# -----------------------------------------------------------------------------
# property functions
# -----------------------------------------------------------------------------
def __virtual__():
'''
This Proxy Module is widely available as there are no external dependencies.
'''
return __virtualname__
# -----------------------------------------------------------------------------
# proxy functions
# -----------------------------------------------------------------------------
def init(opts):
'''
Open the connection to the Nexsu switch over the NX-API.
As the communication is HTTP based, there is no connection to maintain,
however, in order to test the connectivity and make sure we are able to
bring up this Minion, we are executing a very simple command (``show clock``)
which doesn't come with much overhead and it's sufficient to confirm we are
indeed able to connect to the NX-API endpoint as configured.
'''
proxy_dict = opts.get('proxy', {})
conn_args = copy.deepcopy(proxy_dict)
conn_args.pop('proxytype', None)
opts['multiprocessing'] = conn_args.pop('multiprocessing', True)
# This is not a SSH-based proxy, so it should be safe to enable
# multiprocessing.
try:
rpc_reply = __utils__['nxos_api.rpc']('show clock', **conn_args)
# Execute a very simple command to confirm we are able to connect properly
nxos_device['conn_args'] = conn_args
nxos_device['initialized'] = True
nxos_device['up'] = True
except SaltException:
log.error('Unable to connect to %s', conn_args['host'], exc_info=True)
raise
return True
def ping():
'''
Connection open successfully?
'''
return nxos_device.get('up', False)
def initialized():
'''
Connection finished initializing?
'''
return nxos_device.get('initialized', False)
def shutdown(opts):
'''
Closes connection with the device.
'''
log.debug('Shutting down the nxos_api Proxy Minion %s', opts['id'])
# -----------------------------------------------------------------------------
# callable functions
# -----------------------------------------------------------------------------
def get_conn_args():
'''
Returns the connection arguments of the Proxy Minion.
'''
conn_args = copy.deepcopy(nxos_device['conn_args'])
return conn_args
def rpc(commands, method='cli', **kwargs):
'''
Executes an RPC request over the NX-API.
'''
conn_args = nxos_device['conn_args']
conn_args.update(kwargs)
return __utils__['nxos_api.rpc'](commands, method=method, **conn_args)

144
salt/utils/nxos_api.py Normal file
View File

@ -0,0 +1,144 @@
# -*- coding: utf-8 -*-
'''
Util functions for the NXOS API modules.
'''
from __future__ import absolute_import, print_function, unicode_literals
# Import Python std lib
import json
import logging
# Import salt libs
import salt.utils.http
from salt.ext import six
from salt.exceptions import SaltException
try:
from salt.utils.args import clean_kwargs
except ImportError:
from salt.utils import clean_kwargs
log = logging.getLogger(__name__)
RPC_INIT_KWARGS = [
'transport',
'host',
'username',
'password',
'port',
'timeout',
'verify',
'rpc_version'
]
def _prepare_connection(**nxos_api_kwargs):
'''
Prepare the connection with the remote network device, and clean up the key
value pairs, removing the args used for the connection init.
'''
nxos_api_kwargs = clean_kwargs(**nxos_api_kwargs)
init_kwargs = {}
# Clean up any arguments that are not required
for karg, warg in six.iteritems(nxos_api_kwargs):
if karg in RPC_INIT_KWARGS:
init_kwargs[karg] = warg
if 'host' not in init_kwargs:
init_kwargs['host'] = 'localhost'
if 'transport' not in init_kwargs:
init_kwargs['transport'] = 'https'
if 'port' not in init_kwargs:
init_kwargs['port'] = 80 if init_kwargs['transport'] == 'http' else 443
verify = init_kwargs.get('verify', True)
if isinstance(verify, bool):
init_kwargs['verify_ssl'] = verify
else:
init_kwargs['ca_bundle'] = verify
if 'rpc_version' not in init_kwargs:
init_kwargs['rpc_version'] = '2.0'
if 'timeout' not in init_kwargs:
init_kwargs['timeout'] = 60
return init_kwargs
def rpc(commands,
method='cli',
**kwargs):
'''
Execute an arbitrary RPC request via the Nexus API.
commands
The commands to be executed.
method: ``cli``
The type of the response, i.e., raw text (``cli_ascii``) or structured
document (``cli``). Defaults to ``cli`` (structured data).
transport: ``https``
Specifies the type of connection transport to use. Valid values for the
connection are ``http``, and ``https``.
host: ``localhost``
The IP address or DNS host name of the connection device.
username: ``admin``
The username to pass to the device to authenticate the NX-API connection.
password
The password to pass to the device to authenticate the NX-API connection.
port
The TCP port of the endpoint for the NX-API connection. If this keyword is
not specified, the default value is automatically determined by the
transport type (``80`` for ``http``, or ``443`` for ``https``).
timeout: ``60``
Time in seconds to wait for the device to respond. Default: 60 seconds.
verify: ``True``
Either a boolean, in which case it controls whether we verify the NX-API
TLS certificate, or a string, in which case it must be a path to a CA bundle
to use. Defaults to ``True``.
'''
init_args = _prepare_connection(**kwargs)
log.error('These are the init args:')
log.error(init_args)
url = '{transport}://{host}:{port}/ins'.format(
transport=init_args['transport'],
host=init_args['host'],
port=init_args['port']
)
headers = {
'content-type': 'application/json-rpc'
}
payload = []
if not isinstance(commands, (list, tuple)):
commands = [commands]
for index, command in enumerate(commands):
payload.append({
'jsonrpc': init_args['rpc_version'],
'method': method,
'params': {
'cmd': command,
'version': 1
},
'id': index + 1
})
opts = {
'http_request_timeout': init_args['timeout']
}
response = salt.utils.http.query(url,
method='POST',
opts=opts,
data=json.dumps(payload),
header_dict=headers,
decode=True,
decode_type='json',
**init_args)
if 'error' in response:
raise SaltException(response['error'])
response_list = response['dict']
if isinstance(response_list, dict):
response_list = [response_list]
for index, command in enumerate(commands):
response_list[index]['command'] = command
return response_list