From 5981a7489b1e617cba3c61893ce22dfc0dccbbdb Mon Sep 17 00:00:00 2001 From: Lindsay Hill Date: Fri, 10 Aug 2018 13:50:02 -0700 Subject: [PATCH] new nos_command module (#43056) --- .github/BOTMETA.yml | 10 + .../module_utils/network/nos/__init__.py | 0 lib/ansible/module_utils/network/nos/nos.py | 160 +++++ lib/ansible/modules/network/nos/__init__.py | 0 .../modules/network/nos/nos_command.py | 243 ++++++++ lib/ansible/plugins/cliconf/nos.py | 96 +++ lib/ansible/plugins/terminal/nos.py | 54 ++ .../module_utils/network/nos/test_nos.py | 149 +++++ test/units/modules/network/nos/__init__.py | 0 .../modules/network/nos/fixtures/show_version | 17 + test/units/modules/network/nos/nos_module.py | 87 +++ .../modules/network/nos/test_nos_command.py | 121 ++++ .../plugins/cliconf/fixtures/nos/show_chassis | 30 + .../cliconf/fixtures/nos/show_running-config | 549 ++++++++++++++++++ .../plugins/cliconf/fixtures/nos/show_version | 17 + test/units/plugins/cliconf/test_nos.py | 136 +++++ 16 files changed, 1669 insertions(+) create mode 100644 lib/ansible/module_utils/network/nos/__init__.py create mode 100644 lib/ansible/module_utils/network/nos/nos.py create mode 100644 lib/ansible/modules/network/nos/__init__.py create mode 100644 lib/ansible/modules/network/nos/nos_command.py create mode 100644 lib/ansible/plugins/cliconf/nos.py create mode 100644 lib/ansible/plugins/terminal/nos.py create mode 100644 test/units/module_utils/network/nos/test_nos.py create mode 100644 test/units/modules/network/nos/__init__.py create mode 100644 test/units/modules/network/nos/fixtures/show_version create mode 100644 test/units/modules/network/nos/nos_module.py create mode 100644 test/units/modules/network/nos/test_nos_command.py create mode 100644 test/units/plugins/cliconf/fixtures/nos/show_chassis create mode 100644 test/units/plugins/cliconf/fixtures/nos/show_running-config create mode 100644 test/units/plugins/cliconf/fixtures/nos/show_version create mode 100644 test/units/plugins/cliconf/test_nos.py diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 4b824d10c7..f0dc389187 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -486,6 +486,7 @@ files: $modules/network/netconf/netconf_rpc.py: wisotzky $team_networking $modules/network/netscaler/: $team_netscaler $modules/network/netvisor/: $team_netvisor + $modules/network/nos/: $team_extreme $modules/network/nuage/: pdellaert $modules/network/nxos/: $team_nxos $modules/network/nso/: $team_nso @@ -892,6 +893,9 @@ files: $module_utils/network/netscaler: maintainers: $team_netscaler labels: networking + $module_utils/network/nos: + maintainers: $team_extreme + labels: networking $module_utils/network/nso: maintainers: $team_nso labels: networking @@ -1024,6 +1028,9 @@ files: lib/ansible/plugins/cliconf/ironware.py: maintainers: paulquack labels: networking + lib/ansible/plugins/cliconf/nos.py: + maintainers: $team_extreme + labels: networking lib/ansible/plugins/cliconf/nxos.py: maintainers: $team_nxos labels: @@ -1137,6 +1144,9 @@ files: lib/ansible/plugins/terminal/junos.py: maintainers: $team_networking labels: networking + lib/ansible/plugins/terminal/nos.py: + maintainers: $team_extreme + labels: networking lib/ansible/plugins/terminal/nxos.py: maintainers: $team_networking labels: diff --git a/lib/ansible/module_utils/network/nos/__init__.py b/lib/ansible/module_utils/network/nos/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/ansible/module_utils/network/nos/nos.py b/lib/ansible/module_utils/network/nos/nos.py new file mode 100644 index 0000000000..e031a92a20 --- /dev/null +++ b/lib/ansible/module_utils/network/nos/nos.py @@ -0,0 +1,160 @@ +# +# (c) 2018 Extreme Networks 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 . +# +import json +from ansible.module_utils._text import to_text +from ansible.module_utils.network.common.utils import to_list +from ansible.module_utils.connection import Connection, ConnectionError + + +def get_connection(module): + """Get switch connection + + Creates reusable SSH connection to the switch described in a given module. + + Args: + module: A valid AnsibleModule instance. + + Returns: + An instance of `ansible.module_utils.connection.Connection` with a + connection to the switch described in the provided module. + + Raises: + AnsibleConnectionFailure: An error occurred connecting to the device + """ + if hasattr(module, 'nos_connection'): + return module.nos_connection + + capabilities = get_capabilities(module) + network_api = capabilities.get('network_api') + if network_api == 'cliconf': + module.nos_connection = Connection(module._socket_path) + else: + module.fail_json(msg='Invalid connection type %s' % network_api) + + return module.nos_connection + + +def get_capabilities(module): + """Get switch capabilities + + Collects and returns a python object with the switch capabilities. + + Args: + module: A valid AnsibleModule instance. + + Returns: + A dictionary containing the switch capabilities. + """ + if hasattr(module, 'nos_capabilities'): + return module.nos_capabilities + + try: + capabilities = Connection(module._socket_path).get_capabilities() + except ConnectionError as exc: + module.fail_json(msg=to_text(exc, errors='surrogate_then_replace')) + module.nos_capabilities = json.loads(capabilities) + return module.nos_capabilities + + +def run_commands(module, commands): + """Run command list against connection. + + Get new or previously used connection and send commands to it one at a time, + collecting response. + + Args: + module: A valid AnsibleModule instance. + commands: Iterable of command strings. + + Returns: + A list of output strings. + """ + responses = list() + connection = get_connection(module) + + for cmd in to_list(commands): + if isinstance(cmd, dict): + command = cmd['command'] + prompt = cmd['prompt'] + answer = cmd['answer'] + else: + command = cmd + prompt = None + answer = None + + try: + out = connection.get(command, prompt, answer) + out = to_text(out, errors='surrogate_or_strict') + except ConnectionError as exc: + module.fail_json(msg=to_text(exc)) + except UnicodeError: + module.fail_json(msg=u'Failed to decode output from %s: %s' % (cmd, to_text(out))) + + responses.append(out) + + return responses + + +def get_config(module): + """Get switch configuration + + Gets the described device's current configuration. If a configuration has + already been retrieved it will return the previously obtained configuration. + + Args: + module: A valid AnsibleModule instance. + + Returns: + A string containing the configuration. + """ + if not hasattr(module, 'device_configs'): + module.device_configs = {} + elif module.device_configs != {}: + return module.device_configs + + connection = get_connection(module) + try: + out = connection.get_config() + except ConnectionError as exc: + module.fail_json(msg=to_text(exc, errors='surrogate_then_replace')) + cfg = to_text(out, errors='surrogate_then_replace').strip() + module.device_configs = cfg + return cfg + + +def load_config(module, commands): + """Apply a list of commands to a device. + + Given a list of commands apply them to the device to modify the + configuration in bulk. + + Args: + module: A valid AnsibleModule instance. + commands: Iterable of command strings. + + Returns: + None + """ + connection = get_connection(module) + + try: + resp = connection.edit_config(commands) + return resp.get('response') + except ConnectionError as exc: + module.fail_json(msg=to_text(exc)) diff --git a/lib/ansible/modules/network/nos/__init__.py b/lib/ansible/modules/network/nos/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/ansible/modules/network/nos/nos_command.py b/lib/ansible/modules/network/nos/nos_command.py new file mode 100644 index 0000000000..382acacc50 --- /dev/null +++ b/lib/ansible/modules/network/nos/nos_command.py @@ -0,0 +1,243 @@ +#!/usr/bin/python +# +# (c) 2018 Extreme Networks 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) +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = """ +--- +module: nos_command +version_added: "2.7" +author: "Lindsay Hill (@LindsayHill)" +short_description: Run commands on remote devices running Extreme Networks NOS +description: + - Sends arbitrary commands to a NOS device and returns the results + 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. + Please use M(nos_config) to configure NOS devices. +notes: + - Tested against NOS 7.2.0 + - If a command sent to the device requires answering a prompt, it is possible + to pass a dict containing I(command), I(answer) and I(prompt). See examples. +options: + commands: + description: + - List of commands to send to the remote NOS device over the + 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 retries has expired. + required: true + wait_for: + description: + - List of conditions to evaluate against the output of the + command. The task will wait for each condition to be true + before moving forward. If the conditional is not true + within the configured number of retries, the task fails. + See examples. + default: null + 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'] + retries: + description: + - Specifies the number of retries a command should by tried + before it is considered failed. The command is run on the + target device every retry and evaluated against the + I(wait_for) conditions. + required: false + default: 10 + interval: + description: + - Configures the interval in seconds to wait between retries + of the command. If the command does not pass the specified + conditions, the interval indicates how long to wait before + trying the command again. + required: false + default: 1 +""" + +EXAMPLES = """ +tasks: + - name: run show version on remote devices + nos_command: + commands: show version + + - name: run show version and check to see if output contains NOS + nos_command: + commands: show version + wait_for: result[0] contains NOS + + - name: run multiple commands on remote nodes + nos_command: + commands: + - show version + - show interfaces + + - name: run multiple commands and evaluate the output + nos_command: + commands: + - show version + - show interface status + wait_for: + - result[0] contains NOS + - result[1] contains Te + - name: run command that requires answering a prompt + nos_command: + commands: + - command: 'clear sessions' + prompt: 'This operation will logout all the user sessions. Do you want to continue (yes/no)?:' + answer: y +""" + +RETURN = """ +stdout: + description: The set of responses from the commands + 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: ['...', '...'] +""" +import re +import time + +from ansible.module_utils.network.nos.nos import run_commands +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import ComplexList +from ansible.module_utils.network.common.parsing import Conditional +from ansible.module_utils.six import string_types + + +__metaclass__ = type + + +def to_lines(stdout): + for item in stdout: + if isinstance(item, string_types): + item = str(item).split('\n') + yield item + + +def parse_commands(module, warnings): + command = ComplexList(dict( + command=dict(key=True), + prompt=dict(), + answer=dict() + ), module) + commands = command(module.params['commands']) + for item in list(commands): + configure_type = re.match(r'conf(?:\w*)(?:\s+(\w+))?', item['command']) + if module.check_mode: + if configure_type and configure_type.group(1) not in ('confirm', 'replace', 'revert', 'network'): + module.fail_json( + msg='nos_command does not support running config mode ' + 'commands. Please use nos_config instead' + ) + if not item['command'].startswith('show'): + warnings.append( + 'only show commands are supported when using check mode, not ' + 'executing `%s`' % item['command'] + ) + commands.remove(item) + return commands + + +def main(): + """main entry point for module execution + """ + argument_spec = dict( + commands=dict(type='list', required=True), + + wait_for=dict(type='list'), + match=dict(default='all', choices=['all', 'any']), + + retries=dict(default=10, type='int'), + interval=dict(default=1, type='int') + ) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) + + result = {'changed': False} + + warnings = list() + commands = parse_commands(module, warnings) + result['warnings'] = warnings + + wait_for = module.params['wait_for'] or list() + conditionals = [Conditional(c) for c in wait_for] + + retries = module.params['retries'] + interval = module.params['interval'] + match = module.params['match'] + + while retries > 0: + responses = run_commands(module, commands) + + for item in list(conditionals): + if item(responses): + if match == 'any': + conditionals = list() + break + conditionals.remove(item) + + if not conditionals: + break + + time.sleep(interval) + retries -= 1 + + if conditionals: + failed_conditions = [item.raw for item in conditionals] + msg = 'One or more conditional statements have not been satisfied' + module.fail_json(msg=msg, failed_conditions=failed_conditions) + + result.update({ + 'changed': False, + 'stdout': responses, + 'stdout_lines': list(to_lines(responses)) + }) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/plugins/cliconf/nos.py b/lib/ansible/plugins/cliconf/nos.py new file mode 100644 index 0000000000..d4076c3c33 --- /dev/null +++ b/lib/ansible/plugins/cliconf/nos.py @@ -0,0 +1,96 @@ +# +# (c) 2018 Extreme Networks 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 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 + + +class Cliconf(CliconfBase): + + def get_device_info(self): + device_info = {} + + device_info['network_os'] = 'nos' + reply = self.get(b'show version') + data = to_text(reply, errors='surrogate_or_strict').strip() + + match = re.search(r'Network Operating System Version: (\S+)', data) + if match: + device_info['network_os_version'] = match.group(1) + + reply = self.get(b'show chassis') + data = to_text(reply, errors='surrogate_or_strict').strip() + + match = re.search(r'^Chassis Name:(\s+)(\S+)', data, re.M) + if match: + device_info['network_os_model'] = match.group(2) + + reply = self.get(b'show running-config | inc "switch-attributes host-name"') + data = to_text(reply, errors='surrogate_or_strict').strip() + + match = re.search(r'switch-attributes host-name (\S+)', data, re.M) + if match: + device_info['network_os_hostname'] = match.group(1) + + return device_info + + def get_config(self, source='running', flags=None): + if source not in ('running'): + return self.invalid_params("fetching configuration from %s is not supported" % source) + if source == 'running': + cmd = 'show running-config' + + flags = [] if flags is None else flags + cmd += ' '.join(flags) + cmd = cmd.strip() + + return self.send_command(cmd) + + def edit_config(self, command): + for cmd in chain(['configure terminal'], to_list(command), ['end']): + if isinstance(cmd, dict): + command = cmd['command'] + prompt = cmd['prompt'] + answer = cmd['answer'] + newline = cmd.get('newline', True) + else: + command = cmd + prompt = None + answer = None + newline = True + + self.send_command(command, prompt, answer, False, newline) + + 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/nos.py b/lib/ansible/plugins/terminal/nos.py new file mode 100644 index 0000000000..245189468f --- /dev/null +++ b/lib/ansible/plugins/terminal/nos.py @@ -0,0 +1,54 @@ +# +# (c) 2018 Extreme Networks 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 + +from ansible.errors import AnsibleConnectionFailure +from ansible.plugins.terminal import TerminalBase + + +class TerminalModule(TerminalBase): + + terminal_stdout_re = [ + re.compile(br"([\r\n]|(\x1b\[\?7h))[\w\+\-\.:\/\[\]]+(?:\([^\)]+\)){0,3}(?:[>#]) ?$") + ] + + terminal_stderr_re = [ + re.compile(br"% ?Error"), + # re.compile(br"^% \w+", re.M), + re.compile(br"% ?Bad secret"), + re.compile(br"[\r\n%] Bad passwords"), + re.compile(br"invalid input", re.I), + re.compile(br"(?:incomplete|ambiguous) command", re.I), + re.compile(br"connection timed out", re.I), + re.compile(br"[^\r\n]+ not found"), + re.compile(br"'[^']' +returned error code: ?\d+"), + re.compile(br"Bad mask", re.I), + re.compile(br"% ?(\S+) ?overlaps with ?(\S+)", re.I), + re.compile(br"[%\S] ?Informational: ?[\s]+", re.I), + re.compile(br"syntax error: unknown argument.", re.I) + ] + + def on_open_shell(self): + try: + self._exec_cli_command(u'terminal length 0') + except AnsibleConnectionFailure: + raise AnsibleConnectionFailure('unable to set terminal parameters') diff --git a/test/units/module_utils/network/nos/test_nos.py b/test/units/module_utils/network/nos/test_nos.py new file mode 100644 index 0000000000..8210a47728 --- /dev/null +++ b/test/units/module_utils/network/nos/test_nos.py @@ -0,0 +1,149 @@ +# +# (c) 2018 Extreme Networks 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 + +from os import path +import json + +from mock import MagicMock, patch, call + +from ansible.compat.tests import unittest +from ansible.module_utils.network.nos import nos + + +class TestPluginCLIConfNOS(unittest.TestCase): + """ Test class for NOS CLI Conf Methods + """ + + def test_get_connection_established(self): + """ Test get_connection with established connection + """ + module = MagicMock() + connection = nos.get_connection(module) + self.assertEqual(connection, module.nos_connection) + + @patch('ansible.module_utils.network.nos.nos.Connection') + def test_get_connection_new(self, connection): + """ Test get_connection with new connection + """ + socket_path = "little red riding hood" + module = MagicMock(spec=[ + 'fail_json', + ]) + module._socket_path = socket_path + + connection().get_capabilities.return_value = '{"network_api": "cliconf"}' + returned_connection = nos.get_connection(module) + connection.assert_called_with(socket_path) + self.assertEqual(returned_connection, module.nos_connection) + + @patch('ansible.module_utils.network.nos.nos.Connection') + def test_get_connection_incorrect_network_api(self, connection): + """ Test get_connection with incorrect network_api response + """ + socket_path = "little red riding hood" + module = MagicMock(spec=[ + 'fail_json', + ]) + module._socket_path = socket_path + module.fail_json.side_effect = TypeError + + connection().get_capabilities.return_value = '{"network_api": "nope"}' + + with self.assertRaises(TypeError): + nos.get_connection(module) + + @patch('ansible.module_utils.network.nos.nos.Connection') + def test_get_capabilities(self, connection): + """ Test get_capabilities + """ + socket_path = "little red riding hood" + module = MagicMock(spec=[ + 'fail_json', + ]) + module._socket_path = socket_path + module.fail_json.side_effect = TypeError + + capabilities = {'network_api': 'cliconf'} + + connection().get_capabilities.return_value = json.dumps(capabilities) + + capabilities_returned = nos.get_capabilities(module) + + self.assertEqual(capabilities, capabilities_returned) + + @patch('ansible.module_utils.network.nos.nos.Connection') + def test_run_commands(self, connection): + """ Test get_capabilities + """ + module = MagicMock() + + commands = [ + 'hello', + 'dolly', + 'well hello', + 'dolly', + 'its so nice to have you back', + 'where you belong', + ] + + responses = [ + 'Dolly, never go away again1', + 'Dolly, never go away again2', + 'Dolly, never go away again3', + 'Dolly, never go away again4', + 'Dolly, never go away again5', + 'Dolly, never go away again6', + ] + + module.nos_connection.get.side_effect = responses + + run_command_responses = nos.run_commands(module, commands) + + calls = [] + + for command in commands: + calls.append(call( + command, + None, + None + )) + + module.nos_connection.get.assert_has_calls(calls) + + self.assertEqual(responses, run_command_responses) + + @patch('ansible.module_utils.network.nos.nos.Connection') + def test_load_config(self, connection): + """ Test load_config + """ + module = MagicMock() + + commands = [ + 'what does it take', + 'to be', + 'number one?', + 'two is not a winner', + 'and three nobody remember', + ] + + nos.load_config(module, commands) + + module.nos_connection.edit_config.assert_called_once_with(commands) diff --git a/test/units/modules/network/nos/__init__.py b/test/units/modules/network/nos/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/units/modules/network/nos/fixtures/show_version b/test/units/modules/network/nos/fixtures/show_version new file mode 100644 index 0000000000..1accd8191e --- /dev/null +++ b/test/units/modules/network/nos/fixtures/show_version @@ -0,0 +1,17 @@ +Network Operating System Software +Network Operating System Version: 7.2.0 +Copyright (c) 1995-2017 Brocade Communications Systems, Inc. +Firmware name: 7.2.0 +Build Time: 10:52:47 Jul 10, 2017 +Install Time: 01:32:03 Jan 5, 2018 +Kernel: 2.6.34.6 + +BootProm: 1.0.1 +Control Processor: e500mc with 4096 MB of memory + +Slot Name Primary/Secondary Versions Status +--------------------------------------------------------------------------- +SW/0 NOS 7.2.0 ACTIVE* + 7.2.0 +SW/1 NOS 7.2.0 STANDBY + 7.2.0 diff --git a/test/units/modules/network/nos/nos_module.py b/test/units/modules/network/nos/nos_module.py new file mode 100644 index 0000000000..bc9e56580f --- /dev/null +++ b/test/units/modules/network/nos/nos_module.py @@ -0,0 +1,87 @@ +# (c) 2018 Extreme Networks 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 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 file_desc: + data = file_desc.read() + + try: + data = json.loads(data) + except: + pass + + fixture_data[path] = data + return data + + +class TestNosModule(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['commands']), result['commands']) + else: + self.assertEqual(commands, result['commands'], result['commands']) + + 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/nos/test_nos_command.py b/test/units/modules/network/nos/test_nos_command.py new file mode 100644 index 0000000000..1838d85d86 --- /dev/null +++ b/test/units/modules/network/nos/test_nos_command.py @@ -0,0 +1,121 @@ +# +# (c) 2018 Extreme Networks 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 json + +from ansible.compat.tests.mock import patch +from ansible.modules.network.nos import nos_command +from units.modules.utils import set_module_args +from .nos_module import TestNosModule, load_fixture + + +class TestNosCommandModule(TestNosModule): + + module = nos_command + + def setUp(self): + super(TestNosCommandModule, self).setUp() + + self.mock_run_commands = patch('ansible.modules.network.nos.nos_command.run_commands') + self.run_commands = self.mock_run_commands.start() + + def tearDown(self): + super(TestNosCommandModule, 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_nos_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('Network Operating System Software')) + + def test_nos_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('Network Operating System Software')) + + def test_nos_command_wait_for(self): + wait_for = 'result[0] contains "Network Operating System Software"' + set_module_args(dict(commands=['show version'], wait_for=wait_for)) + self.execute_module() + + def test_nos_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_nos_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_nos_command_match_any(self): + wait_for = ['result[0] contains "Network"', + 'result[0] contains "test string"'] + set_module_args(dict(commands=['show version'], wait_for=wait_for, match='any')) + self.execute_module() + + def test_nos_command_match_all(self): + wait_for = ['result[0] contains "Network"', + 'result[0] contains "Network Operating System Software"'] + set_module_args(dict(commands=['show version'], wait_for=wait_for, match='all')) + self.execute_module() + + def test_nos_command_match_all_failure(self): + wait_for = ['result[0] contains "Network Operating System Software"', + '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) + + def test_nos_command_configure_error(self): + commands = ['configure terminal'] + set_module_args({ + 'commands': commands, + '_ansible_check_mode': True, + }) + result = self.execute_module(failed=True) + self.assertEqual( + result['msg'], + 'nos_command does not support running config mode commands. ' + 'Please use nos_config instead' + ) diff --git a/test/units/plugins/cliconf/fixtures/nos/show_chassis b/test/units/plugins/cliconf/fixtures/nos/show_chassis new file mode 100644 index 0000000000..af51cbaff7 --- /dev/null +++ b/test/units/plugins/cliconf/fixtures/nos/show_chassis @@ -0,0 +1,30 @@ + +Chassis Name: BR-VDX6740 +switchType: 131 + +FAN Unit: 1 +Time Awake: 0 days + +FAN Unit: 2 +Time Awake: 0 days + +POWER SUPPLY Unit: 1 +Factory Part Num: 23-1000043-01 +Factory Serial Num: +Time Awake: 0 days + +POWER SUPPLY Unit: 2 +Factory Part Num: 23-1000043-01 +Factory Serial Num: +Time Awake: 0 days + +CHASSIS/WWN Unit: 1 +Power Consume Factor: 0 +Factory Part Num: 40-1000927-06 +Factory Serial Num: CPL2541K01E +Manufacture: Day: 11 Month: 8 Year: 14 +Update: Day: 18 Month: 7 Year: 2018 +Time Alive: 1116 days +Time Awake: 0 days + +Airflow direction : Port side INTAKE diff --git a/test/units/plugins/cliconf/fixtures/nos/show_running-config b/test/units/plugins/cliconf/fixtures/nos/show_running-config new file mode 100644 index 0000000000..8a4f631fe2 --- /dev/null +++ b/test/units/plugins/cliconf/fixtures/nos/show_running-config @@ -0,0 +1,549 @@ +diag post rbridge-id 104 enable +ntp server 10.10.10.1 use-vrf mgmt-vrf +logging raslog console INFO +logging auditlog class SECURITY +logging auditlog class CONFIGURATION +logging auditlog class FIRMWARE +logging syslog-facility local LOG_LOCAL7 +logging syslog-client localip CHASSIS_IP +switch-attributes 104 + chassis-name VDX6740 + host-name LEAF4 +! +no support autoupload enable +line vty + exec-timeout 10 +! +zoning enabled-configuration cfg-name "" +zoning enabled-configuration default-zone-access allaccess +zoning enabled-configuration cfg-action cfg-save +dpod 104/0/1 + reserve +! +dpod 104/0/2 +! +dpod 104/0/3 +! +dpod 104/0/4 +! +dpod 104/0/5 +! +dpod 104/0/6 +! +dpod 104/0/7 +! +dpod 104/0/8 +! +dpod 104/0/9 +! +dpod 104/0/10 +! +dpod 104/0/11 +! +dpod 104/0/12 +! +dpod 104/0/13 +! +dpod 104/0/14 +! +dpod 104/0/15 +! +dpod 104/0/16 +! +dpod 104/0/17 +! +dpod 104/0/18 +! +dpod 104/0/19 +! +dpod 104/0/20 +! +dpod 104/0/21 +! +dpod 104/0/22 +! +dpod 104/0/23 +! +dpod 104/0/24 +! +dpod 104/0/25 +! +dpod 104/0/26 +! +dpod 104/0/27 +! +dpod 104/0/28 +! +dpod 104/0/29 +! +dpod 104/0/30 +! +dpod 104/0/31 +! +dpod 104/0/32 +! +dpod 104/0/33 +! +dpod 104/0/34 +! +dpod 104/0/35 +! +dpod 104/0/36 +! +dpod 104/0/37 +! +dpod 104/0/38 +! +dpod 104/0/39 +! +dpod 104/0/40 +! +dpod 104/0/41 +! +dpod 104/0/42 +! +dpod 104/0/43 +! +dpod 104/0/44 +! +dpod 104/0/45 +! +dpod 104/0/46 +! +dpod 104/0/47 +! +dpod 104/0/48 +! +dpod 104/0/49 +! +dpod 104/0/50 +! +dpod 104/0/51 +! +dpod 104/0/52 +! +role name admin desc Administrator +role name user desc User +aaa authentication login local +aaa accounting exec default start-stop none +aaa accounting commands default start-stop none +service password-encryption +username admin password "BwrsDbB+tABWGWpINOVKoQ==\n" encryption-level 7 role admin desc Administrator +username user password "BwrsDbB+tABWGWpINOVKoQ==\n" encryption-level 7 role user desc User +ip access-list extended test + seq 10 permit ip host 1.1.1.1 any log +! +snmp-server contact "Field Support." +snmp-server location "End User Premise." +snmp-server sys-descr "Extreme VDX Switch." +snmp-server enable trap +snmp-server community private groupname admin +snmp-server community public groupname user +snmp-server view All 1 included +snmp-server group admin v1 read All write All notify All +snmp-server group public v1 read All +snmp-server group public v2c read All +snmp-server group user v1 read All +snmp-server group user v2c read All +hardware + connector-group 104/0/1 + speed LowMixed + ! + connector-group 104/0/3 + speed LowMixed + ! + connector-group 104/0/5 + speed LowMixed + ! + connector-group 104/0/6 + speed LowMixed + ! +! +cee-map default + precedence 1 + priority-group-table 1 weight 40 pfc on + priority-group-table 15.0 pfc off + priority-group-table 15.1 pfc off + priority-group-table 15.2 pfc off + priority-group-table 15.3 pfc off + priority-group-table 15.4 pfc off + priority-group-table 15.5 pfc off + priority-group-table 15.6 pfc off + priority-group-table 15.7 pfc off + priority-group-table 2 weight 60 pfc off + priority-table 2 2 2 1 2 2 2 15.0 + remap fabric-priority priority 0 + remap lossless-priority priority 0 +! +fcoe + fabric-map default + vlan 1002 + san-mode local + priority 3 + virtual-fabric 128 + fcmap 0E:FC:00 + advertisement interval 8000 + keep-alive timeout + ! +! +interface Vlan 1 +! +fabric route mcast rbridge-id 104 +! +protocol lldp + advertise dcbx-fcoe-app-tlv + advertise dcbx-fcoe-logical-link-tlv + advertise dcbx-tlv + advertise bgp-auto-nbr-tlv + advertise optional-tlv management-address + advertise optional-tlv system-name + system-description Extreme-VDX-VCS 120 +! +vlan dot1q tag native +port-profile UpgradedVlanProfile + vlan-profile + switchport + switchport mode trunk + switchport trunk allowed vlan all + ! +! +port-profile default + vlan-profile + switchport + switchport mode trunk + switchport trunk native-vlan 1 + ! +! +port-profile-domain default + port-profile UpgradedVlanProfile +! +class-map cee +! +class-map default +! +rbridge-id 104 + switch-attributes chassis-name VDX6740 + switch-attributes host-name LEAF4 + vrf mgmt-vrf + address-family ipv4 unicast + ip route 0.0.0.0/0 10.26.0.1 + ! + address-family ipv6 unicast + ! + ! + system-monitor fan threshold marginal-threshold 1 down-threshold 2 + system-monitor fan alert state removed action raslog + system-monitor power threshold marginal-threshold 1 down-threshold 2 + system-monitor power alert state removed action raslog + system-monitor temp threshold marginal-threshold 1 down-threshold 2 + system-monitor cid-card threshold marginal-threshold 1 down-threshold 2 + system-monitor cid-card alert state none action none + system-monitor sfp alert state none action none + system-monitor compact-flash threshold marginal-threshold 1 down-threshold 0 + system-monitor MM threshold marginal-threshold 1 down-threshold 0 + system-monitor LineCard threshold marginal-threshold 1 down-threshold 2 + system-monitor LineCard alert state none action none + system-monitor SFM threshold marginal-threshold 1 down-threshold 2 + resource-monitor cpu enable + resource-monitor memory enable threshold 100 action raslog + resource-monitor process memory enable alarm 500 critical 600 + no protocol vrrp + no protocol vrrp-extended + hardware-profile tcam default + hardware-profile route-table default maximum_paths 8 openflow off + hardware-profile kap default + fabric neighbor-discovery + clock timezone America/Los_Angeles + ag + enable + counter reliability 25 + timeout fnm 120 + pg 0 + modes lb + rename pg0 + ! + ! + telnet server use-vrf default-vrf + telnet server use-vrf mgmt-vrf + ssh server key rsa 2048 + ssh server key ecdsa 256 + ssh server key dsa + ssh server use-vrf default-vrf + ssh server use-vrf mgmt-vrf + http server use-vrf default-vrf + http server use-vrf mgmt-vrf + fcoe + fcoe-enodes 0 + ! +! +interface Management 104/0 + no tcp burstrate + ip icmp echo-reply + no ip address dhcp + ip address 10.26.7.226/17 + ipv6 icmpv6 echo-reply + no ipv6 address autoconfig + no ipv6 address dhcp + vrf forwarding mgmt-vrf + no shutdown +! +interface TenGigabitEthernet 104/0/1 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/2 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/3 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/4 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/5 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/6 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/7 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/8 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/9 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/10 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/11 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/12 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/13 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/14 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/15 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/16 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/17 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/18 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/19 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/20 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/21 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/22 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/23 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/24 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/25 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/26 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/27 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/28 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/29 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/30 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/31 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/32 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/33 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/34 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/35 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/36 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/37 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/38 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/39 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/40 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/41 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/42 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/43 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/44 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/45 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/46 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/47 + fabric isl enable + fabric trunk enable + no shutdown +! +interface TenGigabitEthernet 104/0/48 + fabric isl enable + fabric trunk enable + no shutdown +! +interface FortyGigabitEthernet 104/0/49 + fabric isl enable + fabric trunk enable + no shutdown +! +interface FortyGigabitEthernet 104/0/50 + fabric isl enable + fabric trunk enable + no shutdown +! +interface FortyGigabitEthernet 104/0/51 + fabric isl enable + fabric trunk enable + no shutdown +! +interface FortyGigabitEthernet 104/0/52 + fabric isl enable + fabric trunk enable + no shutdown +! diff --git a/test/units/plugins/cliconf/fixtures/nos/show_version b/test/units/plugins/cliconf/fixtures/nos/show_version new file mode 100644 index 0000000000..1accd8191e --- /dev/null +++ b/test/units/plugins/cliconf/fixtures/nos/show_version @@ -0,0 +1,17 @@ +Network Operating System Software +Network Operating System Version: 7.2.0 +Copyright (c) 1995-2017 Brocade Communications Systems, Inc. +Firmware name: 7.2.0 +Build Time: 10:52:47 Jul 10, 2017 +Install Time: 01:32:03 Jan 5, 2018 +Kernel: 2.6.34.6 + +BootProm: 1.0.1 +Control Processor: e500mc with 4096 MB of memory + +Slot Name Primary/Secondary Versions Status +--------------------------------------------------------------------------- +SW/0 NOS 7.2.0 ACTIVE* + 7.2.0 +SW/1 NOS 7.2.0 STANDBY + 7.2.0 diff --git a/test/units/plugins/cliconf/test_nos.py b/test/units/plugins/cliconf/test_nos.py new file mode 100644 index 0000000000..4f8ee41007 --- /dev/null +++ b/test/units/plugins/cliconf/test_nos.py @@ -0,0 +1,136 @@ +# +# (c) 2018 Extreme Networks 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 + +from os import path +import json + +from mock import MagicMock, call + +from ansible.compat.tests import unittest +from ansible.plugins.cliconf import nos + +FIXTURE_DIR = b'%s/fixtures/nos' % ( + path.dirname(path.abspath(__file__)).encode('utf-8') +) + + +def _connection_side_effect(*args, **kwargs): + try: + if args: + value = args[0] + else: + value = kwargs.get('command') + + fixture_path = path.abspath( + b'%s/%s' % (FIXTURE_DIR, b'_'.join(value.split(b' '))) + ) + with open(fixture_path, 'rb') as file_desc: + return file_desc.read() + except (OSError, IOError): + if args: + value = args[0] + return value + elif kwargs.get('command'): + value = kwargs.get('command') + return value + + return 'Nope' + + +class TestPluginCLIConfNOS(unittest.TestCase): + """ Test class for NOS CLI Conf Methods + """ + def setUp(self): + self._mock_connection = MagicMock() + self._mock_connection.send.side_effect = _connection_side_effect + self._cliconf = nos.Cliconf(self._mock_connection) + self.maxDiff = None + + def tearDown(self): + pass + + def test_get_device_info(self): + """ Test get_device_info + """ + device_info = self._cliconf.get_device_info() + + mock_device_info = { + 'network_os': 'nos', + 'network_os_model': 'BR-VDX6740', + 'network_os_version': '7.2.0', + } + + self.assertEqual(device_info, mock_device_info) + + def test_get_config(self): + """ Test get_config + """ + running_config = self._cliconf.get_config() + + fixture_path = path.abspath(b'%s/show_running-config' % FIXTURE_DIR) + with open(fixture_path, 'rb') as file_desc: + mock_running_config = file_desc.read() + self.assertEqual(running_config, mock_running_config) + + def test_edit_config(self): + """ Test edit_config + """ + test_config_command = b'this\nis\nthe\nsong\nthat\nnever\nends' + + self._cliconf.edit_config(test_config_command) + + send_calls = [] + + for command in [b'configure terminal', test_config_command, b'end']: + send_calls.append(call( + command=command, + prompt_retry_check=False, + sendonly=False, + newline=True + )) + + self._mock_connection.send.assert_has_calls(send_calls) + + def test_get_capabilities(self): + """ Test get_capabilities + """ + capabilities = json.loads(self._cliconf.get_capabilities()) + mock_capabilities = { + 'network_api': 'cliconf', + 'rpc': [ + 'get_config', + 'edit_config', + 'get_capabilities', + 'get', + 'enable_response_logging', + 'disable_response_logging' + ], + 'device_info': { + 'network_os_model': 'BR-VDX6740', + 'network_os_version': '7.2.0', + 'network_os': 'nos' + } + } + + self.assertEqual( + mock_capabilities, + capabilities + )