From 775118aae2cf35ad103007ede59cf20cf79f84ae Mon Sep 17 00:00:00 2001 From: abirami-n Date: Mon, 22 Jan 2018 18:04:21 +0530 Subject: [PATCH] 2.5 Plugin Environment support and Testcases for dellos6 modules (#34890) * Dellos6_2.5_support * cleanup * Fix pep8 * fix waitfor * fix_facts * fix_assert --- .../module_utils/network/dellos6/dellos6.py | 7 +- .../network/dellos6/dellos6_command.py | 76 +++++---- .../modules/network/dellos6/dellos6_config.py | 147 +++++++++++------- .../modules/network/dellos6/dellos6_facts.py | 23 +-- lib/ansible/plugins/action/dellos6.py | 69 ++++---- lib/ansible/plugins/cliconf/dellos6.py | 82 ++++++++++ lib/ansible/plugins/terminal/dellos6.py | 12 +- .../units/modules/network/dellos6/__init__.py | 0 .../modules/network/dellos6/dellos6_module.py | 88 +++++++++++ .../fixtures/dellos6_config_config.cfg | 16 ++ .../dellos6/fixtures/dellos6_config_src.cfg | 7 + .../network/dellos6/fixtures/show_interfaces | 41 +++++ .../dellos6/fixtures/show_interfaces_status | 48 ++++++ .../show_interfaces_transceiver_properties | 6 + .../network/dellos6/fixtures/show_ip_int | 15 ++ .../network/dellos6/fixtures/show_lldp | 11 ++ .../fixtures/show_lldp_remote-device_all | 9 ++ .../network/dellos6/fixtures/show_memory_cpu | 3 + .../dellos6/fixtures/show_running-config | 124 +++++++++++++++ .../show_running-config__include_hostname | 3 + .../network/dellos6/fixtures/show_version | 17 ++ .../network/dellos6/test_dellos6_command.py | 108 +++++++++++++ .../network/dellos6/test_dellos6_config.py | 147 ++++++++++++++++++ .../network/dellos6/test_dellos6_facts.py | 105 +++++++++++++ 24 files changed, 1017 insertions(+), 147 deletions(-) create mode 100644 lib/ansible/plugins/cliconf/dellos6.py create mode 100644 test/units/modules/network/dellos6/__init__.py create mode 100644 test/units/modules/network/dellos6/dellos6_module.py create mode 100644 test/units/modules/network/dellos6/fixtures/dellos6_config_config.cfg create mode 100644 test/units/modules/network/dellos6/fixtures/dellos6_config_src.cfg create mode 100644 test/units/modules/network/dellos6/fixtures/show_interfaces create mode 100644 test/units/modules/network/dellos6/fixtures/show_interfaces_status create mode 100644 test/units/modules/network/dellos6/fixtures/show_interfaces_transceiver_properties create mode 100644 test/units/modules/network/dellos6/fixtures/show_ip_int create mode 100644 test/units/modules/network/dellos6/fixtures/show_lldp create mode 100644 test/units/modules/network/dellos6/fixtures/show_lldp_remote-device_all create mode 100644 test/units/modules/network/dellos6/fixtures/show_memory_cpu create mode 100644 test/units/modules/network/dellos6/fixtures/show_running-config create mode 100644 test/units/modules/network/dellos6/fixtures/show_running-config__include_hostname create mode 100644 test/units/modules/network/dellos6/fixtures/show_version create mode 100644 test/units/modules/network/dellos6/test_dellos6_command.py create mode 100644 test/units/modules/network/dellos6/test_dellos6_config.py create mode 100644 test/units/modules/network/dellos6/test_dellos6_facts.py diff --git a/lib/ansible/module_utils/network/dellos6/dellos6.py b/lib/ansible/module_utils/network/dellos6/dellos6.py index e6685677366..0077f399b6b 100644 --- a/lib/ansible/module_utils/network/dellos6/dellos6.py +++ b/lib/ansible/module_utils/network/dellos6/dellos6.py @@ -34,7 +34,7 @@ from ansible.module_utils._text import to_text from ansible.module_utils.basic import env_fallback, return_values from ansible.module_utils.network.common.utils import to_list, ComplexList from ansible.module_utils.connection import exec_command -from ansible.module_utils.network.common.config import NetworkConfig, ConfigLine, ignore_line, DEFAULT_COMMENT_TOKENS +from ansible.module_utils.network.common.config import NetworkConfig, ConfigLine, ignore_line _DEVICE_CONFIGS = {} @@ -122,8 +122,7 @@ def load_config(module, commands): 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)) + rc, out, err = exec_command(module, command) if rc != 0: module.fail_json(msg=to_text(err, errors='surrogate_or_strict'), command=command, rc=rc) exec_command(module, 'end') @@ -233,7 +232,7 @@ def os6_parse(lines, indent=None, comment_tokens=None): class Dellos6NetworkConfig(NetworkConfig): def load(self, contents): - self._items = os6_parse(contents, self._indent, DEFAULT_COMMENT_TOKENS) + self._items = os6_parse(contents, self._indent) def _diff_line(self, other, path=None): diff = list() diff --git a/lib/ansible/modules/network/dellos6/dellos6_command.py b/lib/ansible/modules/network/dellos6/dellos6_command.py index 2159a33b100..1c0d5a7c7e0 100644 --- a/lib/ansible/modules/network/dellos6/dellos6_command.py +++ b/lib/ansible/modules/network/dellos6/dellos6_command.py @@ -12,7 +12,6 @@ ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} - DOCUMENTATION = """ --- module: dellos6_command @@ -34,7 +33,7 @@ options: configured provider. The resulting output from the command is returned. If the I(wait_for) argument is provided, the module is not returned until the condition is satisfied or - the number of I(retries) as expired. + the number of retries has expired. required: true wait_for: description: @@ -45,6 +44,19 @@ options: See examples. required: false default: null + version_added: "2.2" + match: + description: + - The I(match) argument is used in conjunction with the + I(wait_for) argument to specify the match policy. Valid + values are C(all) or C(any). If the value is set to C(all) + then all conditionals in the wait_for must be satisfied. If + the value is set to C(any) then only one of the values must be + satisfied. + required: false + default: all + choices: ['any', 'all'] + version_added: "2.5" retries: description: - Specifies the number of retries a command should be tried @@ -64,43 +76,30 @@ options: """ EXAMPLES = """ -# Note: examples below use the following provider dict to handle -# transport and authentication to the node. -vars: - cli: - host: "{{ inventory_hostname }}" - username: admin - password: admin - transport: cli - tasks: - - name: run show version on remote devices - dellos6_command: - commands: show version - provider: "{{ cli }}" + - name: run show version on remote devices + dellos6_command: + commands: show version - - name: run show version and check to see if output contains Dell - dellos6_command: - commands: show version - wait_for: result[0] contains Dell - provider: "{{ cli }}" + - name: run show version and check to see if output contains Dell + dellos6_command: + commands: show version + wait_for: result[0] contains Dell - - name: run multiple commands on remote nodes - dellos6_command: - commands: - - show version - - show interfaces - provider: "{{ cli }}" + - name: run multiple commands on remote nodes + dellos6_command: + commands: + - show version + - show interfaces - - name: run multiple commands and evaluate the output - dellos6_command: - commands: - - show version - - show interfaces - wait_for: - - result[0] contains Dell - - result[1] contains Access - provider: "{{ cli }}" + - name: run multiple commands and evaluate the output + dellos6_command: + commands: + - show version + - show interfaces + wait_for: + - result[0] contains Dell + - result[1] contains Access """ RETURN = """ @@ -109,19 +108,16 @@ stdout: returned: always apart from low level errors (such as action plugin) type: list sample: ['...', '...'] - stdout_lines: description: The value of stdout split into a list returned: always apart from low level errors (such as action plugin) type: list sample: [['...', '...'], ['...'], ['...']] - failed_conditions: description: The list of conditionals that have failed returned: failed type: list sample: ['...', '...'] - warnings: description: The list of warnings (if any) generated by module based on arguments returned: always @@ -220,11 +216,11 @@ def main(): msg = 'One or more conditional statements have not be satisfied' module.fail_json(msg=msg, failed_conditions=failed_conditions) - result = { + result.update({ 'changed': False, 'stdout': responses, 'stdout_lines': list(to_lines(responses)) - } + }) module.exit_json(**result) diff --git a/lib/ansible/modules/network/dellos6/dellos6_config.py b/lib/ansible/modules/network/dellos6/dellos6_config.py index a6f35050bb6..8e67c955e04 100644 --- a/lib/ansible/modules/network/dellos6/dellos6_config.py +++ b/lib/ansible/modules/network/dellos6/dellos6_config.py @@ -12,7 +12,6 @@ ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} - DOCUMENTATION = """ --- module: dellos6_config @@ -30,15 +29,17 @@ options: description: - The ordered set of commands that should be configured in the section. The commands must be the exact same commands as found - in the device running-config. Note the configuration - command syntax as the device config parser automatically modifies some commands. This argument is mutually exclusive with I(src). + in the device running-config. Be sure to note the configuration + command syntax as some commands are automatically modified by the + device config parser. This argument is mutually exclusive with I(src). required: false default: null aliases: ['commands'] parents: description: - The ordered set of parents that uniquely identify the section - the commands should be checked against. If you omit the parents argument, the commands are checked against the set of top + the commands should be checked against. If the parents argument + is omitted, the commands are checked against the set of top level or global commands. required: false default: null @@ -47,8 +48,8 @@ options: - Specifies the source path to the file that contains the configuration or configuration template to load. The path to the source file can either be the full path on the Ansible control host or a relative - path from the playbook or role root directory. This argument is mutually - exclusive with I(lines), I(parents). + path from the playbook or role root directory. This argument is + mutually exclusive with I(lines). required: false default: null before: @@ -63,19 +64,20 @@ options: after: description: - The ordered set of commands to append to the end of the command - stack if a change needs to be made. As with I(before), the playbook designer can use this to append a set of commands to be + stack if a change needs to be made. Just like with I(before) this + allows the playbook designer to append a set of commands to be executed after the command set. required: false default: null match: description: - Instructs the module on the way to perform the matching of - the set of commands against the current device config. If you set - match to I(line), commands match line by line. If you set - match to I(strict), command lines matched by respect - to position. If you set match to I(exact), command lines - must be an equal match. Finally, if you set match to I(none), the - module does not attempt to compare the source configuration with + the set of commands against the current device config. If + match is set to I(line), commands are matched line by line. If + match is set to I(strict), command lines are matched with respect + to position. If match is set to I(exact), command lines + must be an equal match. Finally, if match is set to I(none), the + module will not attempt to compare the source configuration with the running configuration on the remote device. required: false default: line @@ -83,9 +85,9 @@ options: replace: description: - Instructs the module on the way to perform the configuration - on the device. If you set the replace argument to I(line), then + on the device. If the replace argument is set to I(line) then the modified lines are pushed to the device in configuration - mode. If you set the replace argument to I(block) then the entire + mode. If the replace argument is set to I(block) then the entire command block is pushed to the device in configuration mode if any line is not correct. required: false @@ -107,34 +109,36 @@ options: description: - The C(save) argument instructs the module to save the running- config to the startup-config at the conclusion of the module - running. If you specify check mode, this argument is ignored. + running. If check mode is specified, this argument is ignored. required: false default: no choices: ['yes', 'no'] config: description: - - The playbook designer can use the C(config) argument to supply - the base configuration to be used to validate necessary configuration - changes. If you specify this argument, the module - does not download the running-config from the remote node. + - The module, by default, will connect to the remote device and + retrieve the current running-config to use as a base for comparing + against the contents of source. There are times when it is not + desirable to have the task get the current running-config for + every task in a playbook. The I(config) argument allows the + implementer to pass in the configuration to use as the base + config for comparison. required: false default: null backup: description: - - This argument causes the module to create a full backup of + - This argument will cause the module to create a full backup of the current C(running-config) from the remote device before any changes are made. The backup file is written to the C(backup) folder in the playbook root directory. If the directory does not exist, it is created. required: false default: no - choices: ['yes', 'no'] + type: bool """ EXAMPLES = """ - dellos6_config: lines: ['hostname {{ inventory_hostname }}'] - provider: "{{ cli }}" - dellos6_config: lines: @@ -146,7 +150,6 @@ EXAMPLES = """ parents: ['ip access-list test'] before: ['no ip access-list test'] match: exact - provider: "{{ cli }}" - dellos6_config: lines: @@ -157,22 +160,21 @@ EXAMPLES = """ parents: ['ip access-list test'] before: ['no ip access-list test'] replace: block - provider: "{{ cli }}" """ RETURN = """ updates: - description: The set of commands pushed to the remote device. - returned: Always. + description: The set of commands that will be pushed to the remote device. + returned: always type: list - sample: ['...', '...'] + sample: ['interface Te1/0/1', 'no shutdown', 'exit'] -responses: - description: The set of responses from issuing the commands on the device. - returned: When not check_mode. +commands: + description: The set of commands that will be pushed to the remote device + returned: always type: list - sample: ['...', '...'] + sample: ['interface Te1/0/1', 'no shutdown', 'exit'] saved: description: Returns whether the configuration is saved to the startup @@ -185,7 +187,7 @@ backup_path: description: The full path to the backup file returned: when backup is yes type: string - sample: /playbooks/ansible/backup/dellos6_config.2016-07-16@22:28:34 + sample: /playbooks/ansible/backup/dellos6_config.2017-07-16@22:28:34 """ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.network.dellos6.dellos6 import get_config, get_sublevel_config, Dellos6NetworkConfig @@ -201,10 +203,23 @@ def get_candidate(module): candidate.load(module.params['src']) elif module.params['lines']: parents = module.params['parents'] or list() - candidate.add(module.params['lines'], parents=parents) + commands = module.params['lines'][0] + if (isinstance(commands, dict)) and (isinstance(commands['command'], list)): + candidate.add(commands['command'], parents=parents) + elif (isinstance(commands, dict)) and (isinstance(commands['command'], str)): + candidate.add([commands['command']], parents=parents) + else: + candidate.add(module.params['lines'], parents=parents) return candidate +def get_running_config(module): + contents = module.params['config'] + if not contents: + contents = get_config(module) + return contents + + def main(): argument_spec = dict( @@ -227,7 +242,6 @@ def main(): ) argument_spec.update(dellos6_argument_spec) - mutually_exclusive = [('lines', 'src'), ('parents', 'src')] @@ -245,42 +259,59 @@ def main(): result = dict(changed=False, saved=False, warnings=warnings) candidate = get_candidate(module) - if match != 'none': - config = get_config(module) - config = Dellos6NetworkConfig(contents=config, indent=0) - if parents: - config = get_sublevel_config(config, module) - configobjs = candidate.difference(config, match=match, replace=replace) - - else: - configobjs = candidate.items if module.params['backup']: if not module.check_mode: result['__backup__'] = get_config(module) commands = list() - if configobjs: - commands = dumps(configobjs, 'commands') - commands = commands.split('\n') + if any((module.params['lines'], module.params['src'])): + if match != 'none': + config = get_running_config(module) + config = Dellos6NetworkConfig(contents=config, indent=0) + if parents: + config = get_sublevel_config(config, module) + configobjs = candidate.difference(config, match=match, replace=replace) + else: + configobjs = candidate.items - if module.params['before']: - commands[:0] = module.params['before'] + if configobjs: + commands = dumps(configobjs, 'commands') + if ((isinstance(module.params['lines'], list)) and + (isinstance(module.params['lines'][0], dict)) and + ['prompt', 'answer'].issubset(module.params['lines'][0])): + cmd = {'command': commands, + 'prompt': module.params['lines'][0]['prompt'], + 'answer': module.params['lines'][0]['answer']} + commands = [module.jsonify(cmd)] + else: + commands = commands.split('\n') - if module.params['after']: - commands.extend(module.params['after']) + if module.params['before']: + commands[:0] = module.params['before'] - if not module.check_mode and module.params['update'] == 'merge': - load_config(module, commands) + if module.params['after']: + commands.extend(module.params['after']) - if module.params['save']: - cmd = {'command': 'copy runing-config startup-config', 'prompt': WARNING_PROMPTS_RE, 'answer': 'yes'} + if not module.check_mode and module.params['update'] == 'merge': + load_config(module, commands) + + result['changed'] = True + result['commands'] = commands + result['updates'] = commands + + if module.params['save']: + result['changed'] = True + if not module.check_mode: + cmd = {'command': 'copy running-config startup-config', + 'prompt': r'\(y/n\)$', 'answer': 'yes'} run_commands(module, [cmd]) result['saved'] = True + else: + module.warn('Skipping command `copy running-config startup-config`' + 'due to check_mode. Configuration not copied to ' + 'non-volatile storage') - result['changed'] = True - - result['updates'] = commands module.exit_json(**result) diff --git a/lib/ansible/modules/network/dellos6/dellos6_facts.py b/lib/ansible/modules/network/dellos6/dellos6_facts.py index 438fc46e5f3..a36ea714f01 100644 --- a/lib/ansible/modules/network/dellos6/dellos6_facts.py +++ b/lib/ansible/modules/network/dellos6/dellos6_facts.py @@ -23,16 +23,17 @@ description: - Collects a base set of device facts from a remote device that is running OS6. This module prepends all of the base network fact keys with C(ansible_net_). The facts - module always collects a base set of facts from the device + module will always collect a base set of facts from the device and can enable or disable collection of additional facts. extends_documentation_fragment: dellos6 options: gather_subset: description: - - When specified, this argument restricts the facts collected + - When supplied, this argument will restrict the facts collected to a given subset. Possible values for this argument include - all, hardware, config, and interfaces. You can specify a list of - values to include a larger subset. You can also use values with an initial M(!) to specify that a specific subset should + all, hardware, config, and interfaces. Can specify a list of + values to include a larger subset. Values can also be used + with an initial C(M(!)) to specify that a specific subset should not be collected. required: false default: '!config' @@ -57,29 +58,29 @@ EXAMPLES = """ RETURN = """ ansible_net_gather_subset: description: The list of fact subsets collected from the device. - returned: Always. + returned: always. type: list # default ansible_net_model: description: The model name returned from the device. - returned: Always. + returned: always. type: str ansible_net_serialnum: description: The serial number of the remote device. - returned: Always. + returned: always. type: str ansible_net_version: description: The operating system version running on the remote device. - returned: Always. + returned: always. type: str ansible_net_hostname: description: The configured hostname of the device. - returned: Always. + returned: always. type: string ansible_net_image: description: The image file that the device is running. - returned: Always + returned: always type: string # hardware @@ -147,7 +148,7 @@ class Default(FactsBase): self.facts['serialnum'] = self.parse_serialnum(data) self.facts['model'] = self.parse_model(data) self.facts['image'] = self.parse_image(data) - hdata = self.responses[0] + hdata = self.responses[1] self.facts['hostname'] = self.parse_hostname(hdata) def parse_version(self, data): diff --git a/lib/ansible/plugins/action/dellos6.py b/lib/ansible/plugins/action/dellos6.py index 81959e44fb5..387cdfa00d4 100644 --- a/lib/ansible/plugins/action/dellos6.py +++ b/lib/ansible/plugins/action/dellos6.py @@ -1,4 +1,7 @@ -# 2016 Red Hat Inc. +# +# (c) 2016 Red Hat Inc. +# +# (c) 2017 Dell EMC. # # This file is part of Ansible # @@ -38,39 +41,45 @@ except ImportError: 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 - ) + socket_path = None - provider = load_provider(dellos6_provider_spec, self._task.args) + if self._play_context.connection == 'network_cli': + provider = self._task.args.get('provider', {}) + if any(provider.values()): + display.warning('provider is unnecessary when using network_cli and will be ignored') + elif self._play_context.connection == 'local': + provider = load_provider(dellos6_provider_spec, self._task.args) + pc = copy.deepcopy(self._play_context) + pc.connection = 'network_cli' + pc.network_os = 'dellos6' + pc.remote_addr = provider['host'] or self._play_context.remote_addr + pc.port = int(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 = int(provider['timeout'] or C.PERSISTENT_COMMAND_TIMEOUT) + pc.become = provider['authorize'] or False + if pc.become: + pc.become_method = 'enable' + pc.become_pass = provider['auth_pass'] - pc = copy.deepcopy(self._play_context) - pc.connection = 'network_cli' - pc.network_os = 'dellos6' - pc.remote_addr = provider['host'] or self._play_context.remote_addr - pc.port = int(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 = int(provider['timeout'] or C.PERSISTENT_COMMAND_TIMEOUT) - pc.become = provider['authorize'] or False - pc.become_pass = provider['auth_pass'] + display.vvv('using connection plugin %s' % pc.connection, pc.remote_addr) + connection = self._shared_loader_obj.connection_loader.get('persistent', pc, sys.stdin) - display.vvv('using connection plugin %s (was local)' % pc.connection, pc.remote_addr) - connection = self._shared_loader_obj.connection_loader.get('persistent', pc, sys.stdin) + socket_path = connection.run() + display.vvvv('socket_path: %s' % socket_path, pc.remote_addr) + if not socket_path: + return {'failed': True, + 'msg': 'unable to open shell. Please see: ' + + 'https://docs.ansible.com/ansible/network_debug_troubleshooting.html#unable-to-open-shell'} - socket_path = connection.run() - display.vvvv('socket_path: %s' % socket_path, pc.remote_addr) - if not socket_path: - return {'failed': True, - 'msg': 'unable to open shell. Please see: ' + - 'https://docs.ansible.com/ansible/network_debug_troubleshooting.html#unable-to-open-shell'} + task_vars['ansible_socket'] = socket_path # make sure we are in the right cli context which should be # enable mode and not config module + if socket_path is None: + socket_path = self._connection.socket_path + conn = Connection(socket_path) out = conn.get_prompt() while to_text(out, errors='surrogate_then_replace').strip().endswith(')#'): @@ -78,11 +87,5 @@ class ActionModule(_ActionModule): conn.send_command('exit') out = conn.get_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 - result = super(ActionModule, self).run(tmp, task_vars) return result diff --git a/lib/ansible/plugins/cliconf/dellos6.py b/lib/ansible/plugins/cliconf/dellos6.py new file mode 100644 index 00000000000..df9d64fea19 --- /dev/null +++ b/lib/ansible/plugins/cliconf/dellos6.py @@ -0,0 +1,82 @@ +# +# (c) 2017 Red Hat Inc. +# +# (c) 2017 Dell EMC. +# +# 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 itertools import chain + +from ansible.module_utils._text import to_bytes, to_text +from ansible.module_utils.network.common.utils import to_list +from ansible.plugins.cliconf import CliconfBase, enable_mode + + +class Cliconf(CliconfBase): + + def get_device_info(self): + device_info = {} + + device_info['network_os'] = 'dellos6' + reply = self.get(b'show version') + data = to_text(reply, errors='surrogate_or_strict').strip() + + match = re.search(r'Software Version (\S+)', data) + if match: + device_info['network_os_version'] = match.group(1) + + match = re.search(r'System Type (\S+)', data, re.M) + if match: + device_info['network_os_model'] = match.group(1) + + reply = self.get(b'show running-config | grep hostname') + data = to_text(reply, errors='surrogate_or_strict').strip() + match = re.search(r'^hostname (.+)', data, re.M) + if match: + device_info['network_os_hostname'] = match.group(1) + + return device_info + + @enable_mode + def get_config(self, source='running'): + if source not in ('running', 'startup'): + return self.invalid_params("fetching configuration from %s is not supported" % source) +# if source == 'running': +# cmd = b'show running-config all' + else: + cmd = b'show startup-config' + return self.send_command(cmd) + + @enable_mode + def edit_config(self, command): + for cmd in chain([b'configure terminal'], to_list(command), [b'end']): + self.send_command(cmd) + + def get(self, command, prompt=None, answer=None, sendonly=False): + return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly) + + def get_capabilities(self): + result = {} + result['rpc'] = self.get_base_rpc() + result['network_api'] = 'cliconf' + result['device_info'] = self.get_device_info() + return json.dumps(result) diff --git a/lib/ansible/plugins/terminal/dellos6.py b/lib/ansible/plugins/terminal/dellos6.py index 31d5d7a3dd0..6da6f15a15a 100644 --- a/lib/ansible/plugins/terminal/dellos6.py +++ b/lib/ansible/plugins/terminal/dellos6.py @@ -1,4 +1,7 @@ -# 2016 Red Hat Inc. +# +# (c) 2016 Red Hat Inc. +# +# (c) 2017 Dell EMC. # # This file is part of Ansible # @@ -37,6 +40,13 @@ class TerminalModule(TerminalBase): re.compile(br"% ?Error: (?:(?!\bdoes not exist\b)(?!\balready exists\b)(?!\bHost not found\b)(?!\bnot active\b).)*$"), re.compile(br"% ?Bad secret"), re.compile(br"invalid input", re.I), + re.compile(br"Cannot add a dynamic member to a LAG with static members", re.I), + re.compile(br"VLAN ID not found", re.I), + re.compile(br"The maximum number of users have already been created.", re.I), + re.compile(br"Invalid access level. Access level can be either 0, 1 or 15", re.I), + re.compile(br"An invalid interface has been used for this function.", re.I), + re.compile(br"Error:Community does not exist.", re.I), + re.compile(br"Value is out of range.", re.I), re.compile(br"(?:incomplete|ambiguous) command", re.I), re.compile(br"connection timed out", re.I), re.compile(br"'[^']' +returned error code: ?\d+"), diff --git a/test/units/modules/network/dellos6/__init__.py b/test/units/modules/network/dellos6/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/units/modules/network/dellos6/dellos6_module.py b/test/units/modules/network/dellos6/dellos6_module.py new file mode 100644 index 00000000000..7f0b3da4b46 --- /dev/null +++ b/test/units/modules/network/dellos6/dellos6_module.py @@ -0,0 +1,88 @@ +# (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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import json + +from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase + + +fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures') +fixture_data = {} + + +def load_fixture(name): + path = os.path.join(fixture_path, name) + + if path in fixture_data: + return fixture_data[path] + + with open(path) as f: + data = f.read() + + try: + data = json.loads(data) + except: + pass + + fixture_data[path] = data + return data + + +class TestDellos6Module(ModuleTestCase): + + def execute_module(self, failed=False, changed=False, commands=None, sort=True, defaults=False): + + self.load_fixtures(commands) + + if failed: + result = self.failed() + self.assertTrue(result['failed'], result) + else: + result = self.changed(changed) + self.assertEqual(result['changed'], changed, result) + + if commands is not None: + if sort: + self.assertEqual(sorted(commands), sorted(result['updates']), result['updates']) + else: + self.assertEqual(commands, result['updates'], result['updates']) + + return result + + def failed(self): + with self.assertRaises(AnsibleFailJson) as exc: + self.module.main() + + result = exc.exception.args[0] + self.assertTrue(result['failed'], result) + return result + + def changed(self, changed=False): + with self.assertRaises(AnsibleExitJson) as exc: + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], changed, result) + return result + + def load_fixtures(self, commands=None): + pass diff --git a/test/units/modules/network/dellos6/fixtures/dellos6_config_config.cfg b/test/units/modules/network/dellos6/fixtures/dellos6_config_config.cfg new file mode 100644 index 00000000000..a8ed721c86b --- /dev/null +++ b/test/units/modules/network/dellos6/fixtures/dellos6_config_config.cfg @@ -0,0 +1,16 @@ +! +hostname router +exit +! +interface Te1/0/1 +description "test_string" +exit +! +interface Te1/0/2 +no shutdown +exit +! +interface Te1/0/9 +switchport access vlan 2 +exit + diff --git a/test/units/modules/network/dellos6/fixtures/dellos6_config_src.cfg b/test/units/modules/network/dellos6/fixtures/dellos6_config_src.cfg new file mode 100644 index 00000000000..70d5f6653e7 --- /dev/null +++ b/test/units/modules/network/dellos6/fixtures/dellos6_config_src.cfg @@ -0,0 +1,7 @@ +! +hostname foo +exit +! +interface Te1/0/2 +shutdown +exit diff --git a/test/units/modules/network/dellos6/fixtures/show_interfaces b/test/units/modules/network/dellos6/fixtures/show_interfaces new file mode 100644 index 00000000000..f6aede901ba --- /dev/null +++ b/test/units/modules/network/dellos6/fixtures/show_interfaces @@ -0,0 +1,41 @@ +Interface Name................................. Te1/0/1 +SOC Hardware Info.............................. BCM56842_A1 +Link Status.................................... Up /None +Keepalive Enabled.............................. FALSE +Err-disable Cause.............................. None +VLAN Membership Mode........................... Trunk Mode +VLAN Membership................................ (1),2-4096 +MTU Size....................................... 1518 +Port Mode [Duplex]............................. Full +Port Speed..................................... 1000 +Link Debounce Flaps............................ 0 +Auto-Negotation Status......................... Auto +Burned MAC Address............................. F8B1.565B.615E +L3 MAC Address................................. F8B1.565B.615F +Sample Load Interval........................... 300 +Received Input Rate Bits/Sec................... 0 +Received Input Rate Packets/Sec................ 0 +Transmitted Input Rate Bits/Sec................ 440 +Transmitted Input Rate Packets/Sec : .......... 0 +Total Packets Received Without Errors.......... 0 +Unicast Packets Received....................... 0 +Multicast Packets Received..................... 0 +Broadcast Packets Received..................... 0 +Total Packets Received with MAC Errors......... 0 +Jabbers Received............................... 0 +Fragments/Undersize Received................... 0 +Alignment Errors............................... 0 +FCS Errors..................................... 0 +Overruns....................................... 0 +Total Received Packets Not Forwarded........... 0 +Total Packets Transmitted Successfully......... 381302 +Unicast Packets Transmitted.................... 1 +Multicast Packets Transmitted.................. 351645 +Broadcast Packets Transmitted.................. 29656 +Transmit Packets Discarded..................... 0 +Total Transmit Errors.......................... 0 +Total Transmit Packets Discarded............... 0 +Single Collision Frames........................ 0 +Multiple Collision Frames...................... 0 +Excessive Collision Frames..................... 0 + diff --git a/test/units/modules/network/dellos6/fixtures/show_interfaces_status b/test/units/modules/network/dellos6/fixtures/show_interfaces_status new file mode 100644 index 00000000000..28defda61e9 --- /dev/null +++ b/test/units/modules/network/dellos6/fixtures/show_interfaces_status @@ -0,0 +1,48 @@ +Port Description Duplex Speed Neg Link Flow M VLAN + State Ctrl +--------- --------------- ------ ------- ---- ------ ----- -- ------------------- +Te1/0/1 connected to sp Full 1000 Auto Up Off T (1),2-4096 +Te1/0/2 to_NIC_1 Full 1000 Auto Up Off A 99 +Te1/0/3 N/A Unknown Auto Down Off A 1 +Te1/0/4 N/A Unknown Auto Down Off A 1 +Te1/0/5 N/A Unknown Auto Down Off A 1 +Te1/0/6 N/A Unknown Auto Down Off A 1 +Te1/0/7 N/A Unknown Auto Down Off A 1 +Te1/0/8 N/A Unknown Auto Down Off A 1 +Te1/0/9 N/A Unknown Auto Down Off A 2 +Te1/0/10 N/A Unknown Auto Down Off A 1 +Te1/0/11 N/A Unknown Auto Down Off A 1 +Te1/0/12 N/A Unknown Auto Down Off A 1 +Te1/0/13 N/A Unknown Auto Down Off A 1 +Te1/0/14 N/A Unknown Auto Down Off A 1 +Te1/0/15 N/A Unknown Auto Down Off A 1 +Te1/0/16 N/A Unknown Auto Down Off A 1 +Te1/0/17 N/A Unknown Auto Down Off A 1 +Te1/0/18 N/A Unknown Auto Down Off A 1 +Te1/0/19 N/A Unknown Auto Down Off A 1 +Te1/0/20 N/A Unknown Auto Down Off A 1 +Te1/0/21 N/A Unknown Auto Down Off A 1 +Te1/0/22 N/A Unknown Auto Down Off A 100 +Te1/0/23 N/A Unknown Auto Down Off A 1 +Te1/0/24 N/A Unknown Auto Down Off A 1 +Fo1/1/1 N/A N/A N/A Detach N/A +Fo1/1/2 Full 40000 Off Down Off A 1 +Te1/1/1 N/A N/A N/A Detach N/A +Te1/1/2 N/A N/A N/A Detach N/A +Te1/1/3 N/A N/A N/A Detach N/A +Te1/1/4 N/A N/A N/A Detach N/A +Te1/1/5 N/A N/A N/A Detach N/A +Te1/1/6 N/A N/A N/A Detach N/A +Te1/1/7 N/A N/A N/A Detach N/A +Te1/1/8 N/A N/A N/A Detach N/A + +Oob Type Link + State +--- ------------------------------ ----- +oob Out-Of-Band Up + + +Port Description Link M VLAN +Channel State +------- ------------------------------ ------- -- ------------------- + diff --git a/test/units/modules/network/dellos6/fixtures/show_interfaces_transceiver_properties b/test/units/modules/network/dellos6/fixtures/show_interfaces_transceiver_properties new file mode 100644 index 00000000000..976f45a8215 --- /dev/null +++ b/test/units/modules/network/dellos6/fixtures/show_interfaces_transceiver_properties @@ -0,0 +1,6 @@ +Yes: Dell Qualified No: Not Qualified +N/A : Not Applicable +Port Type Media Serial Number Dell Qualified +--------- ------- --------------------- --------------------- -------------- + + diff --git a/test/units/modules/network/dellos6/fixtures/show_ip_int b/test/units/modules/network/dellos6/fixtures/show_ip_int new file mode 100644 index 00000000000..043ee2cc294 --- /dev/null +++ b/test/units/modules/network/dellos6/fixtures/show_ip_int @@ -0,0 +1,15 @@ +Default Gateway................................ 0.0.0.0 +L3 MAC Address................................. F8B1.565B.615F + +Routing Interfaces: + +Interface State IP Address IP Mask Method +---------- ----- --------------- --------------- ------- +Vl1 Down 0.0.0.0 0.0.0.0 None +Vl2 Up 0.0.0.0 0.0.0.0 DHCP +Vl99 Up 10.99.1.2 255.255.0.0 Manual +Vl100 Up 3.3.3.3 255.255.255.0 Manual +Vl999 Up 10.250.1.2 255.255.255.0 Manual +Vl1010 Up 10.1.1.1 255.255.255.0 Manual +Vl1681 Up 192.168.100.1 255.255.255.0 Manual + diff --git a/test/units/modules/network/dellos6/fixtures/show_lldp b/test/units/modules/network/dellos6/fixtures/show_lldp new file mode 100644 index 00000000000..be89c415b5a --- /dev/null +++ b/test/units/modules/network/dellos6/fixtures/show_lldp @@ -0,0 +1,11 @@ +LLDP Global Configuration + + +Transmit Interval............................ 30 seconds + +Transmit Hold Multiplier..................... 4 + +Reinit Delay................................. 2 seconds + +Notification Interval........................ 5 seconds + diff --git a/test/units/modules/network/dellos6/fixtures/show_lldp_remote-device_all b/test/units/modules/network/dellos6/fixtures/show_lldp_remote-device_all new file mode 100644 index 00000000000..5a135c88e6e --- /dev/null +++ b/test/units/modules/network/dellos6/fixtures/show_lldp_remote-device_all @@ -0,0 +1,9 @@ +LLDP Remote Device Summary + +Local +Interface RemID Chassis ID Port ID System Name +--------- ------- ------------------- ----------------- ----------------- +Te1/0/5 14 F8:B1:56:70:49:38 Gi1/0/5 MAA-N2048-6884 + + + diff --git a/test/units/modules/network/dellos6/fixtures/show_memory_cpu b/test/units/modules/network/dellos6/fixtures/show_memory_cpu new file mode 100644 index 00000000000..426576938a6 --- /dev/null +++ b/test/units/modules/network/dellos6/fixtures/show_memory_cpu @@ -0,0 +1,3 @@ +Total Memory................................... 1723232 KBytes +Available Memory Space......................... 638144 KBytes + diff --git a/test/units/modules/network/dellos6/fixtures/show_running-config b/test/units/modules/network/dellos6/fixtures/show_running-config new file mode 100644 index 00000000000..b589c2968db --- /dev/null +++ b/test/units/modules/network/dellos6/fixtures/show_running-config @@ -0,0 +1,124 @@ +!Current Configuration: +!System Description "Dell Networking N4064F, 6.3.3.10, Linux 3.7.10-e54850e7" +!System Software Version 6.3.3.10 +!Cut-through mode is configured as disabled +! +configure +hostname "os6" +slot 1/0 5 ! Dell Networking N4064F +slot 1/1 8 ! Dell 10GBase-T Card +stack +member 1 4 ! N4064F +exit +interface out-of-band +ip address 10.16.148.73 255.255.0.0 10.16.144.254 +exit +no logging console +interface vlan 1 +ip address dhcp +exit +no passwords min-length +username "admin" password 21232f297a57a5a743894a0e4a801fc3 privilege 1 encrypted +line telnet +exec-timeout 0 +exit +ip ssh server +application install SupportAssist auto-restart start-on-boot +! +interface Te1/0/1 +no switchport port-security violation protect +exit +! +interface Te1/0/2 +no switchport port-security violation protect +exit +! +interface Te1/0/3 +no switchport port-security violation protect +exit +! +interface Te1/0/4 +no switchport port-security violation protect +exit +! +interface Te1/0/5 +no switchport port-security violation protect +exit +! +interface Te1/0/6 +no switchport port-security violation protect +exit +! +interface Te1/0/7 +no switchport port-security violation protect +exit +! +interface Te1/0/8 +no switchport port-security violation protect +exit +! +interface Te1/0/9 +no switchport port-security violation protect +exit +! +interface Te1/0/10 +no switchport port-security violation protect +exit +! +interface Te1/0/11 +no switchport port-security violation protect +exit +! +interface port-channel 1 +no switchport port-security violation protect +exit +! +interface port-channel 2 +no switchport port-security violation protect +exit +! +interface port-channel 3 +no switchport port-security violation protect +exit +! +interface port-channel 4 +no switchport port-security violation protect +exit +! +interface port-channel 5 +no switchport port-security violation protect +exit +! +snmp-server enable traps dvmrp +snmp-server enable traps pim +no snmp-server enable traps vrrp +no snmp-server enable traps acl +snmp-server enable traps captive-portal +snmp-server enable traps captive-portal client-auth-failure +snmp-server enable traps captive-portal client-connect +snmp-server enable traps captive-portal client-db-full +snmp-server enable traps captive-portal client-disconnect +router bgp 11 +bgp router-id 1.1.1.1 +maximum-paths 2 +maximum-paths ibgp 2 +network 101.1.2.0 mask 255.255.255.0 +template peer MUX_HNV_ACCESS +remote-as 64918 +exit +neighbor 10.10.234.16 remote-as 64818 +neighbor 10.10.234.16 default-originate +neighbor 10.10.234.16 timers 2 5 +neighbor 2001:4898:5808:ffa2::1 remote-as 64818 +neighbor 2001:4898:5808:ffa2::1 default-originate +neighbor 2001:4898:5808:ffa2::1 timers 2 4 +address-family ipv6 +network 2001:4898:5808:ffa0::/126 +redistribute connected +exit +exit +enable password c4f25f005187e9a85ad6480d3507a541 encrypted +openflow +exit +eula-consent support-assist reject +exit diff --git a/test/units/modules/network/dellos6/fixtures/show_running-config__include_hostname b/test/units/modules/network/dellos6/fixtures/show_running-config__include_hostname new file mode 100644 index 00000000000..35b72bacc60 --- /dev/null +++ b/test/units/modules/network/dellos6/fixtures/show_running-config__include_hostname @@ -0,0 +1,3 @@ +hostname "dellos6_sw1" + + diff --git a/test/units/modules/network/dellos6/fixtures/show_version b/test/units/modules/network/dellos6/fixtures/show_version new file mode 100644 index 00000000000..37c58e8b09f --- /dev/null +++ b/test/units/modules/network/dellos6/fixtures/show_version @@ -0,0 +1,17 @@ +Machine Description............... Dell Networking Switch +System Model ID................... N4032 +Machine Type...................... Dell Networking N4032 +Serial Number..................... CN04G4FP282984AI0097A01 +Manufacturer...................... 0xbc00 +Burned In MAC Address............. F8B1.565B.615C +System Object ID.................. 1.3.6.1.4.1.674.10895.3042 +CPU Version....................... XLP308H-B2 +SOC Version....................... BCM56842_A1 +HW Version........................ 3 +CPLD Version...................... 17 + +unit active backup current-active next-active +---- ----------- ----------- -------------- -------------- +1 6.3.3.7 6.3.2.7 6.3.3.7 6.3.3.7 + + diff --git a/test/units/modules/network/dellos6/test_dellos6_command.py b/test/units/modules/network/dellos6/test_dellos6_command.py new file mode 100644 index 00000000000..df6c3729b41 --- /dev/null +++ b/test/units/modules/network/dellos6/test_dellos6_command.py @@ -0,0 +1,108 @@ +# (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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import json + +from ansible.compat.tests.mock import patch +from ansible.modules.network.dellos6 import dellos6_command +from units.modules.utils import set_module_args +from .dellos6_module import TestDellos6Module, load_fixture + + +class TestDellos6CommandModule(TestDellos6Module): + + module = dellos6_command + + def setUp(self): + super(TestDellos6CommandModule, self).setUp() + + self.mock_run_commands = patch('ansible.modules.network.dellos6.dellos6_command.run_commands') + self.run_commands = self.mock_run_commands.start() + + def tearDown(self): + super(TestDellos6CommandModule, self).tearDown() + self.mock_run_commands.stop() + + def load_fixtures(self, commands=None): + + def load_from_file(*args, **kwargs): + module, commands = args + output = list() + + for item in commands: + try: + obj = json.loads(item['command']) + command = obj['command'] + except ValueError: + command = item['command'] + filename = str(command).replace(' ', '_') + output.append(load_fixture(filename)) + return output + + self.run_commands.side_effect = load_from_file + + def test_dellos6_command_simple(self): + set_module_args(dict(commands=['show version'])) + result = self.execute_module() + self.assertEqual(len(result['stdout']), 1) + self.assertTrue(result['stdout'][0].startswith('Machine Description')) + + def test_dellos6_command_multiple(self): + set_module_args(dict(commands=['show version', 'show version'])) + result = self.execute_module() + self.assertEqual(len(result['stdout']), 2) + self.assertTrue(result['stdout'][0].startswith('Machine Description')) + + def test_dellos6_command_wait_for(self): + wait_for = 'result[0] contains "Machine Description"' + set_module_args(dict(commands=['show version'], wait_for=wait_for)) + self.execute_module() + + def test_dellos6_command_wait_for_fails(self): + wait_for = 'result[0] contains "test string"' + set_module_args(dict(commands=['show version'], wait_for=wait_for)) + self.execute_module(failed=True) + self.assertEqual(self.run_commands.call_count, 10) + + def test_dellos6_command_retries(self): + wait_for = 'result[0] contains "test string"' + set_module_args(dict(commands=['show version'], wait_for=wait_for, retries=2)) + self.execute_module(failed=True) + self.assertEqual(self.run_commands.call_count, 2) + + def test_dellos6_command_match_any(self): + wait_for = ['result[0] contains "Machine Description"', + 'result[0] contains "test string"'] + set_module_args(dict(commands=['show version'], wait_for=wait_for, match='any')) + self.execute_module() + + def test_dellos6_command_match_all(self): + wait_for = ['result[0] contains "Machine Description"', + 'result[0] contains "Dell Networking"'] + set_module_args(dict(commands=['show version'], wait_for=wait_for, match='all')) + self.execute_module() + + def test_dellos6_command_match_all_failure(self): + wait_for = ['result[0] contains "Machine Description"', + 'result[0] contains "test string"'] + commands = ['show version', 'show version'] + set_module_args(dict(commands=commands, wait_for=wait_for, match='all')) + self.execute_module(failed=True) diff --git a/test/units/modules/network/dellos6/test_dellos6_config.py b/test/units/modules/network/dellos6/test_dellos6_config.py new file mode 100644 index 00000000000..69c1ec79f62 --- /dev/null +++ b/test/units/modules/network/dellos6/test_dellos6_config.py @@ -0,0 +1,147 @@ +# +# (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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.compat.tests.mock import patch +from ansible.modules.network.dellos6 import dellos6_config +from units.modules.utils import set_module_args +from .dellos6_module import TestDellos6Module, load_fixture + + +class TestDellos6ConfigModule(TestDellos6Module): + + module = dellos6_config + + def setUp(self): + super(TestDellos6ConfigModule, self).setUp() + + self.mock_get_config = patch('ansible.modules.network.dellos6.dellos6_config.get_config') + self.get_config = self.mock_get_config.start() + + self.mock_load_config = patch('ansible.modules.network.dellos6.dellos6_config.load_config') + self.load_config = self.mock_load_config.start() + + self.mock_run_commands = patch('ansible.modules.network.dellos6.dellos6_config.run_commands') + self.run_commands = self.mock_run_commands.start() + + def tearDown(self): + super(TestDellos6ConfigModule, self).tearDown() + self.mock_get_config.stop() + self.mock_load_config.stop() + self.mock_run_commands.stop() + + def load_fixtures(self, commands=None): + config_file = 'dellos6_config_config.cfg' + self.get_config.return_value = load_fixture(config_file) + self.load_config.return_value = None + + def test_dellos6_config_unchanged(self): + src = load_fixture('dellos6_config_config.cfg') + set_module_args(dict(src=src)) + self.execute_module() + + def test_dellos6_config_src(self): + src = load_fixture('dellos6_config_src.cfg') + set_module_args(dict(src=src)) + commands = ['hostname foo', 'exit', 'interface Te1/0/2', + 'shutdown', 'exit'] + self.execute_module(changed=True, commands=commands) + + def test_dellos6_config_backup(self): + set_module_args(dict(backup=True)) + result = self.execute_module() + self.assertIn('__backup__', result) + + def test_dellos6_config_save(self): + set_module_args(dict(save=True)) + self.execute_module(changed=True) + self.assertEqual(self.run_commands.call_count, 1) + self.assertEqual(self.get_config.call_count, 0) + self.assertEqual(self.load_config.call_count, 0) + args = self.run_commands.call_args[0][1] + self.assertDictContainsSubset({'command': 'copy running-config startup-config'}, args[0]) +# self.assertIn('copy running-config startup-config\r', args) + + def test_dellos6_config_lines_wo_parents(self): + set_module_args(dict(lines=['hostname foo'])) + commands = ['hostname foo'] + self.execute_module(changed=True, commands=commands) + + def test_dellos6_config_lines_w_parents(self): + set_module_args(dict(lines=['description "teest"', 'exit'], parents=['interface Te1/0/2'])) + commands = ['interface Te1/0/2', 'description "teest"', 'exit'] + self.execute_module(changed=True, commands=commands) + + def test_dellos6_config_before(self): + set_module_args(dict(lines=['hostname foo'], before=['snmp-server contact bar'])) + commands = ['snmp-server contact bar', 'hostname foo'] + self.execute_module(changed=True, commands=commands, sort=False) + + def test_dellos6_config_after(self): + set_module_args(dict(lines=['hostname foo'], after=['snmp-server contact bar'])) + commands = ['hostname foo', 'snmp-server contact bar'] + self.execute_module(changed=True, commands=commands, sort=False) + + def test_dellos6_config_before_after_no_change(self): + set_module_args(dict(lines=['hostname router'], + before=['snmp-server contact bar'], + after=['snmp-server location chennai'])) + self.execute_module() + + def test_dellos6_config_config(self): + config = 'hostname localhost' + set_module_args(dict(lines=['hostname router'], config=config)) + commands = ['hostname router'] + self.execute_module(changed=True, commands=commands) + + def test_dellos6_config_replace_block(self): + lines = ['description test string', 'shutdown'] + parents = ['interface Te1/0/2'] + set_module_args(dict(lines=lines, replace='block', parents=parents)) + commands = parents + lines + self.execute_module(changed=True, commands=commands) + + def test_dellos6_config_match_none(self): + lines = ['hostname router'] + set_module_args(dict(lines=lines, match='none')) + self.execute_module(changed=True, commands=lines) + + def test_dellos6_config_match_none(self): + lines = ['description test string', 'shutdown'] + parents = ['interface Te1/0/2'] + set_module_args(dict(lines=lines, parents=parents, match='none')) + commands = parents + lines + self.execute_module(changed=True, commands=commands, sort=False) + + def test_dellos6_config_match_strict(self): + lines = ['description "test_string"', + 'shutdown'] + parents = ['interface Te1/0/1'] + set_module_args(dict(lines=lines, parents=parents, match='strict')) + commands = parents + ['shutdown'] + self.execute_module(changed=True, commands=commands, sort=False) + + def test_dellos6_config_match_exact(self): + lines = ['description test_string', 'shutdown'] + parents = ['interface Te1/0/1'] + set_module_args(dict(lines=lines, parents=parents, match='exact')) + commands = parents + lines + self.execute_module(changed=True, commands=commands, sort=False) diff --git a/test/units/modules/network/dellos6/test_dellos6_facts.py b/test/units/modules/network/dellos6/test_dellos6_facts.py new file mode 100644 index 00000000000..8339a64723f --- /dev/null +++ b/test/units/modules/network/dellos6/test_dellos6_facts.py @@ -0,0 +1,105 @@ +# (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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import json + +from ansible.compat.tests.mock import patch +from units.modules.utils import set_module_args +from .dellos6_module import TestDellos6Module, load_fixture +from ansible.modules.network.dellos6 import dellos6_facts + + +class TestDellos6Facts(TestDellos6Module): + + module = dellos6_facts + + def setUp(self): + super(TestDellos6Facts, self).setUp() + + self.mock_run_command = patch( + 'ansible.modules.network.dellos6.dellos6_facts.run_commands') + self.run_command = self.mock_run_command.start() + + def tearDown(self): + super(TestDellos6Facts, self).tearDown() + + self.mock_run_command.stop() + + def load_fixtures(self, commands=None): + + def load_from_file(*args, **kwargs): + module, commands = args + output = list() + + for item in commands: + try: + obj = json.loads(item) + command = obj['command'] + except ValueError: + command = item + if '|' in command: + command = str(command).replace('|', '') + filename = str(command).replace(' ', '_') + filename = filename.replace('/', '7') + output.append(load_fixture(filename)) + return output + + self.run_command.side_effect = load_from_file + + def test_dellos6_facts_gather_subset_default(self): + set_module_args(dict()) + result = self.execute_module() + ansible_facts = result['ansible_facts'] + self.assertIn('hardware', ansible_facts['ansible_net_gather_subset']) + self.assertIn('default', ansible_facts['ansible_net_gather_subset']) + self.assertIn('interfaces', ansible_facts['ansible_net_gather_subset']) + self.assertEquals('"dellos6_sw1"', ansible_facts['ansible_net_hostname']) + self.assertIn('Te1/0/1', ansible_facts['ansible_net_interfaces'].keys()) + self.assertEquals(1682, ansible_facts['ansible_net_memtotal_mb']) + self.assertEquals(623, ansible_facts['ansible_net_memfree_mb']) + + def test_dellos6_facts_gather_subset_config(self): + set_module_args({'gather_subset': 'config'}) + result = self.execute_module() + ansible_facts = result['ansible_facts'] + self.assertIn('default', ansible_facts['ansible_net_gather_subset']) + self.assertIn('config', ansible_facts['ansible_net_gather_subset']) + self.assertEquals('"dellos6_sw1"', ansible_facts['ansible_net_hostname']) + self.assertIn('ansible_net_config', ansible_facts) + + def test_dellos6_facts_gather_subset_hardware(self): + set_module_args({'gather_subset': 'hardware'}) + result = self.execute_module() + ansible_facts = result['ansible_facts'] + self.assertIn('default', ansible_facts['ansible_net_gather_subset']) + self.assertIn('hardware', ansible_facts['ansible_net_gather_subset']) + self.assertEquals(1682, ansible_facts['ansible_net_memtotal_mb']) + self.assertEquals(623, ansible_facts['ansible_net_memfree_mb']) + + def test_dellos6_facts_gather_subset_interfaces(self): + set_module_args({'gather_subset': 'interfaces'}) + result = self.execute_module() + ansible_facts = result['ansible_facts'] + self.assertIn('default', ansible_facts['ansible_net_gather_subset']) + self.assertIn('interfaces', ansible_facts['ansible_net_gather_subset']) + self.assertIn('Te1/0/1', ansible_facts['ansible_net_interfaces'].keys()) + self.assertEquals(['Te1/0/5'], list(ansible_facts['ansible_net_neighbors'].keys())) + self.assertIn('ansible_net_interfaces', ansible_facts)