From 6027e5b58088113ecc58db343c72d7e8a00c0107 Mon Sep 17 00:00:00 2001 From: ogenstad Date: Wed, 25 May 2016 19:25:22 +0200 Subject: [PATCH] Module util, template action and doc fragment for Cisco ASA --- lib/ansible/module_utils/asa.py | 194 ++++++++++++++++++ lib/ansible/plugins/action/asa_template.py | 26 +++ .../utils/module_docs_fragments/asa.py | 113 ++++++++++ 3 files changed, 333 insertions(+) create mode 100644 lib/ansible/module_utils/asa.py create mode 100644 lib/ansible/plugins/action/asa_template.py create mode 100644 lib/ansible/utils/module_docs_fragments/asa.py diff --git a/lib/ansible/module_utils/asa.py b/lib/ansible/module_utils/asa.py new file mode 100644 index 0000000000..3d1f40b4a8 --- /dev/null +++ b/lib/ansible/module_utils/asa.py @@ -0,0 +1,194 @@ +# +# (c) 2016 Peter Sprygada, +# (c) 2016 Patrick Ogenstad, <@ogenstad> +# +# 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 . +# + +import re +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.shell import Shell, ShellError, Command, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(default=22, type='int'), + show_command=dict(default='show running-config', choices=['show running-config', 'more system:running-config']), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + authorize=dict(default=False, fallback=(env_fallback, ['ANSIBLE_NET_AUTHORIZE']), type='bool'), + auth_pass=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_AUTH_PASS'])), + context=dict(required=False), + provider=dict(), + timeout=dict(default=10, type='int') +) + +CLI_PROMPTS_RE = [ + re.compile(r"[\r\n]?[\w+\-\.:\/\[\]]+(?:\([^\)]+\)){,3}(?:>|#) ?$"), + re.compile(r"\[\w+\@[\w\-\.]+(?: [^\]])\] ?[>#\$] ?$") +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"is not valid", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +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'] + key_filename = self.module.params['ssh_keyfile'] + timeout = self.module.params['timeout'] + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, password=password, key_filename=key_filename, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def change_context(self): + context = self.module.params['command'] + if context == 'system': + command = 'changeto system' + else: + command = 'changeto context %s' % context + + self.send(Command(command)) + + def authorize(self): + passwd = self.module.params['auth_pass'] + self.send(Command('enable', prompt=NET_PASSWD_RE, response=passwd)) + + def send(self, commands): + return self.shell.send(commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + self.filter = None + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS.keys(): + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + self.connection = Cli(self) + self.connection.connect() + if self.params['authorize']: + self.connection.authorize() + self.connection.send('no terminal pager') + + if self.params['context']: + if self.params['context'] == 'system': + self.connection.send('changeto system') + else: + self.connection.send('changeto context %s' % self.params['context']) + + self._connected = True + + 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): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=1) + + def get_config(self): + if self.filter: + cmd = 'show running-config %s ' % self.filter + else: + cmd = self.params['show_command'] + if self.params.get('include_defaults'): + cmd += ' all' + return self.execute(cmd)[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + # HAS_PARAMIKO is set by module_utils/shell.py + if not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module diff --git a/lib/ansible/plugins/action/asa_template.py b/lib/ansible/plugins/action/asa_template.py new file mode 100644 index 0000000000..cc150d6183 --- /dev/null +++ b/lib/ansible/plugins/action/asa_template.py @@ -0,0 +1,26 @@ +# +# Copyright 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 . +# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.plugins.action import ActionBase +from ansible.plugins.action.net_template import ActionModule as NetActionModule + +class ActionModule(NetActionModule, ActionBase): + pass diff --git a/lib/ansible/utils/module_docs_fragments/asa.py b/lib/ansible/utils/module_docs_fragments/asa.py new file mode 100644 index 0000000000..91a806351e --- /dev/null +++ b/lib/ansible/utils/module_docs_fragments/asa.py @@ -0,0 +1,113 @@ +# +# (c) 2016, Peter Sprygada +# (c) 2016, Patrick Ogenstad <@ogenstad> +# +# 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. The port value will default to the well known SSH port + of 22 + required: false + default: 22 + username: + description: + - Configures the usename to use to authenticate the connection to + the remote device. The value of I(username) is used to authenticate + the SSH session. If the value is not specified in the task, the + value of environment variable ANSIBLE_NET_USERNAME will be used instead. + required: false + password: + description: + - Specifies the password to use to authenticate the connection to + the remote device. The value of I(password) is used to authenticate + the SSH session. If the value is not specified in the task, the + value of environment variable ANSIBLE_NET_PASSWORD will be used instead. + required: false + default: null + ssh_keyfile: + description: + - Specifies the SSH key to use to authenticate the connection to + the remote device. The value of I(ssh_keyfile) is the path to the + key used to authenticate the SSH session. If the value is not specified + in the task, the value of environment variable ANSIBLE_NET_SSH_KEYFILE + will be used instead. + required: false + 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. If the value + is not specified in the task, the value of environment variable + ANSIBLE_NET_AUTHORIZE will be used instead. + required: false + default: no + choices: ['yes', 'no'] + 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. If the value is not specified in the task, the value of + environment variable ANSIBLE_NET_AUTH_PASS will be used instead. + required: false + default: none + timeout: + description: + - Specifies idle timeout for the connection. Useful if the console + freezes before continuing. For example when saving configurations. + required: false + default: 10 + provider: + description: + - Convience method that allows all M(ios) arguments to be passed as + a dict object. All constraints (required, choices, etc) must be + met either by individual arguments or values in this dict. + required: false + default: null + show_command: + description: + - Specifies which command will be used to get the current configuration. + By default the 'show running-config' command will be used, this command + masks some passwords. For example ike passwords for VPN. If you need to + match against masked passwords use 'more system:running-config'. + Note that the 'more system:running-config' only works in the system + context if you are running the ASA in multiple context mode. + before sending any commands. If not specified, the device will + required: false + default: show running-config + choices: ['show running-config', 'more system:running-config'] + context: + description: + - Specifies which context to target if you are running in the ASA in + multiple context mode. Defaults to the current context you login to. + required: false + default: null + + +"""