From 3169cbd493e2112b593932af0adb69357674bf7b Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Sat, 25 Mar 2017 10:35:15 -0400 Subject: [PATCH] roll up of fixes for sros modules (#22972) * fixes action handlers for sros * fixes sros_config module execution to use AnsibleModule * fixes sros_command module to use socket connection * adds sros to constants --- lib/ansible/constants.py | 3 +- lib/ansible/module_utils/netcfg.py | 2 +- lib/ansible/module_utils/sros.py | 145 +++++++++-------- .../modules/network/sros/sros_command.py | 147 +++++++++--------- .../modules/network/sros/sros_config.py | 77 ++++----- .../modules/network/sros/sros_rollback.py | 68 ++++---- lib/ansible/plugins/action/sros.py | 111 +++++++++++++ lib/ansible/plugins/action/sros_config.py | 93 ++++++++++- lib/ansible/plugins/terminal/sros.py | 2 +- 9 files changed, 434 insertions(+), 214 deletions(-) create mode 100644 lib/ansible/plugins/action/sros.py diff --git a/lib/ansible/constants.py b/lib/ansible/constants.py index c40320462d..c1415dd9d1 100644 --- a/lib/ansible/constants.py +++ b/lib/ansible/constants.py @@ -329,7 +329,8 @@ DEFAULT_TEST_PLUGIN_PATH = get_config(p, DEFAULTS, 'test_plugins', 'ANSIBL DEFAULT_STRATEGY_PLUGIN_PATH = get_config(p, DEFAULTS, 'strategy_plugins', 'ANSIBLE_STRATEGY_PLUGINS', '~/.ansible/plugins/strategy:/usr/share/ansible/plugins/strategy', value_type='pathlist') -NETWORK_GROUP_MODULES = get_config(p, DEFAULTS, 'network_group_modules','NETWORK_GROUP_MODULES', ['eos', 'nxos', 'ios', 'iosxr', 'junos', 'vyos'], +NETWORK_GROUP_MODULES = get_config(p, DEFAULTS, 'network_group_modules','NETWORK_GROUP_MODULES', ['eos', 'nxos', 'ios', 'iosxr', 'junos', + 'vyos', 'sros'], value_type='list') DEFAULT_STRATEGY = get_config(p, DEFAULTS, 'strategy', 'ANSIBLE_STRATEGY', 'linear') diff --git a/lib/ansible/module_utils/netcfg.py b/lib/ansible/module_utils/netcfg.py index abdbffd784..885205381e 100644 --- a/lib/ansible/module_utils/netcfg.py +++ b/lib/ansible/module_utils/netcfg.py @@ -30,7 +30,7 @@ import re from ansible.module_utils.six.moves import zip from ansible.module_utils.network_common import to_list -DEFAULT_COMMENT_TOKENS = ['#', '!', '/*', '*/'] +DEFAULT_COMMENT_TOKENS = ['#', '!', '/*', '*/', 'echo'] class ConfigLine(object): diff --git a/lib/ansible/module_utils/sros.py b/lib/ansible/module_utils/sros.py index 87d36fd0a8..bbb8679cd0 100644 --- a/lib/ansible/module_utils/sros.py +++ b/lib/ansible/module_utils/sros.py @@ -28,82 +28,93 @@ # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # -import re +from ansible.module_utils.basic import env_fallback +from ansible.module_utils.network_common import to_list, ComplexList +from ansible.module_utils.connection import exec_command -from ansible.module_utils.network import NetworkModule, NetworkError -from ansible.module_utils.network import register_transport, to_list -from ansible.module_utils.shell import CliBase -from ansible.module_utils.netcli import Command +_DEVICE_CONFIGS = {} + +sros_argument_spec = { + 'host': dict(), + 'port': dict(type='int'), + 'username': dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + 'password': dict(fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD']), no_log=True), + 'ssh_keyfile': dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + 'timeout': dict(type='int'), + 'provider': dict(type='dict') +} + +def check_args(module, warnings): + provider = module.params['provider'] or {} + for key in sros_argument_spec: + if key != 'provider' and module.params[key]: + warnings.append('argument %s has been deprecated and will be ' + 'removed in a future version' % key) + +def get_config(module, flags=[]): + cmd = 'admin display-config ' + cmd += ' '.join(flags) + cmd = cmd.strip() + + try: + return _DEVICE_CONFIGS[cmd] + except KeyError: + rc, out, err = exec_command(module, cmd) + if rc != 0: + module.fail_json(msg='unable to retrieve current config', stderr=err) + cfg = str(out).strip() + _DEVICE_CONFIGS[cmd] = cfg + return cfg + +def to_commands(module, commands): + spec = { + 'command': dict(key=True), + 'prompt': dict(), + 'answer': dict() + } + transform = ComplexList(spec, module) + return transform(commands) -class Cli(CliBase): +def run_commands(module, commands, check_rc=True): + responses = list() + commands = to_commands(module, to_list(commands)) + for cmd in commands: + cmd = module.jsonify(cmd) + rc, out, err = exec_command(module, cmd) + if check_rc and rc != 0: + module.fail_json(msg=err, rc=rc) + responses.append(out) + return responses - NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) +def load_config(module, commands): + for command in to_list(commands): + rc, out, err = exec_command(module, command) + if rc != 0: + module.fail_json(msg=err, command=command, rc=rc) + exec_command(module, 'exit all') - CLI_PROMPTS_RE = [ - re.compile(r"[\r\n]?[\w+\-\.:\/\[\]]+(?:\([^\)]+\)){,3}(?:>|#) ?$"), - re.compile(r"\[\w+\@[\w\-\.]+(?: [^\]])\] ?[>#\$] ?$") - ] - - CLI_ERRORS_RE = [ - re.compile(r"^\r\nError:", re.M), - ] - - def __init__(self): - super(Cli, self).__init__() - self._rollback_enabled = None - - @property - def rollback_enabled(self): - if self._rollback_enabled is not None: - return self._rollback_enabled - resp = self.execute(['show system rollback']) - match = re.search(r'^Rollback Location\s+:\s(\S+)', resp[0], re.M) - self._rollback_enabled = match.group(1) != 'None' +def rollback_enabled(self): + if self._rollback_enabled is not None: return self._rollback_enabled + resp = self.execute(['show system rollback']) + match = re.search(r'^Rollback Location\s+:\s(\S+)', resp[0], re.M) + self._rollback_enabled = match.group(1) != 'None' + return self._rollback_enabled - def connect(self, params, **kwargs): - super(Cli, self).connect(params, kickstart=False, **kwargs) - self.shell.send('environment no more') - self._connected = True +def load_config_w_rollback(self, commands): + if self.rollback_enabled: + self.execute(['admin rollback save']) - ### implementation of netcli.Cli ### - - def run_commands(self, commands, **kwargs): - return self.execute(to_list(commands)) - - ### implementation of netcfg.Config ### - - def configure(self, commands, **kwargs): - cmds = to_list(commands) - responses = self.execute(cmds) - self.execute(['exit all']) - return responses - - def get_config(self, detail=False, **kwargs): - cmd = 'admin display-config' - if detail: - cmd += ' detail' - return self.execute(cmd)[0] - - def load_config(self, commands): + try: + self.configure(commands) + except NetworkError: if self.rollback_enabled: - self.execute(['admin rollback save']) + self.execute(['admin rollback revert latest-rb', + 'admin rollback delete latest-rb']) + raise - try: - self.configure(commands) - except NetworkError: - if self.rollback_enabled: - self.execute(['admin rollback revert latest-rb', - 'admin rollback delete latest-rb']) - raise - - if self.rollback_enabled: - self.execute(['admin rollback delete latest-rb']) - - def save_config(self): - self.execute(['admin save']) - -Cli = register_transport('cli', default=True)(Cli) + if self.rollback_enabled: + self.execute(['admin rollback delete latest-rb']) diff --git a/lib/ansible/modules/network/sros/sros_command.py b/lib/ansible/modules/network/sros/sros_command.py index 4fa84d88a2..ec8d18bebe 100644 --- a/lib/ansible/modules/network/sros/sros_command.py +++ b/lib/ansible/modules/network/sros/sros_command.py @@ -16,10 +16,11 @@ # along with Ansible. If not, see . # -ANSIBLE_METADATA = {'metadata_version': '1.0', - 'status': ['preview'], - 'supported_by': 'community'} - +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'community' +} DOCUMENTATION = """ --- @@ -143,34 +144,45 @@ failed_conditions: type: list sample: ['...', '...'] """ -from ansible.module_utils.basic import get_exception -from ansible.module_utils.netcli import CommandRunner -from ansible.module_utils.netcli import AddCommandError, FailedConditionsError -from ansible.module_utils.sros import NetworkModule, NetworkError +import time -VALID_KEYS = ['command', 'output', 'prompt', 'response'] +from ansible.module_utils.sros import run_commands +from ansible.module_utils.sros import sros_argument_spec, check_args +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network_common import ComplexList +from ansible.module_utils.netcli import Conditional +from ansible.module_utils.six import string_types def to_lines(stdout): for item in stdout: - if isinstance(item, basestring): + if isinstance(item, string_types): item = str(item).split('\n') yield item -def parse_commands(module): - for cmd in module.params['commands']: - if isinstance(cmd, basestring): - cmd = dict(command=cmd, output=None) - elif 'command' not in cmd: - module.fail_json(msg='command keyword argument is required') - elif cmd.get('output') not in [None, 'text']: - module.fail_json(msg='invalid output specified for command') - elif not set(cmd.keys()).issubset(VALID_KEYS): - module.fail_json(msg='unknown keyword specified') - yield cmd +def parse_commands(module, warnings): + command = ComplexList(dict( + command=dict(key=True), + prompt=dict(), + answer=dict() + ), module) + commands = command(module.params['commands']) + for index, item in enumerate(commands): + if module.check_mode and not item['command'].startswith('show'): + warnings.append( + 'only show commands are supported when using check mode, not ' + 'executing `%s`' % item['command'] + ) + elif item['command'].startswith('conf'): + module.fail_json( + msg='sros_command does not support running config mode ' + 'commands. Please use sros_config instead' + ) + return commands def main(): - spec = dict( - # { command: , output: , prompt: , response: } + """main entry point for module execution + """ + argument_spec = dict( commands=dict(type='list', required=True), wait_for=dict(type='list', aliases=['waitfor']), @@ -180,59 +192,52 @@ def main(): interval=dict(default=1, type='int') ) - module = NetworkModule(argument_spec=spec, - connect_on_load=False, + argument_spec.update(sros_argument_spec) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - commands = list(parse_commands(module)) - conditionals = module.params['wait_for'] or list() + result = {'changed': False} warnings = list() - - runner = CommandRunner(module) - - for cmd in commands: - if module.check_mode and not cmd['command'].startswith('show'): - warnings.append('only show commands are supported when using ' - 'check mode, not executing `%s`' % cmd['command']) - else: - if cmd['command'].startswith('conf'): - module.fail_json(msg='sros_command does not support running ' - 'config mode commands. Please use ' - 'sros_config instead') - try: - runner.add_command(**cmd) - except AddCommandError: - exc = get_exception() - warnings.append('duplicate command detected: %s' % cmd) - - for item in conditionals: - runner.add_conditional(item) - - runner.retries = module.params['retries'] - runner.interval = module.params['interval'] - runner.match = module.params['match'] - - try: - runner.run() - except FailedConditionsError: - exc = get_exception() - module.fail_json(msg=str(exc), failed_conditions=exc.failed_conditions) - except NetworkError: - exc = get_exception() - module.fail_json(msg=str(exc)) - - result = dict(changed=False, stdout=list()) - - for cmd in commands: - try: - output = runner.get_command(cmd['command']) - except ValueError: - output = 'command not executed due to check_mode, see warnings' - result['stdout'].append(output) - + check_args(module, warnings) + commands = parse_commands(module, warnings) result['warnings'] = warnings - result['stdout_lines'] = list(to_lines(result['stdout'])) + + wait_for = module.params['wait_for'] or list() + conditionals = [Conditional(c) for c in wait_for] + + retries = module.params['retries'] + interval = module.params['interval'] + match = module.params['match'] + + while retries > 0: + responses = run_commands(module, commands) + + for item in list(conditionals): + if item(responses): + if match == 'any': + conditionals = list() + break + conditionals.remove(item) + + if not conditionals: + break + + time.sleep(interval) + retries -= 1 + + if conditionals: + failed_conditions = [item.raw for item in conditionals] + msg = 'One or more conditional statements have not be satisfied' + module.fail_json(msg=msg, failed_conditions=failed_conditions) + + + result = { + 'changed': False, + 'stdout': responses, + 'stdout_lines': list(to_lines(responses)) + } module.exit_json(**result) diff --git a/lib/ansible/modules/network/sros/sros_config.py b/lib/ansible/modules/network/sros/sros_config.py index 48964e0e46..6d7366431c 100644 --- a/lib/ansible/modules/network/sros/sros_config.py +++ b/lib/ansible/modules/network/sros/sros_config.py @@ -16,9 +16,11 @@ # along with Ansible. If not, see . # -ANSIBLE_METADATA = {'metadata_version': '1.0', - 'status': ['preview'], - 'supported_by': 'community'} +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'community' +} DOCUMENTATION = """ @@ -203,16 +205,22 @@ updates: description: The set of commands that will be pushed to the remote device returned: always type: list - sample: ['...', '...'] + sample: ['config system name "sros01"'] +commands: + description: The set of commands that will be pushed to the remote device + returned: always + type: list + sample: ['config system name "sros01"'] backup_path: description: The full path to the backup file returned: when backup is yes type: path sample: /playbooks/ansible/backup/sros_config.2016-07-16@22:28:34 """ -from ansible.module_utils.basic import get_exception -from ansible.module_utils.sros import NetworkModule, NetworkError +from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.netcfg import NetworkConfig, dumps +from ansible.module_utils.sros import sros_argument_spec, check_args +from ansible.module_utils.sros import load_config, run_commands, get_config def sanitize_config(lines): commands = list() @@ -224,15 +232,17 @@ def sanitize_config(lines): commands.append(line) return commands -def get_config(module, result): +def get_active_config(module): contents = module.params['config'] if not contents: - defaults = module.params['defaults'] - contents = module.config.get_config(detail=defaults) - return NetworkConfig(device_os='sros', contents=contents) + flags = [] + if module.params['defaults']: + flags = ['detail'] + return get_config(module, flags) + return contents def get_candidate(module): - candidate = NetworkConfig(device_os='sros') + candidate = NetworkConfig(indent=4) if module.params['src']: candidate.load(module.params['src']) elif module.params['lines']: @@ -246,33 +256,23 @@ def run(module, result): candidate = get_candidate(module) if match != 'none': - config = get_config(module, result) + config_text = get_active_config(module) + config = NetworkConfig(indent=4, contents=config_text) configobjs = candidate.difference(config) else: configobjs = candidate.items if configobjs: - commands = dumps(configobjs, 'lines') + commands = dumps(configobjs, 'commands') commands = sanitize_config(commands.split('\n')) + result['commands'] = commands result['updates'] = commands - # check if creating checkpoints is possible - if not module.connection.rollback_enabled: - warn = 'Cannot create checkpoint. Please enable this feature ' \ - 'using the sros_rollback module. Automatic rollback ' \ - 'will be disabled' - result['warnings'].append(warn) - # send the configuration commands to the device and merge # them with the current running config if not module.check_mode: - module.config.load_config(commands) - result['changed'] = True - - if module.params['save']: - if not module.check_mode: - module.config.save_config() + load_config(module, commands) result['changed'] = True def main(): @@ -293,23 +293,30 @@ def main(): save=dict(type='bool', default=False), ) + argument_spec.update(sros_argument_spec) + mutually_exclusive = [('lines', 'src')] - module = NetworkModule(argument_spec=argument_spec, - connect_on_load=False, + module = AnsibleModule(argument_spec=argument_spec, mutually_exclusive=mutually_exclusive, supports_check_mode=True) result = dict(changed=False, warnings=list()) - if module.params['backup']: - result['__backup__'] = module.config.get_config() + warnings = list() + check_args(module, warnings) + if warnings: + result['warnings'] = warnings - try: - run(module, result) - except NetworkError: - exc = get_exception() - module.fail_json(msg=str(exc), **exc.kwargs) + if module.params['backup']: + result['__backup__'] = get_config(module) + + run(module, result) + + if module.params['save']: + if not module.check_mode: + run_commands(module, ['admin save']) + result['changed'] = True module.exit_json(**result) diff --git a/lib/ansible/modules/network/sros/sros_rollback.py b/lib/ansible/modules/network/sros/sros_rollback.py index 6a283174be..b6746c0e9c 100644 --- a/lib/ansible/modules/network/sros/sros_rollback.py +++ b/lib/ansible/modules/network/sros/sros_rollback.py @@ -16,9 +16,11 @@ # along with Ansible. If not, see . # -ANSIBLE_METADATA = {'metadata_version': '1.0', - 'status': ['preview'], - 'supported_by': 'community'} +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'community' +} DOCUMENTATION = """ @@ -107,10 +109,12 @@ updates: type: list sample: ['...', '...'] """ -from ansible.module_utils.basic import get_exception -from ansible.module_utils.sros import NetworkModule, NetworkError +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.sros import load_config, get_config +from ansible.module_utils.sros import sros_argument_spec, check_args from ansible.module_utils.netcfg import NetworkConfig, dumps + def invoke(name, *args, **kwargs): func = globals().get(name) if func: @@ -136,7 +140,7 @@ def present(module, commands): invoke(setter, module, commands) def absent(module, commands): - config = module.config.get_config() + config = get_config(module) if 'rollback-location' in config: commands.append('configure system rollback no rollback-location') if 'rescue-location' in config: @@ -166,27 +170,9 @@ def set_rescue_location(module, commands): value = module.params['rescue_location'] commands.append('configure system rollback rescue-location "%s"' % value) -def get_config(module): - contents = module.config.get_config() - return NetworkConfig(device_os='sros', contents=contents) - -def load_config(module, commands, result): - candidate = NetworkConfig(device_os='sros', contents='\n'.join(commands)) - config = get_config(module) - configobjs = candidate.difference(config) - - if configobjs: - commands = dumps(configobjs, 'lines') - commands = sanitize_config(commands.split('\n')) - - result['updates'] = commands - - # send the configuration commands to the device and merge - # them with the current running config - if not module.check_mode: - module.config(commands) - - result['changed'] = True +def get_device_config(module): + contents = get_config(module) + return NetworkConfig(indent=4, contents=contents) def main(): """ main entry point for module execution @@ -202,8 +188,9 @@ def main(): state=dict(default='present', choices=['present', 'absent']) ) - module = NetworkModule(argument_spec=argument_spec, - connect_on_load=False, + argument_spec.update(sros_argument_spec) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) state = module.params['state'] @@ -213,11 +200,24 @@ def main(): commands = list() invoke(state, module, commands) - try: - load_config(module, commands, result) - except NetworkError: - exc = get_exception() - module.fail_json(msg=str(exc), **exc.kwargs) + candidate = NetworkConfig(indent=4, contents='\n'.join(commands)) + config = get_device_config(module) + configobjs = candidate.difference(config) + + if configobjs: + #commands = dumps(configobjs, 'lines') + commands = dumps(configobjs, 'commands') + commands = sanitize_config(commands.split('\n')) + + result['updates'] = commands + result['commands'] = commands + + # send the configuration commands to the device and merge + # them with the current running config + if not module.check_mode: + load_config(module, commands) + + result['changed'] = True module.exit_json(**result) diff --git a/lib/ansible/plugins/action/sros.py b/lib/ansible/plugins/action/sros.py new file mode 100644 index 0000000000..49738cd0ce --- /dev/null +++ b/lib/ansible/plugins/action/sros.py @@ -0,0 +1,111 @@ +# +# (c) 2016 Red Hat Inc. +# +# 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 + +import os +import sys +import copy + +from ansible.plugins.action.normal import ActionModule as _ActionModule +from ansible.utils.path import unfrackpath +from ansible.plugins import connection_loader +from ansible.module_utils.sros import sros_argument_spec +from ansible.module_utils.basic import AnsibleFallbackNotFound +from ansible.module_utils.six import iteritems +from ansible.module_utils._text import to_bytes + +try: + from __main__ import display +except ImportError: + from ansible.utils.display import Display + display = Display() + + +class ActionModule(_ActionModule): + + def run(self, tmp=None, task_vars=None): + + if self._play_context.connection != 'local': + return dict( + failed=True, + msg='invalid connection specified, expected connection=local, ' + 'got %s' % self._play_context.connection + ) + + provider = self.load_provider() + + pc = copy.deepcopy(self._play_context) + pc.connection = 'network_cli' + pc.network_os = 'sros' + pc.remote_addr = provider['host'] or self._play_context.remote_addr + pc.port = provider['port'] or self._play_context.port or 22 + pc.remote_user = provider['username'] or self._play_context.connection_user + pc.password = provider['password'] or self._play_context.password + pc.private_key_file = provider['ssh_keyfile'] or self._play_context.private_key_file + pc.timeout = provider['timeout'] or self._play_context.timeout + + display.vvv('using connection plugin %s' % pc.connection, pc.remote_addr) + connection = self._shared_loader_obj.connection_loader.get('persistent', pc, sys.stdin) + + socket_path = self._get_socket_path(pc) + display.vvvv('socket_path: %s' % socket_path, pc.remote_addr) + + if not os.path.exists(socket_path): + # start the connection if it isn't started + rc, out, err = connection.exec_command('open_shell()') + if not rc == 0: + return {'failed': True, 'msg': 'unable to open shell', 'rc': rc} + + task_vars['ansible_socket'] = socket_path + + return super(ActionModule, self).run(tmp, task_vars) + + def _get_socket_path(self, play_context): + ssh = connection_loader.get('ssh', class_only=True) + cp = ssh._create_control_path(play_context.remote_addr, play_context.port, play_context.remote_user) + path = unfrackpath("$HOME/.ansible/pc") + return cp % dict(directory=path) + + def load_provider(self): + provider = self._task.args.get('provider', {}) + for key, value in iteritems(sros_argument_spec): + if key != 'provider' and key not in provider: + if key in self._task.args: + provider[key] = self._task.args[key] + elif 'fallback' in value: + provider[key] = self._fallback(value['fallback']) + elif key not in provider: + provider[key] = None + return provider + + def _fallback(self, fallback): + strategy = fallback[0] + args = [] + kwargs = {} + + for item in fallback[1:]: + if isinstance(item, dict): + kwargs = item + else: + args = item + try: + return strategy(*args, **kwargs) + except AnsibleFallbackNotFound: + pass diff --git a/lib/ansible/plugins/action/sros_config.py b/lib/ansible/plugins/action/sros_config.py index c1a9a065b3..dcd84deff4 100644 --- a/lib/ansible/plugins/action/sros_config.py +++ b/lib/ansible/plugins/action/sros_config.py @@ -19,10 +19,95 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from ansible.plugins.action import ActionBase -from ansible.plugins.action.net_config import ActionModule as NetActionModule +import os +import re +import time +import glob -class ActionModule(NetActionModule, ActionBase): - pass +from ansible.plugins.action.sros import ActionModule as _ActionModule +from ansible.module_utils._text import to_text +from ansible.module_utils.six.moves.urllib.parse import urlsplit +from ansible.utils.vars import merge_hash + +PRIVATE_KEYS_RE = re.compile('__.+__') +class ActionModule(_ActionModule): + + def run(self, tmp=None, task_vars=None): + + if self._task.args.get('src'): + try: + self._handle_template() + except ValueError as exc: + return dict(failed=True, msg=exc.message) + + result = super(ActionModule, self).run(tmp, task_vars) + + if self._task.args.get('backup') and result.get('__backup__'): + # User requested backup and no error occurred in module. + # NOTE: If there is a parameter error, _backup key may not be in results. + filepath = self._write_backup(task_vars['inventory_hostname'], + result['__backup__']) + + result['backup_path'] = filepath + + # strip out any keys that have two leading and two trailing + # underscore characters + for key in result.keys(): + if PRIVATE_KEYS_RE.match(key): + del result[key] + + return result + + def _get_working_path(self): + cwd = self._loader.get_basedir() + if self._task._role is not None: + cwd = self._task._role._role_path + return cwd + + def _write_backup(self, host, contents): + backup_path = self._get_working_path() + '/backup' + if not os.path.exists(backup_path): + os.mkdir(backup_path) + for fn in glob.glob('%s/%s*' % (backup_path, host)): + os.remove(fn) + tstamp = time.strftime("%Y-%m-%d@%H:%M:%S", time.localtime(time.time())) + filename = '%s/%s_config.%s' % (backup_path, host, tstamp) + open(filename, 'w').write(contents) + return filename + + def _handle_template(self): + src = self._task.args.get('src') + working_path = self._get_working_path() + + if os.path.isabs(src) or urlsplit('src').scheme: + source = src + else: + source = self._loader.path_dwim_relative(working_path, 'templates', src) + if not source: + source = self._loader.path_dwim_relative(working_path, src) + + if not os.path.exists(source): + raise ValueError('path specified in src not found') + + try: + with open(source, 'r') as f: + template_data = to_text(f.read()) + except IOError: + return dict(failed=True, msg='unable to load src file') + + # Create a template search path in the following order: + # [working_path, self_role_path, dependent_role_paths, dirname(source)] + searchpath = [working_path] + if self._task._role is not None: + searchpath.append(self._task._role._role_path) + if hasattr(self._task, "_block:"): + dep_chain = self._task._block.get_dep_chain() + if dep_chain is not None: + for role in dep_chain: + searchpath.append(role._role_path) + searchpath.append(os.path.dirname(source)) + self._templar.environment.loader.searchpath = searchpath + self._task.args['src'] = self._templar.template(template_data) + diff --git a/lib/ansible/plugins/terminal/sros.py b/lib/ansible/plugins/terminal/sros.py index 6eea535f4f..07d8f37b86 100644 --- a/lib/ansible/plugins/terminal/sros.py +++ b/lib/ansible/plugins/terminal/sros.py @@ -34,7 +34,7 @@ class TerminalModule(TerminalBase): ] terminal_stderr_re = [ - re.compile(r"^\r\nError:"), + re.compile(r"Error:"), ] def on_open_shell(self):