diff options
author | Dhivyap <dhivya.p@dell.com> | 2018-01-23 19:29:27 +0530 |
---|---|---|
committer | John R Barker <john@johnrbarker.com> | 2018-01-23 13:59:27 +0000 |
commit | 19ff2f4e8ce78ebe8f17289add6cb893302c1745 (patch) | |
tree | 9e6743f4eb5ea0739f91ff416eced035fb8106ee /lib/ansible | |
parent | 9f84a12adf71a3069f8e1cd0645182c9afc7ba86 (diff) | |
download | ansible-19ff2f4e8ce78ebe8f17289add6cb893302c1745.tar.gz |
Ansible 2.5 feature support for dellos9 (#34880)
* Add ansible-2.5 support for dellos9
* Fix ansible-test issues and update copyright
* fix ansible-test errors in dellos9_config
* fix izip attribute error in python3
* fix python3 dict.keys() issue
* Remove waitfor aliases in dellos9_command
Diffstat (limited to 'lib/ansible')
-rw-r--r-- | lib/ansible/module_utils/network/dellos9/dellos9.py | 3 | ||||
-rw-r--r-- | lib/ansible/modules/network/dellos9/dellos9_command.py | 32 | ||||
-rw-r--r-- | lib/ansible/modules/network/dellos9/dellos9_config.py | 163 | ||||
-rw-r--r-- | lib/ansible/modules/network/dellos9/dellos9_facts.py | 71 | ||||
-rw-r--r-- | lib/ansible/plugins/action/dellos9.py | 67 | ||||
-rw-r--r-- | lib/ansible/plugins/cliconf/dellos9.py | 82 | ||||
-rw-r--r-- | lib/ansible/plugins/terminal/dellos9.py | 2 |
7 files changed, 265 insertions, 155 deletions
diff --git a/lib/ansible/module_utils/network/dellos9/dellos9.py b/lib/ansible/module_utils/network/dellos9/dellos9.py index c8c1d06da6..2eb3e7aa41 100644 --- a/lib/ansible/module_utils/network/dellos9/dellos9.py +++ b/lib/ansible/module_utils/network/dellos9/dellos9.py @@ -123,8 +123,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) diff --git a/lib/ansible/modules/network/dellos9/dellos9_command.py b/lib/ansible/modules/network/dellos9/dellos9_command.py index 1d1f6bbeb8..5087381ea9 100644 --- a/lib/ansible/modules/network/dellos9/dellos9_command.py +++ b/lib/ansible/modules/network/dellos9/dellos9_command.py @@ -20,7 +20,7 @@ author: "Dhivya P (@dhivyap)" short_description: Run commands on remote devices running Dell OS9 description: - Sends arbitrary commands to a Dell OS9 node and returns the results - read from the device. This module includes an + read from the device. This module includes an argument that will cause the module to wait for a specific condition before returning or timing out if the condition is not met. - This module does not support running commands in configuration mode. @@ -44,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 @@ -72,33 +85,21 @@ notes: """ 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 dellos9_command: commands: show version - provider: "{{ cli }}" - name: run show version and check to see if output contains OS9 dellos9_command: commands: show version wait_for: result[0] contains OS9 - provider: "{{ cli }}" - name: run multiple commands on remote nodes dellos9_command: commands: - show version - show interfaces - provider: "{{ cli }}" - name: run multiple commands and evaluate the output dellos9_command: @@ -108,7 +109,6 @@ tasks: wait_for: - result[0] contains OS9 - result[1] contains Loopback - provider: "{{ cli }}" """ RETURN = """ @@ -225,11 +225,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/dellos9/dellos9_config.py b/lib/ansible/modules/network/dellos9/dellos9_config.py index 439c98d70c..d2633ad802 100644 --- a/lib/ansible/modules/network/dellos9/dellos9_config.py +++ b/lib/ansible/modules/network/dellos9/dellos9_config.py @@ -30,15 +30,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,14 +49,15 @@ 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: description: - The ordered set of commands to push on to the command stack if - a change needs to be made. The playbook designer can use this opportunity to perform configuration commands prior to pushing + a change needs to be made. This allows the playbook designer + the opportunity to perform configuration commands prior to pushing any changes without affecting how the set of commands are matched against the system. required: false @@ -62,19 +65,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), this - the playbook designer can 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 match by 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 @@ -82,10 +86,10 @@ 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 - the modified lines push to the device in configuration - mode. If you set the replace argument to I(block), then the entire - command block pushes to the device in configuration mode if any + 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 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 default: line @@ -112,29 +116,32 @@ options: 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 notes: - This module requires Dell OS9 version 9.10.0.1P13 or above. - This module requires to increase the ssh connection rate limit. Use the following command I(ip ssh connection-rate-limit 60) - to configure the same. This can also be done with the M(dellos9_config) module. + to configure the same. This can also be done with the + M(dellos9_config) module. """ EXAMPLES = """ @@ -152,7 +159,6 @@ EXAMPLES = """ parents: ['ip access-list extended test'] before: ['no ip access-list extended test'] match: exact - provider: "{{ cli }}" - dellos9_config: lines: @@ -163,30 +169,25 @@ EXAMPLES = """ parents: ['ip access-list extended test'] before: ['no ip access-list extended test'] replace: block - provider: "{{ cli }}" - """ RETURN = """ updates: description: The set of commands that will be pushed to the remote device. - returned: Always. + returned: always type: list - sample: ['...', '...'] - -responses: - description: The set of responses from issuing the commands on the device. - returned: When not check_mode. + sample: ['hostname foo', 'router bgp 1', 'bgp router-id 1.1.1.1'] +commands: + description: The set of commands that will be pushed to the remote device + returned: always type: list - sample: ['...', '...'] - + sample: ['hostname foo', 'router bgp 1', 'bgp router-id 1.1.1.1'] saved: description: Returns whether the configuration is saved to the startup configuration or not. returned: When not check_mode. type: bool sample: True - backup_path: description: The full path to the backup file returned: when backup is yes @@ -207,10 +208,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( @@ -236,7 +250,6 @@ def main(): mutually_exclusive = [('lines', 'src'), ('parents', 'src')] - module = AnsibleModule(argument_spec=argument_spec, mutually_exclusive=mutually_exclusive, supports_check_mode=True) @@ -253,48 +266,62 @@ def main(): 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']: 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) + 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 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])): - if module.params['before']: - commands[:0] = module.params['before'] + 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'} - run_commands(module, [cmd]) - result['saved'] = True + if not module.check_mode and module.params['update'] == 'merge': + load_config(module, commands) - result['changed'] = True + result['changed'] = True + result['commands'] = commands + result['updates'] = 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'\[confirm yes/no\]:\s?$', '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') module.exit_json(**result) - if __name__ == '__main__': main() diff --git a/lib/ansible/modules/network/dellos9/dellos9_facts.py b/lib/ansible/modules/network/dellos9/dellos9_facts.py index 409f79817e..23d7a41d8d 100644 --- a/lib/ansible/modules/network/dellos9/dellos9_facts.py +++ b/lib/ansible/modules/network/dellos9/dellos9_facts.py @@ -23,17 +23,17 @@ description: - Collects a base set of device facts from a remote device that is running OS9. This module prepends all of the base network fact keys with C(ansible_net_<fact>). 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: dellos9 options: gather_subset: description: - - When supplied, 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' @@ -63,72 +63,75 @@ EXAMPLES = """ RETURN = """ ansible_net_gather_subset: - description: The list of fact subsets collected from the device. - returned: Always. + description: The list of fact subsets collected from the device + returned: always type: list # default ansible_net_model: - description: The model name returned from the device. - returned: Always. + description: The model name returned from the device + returned: always type: str ansible_net_serialnum: - description: The serial number of the remote device. - returned: Always. + description: The serial number of the remote device + returned: always type: str ansible_net_version: - description: The operating system version running on the remote device. - returned: Always. + description: The operating system version running on the remote device + returned: always type: str ansible_net_hostname: - description: The configured hostname of the device. - returned: Always. + description: The configured hostname of the device + returned: always type: string ansible_net_image: - description: The image file the device is running. - returned: Always. + description: The image file the device is running + returned: always type: string # hardware ansible_net_filesystems: - description: All file system names available on the device. - returned: When hardware is configured. + description: All file system names available on the device + returned: when hardware is configured type: list ansible_net_memfree_mb: - description: The available free memory on the remote device in MB. - returned: When hardware is configured. + description: The available free memory on the remote device in Mb + returned: when hardware is configured type: int ansible_net_memtotal_mb: - description: The total memory on the remote device in MB. - returned: When hardware is configured. + description: The total memory on the remote device in Mb + returned: when hardware is configured type: int # config ansible_net_config: - description: The current active config from the device. - returned: When config is configured. + description: The current active config from the device + returned: when config is configured type: str # interfaces ansible_net_all_ipv4_addresses: - description: All IPv4 addresses configured on the device. - returned: When interfaces is configured. + description: All IPv4 addresses configured on the device + returned: when interfaces is configured type: list ansible_net_all_ipv6_addresses: - description: All IPv6 addresses configured on the device. - returned: When interfaces is configured. + description: All IPv6 addresses configured on the device + returned: when interfaces is configured type: list ansible_net_interfaces: - description: A hash of all interfaces running on the system. - returned: When interfaces is configured. + description: A hash of all interfaces running on the system + returned: when interfaces is configured type: dict ansible_net_neighbors: description: The list of LLDP neighbors from the remote device - returned: When interfaces is configured. + returned: when interfaces is configured type: dict """ import re -import itertools +try: + from itertools import izip +except ImportError: + izip = zip from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.network.dellos9.dellos9 import run_commands @@ -316,7 +319,7 @@ class Interfaces(FactsBase): self.facts['interfaces'][key]['ipv6'] = list() addresses = re.findall(r'\s+(.+), subnet', value, re.M) subnets = re.findall(r', subnet is (\S+)', value, re.M) - for addr, subnet in itertools.izip(addresses, subnets): + for addr, subnet in izip(addresses, subnets): ipv6 = dict(address=addr.strip(), subnet=subnet.strip()) self.add_ip_address(addr.strip(), 'ipv6') self.facts['interfaces'][key]['ipv6'].append(ipv6) diff --git a/lib/ansible/plugins/action/dellos9.py b/lib/ansible/plugins/action/dellos9.py index 7de604b999..463a4c5a9e 100644 --- a/lib/ansible/plugins/action/dellos9.py +++ b/lib/ansible/plugins/action/dellos9.py @@ -28,8 +28,8 @@ from ansible import constants as C from ansible.module_utils._text import to_text from ansible.module_utils.connection import Connection from ansible.plugins.action.normal import ActionModule as _ActionModule -from ansible.module_utils.network.dellos9.dellos9 import dellos9_provider_spec from ansible.module_utils.network.common.utils import load_provider +from ansible.module_utils.network.dellos9.dellos9 import dellos9_provider_spec try: from __main__ import display @@ -41,40 +41,45 @@ except ImportError: class ActionModule(_ActionModule): def run(self, tmp=None, task_vars=None): + socket_path = 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 = load_provider(dellos9_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(dellos9_provider_spec, self._task.args) + pc = copy.deepcopy(self._play_context) + pc.connection = 'network_cli' + pc.network_os = 'dellos9' + 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 = 'dellos9' - 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(')#'): @@ -82,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/dellos9.py b/lib/ansible/plugins/cliconf/dellos9.py new file mode 100644 index 0000000000..e445da3f14 --- /dev/null +++ b/lib/ansible/plugins/cliconf/dellos9.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 <http://www.gnu.org/licenses/>. +# +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'] = 'dellos9' + 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/dellos9.py b/lib/ansible/plugins/terminal/dellos9.py index 1921571dd1..8446f1aac8 100644 --- a/lib/ansible/plugins/terminal/dellos9.py +++ b/lib/ansible/plugins/terminal/dellos9.py @@ -37,7 +37,7 @@ class TerminalModule(TerminalBase): ] terminal_stderr_re = [ - re.compile(br"% ?Error: (?:(?!\bdoes not exist\b)(?!\balready exists\b)(?!\bHost not found\b)(?!\bnot active\b).)*$"), + re.compile(br"% ?Error: (?:(?!\bdoes not exist\b)(?!\balready exists\b)(?!\bHost not found\b)(?!\bnot active\b).)*\n"), re.compile(br"% ?Bad secret"), re.compile(br"invalid input", re.I), re.compile(br"(?:incomplete|ambiguous) command", re.I), |