From dd63dfcf1ec50bade4eaf8925330d7c96e475cc5 Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Mon, 27 Mar 2017 11:32:57 -0700 Subject: [PATCH] Ansible 2.3 feature support for dellos9 and dellos10 (#22856) * Ansible 2.3 feature support for dellos9 and dellos10 * Use Persistent Connection Manager * Fix CI issue, revert the doc and metadata changes * Reverted the meta_info (supported_by) to community from core * Fixed the CI issues, use module_utisl.six and updated legacy-files --- lib/ansible/constants.py | 3 +- lib/ansible/module_utils/dellos10.py | 164 +++++++++-------- lib/ansible/module_utils/dellos9.py | 171 +++++++++--------- .../network/dellos10/dellos10_command.py | 134 +++++++------- .../network/dellos10/dellos10_config.py | 32 +++- .../network/dellos10/dellos10_facts.py | 126 +++++++------ .../network/dellos9/dellos9_command.py | 140 +++++++------- .../modules/network/dellos9/dellos9_config.py | 27 ++- .../modules/network/dellos9/dellos9_facts.py | 106 ++++++----- lib/ansible/plugins/action/dellos10.py | 125 +++++++++++++ lib/ansible/plugins/action/dellos10_config.py | 94 +++++++++- lib/ansible/plugins/action/dellos6_config.py | 2 + lib/ansible/plugins/action/dellos9.py | 125 +++++++++++++ lib/ansible/plugins/action/dellos9_config.py | 94 +++++++++- lib/ansible/plugins/terminal/dellos10.py | 78 ++++++++ lib/ansible/plugins/terminal/dellos9.py | 78 ++++++++ test/sanity/pep8/legacy-files.txt | 6 - 17 files changed, 1086 insertions(+), 419 deletions(-) create mode 100644 lib/ansible/plugins/action/dellos10.py create mode 100644 lib/ansible/plugins/action/dellos9.py create mode 100644 lib/ansible/plugins/terminal/dellos10.py create mode 100644 lib/ansible/plugins/terminal/dellos9.py diff --git a/lib/ansible/constants.py b/lib/ansible/constants.py index c1415dd9d12..46a08c758d1 100644 --- a/lib/ansible/constants.py +++ b/lib/ansible/constants.py @@ -330,9 +330,8 @@ DEFAULT_STRATEGY_PLUGIN_PATH = get_config(p, DEFAULTS, 'strategy_plugins', 'AN '~/.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', 'sros'], + 'vyos', 'sros', 'dellos9', 'dellos10'], value_type='list') - DEFAULT_STRATEGY = get_config(p, DEFAULTS, 'strategy', 'ANSIBLE_STRATEGY', 'linear') DEFAULT_STDOUT_CALLBACK = get_config(p, DEFAULTS, 'stdout_callback', 'ANSIBLE_STDOUT_CALLBACK', 'default') # cache diff --git a/lib/ansible/module_utils/dellos10.py b/lib/ansible/module_utils/dellos10.py index 2b395a2a369..db2c5eedbd0 100644 --- a/lib/ansible/module_utils/dellos10.py +++ b/lib/ansible/module_utils/dellos10.py @@ -1,5 +1,6 @@ # # (c) 2015 Peter Sprygada, +# (c) 2017 Red Hat, Inc # # Copyright (c) 2016 Dell Inc. # @@ -28,28 +29,98 @@ # 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.network import register_transport, to_list -from ansible.module_utils.shell import CliBase -from ansible.module_utils.netcfg import NetworkConfig, ConfigLine - - -def get_config(module): - contents = module.params['config'] - - if not contents: - contents = module.config.get_config() - module.params['config'] = contents - return NetworkConfig(indent=1, contents=contents[0]) - else: - return NetworkConfig(indent=1, contents=contents) - +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.netcfg import NetworkConfig,ConfigLine + +_DEVICE_CONFIGS = {} + +WARNING_PROMPTS_RE = [ + r"[\r\n]?\[confirm yes/no\]:\s?$", + r"[\r\n]?\[y/n\]:\s?$", + r"[\r\n]?\[yes/no\]:\s?$" +] + +dellos10_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'), + 'authorize': dict(fallback=(env_fallback, ['ANSIBLE_NET_AUTHORIZE']), type='bool'), + 'auth_pass': dict(fallback=(env_fallback, ['ANSIBLE_NET_AUTH_PASS']), no_log=True), + 'timeout': dict(type='int'), + 'provider': dict(type='dict'), +} + +def check_args(module, warnings): + provider = module.params['provider'] or {} + for key in dellos10_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 = 'show running-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) + + +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 + +def load_config(module, commands): + rc, out, err = exec_command(module, 'configure terminal') + if rc != 0: + module.fail_json(msg='unable to enter configuration mode', err=err) + + commands.append('commit') + for command in to_list(commands): + if command == 'end': + continue + cmd = {'command': command, 'prompt': WARNING_PROMPTS_RE, 'answer': 'yes'} + rc, out, err = exec_command(module, module.jsonify(cmd)) + if rc != 0: + module.fail_json(msg=err, command=command, rc=rc) + + exec_command(module, 'end') def get_sublevel_config(running_config, module): contents = list() current_config_contents = list() + running_config = NetworkConfig(contents=running_config, indent=1) obj = running_config.get_object(module.params['parents']) if obj: contents = obj.children @@ -61,67 +132,8 @@ def get_sublevel_config(running_config, module): current_config_contents.append(c.rjust(len(c) + indent, ' ')) if isinstance(c, ConfigLine): current_config_contents.append(c.raw) - indent = indent + 1 + indent = 1 sublevel_config = '\n'.join(current_config_contents) return sublevel_config - -class Cli(CliBase): - - NET_PASSWD_RE = re.compile(r"[\r\n]?password:\s?$", re.I) - - 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"Syntax error:"), - re.compile(r"invalid input", 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 connect(self, params, **kwargs): - super(Cli, self).connect(params, kickstart=False, **kwargs) - self.shell.send('terminal length 0') - - - def configure(self, commands, **kwargs): - cmds = ['configure terminal'] - cmds.extend(to_list(commands)) - cmds.append('end') - cmds.append('commit') - - responses = self.execute(cmds) - responses.pop(0) - return responses - - - def get_config(self, **kwargs): - return self.execute(['show running-configuration']) - - - def load_config(self, commands, **kwargs): - return self.configure(commands) - - - def commit_config(self, **kwargs): - self.execute(['commit']) - - - def abort_config(self, **kwargs): - self.execute(['discard']) - - - def save_config(self): - self.execute(['copy running-config startup-config']) - - -Cli = register_transport('cli', default=True)(Cli) diff --git a/lib/ansible/module_utils/dellos9.py b/lib/ansible/module_utils/dellos9.py index b8add0e8145..9ad4977dde8 100644 --- a/lib/ansible/module_utils/dellos9.py +++ b/lib/ansible/module_utils/dellos9.py @@ -1,5 +1,6 @@ # # (c) 2015 Peter Sprygada, +# (c) 2017 Red Hat, Inc # # Copyright (c) 2016 Dell Inc. # @@ -28,28 +29,97 @@ # 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.shell import CliBase -from ansible.module_utils.network import register_transport, to_list, Command -from ansible.module_utils.netcfg import NetworkConfig, ConfigLine - - -def get_config(module): - contents = module.params['config'] - - if not contents: - contents = module.config.get_config() - module.params['config'] = contents - return NetworkConfig(indent=1, contents=contents[0]) - else: - return NetworkConfig(indent=1, contents=contents) - +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.netcfg import NetworkConfig,ConfigLine + +_DEVICE_CONFIGS = {} + +WARNING_PROMPTS_RE = [ + r"[\r\n]?\[confirm yes/no\]:\s?$", + r"[\r\n]?\[y/n\]:\s?$", + r"[\r\n]?\[yes/no\]:\s?$" +] + +dellos9_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'), + 'authorize': dict(fallback=(env_fallback, ['ANSIBLE_NET_AUTHORIZE']), type='bool'), + 'auth_pass': dict(fallback=(env_fallback, ['ANSIBLE_NET_AUTH_PASS']), no_log=True), + 'timeout': dict(type='int'), + 'provider': dict(type='dict'), +} + +def check_args(module, warnings): + provider = module.params['provider'] or {} + for key in dellos9_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 = 'show running-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) + + +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 + +def load_config(module, commands): + rc, out, err = exec_command(module, 'configure terminal') + if rc != 0: + module.fail_json(msg='unable to enter configuration mode', err=err) + + for command in to_list(commands): + if command == 'end': + continue + cmd = {'command': command, 'prompt': WARNING_PROMPTS_RE, 'answer': 'yes'} + rc, out, err = exec_command(module, module.jsonify(cmd)) + if rc != 0: + module.fail_json(msg=err, command=command, rc=rc) + + exec_command(module, 'end') def get_sublevel_config(running_config, module): contents = list() current_config_contents = list() + running_config = NetworkConfig(contents=running_config, indent=1) obj = running_config.get_object(module.params['parents']) if obj: contents = obj.children @@ -61,75 +131,8 @@ def get_sublevel_config(running_config, module): current_config_contents.append(c.rjust(len(c) + indent, ' ')) if isinstance(c, ConfigLine): current_config_contents.append(c.raw) - indent = indent + 1 + indent = 1 sublevel_config = '\n'.join(current_config_contents) return sublevel_config - -class Cli(CliBase): - - NET_PASSWD_RE = re.compile(r"[\r\n]?password:\s?$", re.I) - - WARNING_PROMPTS_RE = [ - re.compile(r"[\r\n]?\[confirm yes/no\]:\s?$"), - re.compile(r"[\r\n]?\[y/n\]:\s?$"), - re.compile(r"[\r\n]?\[yes/no\]:\s?$") - ] - - CLI_PROMPTS_RE = [ - re.compile(r"[\r\n]?[\w+\-\.:\/\[\]]+(?:\([^\)]+\)){,3}(?:>|#) ?$"), - re.compile(r"\[\w+\@[\w\-\.]+(?: [^\]])\] ?[>#\$] ?$") - ] - - CLI_ERRORS_RE = [ - re.compile(r"% ?Error: (?:(?!\bdoes not exist\b)(?!\balready exists\b)(?!\bHost not found\b)(?!\bnot active\b).)*$"), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - ] - - def connect(self, params, **kwargs): - super(Cli, self).connect(params, kickstart=False, **kwargs) - self.shell.send('terminal length 0') - - - def authorize(self, params, **kwargs): - passwd = params['auth_pass'] - self.run_commands( - Command('enable', prompt=self.NET_PASSWD_RE, response=passwd) - ) - - - def configure(self, commands, **kwargs): - cmds = ['configure terminal'] - cmdlist = list() - for c in to_list(commands): - cmd = Command(c, prompt=self.WARNING_PROMPTS_RE, response='yes') - cmdlist.append(cmd) - cmds.extend(cmdlist) - cmds.append('end') - - responses = self.execute(cmds) - responses.pop(0) - return responses - - - def get_config(self, **kwargs): - return self.execute(['show running-config']) - - - def load_config(self, commands, **kwargs): - return self.configure(commands) - - - def save_config(self): - cmdlist = list() - cmd = 'copy running-config startup-config' - cmdlist.append(Command(cmd, prompt=self.WARNING_PROMPTS_RE, response='yes')) - self.execute(cmdlist) - - -Cli = register_transport('cli', default=True)(Cli) diff --git a/lib/ansible/modules/network/dellos10/dellos10_command.py b/lib/ansible/modules/network/dellos10/dellos10_command.py index c094bc138a2..b8d35305716 100644 --- a/lib/ansible/modules/network/dellos10/dellos10_command.py +++ b/lib/ansible/modules/network/dellos10/dellos10_command.py @@ -2,7 +2,7 @@ # # (c) 2015 Peter Sprygada, # -# Copyright (c) 2016 Dell Inc. +# Copyright (c) 2017 Dell Inc. # # This file is part of Ansible # @@ -140,14 +140,14 @@ warnings: type: list sample: ['...', '...'] """ +import time -from ansible.module_utils.basic import get_exception -from ansible.module_utils.netcli import CommandRunner, FailedConditionsError -from ansible.module_utils.network import NetworkModule, NetworkError -from ansible.module_utils.six import string_types -import ansible.module_utils.dellos10 +from ansible.module_utils.dellos10 import run_commands +from ansible.module_utils.dellos10 import dellos10_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 -VALID_KEYS = ['command', 'prompt', 'response'] def to_lines(stdout): for item in stdout: @@ -155,75 +155,87 @@ def to_lines(stdout): item = str(item).split('\n') yield item -def parse_commands(module): - for cmd in module.params['commands']: - if isinstance(cmd, string_types): - cmd = dict(command=cmd, output=None) - elif 'command' not in cmd: - module.fail_json(msg='command keyword argument is required') - 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='dellos10_command does not support running config mode ' + 'commands. Please use dellos10_config instead' + ) + return commands + def main(): - spec = dict( + """main entry point for module execution + """ + argument_spec = dict( # { command: , prompt: , response: } commands=dict(type='list', required=True), - wait_for=dict(type='list'), + + wait_for=dict(type='list', aliases=['waitfor']), + match=dict(default='all', choices=['all', 'any']), + retries=dict(default=10, type='int'), interval=dict(default=1, type='int') ) - module = NetworkModule(argument_spec=spec, - connect_on_load=False, + argument_spec.update(dellos10_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() + check_args(module, warnings) + commands = parse_commands(module, warnings) + result['warnings'] = warnings - 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) - else: - if cmd['command'].startswith('conf'): - module.fail_json(msg='dellos10_command does not support running ' - 'config mode commands. Please use ' - 'dellos10_config instead') - runner.add_command(**cmd) - - for item in conditionals: - runner.add_conditional(item) - - runner.retries = module.params['retries'] - runner.interval = module.params['interval'] - - 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) - - result['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) + 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'] - result['warnings'] = warnings - result['stdout_lines'] = list(to_lines(result['stdout'])) + 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/dellos10/dellos10_config.py b/lib/ansible/modules/network/dellos10/dellos10_config.py index c7fc8dbcae6..744c8619e5e 100644 --- a/lib/ansible/modules/network/dellos10/dellos10_config.py +++ b/lib/ansible/modules/network/dellos10/dellos10_config.py @@ -2,7 +2,7 @@ # # (c) 2015 Peter Sprygada, # -# Copyright (c) 2016 Dell Inc. +# Copyright (c) 2017 Dell Inc. # # This file is part of Ansible # @@ -195,9 +195,13 @@ saved: sample: True """ +from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.netcfg import NetworkConfig, dumps -from ansible.module_utils.network import NetworkModule from ansible.module_utils.dellos10 import get_config, get_sublevel_config +from ansible.module_utils.dellos10 import dellos10_argument_spec, check_args +from ansible.module_utils.dellos10 import load_config, run_commands +from ansible.module_utils.dellos10 import WARNING_PROMPTS_RE + def get_candidate(module): candidate = NetworkConfig(indent=1) @@ -223,16 +227,18 @@ def main(): match=dict(default='line', choices=['line', 'strict', 'exact', 'none']), replace=dict(default='line', choices=['line', 'block']), + update=dict(choices=['merge', 'check'], default='merge'), save=dict(type='bool', default=False), config=dict(), backup=dict(type='bool', default=False) ) + argument_spec.update(dellos10_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) @@ -240,24 +246,30 @@ def main(): match = module.params['match'] replace = module.params['replace'] - result = dict(changed=False, saved=False) - candidate = get_candidate(module) + warnings = list() + check_args(module, warnings) + result = dict(changed=False, saved=False, warnings=warnings) + + candidate = get_candidate(module) if match != 'none': config = get_config(module) if parents: contents = get_sublevel_config(config, module) config = NetworkConfig(contents=contents, indent=1) + else: + config = NetworkConfig(contents=config, indent=1) configobjs = candidate.difference(config, match=match, replace=replace) else: configobjs = candidate.items if module.params['backup']: - result['__backup__'] = module.cli('show running-config')[0] + result['__backup__'] = get_config(module) commands = list() + if configobjs: commands = dumps(configobjs, 'commands') commands = commands.split('\n') @@ -269,11 +281,11 @@ def main(): commands.extend(module.params['after']) if not module.check_mode and module.params['update'] == 'merge': - response = module.config.load_config(commands) - result['responses'] = response + load_config(module, commands) if module.params['save']: - module.config.save_config() + cmd = {'command': 'copy runing-config startup-config', 'prompt': WARNING_PROMPTS_RE, 'answer': 'yes'} + run_commands(module, [cmd]) result['saved'] = True result['changed'] = True diff --git a/lib/ansible/modules/network/dellos10/dellos10_facts.py b/lib/ansible/modules/network/dellos10/dellos10_facts.py index c21309e78b6..8b24b7b2dab 100644 --- a/lib/ansible/modules/network/dellos10/dellos10_facts.py +++ b/lib/ansible/modules/network/dellos10/dellos10_facts.py @@ -2,7 +2,7 @@ # # (c) 2015 Peter Sprygada, # -# Copyright (c) 2016 Dell Inc. +# Copyright (c) 2017 Dell Inc. # # This file is part of Ansible # @@ -134,47 +134,57 @@ ansible_net_neighbors: import re -from ansible.module_utils.basic import get_exception -from ansible.module_utils.netcli import CommandRunner -from ansible.module_utils.network import NetworkModule -import ansible.module_utils.dellos10 +from ansible.module_utils.dellos10 import run_commands +from ansible.module_utils.dellos10 import dellos10_argument_spec, check_args +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import iteritems +from ansible.module_utils.six.moves import zip try: from lxml import etree as ET except ImportError: import xml.etree.ElementTree as ET + class FactsBase(object): - def __init__(self, runner): - self.runner = runner + COMMANDS = list() + + def __init__(self, module): + self.module = module self.facts = dict() + self.responses = None + + def populate(self): + self.responses = run_commands(self.module, self.COMMANDS, check_rc=False) - self.commands() + def run(self, cmd): + return run_commands(self.module, cmd, check_rc=False) class Default(FactsBase): - def commands(self): - self.runner.add_command('show version | display-xml') - self.runner.add_command('show system | display-xml') - self.runner.add_command('show running-configuration | grep hostname') + COMMANDS = [ + 'show version | display-xml', + 'show system | display-xml', + 'show running-configuration | grep hostname' + ] def populate(self): - - data = self.runner.get_command('show version | display-xml') + super(Default, self).populate() + data = self.responses[0] xml_data = ET.fromstring(data) self.facts['name'] = self.parse_name(xml_data) self.facts['version'] = self.parse_version(xml_data) - data = self.runner.get_command('show system | display-xml') + data = self.responses[1] xml_data = ET.fromstring(data) self.facts['servicetag'] = self.parse_serialnum(xml_data) self.facts['model'] = self.parse_model(xml_data) - data = self.runner.get_command('show running-configuration | grep hostname') + data = self.responses[2] self.facts['hostname'] = self.parse_hostname(data) def parse_name(self, data): @@ -213,18 +223,21 @@ class Default(FactsBase): class Hardware(FactsBase): - def commands(self): - self.runner.add_command('show processes memory | grep Total') + COMMANDS = [ + 'show version | display-xml', + 'show processes memory | grep Total' + ] def populate(self): - data = self.runner.get_command('show version | display-xml') + super(Hardware, self).populate() + data = self.responses[0] + xml_data = ET.fromstring(data) self.facts['cpu_arch'] = self.parse_cpu_arch(xml_data) - data = self.runner.get_command('show processes memory | grep Total') - + data = self.responses[1] match = self.parse_memory(data) if match: self.facts['memtotal_mb'] = int(match[0]) / 1024 @@ -243,24 +256,25 @@ class Hardware(FactsBase): class Config(FactsBase): - def commands(self): - self.runner.add_command('show running-config') + COMMANDS = ['show running-config'] def populate(self): - config = self.runner.get_command('show running-config') - self.facts['config'] = config + super(Config, self).populate() + self.facts['config'] = self.responses[0] class Interfaces(FactsBase): - def commands(self): - self.runner.add_command('show interface | display-xml') + COMMANDS = [ + 'show interface | display-xml', + ] def populate(self): + super(Interfaces, self).populate() self.facts['all_ipv4_addresses'] = list() self.facts['all_ipv6_addresses'] = list() - data = self.runner.get_command('show interface | display-xml') + data = self.responses[0] xml_data = ET.fromstring(data) @@ -294,21 +308,23 @@ class Interfaces(FactsBase): for interface in interfaces.findall('./data/ports/ports-state/port'): name = self.parse_item(interface, 'name') - fanout = self.parse_item(interface, 'fanout-state') + # media-type name interface name format phy-eth 1/1/1 mediatype = self.parse_item(interface, 'media-type') typ, sname = name.split('-eth') - - if fanout == "BREAKOUT_1x1": - name = "ethernet" + sname + name = "ethernet" + sname + try: intf = int_facts[name] intf['mediatype'] = mediatype - else: - # TODO: Loop for the exact subport + except: + # fanout for subport in xrange(1, 5): name = "ethernet" + sname + ":" + str(subport) - intf = int_facts[name] - intf['mediatype'] = mediatype + try: + intf = int_facts[name] + intf['mediatype'] = mediatype + except: + pass return int_facts @@ -329,7 +345,7 @@ class Interfaces(FactsBase): ipv4 = interface.find('ipv4') ip_address = "" if ipv4 is not None: - prim_ipaddr = ipv4.find('./address/primary-addr') + prim_ipaddr = ipv4.find('./address/primary-addr') if prim_ipaddr is not None: ip_address = prim_ipaddr.text self.add_ip_address(ip_address, 'ipv4') @@ -340,7 +356,7 @@ class Interfaces(FactsBase): ipv4 = interface.find('ipv4') ip_address = "" if ipv4 is not None: - sec_ipaddr = ipv4.find('./address/secondary-addr') + sec_ipaddr = ipv4.find('./address/secondary-addr') if sec_ipaddr is not None: ip_address = sec_ipaddr.text self.add_ip_address(ip_address, 'ipv4') @@ -351,7 +367,7 @@ class Interfaces(FactsBase): ipv6 = interface.find('ipv6') ip_address = "" if ipv6 is not None: - ipv6_addr = ipv6.find('./address/ipv6-address') + ipv6_addr = ipv6.find('./address/ipv6-address') if ipv6_addr is not None: ip_address = ipv6_addr.text self.add_ip_address(ip_address, 'ipv6') @@ -384,11 +400,16 @@ VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) def main(): - spec = dict( + """main entry point for module execution + """ + argument_spec = dict( gather_subset=dict(default=['!config'], type='list') ) - module = NetworkModule(argument_spec=spec, supports_check_mode=True) + argument_spec.update(dellos10_argument_spec) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) gather_subset = module.params['gather_subset'] @@ -426,28 +447,23 @@ def main(): facts = dict() facts['gather_subset'] = list(runable_subsets) - runner = CommandRunner(module) - instances = list() for key in runable_subsets: - runs = FACT_SUBSETS[key](runner) - instances.append(runs) - - runner.run() + instances.append(FACT_SUBSETS[key](module)) - try: - for inst in instances: - inst.populate() - facts.update(inst.facts) - except Exception: - module.exit_json(out=module.from_json(runner.items)) + for inst in instances: + inst.populate() + facts.update(inst.facts) ansible_facts = dict() - for key, value in facts.items(): + for key, value in iteritems(facts): key = 'ansible_net_%s' % key ansible_facts[key] = value - module.exit_json(ansible_facts=ansible_facts) + warnings = list() + check_args(module, warnings) + + module.exit_json(ansible_facts=ansible_facts, warnings=warnings) if __name__ == '__main__': diff --git a/lib/ansible/modules/network/dellos9/dellos9_command.py b/lib/ansible/modules/network/dellos9/dellos9_command.py index e3d2063f541..ac7889f8dab 100644 --- a/lib/ansible/modules/network/dellos9/dellos9_command.py +++ b/lib/ansible/modules/network/dellos9/dellos9_command.py @@ -20,10 +20,9 @@ # along with Ansible. If not, see . # -ANSIBLE_METADATA = {'metadata_version': '1.0', - 'status': ['preview'], - 'supported_by': 'community'} - +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'metadata_version': '1.0'} DOCUMENTATION = """ --- @@ -149,14 +148,14 @@ warnings: type: list sample: ['...', '...'] """ +import time -from ansible.module_utils.basic import get_exception -from ansible.module_utils.netcli import CommandRunner, FailedConditionsError -from ansible.module_utils.network import NetworkModule, NetworkError -from ansible.module_utils.six import string_types -import ansible.module_utils.dellos9 +from ansible.module_utils.dellos9 import run_commands +from ansible.module_utils.dellos9 import dellos9_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 -VALID_KEYS = ['command', 'prompt', 'response'] def to_lines(stdout): for item in stdout: @@ -164,74 +163,87 @@ def to_lines(stdout): item = str(item).split('\n') yield item -def parse_commands(module): - for cmd in module.params['commands']: - if isinstance(cmd, string_types): - cmd = dict(command=cmd, output=None) - elif 'command' not in cmd: - module.fail_json(msg='command keyword argument is required') - 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='dellos9_command does not support running config mode ' + 'commands. Please use dellos9_config instead' + ) + return commands + def main(): - spec = dict( + """main entry point for module execution + """ + argument_spec = dict( # { command: , prompt: , response: } commands=dict(type='list', required=True), - wait_for=dict(type='list'), + + wait_for=dict(type='list', aliases=['waitfor']), + match=dict(default='all', choices=['all', 'any']), + retries=dict(default=10, type='int'), interval=dict(default=1, type='int') ) - module = NetworkModule(argument_spec=spec, - connect_on_load=False, + argument_spec.update(dellos9_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() + check_args(module, warnings) + commands = parse_commands(module, warnings) + result['warnings'] = warnings - 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) - else: - if cmd['command'].startswith('conf'): - module.fail_json(msg='dellos9_command does not support running ' - 'config mode commands. Please use ' - 'dellos9_config instead') - runner.add_command(**cmd) - - for item in conditionals: - runner.add_conditional(item) - - runner.retries = module.params['retries'] - runner.interval = module.params['interval'] - - 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) - - result['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) + wait_for = module.params['wait_for'] or list() + conditionals = [Conditional(c) for c in wait_for] - result['warnings'] = warnings - result['stdout_lines'] = list(to_lines(result['stdout'])) + 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/dellos9/dellos9_config.py b/lib/ansible/modules/network/dellos9/dellos9_config.py index 95d68a0705b..6820d7c7fb3 100644 --- a/lib/ansible/modules/network/dellos9/dellos9_config.py +++ b/lib/ansible/modules/network/dellos9/dellos9_config.py @@ -201,9 +201,12 @@ saved: sample: True """ +from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.netcfg import NetworkConfig, dumps -from ansible.module_utils.network import NetworkModule from ansible.module_utils.dellos9 import get_config, get_sublevel_config +from ansible.module_utils.dellos9 import dellos9_argument_spec, check_args +from ansible.module_utils.dellos9 import load_config, run_commands +from ansible.module_utils.dellos9 import WARNING_PROMPTS_RE def get_candidate(module): @@ -237,10 +240,11 @@ def main(): backup=dict(type='bool', default=False) ) + argument_spec.update(dellos9_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) @@ -248,7 +252,11 @@ def main(): match = module.params['match'] replace = module.params['replace'] - result = dict(changed=False, saved=False) + + warnings = list() + check_args(module, warnings) + + result = dict(changed=False, saved=False, warnings=warnings) candidate = get_candidate(module) @@ -257,15 +265,18 @@ def main(): if parents: contents = get_sublevel_config(config, module) config = NetworkConfig(contents=contents, indent=1) + else: + config = NetworkConfig(contents=config, indent=1) configobjs = candidate.difference(config, match=match, replace=replace) else: configobjs = candidate.items if module.params['backup']: - result['__backup__'] = module.cli('show running-config')[0] + result['__backup__'] = get_config(module) commands = list() + if configobjs: commands = dumps(configobjs, 'commands') commands = commands.split('\n') @@ -277,11 +288,11 @@ def main(): commands.extend(module.params['after']) if not module.check_mode and module.params['update'] == 'merge': - response = module.config.load_config(commands) - result['responses'] = response + load_config(module, commands) if module.params['save']: - module.config.save_config() + cmd = {'command': 'copy runing-config startup-config', 'prompt': WARNING_PROMPTS_RE, 'answer': 'yes'} + run_commands(module, [cmd]) result['saved'] = True result['changed'] = True diff --git a/lib/ansible/modules/network/dellos9/dellos9_facts.py b/lib/ansible/modules/network/dellos9/dellos9_facts.py index f84fafa6fdc..fc49516407b 100644 --- a/lib/ansible/modules/network/dellos9/dellos9_facts.py +++ b/lib/ansible/modules/network/dellos9/dellos9_facts.py @@ -142,37 +142,48 @@ ansible_net_neighbors: import re import itertools -from ansible.module_utils.netcli import CommandRunner -from ansible.module_utils.network import NetworkModule -import ansible.module_utils.dellos9 +from ansible.module_utils.dellos9 import run_commands +from ansible.module_utils.dellos9 import dellos9_argument_spec, check_args +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import iteritems +from ansible.module_utils.six.moves import zip class FactsBase(object): - def __init__(self, runner): - self.runner = runner + COMMANDS = list() + + def __init__(self, module): + self.module = module self.facts = dict() + self.responses = None + + def populate(self): + self.responses = run_commands(self.module, self.COMMANDS, check_rc=False) - self.commands() + def run(self, cmd): + return run_commands(self.module, cmd, check_rc=False) class Default(FactsBase): - def commands(self): - self.runner.add_command('show version') - self.runner.add_command('show inventory') - self.runner.add_command('show running-config | grep hostname') + COMMANDS = [ + 'show version', + 'show inventory', + 'show running-config | grep hostname' + ] def populate(self): - data = self.runner.get_command('show version') + super(Default, self).populate() + data = self.responses[0] self.facts['version'] = self.parse_version(data) self.facts['model'] = self.parse_model(data) self.facts['image'] = self.parse_image(data) - data = self.runner.get_command('show inventory') + data = self.responses[1] self.facts['serialnum'] = self.parse_serialnum(data) - data = self.runner.get_command('show running-config | grep hostname') + data = self.responses[2] self.facts['hostname'] = self.parse_hostname(data) def parse_version(self, data): @@ -206,15 +217,17 @@ class Default(FactsBase): class Hardware(FactsBase): - def commands(self): - self.runner.add_command('show file-systems') - self.runner.add_command('show memory | except Processor') + COMMANDS = [ + 'show file-systems', + 'show memory | except Processor' + ] def populate(self): - data = self.runner.get_command('show file-systems') + super(Hardware, self).populate() + data = self.responses[0] self.facts['filesystems'] = self.parse_filesystems(data) - data = self.runner.get_command('show memory | except Processor') + data = self.responses[1] match = re.findall('\s(\d+)\s', data) if match: self.facts['memtotal_mb'] = int(match[0]) / 1024 @@ -226,25 +239,28 @@ class Hardware(FactsBase): class Config(FactsBase): - def commands(self): - self.runner.add_command('show running-config') + COMMANDS = ['show running-config'] def populate(self): - self.facts['config'] = self.runner.get_command('show running-config') + super(Config, self).populate() + self.facts['config'] = self.responses[0] class Interfaces(FactsBase): - def commands(self): - self.runner.add_command('show interfaces') - self.runner.add_command('show ipv6 interface') - self.runner.add_command('show lldp neighbors detail') + COMMANDS = [ + 'show interfaces', + 'show ipv6 interface', + 'show lldp neighbors detail', + 'show inventory' + ] def populate(self): + super(Interfaces, self).populate() self.facts['all_ipv4_addresses'] = list() self.facts['all_ipv6_addresses'] = list() - data = self.runner.get_command('show interfaces') + data = self.responses[0] interfaces = self.parse_interfaces(data) for key in interfaces.keys(): @@ -261,14 +277,14 @@ class Interfaces(FactsBase): self.facts['interfaces'] = self.populate_interfaces(interfaces) - data = self.runner.get_command('show ipv6 interface') + data = self.responses[1] if len(data) > 0: data = self.parse_ipv6_interfaces(data) self.populate_ipv6_interfaces(data) - data = self.runner.get_command('show inventory') + data = self.responses[3] if 'LLDP' in self.get_protocol_list(data): - neighbors = self.runner.get_command('show lldp neighbors detail') + neighbors = self.responses[2] self.facts['neighbors'] = self.parse_neighbors(neighbors) def get_protocol_list(self, data): @@ -499,11 +515,16 @@ VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) def main(): - spec = dict( + """main entry point for module execution + """ + argument_spec = dict( gather_subset=dict(default=['!config'], type='list') ) - module = NetworkModule(argument_spec=spec, supports_check_mode=True) + argument_spec.update(dellos9_argument_spec) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) gather_subset = module.params['gather_subset'] @@ -541,28 +562,23 @@ def main(): facts = dict() facts['gather_subset'] = list(runable_subsets) - runner = CommandRunner(module) - instances = list() for key in runable_subsets: - runs = FACT_SUBSETS[key](runner) - instances.append(runs) - - runner.run() + instances.append(FACT_SUBSETS[key](module)) - try: - for inst in instances: - inst.populate() - facts.update(inst.facts) - except Exception: - module.exit_json(out=module.from_json(runner.items)) + for inst in instances: + inst.populate() + facts.update(inst.facts) ansible_facts = dict() - for key, value in facts.items(): + for key, value in iteritems(facts): key = 'ansible_net_%s' % key ansible_facts[key] = value - module.exit_json(ansible_facts=ansible_facts) + warnings = list() + check_args(module, warnings) + + module.exit_json(ansible_facts=ansible_facts, warnings=warnings) if __name__ == '__main__': diff --git a/lib/ansible/plugins/action/dellos10.py b/lib/ansible/plugins/action/dellos10.py new file mode 100644 index 00000000000..09a1bdbbbac --- /dev/null +++ b/lib/ansible/plugins/action/dellos10.py @@ -0,0 +1,125 @@ +# +# (c) 2016 Red Hat Inc. +# +# Copyright (c) 2017 Dell 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.six import iteritems +from ansible.module_utils.dellos10 import dellos10_argument_spec +from ansible.module_utils.basic import AnsibleFallbackNotFound +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 = 'dellos10' + 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 + pc.become = provider['authorize'] or False + pc.become_pass = provider['auth_pass'] + + 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} + else: + # make sure we are in the right cli context which should be + # enable mode and not config module + rc, out, err = connection.exec_command('prompt()') + while str(out).strip().endswith(')#'): + display.debug('wrong context, sending exit to device') + connection.exec_command('exit') + rc, out, err = connection.exec_command('prompt()') + + task_vars['ansible_socket'] = socket_path + + if self._play_context.become_method == 'enable': + self._play_context.become = False + self._play_context.become_method = None + + 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(dellos10_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/dellos10_config.py b/lib/ansible/plugins/action/dellos10_config.py index ffcb0f057f8..77769b16c79 100644 --- a/lib/ansible/plugins/action/dellos10_config.py +++ b/lib/ansible/plugins/action/dellos10_config.py @@ -1,6 +1,8 @@ # # Copyright 2015 Peter Sprygada # +# Copyright (c) 2017 Dell Inc. +# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify @@ -19,10 +21,94 @@ 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 + +from ansible.plugins.action.dellos10 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) -class ActionModule(NetActionModule, ActionBase): - pass + 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/action/dellos6_config.py b/lib/ansible/plugins/action/dellos6_config.py index ffcb0f057f8..ec2d1833dbe 100644 --- a/lib/ansible/plugins/action/dellos6_config.py +++ b/lib/ansible/plugins/action/dellos6_config.py @@ -1,6 +1,8 @@ # # Copyright 2015 Peter Sprygada # +# Copyright (c) 2017 Dell Inc. +# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify diff --git a/lib/ansible/plugins/action/dellos9.py b/lib/ansible/plugins/action/dellos9.py new file mode 100644 index 00000000000..3567403d0d3 --- /dev/null +++ b/lib/ansible/plugins/action/dellos9.py @@ -0,0 +1,125 @@ +# +# (c) 2016 Red Hat Inc. +# +# Copyright (c) 2017 Dell 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.six import iteritems +from ansible.module_utils.dellos9 import dellos9_argument_spec +from ansible.module_utils.basic import AnsibleFallbackNotFound +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 = 'dellos9' + 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 + pc.become = provider['authorize'] or False + pc.become_pass = provider['auth_pass'] + + 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} + else: + # make sure we are in the right cli context which should be + # enable mode and not config module + rc, out, err = connection.exec_command('prompt()') + while str(out).strip().endswith(')#'): + display.debug('wrong context, sending exit to device') + connection.exec_command('exit') + rc, out, err = connection.exec_command('prompt()') + + task_vars['ansible_socket'] = socket_path + + if self._play_context.become_method == 'enable': + self._play_context.become = False + self._play_context.become_method = None + + 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(dellos9_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/dellos9_config.py b/lib/ansible/plugins/action/dellos9_config.py index ffcb0f057f8..aded696987a 100644 --- a/lib/ansible/plugins/action/dellos9_config.py +++ b/lib/ansible/plugins/action/dellos9_config.py @@ -1,6 +1,8 @@ # # Copyright 2015 Peter Sprygada # +# Copyright (c) 2017 Dell Inc. +# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify @@ -19,10 +21,94 @@ 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 + +from ansible.plugins.action.dellos9 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) -class ActionModule(NetActionModule, ActionBase): - pass + 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/dellos10.py b/lib/ansible/plugins/terminal/dellos10.py new file mode 100644 index 00000000000..16ac2ad529d --- /dev/null +++ b/lib/ansible/plugins/terminal/dellos10.py @@ -0,0 +1,78 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Copyright (c) 2017 Dell Inc. +# +# 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 re +import json + +from ansible.plugins.terminal import TerminalBase +from ansible.errors import AnsibleConnectionFailure + + +class TerminalModule(TerminalBase): + + terminal_stdout_re = [ + re.compile(r"[\r\n]?[\w+\-\.:\/\[\]]+(?:\([^\)]+\)){,3}(?:>|#) ?$"), + re.compile(r"\[\w+\@[\w\-\.]+(?: [^\]])\] ?[>#\$] ?$") + ] + + terminal_stderr_re = [ + re.compile(r"% ?Error: (?:(?!\bdoes not exist\b)(?!\balready exists\b)(?!\bHost not found\b)(?!\bnot active\b).)*$"), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + ] + + def on_open_shell(self): + try: + self._exec_cli_command('terminal length 0') + except AnsibleConnectionFailure: + raise AnsibleConnectionFailure('unable to set terminal parameters') + + def on_authorize(self, passwd=None): + if self._get_prompt().endswith('#'): + return + + cmd = {'command': 'enable'} + if passwd: + cmd['prompt'] = r"[\r\n]?password: $" + cmd['answer'] = passwd + + try: + self._exec_cli_command(json.dumps(cmd)) + except AnsibleConnectionFailure: + raise AnsibleConnectionFailure('unable to elevate privilege to enable mode') + + def on_deauthorize(self): + prompt = self._get_prompt() + if prompt is None: + # if prompt is None most likely the terminal is hung up at a prompt + return + + if prompt.strip().endswith(')#'): + self._exec_cli_command('end') + self._exec_cli_command('disable') + + elif prompt.endswith('#'): + self._exec_cli_command('disable') diff --git a/lib/ansible/plugins/terminal/dellos9.py b/lib/ansible/plugins/terminal/dellos9.py new file mode 100644 index 00000000000..d021ccfbd5e --- /dev/null +++ b/lib/ansible/plugins/terminal/dellos9.py @@ -0,0 +1,78 @@ +# +# (c) 2016 Red Hat Inc. +# +# Copyright (c) 2017 Dell 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 re +import json + +from ansible.plugins.terminal import TerminalBase +from ansible.errors import AnsibleConnectionFailure + + +class TerminalModule(TerminalBase): + + terminal_stdout_re = [ + re.compile(r"[\r\n]?[\w+\-\.:\/\[\]]+(?:\([^\)]+\)){,3}(?:>|#) ?$"), + re.compile(r"\[\w+\@[\w\-\.]+(?: [^\]])\] ?[>#\$] ?$") + ] + + terminal_stderr_re = [ + re.compile(r"% ?Error: (?:(?!\bdoes not exist\b)(?!\balready exists\b)(?!\bHost not found\b)(?!\bnot active\b).)*$"), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + ] + + def on_open_shell(self): + try: + self._exec_cli_command('terminal length 0') + except AnsibleConnectionFailure: + raise AnsibleConnectionFailure('unable to set terminal parameters') + + def on_authorize(self, passwd=None): + if self._get_prompt().endswith('#'): + return + + cmd = {'command': 'enable'} + if passwd: + cmd['prompt'] = r"[\r\n]?password: $" + cmd['answer'] = passwd + + try: + self._exec_cli_command(json.dumps(cmd)) + except AnsibleConnectionFailure: + raise AnsibleConnectionFailure('unable to elevate privilege to enable mode') + + def on_deauthorize(self): + prompt = self._get_prompt() + if prompt is None: + # if prompt is None most likely the terminal is hung up at a prompt + return + + if prompt.strip().endswith(')#'): + self._exec_cli_command('end') + self._exec_cli_command('disable') + + elif prompt.endswith('#'): + self._exec_cli_command('disable') diff --git a/test/sanity/pep8/legacy-files.txt b/test/sanity/pep8/legacy-files.txt index f17dd92a293..1b1f8696a64 100644 --- a/test/sanity/pep8/legacy-files.txt +++ b/test/sanity/pep8/legacy-files.txt @@ -512,12 +512,8 @@ lib/ansible/modules/network/cumulus/_cl_img_install.py lib/ansible/modules/network/cumulus/_cl_license.py lib/ansible/modules/network/cumulus/_cl_ports.py lib/ansible/modules/network/cumulus/nclu.py -lib/ansible/modules/network/dellos10/dellos10_command.py -lib/ansible/modules/network/dellos10/dellos10_config.py -lib/ansible/modules/network/dellos10/dellos10_facts.py lib/ansible/modules/network/dellos6/dellos6_command.py lib/ansible/modules/network/dellos6/dellos6_facts.py -lib/ansible/modules/network/dellos9/dellos9_command.py lib/ansible/modules/network/dnsimple.py lib/ansible/modules/network/dnsmadeeasy.py lib/ansible/modules/network/eos/_eos_template.py @@ -882,9 +878,7 @@ lib/ansible/plugins/action/asa_config.py lib/ansible/plugins/action/asa_template.py lib/ansible/plugins/action/assemble.py lib/ansible/plugins/action/copy.py -lib/ansible/plugins/action/dellos10_config.py lib/ansible/plugins/action/dellos6_config.py -lib/ansible/plugins/action/dellos9_config.py lib/ansible/plugins/action/eos_template.py lib/ansible/plugins/action/fetch.py lib/ansible/plugins/action/group_by.py