summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Sprygada <psprygada@ansible.com>2016-01-07 23:27:37 -0500
committerPeter Sprygada <psprygada@ansible.com>2016-01-10 15:11:08 -0500
commit9c9bfb962ed2b92c93ce474f1eba95bfadbd11fc (patch)
tree17975cb6e702afecf622799b580f9e8616a554f9
parent0e27fa4540b8dac1b291ebfc6dab90b0124d43bc (diff)
downloadansible-9c9bfb962ed2b92c93ce474f1eba95bfadbd11fc.tar.gz
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
-rw-r--r--lib/ansible/module_utils/eapi.py174
-rw-r--r--lib/ansible/module_utils/eos.py215
-rw-r--r--lib/ansible/utils/module_docs_fragments/eos.py84
3 files changed, 299 insertions, 174 deletions
diff --git a/lib/ansible/module_utils/eapi.py b/lib/ansible/module_utils/eapi.py
deleted file mode 100644
index 6e6129798c..0000000000
--- a/lib/ansible/module_utils/eapi.py
+++ /dev/null
@@ -1,174 +0,0 @@
-#
-# (c) 2015 Peter Sprygada, <psprygada@ansible.com>
-#
-# 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 <http://www.gnu.org/licenses/>.
-#
-"""
-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 0000000000..e3782a9d09
--- /dev/null
+++ b/lib/ansible/module_utils/eos.py
@@ -0,0 +1,215 @@
+#
+# (c) 2015 Peter Sprygada, <psprygada@ansible.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+#
+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 0000000000..7cca8b2a78
--- /dev/null
+++ b/lib/ansible/utils/module_docs_fragments/eos.py
@@ -0,0 +1,84 @@
+#
+# (c) 2015, Peter Sprygada <psprygada@ansible.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+
+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
+
+"""