From 87ccc5c869433a229c56e37d243aab5215d0874b Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Thu, 7 Jan 2016 23:27:37 -0500 Subject: [PATCH] initial add of eos shared module This adds a shared module for communicating with Arista EOS devices over SSH (cli) or JSON-RPC (eapi). This modules replaces the eapi.py module previously added to module_utils. This commit includes a documentation fragment that describes the eos common arguments --- lib/ansible/module_utils/eapi.py | 174 -------------- lib/ansible/module_utils/eos.py | 215 ++++++++++++++++++ .../utils/module_docs_fragments/eos.py | 84 +++++++ 3 files changed, 299 insertions(+), 174 deletions(-) delete mode 100644 lib/ansible/module_utils/eapi.py create mode 100644 lib/ansible/module_utils/eos.py create mode 100644 lib/ansible/utils/module_docs_fragments/eos.py diff --git a/lib/ansible/module_utils/eapi.py b/lib/ansible/module_utils/eapi.py deleted file mode 100644 index 6e6129798cd..00000000000 --- a/lib/ansible/module_utils/eapi.py +++ /dev/null @@ -1,174 +0,0 @@ -# -# (c) 2015 Peter Sprygada, -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . -# -""" -This module adds shared support for Arista EOS devices using eAPI over -HTTP/S transport. It is built on module_utils/urls.py which is required -for proper operation. - -In order to use this module, include it as part of a custom -module as shown below. - -** Note: The order of the import statements does matter. ** - -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -from ansible.module_utils.eapi import * - -The eapi module provides the following common argument spec: - - * host (str) - The IPv4 address or FQDN of the network device - * port (str) - Overrides the default port to use for the HTTP/S - connection. The default values are 80 for HTTP and - 443 for HTTPS - * username (str) - The username to use to authenticate the HTTP/S - connection. - * password (str) - The password to use to authenticate the HTTP/S - connection. - * use_ssl (bool) - Specifies whether or not to use an encrypted (HTTPS) - connection or not. The default value is False. - * enable_mode (bool) - Specifies whether or not to enter `enable` mode - prior to executing the command list. The default value is True - * enable_password (str) - The password for entering `enable` mode - on the switch if configured. - * device (dict) - Used to send the entire set of connectin parameters - as a dict object. This argument is mutually exclusive with the - host argument - -In order to communicate with Arista EOS devices, the eAPI feature -must be enabled and configured on the device. - -""" -EAPI_COMMON_ARGS = dict( - host=dict(), - port=dict(), - username=dict(), - password=dict(no_log=True), - use_ssl=dict(default=True, type='bool'), - enable_mode=dict(default=True, type='bool'), - enable_password=dict(no_log=True), - device=dict() -) - -def eapi_module(**kwargs): - """Append the common args to the argument_spec - """ - spec = kwargs.get('argument_spec') or dict() - - argument_spec = url_argument_spec() - argument_spec.update(EAPI_COMMON_ARGS) - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = AnsibleModule(**kwargs) - - device = module.params.get('device') or dict() - for key, value in device.iteritems(): - if key in EAPI_COMMON_ARGS: - module.params[key] = value - - params = json_dict_unicode_to_bytes(json.loads(MODULE_COMPLEX_ARGS)) - for key, value in params.iteritems(): - if key != 'device': - module.params[key] = value - - return module - -def eapi_url(params): - """Construct a valid Arista eAPI URL - """ - if params['use_ssl']: - proto = 'https' - else: - proto = 'http' - host = params['host'] - url = '{}://{}'.format(proto, host) - if params['port']: - url = '{}:{}'.format(url, params['port']) - return '{}/command-api'.format(url) - -def to_list(arg): - """Convert the argument to a list object - """ - if isinstance(arg, (list, tuple)): - return list(arg) - elif arg is not None: - return [arg] - else: - return [] - -def eapi_body(commands, encoding, reqid=None): - """Create a valid eAPI JSON-RPC request message - """ - params = dict(version=1, cmds=to_list(commands), format=encoding) - return dict(jsonrpc='2.0', id=reqid, method='runCmds', params=params) - -def eapi_enable_mode(params): - """Build commands for entering `enable` mode on the switch - """ - if params['enable_mode']: - passwd = params['enable_password'] - if passwd: - return dict(cmd='enable', input=passwd) - else: - return 'enable' - -def eapi_command(module, commands, encoding='json'): - """Send an ordered list of commands to the device over eAPI - """ - commands = to_list(commands) - url = eapi_url(module.params) - - enable = eapi_enable_mode(module.params) - if enable: - commands.insert(0, enable) - - data = eapi_body(commands, encoding) - data = module.jsonify(data) - - headers = {'Content-Type': 'application/json-rpc'} - - module.params['url_username'] = module.params['username'] - module.params['url_password'] = module.params['password'] - - response, headers = fetch_url(module, url, data=data, headers=headers, - method='POST') - - if headers['status'] != 200: - module.fail_json(**headers) - - response = module.from_json(response.read()) - if 'error' in response: - err = response['error'] - module.fail_json(msg='json-rpc error', **err) - - if enable: - response['result'].pop(0) - - return response['result'], headers - -def eapi_configure(module, commands): - """Send configuration commands to the device over eAPI - """ - commands.insert(0, 'configure') - response, headers = eapi_command(module, commands) - response.pop(0) - return response, headers - - diff --git a/lib/ansible/module_utils/eos.py b/lib/ansible/module_utils/eos.py new file mode 100644 index 00000000000..e3782a9d097 --- /dev/null +++ b/lib/ansible/module_utils/eos.py @@ -0,0 +1,215 @@ +# +# (c) 2015 Peter Sprygada, +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(required=True), + password=dict(no_log=True), + authorize=dict(default=False, type='bool'), + auth_pass=dict(no_log=True), + transport=dict(choices=['cli', 'eapi']), + use_ssl=dict(default=True, type='bool') +) + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + +class Eapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self.enable = None + + def _get_body(self, commands, encoding, reqid=None): + """Create a valid eAPI JSON-RPC request message + """ + params = dict(version=1, cmds=commands, format=encoding) + return dict(jsonrpc='2.0', id=reqid, method='runCmds', params=params) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/command-api' % (proto, host, port) + + def authorize(self): + if self.module.params['auth_pass']: + passwd = self.module.params['auth_pass'] + self.enable = dict(cmd='enable', input=passwd) + else: + self.enable = 'enable' + + def send(self, commands, encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if self.enable is not None: + clist.insert(0, self.enable) + + data = self._get_body(clist, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json-rpc'} + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + if 'error' in response: + err = response['error'] + self.module.fail_json(msg='json-rpc error', **err) + + if self.enable: + response['result'].pop(0) + + return response['result'] + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + + self.shell = Shell() + self.shell.open(host, port=port, username=username, password=password) + + def authorize(self): + passwd = self.module.params['auth_pass'] + self.send(Command('enable', prompt=NET_PASSWD_RE, response=passwd)) + + def send(self, commands, encoding='text'): + return self.shell.send(commands) + +class EosModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(EosModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def connect(self): + if self.params['transport'] == 'eapi': + self.connection = Eapi(self) + else: + self.connection = Cli(self) + + try: + self.connection.connect() + self.execute('terminal length 0') + + if self.params['authorize']: + self.connection.authorize() + + except Exception, exc: + self.fail_json(msg=exc.message) + + def configure(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure terminal') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + try: + return self.connection.send(commands, **kwargs) + except Exception, exc: + self.fail_json(msg=exc.message, commands=commands) + + def disconnect(self): + self.connection.close() + + def parse_config(self, cfg): + return parse(cfg, indent=3) + + def get_config(self): + cmd = 'show running-config' + if self.params['include_defaults']: + cmd += ' all' + if self.params['transport'] == 'cli': + return self.execute(cmd)[0] + else: + resp = self.execute(cmd, encoding='text') + return resp[0]['output'] + + +def get_module(**kwargs): + """Return instance of EosModule + """ + + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + kwargs['check_invalid_arguments'] = False + + module = EosModule(**kwargs) + + # HAS_PARAMIKO is set by module_utils/shell.py + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + # copy in values from local action. + params = json_dict_unicode_to_bytes(json.loads(MODULE_COMPLEX_ARGS)) + for key, value in params.iteritems(): + module.params[key] = value + + module.connect() + + return module + diff --git a/lib/ansible/utils/module_docs_fragments/eos.py b/lib/ansible/utils/module_docs_fragments/eos.py new file mode 100644 index 00000000000..7cca8b2a781 --- /dev/null +++ b/lib/ansible/utils/module_docs_fragments/eos.py @@ -0,0 +1,84 @@ +# +# (c) 2015, Peter Sprygada +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + + +class ModuleDocFragment(object): + + # Standard files documentation fragment + DOCUMENTATION = """ +options: + host: + description: + - Specifies the DNS host name or address for connecting to the remote + device over the specified transport. The value of host is used as + the destination address for the transport. + required: true + port: + description: + - Specifies the port to use when buiding the connection to the remote + device. This value applies to either I(cli) or I(eapi). The port + value will default to the approriate transport common port if + none is provided in the task. (cli=22, http=80, https=443). + required: false + default: 0 (use common port) + username: + description: + - Configures the usename to use to authenticate the connection to + the remote device. The value of I(username) is used to authenticate + either the CLI login or the eAPI authentication depending on which + transport is used. + required: true + password: + description: + - Specifies the password to use when authentication the connection to + the remote device. This is a common argument used for either I(cli) + or I(eapi) transports. + required: false + default: null + authorize: + description: + - Instructs the module to enter priviledged mode on the remote device + before sending any commands. If not specified, the device will + attempt to excecute all commands in non-priviledged mode. + required: false + default: false + choices: BOOLEANS + auth_pass: + description: + - Specifies the password to use if required to enter privileged mode + on the remote device. If I(authorize) is false, then this argument + does nothing + required: false + default: none + transport: + description: + - Configures the transport connection to use when connecting to the + remote device. The transport argument supports connectivity to the + device over cli (ssh) or eapi. + required: true + default: cli + use_ssl: + description: + - Configures the I(transport) to use SSL if set to true only when the + I(transport) argument is configured as eapi. If the transport + argument is not eapi, this value is ignored + required: false + default: true + choices: BOOLEANS + +"""