From 0b98fa33a93777a58005cd58ca6adac11059d982 Mon Sep 17 00:00:00 2001 From: chunwei li Date: Thu, 14 Feb 2019 10:20:01 -0800 Subject: [PATCH 01/17] Add APCON modules --- .../module_utils/network/apconos/__init__.py | 0 .../module_utils/network/apconos/apconos.py | 156 ++++++++++++ .../modules/network/apconos/__init__.py | 0 .../modules/network/apconos/apconos_cert.py | 171 +++++++++++++ .../network/apconos/apconos_command.py | 193 +++++++++++++++ .../modules/network/apconos/apconos_update.py | 227 ++++++++++++++++++ lib/ansible/plugins/action/apconos.py | 81 +++++++ lib/ansible/plugins/action/apconos_config.py | 108 +++++++++ lib/ansible/plugins/cliconf/apconos.py | 64 +++++ lib/ansible/plugins/terminal/apconos.py | 40 +++ 10 files changed, 1040 insertions(+) create mode 100644 lib/ansible/module_utils/network/apconos/__init__.py create mode 100644 lib/ansible/module_utils/network/apconos/apconos.py create mode 100644 lib/ansible/modules/network/apconos/__init__.py create mode 100644 lib/ansible/modules/network/apconos/apconos_cert.py create mode 100644 lib/ansible/modules/network/apconos/apconos_command.py create mode 100644 lib/ansible/modules/network/apconos/apconos_update.py create mode 100644 lib/ansible/plugins/action/apconos.py create mode 100644 lib/ansible/plugins/action/apconos_config.py create mode 100644 lib/ansible/plugins/cliconf/apconos.py create mode 100644 lib/ansible/plugins/terminal/apconos.py diff --git a/lib/ansible/module_utils/network/apconos/__init__.py b/lib/ansible/module_utils/network/apconos/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/lib/ansible/module_utils/network/apconos/apconos.py b/lib/ansible/module_utils/network/apconos/apconos.py new file mode 100644 index 00000000000000..78a469a9d2c109 --- /dev/null +++ b/lib/ansible/module_utils/network/apconos/apconos.py @@ -0,0 +1,156 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by +# Ansible still belong to the author of the module, and may assign their own +# license to the complete work. +# +# Copyright (C) 2018 APCON, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# Contains utility methods +# APCON Networking + +import time +import socket +import re +from distutils.cmd import Command +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, EntityCollection +from ansible.module_utils.connection import Connection, exec_command +from ansible.module_utils.connection import ConnectionError + +_DEVICE_CONFIGS = {} +_CONNECTION = None + +apconos_provider_spec = { + 'host': dict(), + 'port': dict(type='int'), + 'username': dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + 'password': dict(fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD']), + no_log=True), + 'ssh_keyfile': dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), + type='path'), + 'authorize': dict(fallback=(env_fallback, ['ANSIBLE_NET_AUTHORIZE']), + type='bool'), + 'auth_pass': dict(fallback=(env_fallback, ['ANSIBLE_NET_AUTH_PASS']), + no_log=True), + 'timeout': dict(type='int'), + 'context': dict(), + 'passwords': dict() +} + +apconos_argument_spec = { + 'host': dict(), + 'port': dict(type='int'), + 'username': dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + 'password': dict(fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD']), + no_log=True), + 'ssh_keyfile': dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), + type='path'), + 'authorize': dict(fallback=(env_fallback, ['ANSIBLE_NET_AUTHORIZE']), + type='bool'), + 'auth_pass': dict(fallback=(env_fallback, ['ANSIBLE_NET_AUTH_PASS']), + no_log=True), + 'timeout': dict(type='int'), + 'context': dict(), + 'passwords': dict(), + 'provider': dict(type='dict', options=apconos_provider_spec), +} + +command_spec = { + 'command': dict(key=True), + 'prompt': dict(), + 'answer': dict(), + 'check_all': dict() +} + + +def get_provider_argspec(): + return apconos_provider_spec + + +def check_args(module, warnings): + pass + + +def get_connection(module): + global _CONNECTION + if _CONNECTION: + return _CONNECTION + _CONNECTION = Connection(module._socket_path) + + return _CONNECTION + + +def get_config(module, flags=None): + flags = [] if flags is None else flags + + cmd += ' '.join(flags) + cmd = cmd.strip() + + try: + return _DEVICE_CONFIGS[cmd] + except KeyError: + conn = get_connection(module) + out = conn.get(cmd) + cfg = to_text(out, errors='surrogate_then_replace').strip() + _DEVICE_CONFIGS[cmd] = cfg + return cfg + +def run_commands(module, commands, check_rc=True): + connection = get_connection(module) + transform = EntityCollection(module, command_spec) + commands = transform(commands) + + responses = list() + + for cmd in commands: + out = connection.get(**cmd) + responses.append(to_text(out, errors='surrogate_then_replace')) + + return responses + +def load_config(module, config): + try: + conn = get_connection(module) + conn.get('enable') + conn.edit_config(config) + except ConnectionError as exc: + module.fail_json(msg=to_text(exc)) + +def get_defaults_flag(module): + rc, out, err = exec_command(module, 'display running-config ?') + out = to_text(out, errors='surrogate_then_replace') + + commands = set() + for line in out.splitlines(): + if line: + commands.add(line.strip().split()[0]) + + if 'all' in commands: + return 'all' + else: + return 'full' + diff --git a/lib/ansible/modules/network/apconos/__init__.py b/lib/ansible/modules/network/apconos/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/lib/ansible/modules/network/apconos/apconos_cert.py b/lib/ansible/modules/network/apconos/apconos_cert.py new file mode 100644 index 00000000000000..c42ec194162184 --- /dev/null +++ b/lib/ansible/modules/network/apconos/apconos_cert.py @@ -0,0 +1,171 @@ +#!/usr/bin/python + +# (c) 2019, Ansible by Red Hat, inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'network'} + +DOCUMENTATION = """ +--- +module: apconos_cert +version_added: "" +author: "" +short_description: install ssl ipv4 certificate on apcon network devices +description: + - import and install ssl ipv4 certificate with specifying remote filename +notes: + - tested against apcon iis+ii +options: + ipaddress: + description: + - specify a remote ip address at which tftp server resides. + filename: + description: + - specify a remote file name which resides on a remote tftp server. + 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. + 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. + 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. + 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. + default: 1 +""" + +EXAMPLES = """ +- name: Install SSL Certificate + update_cert: + ipaddress: 10.0.0.100 + filename: remoteSSLCert.pem +""" + +RETURN = """ +""" + +import re +import time + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import remove_default_spec +from ansible.module_utils.network.apconos.apconos import load_config, run_commands +from ansible.module_utils.network.apconos.apconos import apconos_argument_spec, check_args +from ansible.module_utils.six import string_types +from ansible.utils.display import Display + +display = Display() + +def to_lines(stdout): + for item in stdout: + if isinstance(item, string_types): + item = str(item).split('\n') + yield item + +def construct_update_command(module): + """construct update command + """ + #command = ['update'] + command = module.params['command'] + ipaddress = module.params['ipaddress'] + filename = module.params['filename'] + command[0] = 'tftp put ssl ipv4 ' + ipaddress[0] + ' ' + filename[0]+time.strftime("%d_%b+%H:%M:%S", time.gmtime()) + command.append('tftp get ssl ipv4 ' + ipaddress[0] + ' ' + filename[0]) + + return command + +def main(): + """ main entry point for module execution + """ + spec = dict( + wait_for=dict(type='list'), + match=dict(default='all', choices=['all', 'any']), + + retries=dict(default=10, type='int'), + interval=dict(default=1, type='int'), + + ipaddress=dict(type='list'), + filename=dict(type='list'), + command=dict(type='list')) + + spec.update(apconos_argument_spec) + + module = AnsibleModule(argument_spec=spec, supports_check_mode=True) + result = {'changed': False} + + 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, construct_update_command(module)) + + 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) + + for item in responses: + if len(item) == 0: + result.update({ + 'changed': True, + 'stdout': responses, + 'stdout_lines': list(to_lines(responses)) + }) + elif 'ERROR' in item: + result.update({ + 'failed': True, + 'stdout': responses, + 'stdout_lines': list(to_lines(responses)) + }) + else: + result.update({ + 'stdout': item, + 'stdout_lines': list(to_lines(responses)) + }) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/network/apconos/apconos_command.py b/lib/ansible/modules/network/apconos/apconos_command.py new file mode 100644 index 00000000000000..ffb59e85e6e81f --- /dev/null +++ b/lib/ansible/modules/network/apconos/apconos_command.py @@ -0,0 +1,193 @@ +#!/usr/bin/python +# +# Copyright (C) 2018 Apcon. +# +# GNU General Public License v3.0+ +# +# This program 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. +# +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Module to execute apconos Commands on Apcon Switches. +# Apcon Networking + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = """ +--- +module: apconos_command +version_added: "" +author: "" +short_description: Run arbitrary commands on APCON devices +description: + - Sends arbitrary commands to an apcon device and returns the results + read from the device. The 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. +extends_documentation_fragment: apconos +options: + commands: + version_added: "" + description: + - List of commands to send to the remote 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 retires as expired. + required: true + wait_for: + version_added: "" + 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. + match: + version_added: "" + 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. + default: all + choices: ['any', 'all'] + retries: + version_added: "" + 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. + default: 10 + interval: + version_added: "" + 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. + default: 1 +""" + +EXAMPLES = """ +--- +vars: + cli: + host: "{{ inventory_hostname }}" + port: 22 + username: admin + password: admin + timeout: 30 + +--- +- name: test contains operator + apconos_command: + commands: + - show version + register: result + +- name: get output for single command + apconos_command: + commands: ['show version'] + provider: "{{ cli }}" + register: result +""" + +RETURN = """ +""" + +import time + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.apconos.apconos import run_commands, check_args +from ansible.module_utils.network.apconos.apconos import apconos_argument_spec +from ansible.module_utils.network.common.parsing import Conditional +from ansible.module_utils.six import string_types + + +def to_lines(stdout): + for item in stdout: + if isinstance(item, string_types): + item = str(item).split('\n') + yield item + + +def main(): + 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') + ) + + spec.update(apconos_argument_spec) + + module = AnsibleModule(argument_spec=spec, supports_check_mode=True) + result = {'changed': False} + + wait_for = module.params['wait_for'] or list() + conditionals = [Conditional(c) for c in wait_for] + + commands = module.params['commands'] + 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) + + for item in responses: + if len(item) == 0: + result.update({ + 'changed': True, + 'stdout': responses, + 'stdout_lines': list(to_lines(responses)) + }) + elif 'ERROR' in item: + result.update({ + 'failed': True, + 'stdout': responses, + 'stdout_lines': list(to_lines(responses)) + }) + else: + result.update({ + 'stdout': item, + 'stdout_lines': list(to_lines(responses)) + }) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/network/apconos/apconos_update.py b/lib/ansible/modules/network/apconos/apconos_update.py new file mode 100644 index 00000000000000..c8c789e3180c40 --- /dev/null +++ b/lib/ansible/modules/network/apconos/apconos_update.py @@ -0,0 +1,227 @@ +#!/usr/bin/python + +# (c) 2019, Ansible by Red Hat, inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'network'} + +DOCUMENTATION = """ +--- +module: apconos_update +version_added: "" +author: "" +short_description: update firmware on apcon network devices +description: + - Sends update commands to APCON Network devices. Commands check + current versions and statues on the devices to decide if update + should be executed. +options: + device: + description: + - specify a device. Valid values are [all], [blade]. + Choosing [all] will sequentially update software on + both controllers, all blades and the backplane from a tftp server. + Choosing [blade] will download the software from the controller + and update the software for any APCON blades. If an tftp server + address is provided, [blade] option will downloads the software from + a TFTP server for a special blade. + default: all + choices: ['all', 'blade'] + blade_letter: + description: + - specify a blade letter. + ipaddress: + description: + - specify an ip address of tftp server. + filename: + description: + - specify a file name of firmware. + 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. + 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. + 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. + 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. + default: 1 +""" + +EXAMPLES = """ +- name: Update APCON Devices + update_update: + device: all + ipaddress: 10.0.0.100 + filename: firmware_6.01_1550.pem +""" + +RETURN = """ +""" + +import re +import time + +from copy import deepcopy + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import remove_default_spec +from ansible.module_utils.network.apconos.apconos import load_config, run_commands +from ansible.module_utils.network.apconos.apconos import apconos_argument_spec, check_args +from ansible.module_utils.six import string_types +from ansible.utils.display import Display + +display = Display() + +def to_lines(stdout): + for item in stdout: + if isinstance(item, string_types): + item = str(item).split('\n') + yield item + +def check_version(module): + """check if current version is higher. + """ + version = module.params['version'] + responses = run_commands(module, ['show version']) + cur_version = list(to_lines(responses))[0][1][-5:-1] + + if version is None: + return cur_version, True + else: + return cur_version, (version[0]) > (cur_version) + +def construct_update_command(module): + """construct update command + """ + command = ['update'] + device = module.params['device'] + blade_letter = module.params['blade_letter'] + ipaddress = module.params['ipaddress'] + filename = module.params['filename'] + if device[0] == 'all': + command[0] = command[0] + ' ' + device[0] + ' ' \ + + ipaddress[0] + ' ' + filename[0] + ' false' + #elif device[0] == 'backplane': + # command[0] = command[0] + ' device backplane' + elif device[0] == 'blade': + #if ipaddress == None: + # command[0] = command[0] + ' ' + ' device blade ' + blade_letter[0] + ' false' + #else: + if ipaddress and blade_letter and filename: + command[0] = command[0] + ' ' + ' device blade standalone ' + blade_letter[0] \ + + ' ' + ipaddress[0] + ' ' + filename[0] + ' false' + #elif device[0] == 'controller': + # command[0] = command[0] + ' standby controller' + + return command + +def main(): + """ main entry point for module execution + """ + spec = dict( + wait_for=dict(type='list'), + match=dict(default='all', choices=['all', 'any']), + + retries=dict(default=10, type='int'), + interval=dict(default=1, type='int'), + + device=dict(type='list'), + blade_letter=dict(default='A', type='list'), + ipaddress=dict(type='list'), + filename=dict(type='list'), + version=dict(type='list'), + ) + + spec.update(apconos_argument_spec) + + module = AnsibleModule(argument_spec=spec, supports_check_mode=True) + result = {'changed': False} + + 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'] + + responses = None + cur_version, cond = check_version(module) + while cond and retries > 0: + responses = run_commands(module, construct_update_command(module)) + + 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) + + if responses is not None: + for item in responses: + if len(item) == 0: + result.update({ + 'changed': True, + 'stdout': responses, + 'current version': cur_version, + 'stdout_lines': list(to_lines(responses)) + }) + elif 'ERROR' in item: + result.update({ + 'failed': True, + 'stdout': responses, + 'current version': cur_version, + 'stdout_lines': list(to_lines(responses)) + }) + else: + result.update({ + 'stdout': item, + 'current version': cur_version, + 'stdout_lines': list(to_lines(responses)) + }) + else: + msg = 'The current version is newer' + module.fail_json(msg=msg) + result.update({ + 'current version': cur_version, + }) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/plugins/action/apconos.py b/lib/ansible/plugins/action/apconos.py new file mode 100644 index 00000000000000..85f76670b37ef5 --- /dev/null +++ b/lib/ansible/plugins/action/apconos.py @@ -0,0 +1,81 @@ +# Copyright (C) 2018 APCON. +# +# GNU General Public License v3.0+ +# +# This program 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. +# +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Contains Action Plugin methods for apconos Config Module +# APCON Networking +# + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import sys +import copy + +from ansible import constants as C +from ansible.plugins.action.normal import ActionModule as _ActionModule +from ansible.module_utils.network.apconos.apconos import apconos_provider_spec +from ansible.module_utils.network.common.utils import load_provider +from ansible.module_utils.connection import Connection +from ansible.module_utils._text import to_text +from ansible.utils.display import Display + +display = Display() + + +class ActionModule(_ActionModule): + + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + socket_path = None + if self._play_context.connection == 'local': + provider = load_provider(apconos_provider_spec, self._task.args) + pc = copy.deepcopy(self._play_context) + pc.connection = 'network_cli' + pc.network_os = 'apconos' + pc.remote_addr = provider['host'] or self._play_context.remote_addr + pc.port = provider['port'] or self._play_context.port or 22 + pc.remote_user = provider['username'] or self._play_context.connection_user + pc.password = provider['password'] or self._play_context.password + pc.private_key_file = provider['ssh_keyfile'] or self._play_context.private_key_file + command_timeout = int(provider['timeout'] or C.PERSISTENT_COMMAND_TIMEOUT) + pc.become = provider['authorize'] or True + pc.become_pass = provider['auth_pass'] + pc.become_method = 'enable' + + 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) + connection.set_options(direct={'persistent_command_timeout': command_timeout}) + + 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 or exec mode + if socket_path is None: + socket_path = self._connection.socket_path + + conn = Connection(socket_path) + out = conn.get_prompt() + if to_text(out, errors='surrogate_then_replace').strip().endswith(')#'): + display.vvvv('In Config mode, sending exit to device', self._play_context.remote_addr) + conn.send_command('exit') + else: + conn.send_command('enable') + + result = super(ActionModule, self).run(task_vars=task_vars) + return result diff --git a/lib/ansible/plugins/action/apconos_config.py b/lib/ansible/plugins/action/apconos_config.py new file mode 100644 index 00000000000000..3f054a9e05ef1c --- /dev/null +++ b/lib/ansible/plugins/action/apconos_config.py @@ -0,0 +1,108 @@ +# Copyright (C) 2017 APCON. +# +# GNU General Public License v3.0+ +# +# This program 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. +# +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Contains Action Plugin methods for apconos Config Module +# APCON Networking +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import re +import time +import glob + +from ansible.plugins.action.apconos import ActionModule as _ActionModule +from ansible.module_utils._text import to_text +from ansible.module_utils.six.moves.urllib.parse import urlsplit +from ansible.utils.vars import merge_hash + +PRIVATE_KEYS_RE = re.compile('__.+__') + + +class ActionModule(_ActionModule): + + def run(self, tmp=None, task_vars=None): + + if self._task.args.get('src'): + try: + self._handle_template() + except ValueError as exc: + return dict(failed=True, msg=to_text(exc)) + + result = super(ActionModule, self).run(tmp, task_vars) + del tmp # tmp no longer has any effect + + if self._task.args.get('backup') and result.get('__backup__'): + # User requested backup and no error occurred in module. + # NOTE: If there is a parameter error, _backup key may not be in results. + filepath = self._write_backup(task_vars['inventory_hostname'], + result['__backup__']) + + result['backup_path'] = filepath + + # strip out any keys that have two leading and two trailing + # underscore characters + for key in list(result): + if PRIVATE_KEYS_RE.match(key): + del result[key] + + return result + + def _get_working_path(self): + cwd = self._loader.get_basedir() + if self._task._role is not None: + cwd = self._task._role._role_path + return cwd + + def _write_backup(self, host, contents): + backup_path = self._get_working_path() + '/backup' + if not os.path.exists(backup_path): + os.mkdir(backup_path) + for fn in glob.glob('%s/%s*' % (backup_path, host)): + os.remove(fn) + tstamp = time.strftime("%Y-%m-%d@%H:%M:%S", time.localtime(time.time())) + filename = '%s/%s_config.%s' % (backup_path, host, tstamp) + open(filename, 'w').write(contents) + return filename + + def _handle_template(self): + src = self._task.args.get('src') + working_path = self._get_working_path() + + if os.path.isabs(src) or urlsplit('src').scheme: + source = src + else: + source = self._loader.path_dwim_relative(working_path, 'templates', src) + if not source: + source = self._loader.path_dwim_relative(working_path, src) + + if not os.path.exists(source): + raise ValueError('path specified in src not found') + + try: + with open(source, 'r') as f: + template_data = to_text(f.read()) + except IOError: + return dict(failed=True, msg='unable to load src file') + + # Create a template search path in the following order: + # [working_path, self_role_path, dependent_role_paths, dirname(source)] + searchpath = [working_path] + if self._task._role is not None: + searchpath.append(self._task._role._role_path) + if hasattr(self._task, "_block:"): + dep_chain = self._task._block.get_dep_chain() + if dep_chain is not None: + for role in dep_chain: + searchpath.append(role._role_path) + searchpath.append(os.path.dirname(source)) + self._templar.environment.loader.searchpath = searchpath + self._task.args['src'] = self._templar.template(template_data) diff --git a/lib/ansible/plugins/cliconf/apconos.py b/lib/ansible/plugins/cliconf/apconos.py new file mode 100644 index 00000000000000..15ef336279f453 --- /dev/null +++ b/lib/ansible/plugins/cliconf/apconos.py @@ -0,0 +1,64 @@ +# (C) 2018 Red Hat Inc. +# Copyright (C) 2018 APCON. +# +# GNU General Public License v3.0+ +# +# This program 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. +# +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Contains CLIConf Plugin methods for apconos Modules +# APCON Networking +# +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'] = 'apconos' + reply = self.get(b'show version') + data = to_text(reply, errors='surrogate_or_strict').strip() + if data: + device_info['network_os_version'] = self.parse_version(data) + device_info['network_os_model'] = self.parse_model(data) + + return device_info + + def parse_version(self, data): + return "" + + def parse_model(self, data): + return "" + + @enable_mode + def get_config(self, source='running', format='text'): + cmd = b'' + 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, check_all=False): + return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, check_all=check_all) + + def get_capabilities(self): + result = {} + return json.dumps(result) diff --git a/lib/ansible/plugins/terminal/apconos.py b/lib/ansible/plugins/terminal/apconos.py new file mode 100644 index 00000000000000..dc5c3aae9900b6 --- /dev/null +++ b/lib/ansible/plugins/terminal/apconos.py @@ -0,0 +1,40 @@ +# (C) 2017 Red Hat Inc. +# Copyright (C) 2018 APCON. +# +# GNU General Public License v3.0+ +# +# This program 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. +# +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Contains terminal Plugin methods for apconos Config Module +# Apcon Networking +# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import json +import re + +from ansible.errors import AnsibleConnectionFailure +from ansible.module_utils._text import to_text, to_bytes +from ansible.plugins.terminal import TerminalBase + + +class TerminalModule(TerminalBase): + + terminal_stdout_re = [ + # re.compile(br"[\r\n]?[\w+\-\.:\/\[\]]+(?:\([^\)]+\)){,3}(?:>|#) ?$"), + # re.compile(br"\[\w+\@[\w\-\.]+(?: [^\]])\] ?[>#\$] ?$"), + # re.compile(br">[\r\n]?") + re.compile(br".*>>") + + ] + + terminal_stderr_re = [ + re.compile(br"connection timed out", re.I), + ] + From c7ba2a0f560cf432b87f2feeba6d860044717e0e Mon Sep 17 00:00:00 2001 From: chunwei li Date: Tue, 28 May 2019 10:41:26 -0700 Subject: [PATCH 02/17] Fix cli regex --- lib/ansible/plugins/terminal/apconos.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/ansible/plugins/terminal/apconos.py b/lib/ansible/plugins/terminal/apconos.py index dc5c3aae9900b6..84273358b76b54 100644 --- a/lib/ansible/plugins/terminal/apconos.py +++ b/lib/ansible/plugins/terminal/apconos.py @@ -27,10 +27,8 @@ class TerminalModule(TerminalBase): terminal_stdout_re = [ - # re.compile(br"[\r\n]?[\w+\-\.:\/\[\]]+(?:\([^\)]+\)){,3}(?:>|#) ?$"), - # re.compile(br"\[\w+\@[\w\-\.]+(?: [^\]])\] ?[>#\$] ?$"), - # re.compile(br">[\r\n]?") - re.compile(br".*>>") + re.compile(br'>>\ |#\ |\$\ ') + ] From 73ac20dd8402c036b66b207c4e61b3279477a843 Mon Sep 17 00:00:00 2001 From: chunwei li Date: Wed, 29 May 2019 13:55:40 -0700 Subject: [PATCH 03/17] Pass Sanity Test --- .../module_utils/network/apconos/apconos.py | 9 +- .../modules/network/apconos/apconos_cert.py | 41 ++++--- .../network/apconos/apconos_command.py | 16 ++- .../modules/network/apconos/apconos_config.py | 36 +++++++ .../modules/network/apconos/apconos_update.py | 73 ++++++++----- lib/ansible/plugins/action/apconos.py | 6 -- lib/ansible/plugins/cliconf/apconos.py | 13 ++- lib/ansible/plugins/doc_fragments/apconos.py | 101 ++++++++++++++++++ lib/ansible/plugins/terminal/apconos.py | 5 +- test/sanity/ansible-doc/skip.txt | 1 + test/sanity/code-smell/action-plugin-docs.py | 1 + 11 files changed, 236 insertions(+), 66 deletions(-) create mode 100644 lib/ansible/modules/network/apconos/apconos_config.py create mode 100644 lib/ansible/plugins/doc_fragments/apconos.py diff --git a/lib/ansible/module_utils/network/apconos/apconos.py b/lib/ansible/module_utils/network/apconos/apconos.py index 78a469a9d2c109..c5a54d44862487 100644 --- a/lib/ansible/module_utils/network/apconos/apconos.py +++ b/lib/ansible/module_utils/network/apconos/apconos.py @@ -36,7 +36,7 @@ import re from distutils.cmd import Command from ansible.module_utils._text import to_text -from ansible.module_utils.basic import env_fallback, return_values +from ansible.module_utils.basic import env_fallback from ansible.module_utils.network.common.utils import to_list, EntityCollection from ansible.module_utils.connection import Connection, exec_command from ansible.module_utils.connection import ConnectionError @@ -107,8 +107,7 @@ def get_connection(module): def get_config(module, flags=None): flags = [] if flags is None else flags - cmd += ' '.join(flags) - cmd = cmd.strip() + cmd = ' '.join(flags).strip() try: return _DEVICE_CONFIGS[cmd] @@ -119,6 +118,7 @@ def get_config(module, flags=None): _DEVICE_CONFIGS[cmd] = cfg return cfg + def run_commands(module, commands, check_rc=True): connection = get_connection(module) transform = EntityCollection(module, command_spec) @@ -132,6 +132,7 @@ def run_commands(module, commands, check_rc=True): return responses + def load_config(module, config): try: conn = get_connection(module) @@ -140,6 +141,7 @@ def load_config(module, config): except ConnectionError as exc: module.fail_json(msg=to_text(exc)) + def get_defaults_flag(module): rc, out, err = exec_command(module, 'display running-config ?') out = to_text(out, errors='surrogate_then_replace') @@ -153,4 +155,3 @@ def get_defaults_flag(module): return 'all' else: return 'full' - diff --git a/lib/ansible/modules/network/apconos/apconos_cert.py b/lib/ansible/modules/network/apconos/apconos_cert.py index c42ec194162184..127b2948a2d481 100644 --- a/lib/ansible/modules/network/apconos/apconos_cert.py +++ b/lib/ansible/modules/network/apconos/apconos_cert.py @@ -1,17 +1,31 @@ #!/usr/bin/python - -# (c) 2019, Ansible by Red Hat, inc -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Copyright (C) 2018 Apcon. +# +# GNU General Public License v3.0+ +# +# This program 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. +# +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Module to execute apconos Commands on Apcon Switches. +# Apcon Networking + +from __future__ import absolute_import, division, print_function +__metaclass__ = type ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], - 'supported_by': 'network'} + 'supported_by': 'community'} DOCUMENTATION = """ --- module: apconos_cert -version_added: "" -author: "" +version_added: "2.9.0" +author: "David Lee (@davidlee-ap)" short_description: install ssl ipv4 certificate on apcon network devices description: - import and install ssl ipv4 certificate with specifying remote filename @@ -59,7 +73,7 @@ EXAMPLES = """ - name: Install SSL Certificate - update_cert: + apconos_cert: ipaddress: 10.0.0.100 filename: remoteSSLCert.pem """ @@ -71,13 +85,12 @@ import time from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.parsing import Conditional from ansible.module_utils.network.common.utils import remove_default_spec -from ansible.module_utils.network.apconos.apconos import load_config, run_commands +from ansible.module_utils.network.apconos.apconos import load_config, run_commands from ansible.module_utils.network.apconos.apconos import apconos_argument_spec, check_args from ansible.module_utils.six import string_types -from ansible.utils.display import Display -display = Display() def to_lines(stdout): for item in stdout: @@ -85,17 +98,18 @@ def to_lines(stdout): item = str(item).split('\n') yield item + def construct_update_command(module): """construct update command """ - #command = ['update'] command = module.params['command'] ipaddress = module.params['ipaddress'] filename = module.params['filename'] - command[0] = 'tftp put ssl ipv4 ' + ipaddress[0] + ' ' + filename[0]+time.strftime("%d_%b+%H:%M:%S", time.gmtime()) + command[0] = 'tftp put ssl ipv4 ' + ipaddress[0] + ' ' + filename[0] + time.strftime("%d_%b+%H:%M:%S", time.gmtime()) command.append('tftp get ssl ipv4 ' + ipaddress[0] + ' ' + filename[0]) - return command + return command + def main(): """ main entry point for module execution @@ -123,7 +137,6 @@ def main(): interval = module.params['interval'] match = module.params['match'] - while retries > 0: responses = run_commands(module, construct_update_command(module)) diff --git a/lib/ansible/modules/network/apconos/apconos_command.py b/lib/ansible/modules/network/apconos/apconos_command.py index ffb59e85e6e81f..bc8ce936978a21 100644 --- a/lib/ansible/modules/network/apconos/apconos_command.py +++ b/lib/ansible/modules/network/apconos/apconos_command.py @@ -14,6 +14,8 @@ # Module to execute apconos Commands on Apcon Switches. # Apcon Networking +from __future__ import absolute_import, division, print_function +__metaclass__ = type ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], @@ -23,18 +25,18 @@ DOCUMENTATION = """ --- module: apconos_command -version_added: "" -author: "" +version_added: "2.9.0" +author: "David Lee (@davidlee-ap)" short_description: Run arbitrary commands on APCON devices description: - Sends arbitrary commands to an apcon device and returns the results read from the device. The 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. -extends_documentation_fragment: apconos +notes: + - tested against apcon iis+ii options: commands: - version_added: "" description: - List of commands to send to the remote device over the configured provider. The resulting output from the command @@ -43,7 +45,6 @@ the number of retires as expired. required: true wait_for: - version_added: "" description: - List of conditions to evaluate against the output of the command. The task will wait for each condition to be true @@ -51,7 +52,6 @@ within the configured number of retries, the task fails. See examples. match: - version_added: "" description: - The I(match) argument is used in conjunction with the I(wait_for) argument to specify the match policy. Valid @@ -62,7 +62,6 @@ default: all choices: ['any', 'all'] retries: - version_added: "" description: - Specifies the number of retries a command should by tried before it is considered failed. The command is run on the @@ -70,7 +69,6 @@ I(wait_for) conditions. default: 10 interval: - version_added: "" description: - Configures the interval in seconds to wait between retries of the command. If the command does not pass the specified @@ -80,7 +78,6 @@ """ EXAMPLES = """ ---- vars: cli: host: "{{ inventory_hostname }}" @@ -89,7 +86,6 @@ password: admin timeout: 30 ---- - name: test contains operator apconos_command: commands: diff --git a/lib/ansible/modules/network/apconos/apconos_config.py b/lib/ansible/modules/network/apconos/apconos_config.py new file mode 100644 index 00000000000000..bed388b500a1ed --- /dev/null +++ b/lib/ansible/modules/network/apconos/apconos_config.py @@ -0,0 +1,36 @@ +#!/usr/bin/python +# +# Copyright (C) 2018 Apcon. +# +# GNU General Public License v3.0+ +# +# This program 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. +# +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Module to execute apconos Commands on Apcon Switches. +# Apcon Networking + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: apconos_config +version_added: "2.9.0" +author: "David Lee (@davidlee-ap)" +short_description: "Configure Apcon modules" +""" + +EXAMPLES = """ +""" + +RETURN = """ +""" diff --git a/lib/ansible/modules/network/apconos/apconos_update.py b/lib/ansible/modules/network/apconos/apconos_update.py index c8c789e3180c40..bc5244e2371907 100644 --- a/lib/ansible/modules/network/apconos/apconos_update.py +++ b/lib/ansible/modules/network/apconos/apconos_update.py @@ -1,34 +1,50 @@ #!/usr/bin/python - -# (c) 2019, Ansible by Red Hat, inc -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Copyright (C) 2018 Apcon. +# +# GNU General Public License v3.0+ +# +# This program 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. +# +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Module to execute apconos Commands on Apcon Switches. +# Apcon Networking + +from __future__ import absolute_import, division, print_function +__metaclass__ = type ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], - 'supported_by': 'network'} + 'supported_by': 'community'} DOCUMENTATION = """ --- module: apconos_update -version_added: "" -author: "" +version_added: "2.9.0" +author: "David Lee (@davidlee-ap)" short_description: update firmware on apcon network devices description: - Sends update commands to APCON Network devices. Commands check - current versions and statues on the devices to decide if update + current versions and statues on the devices to decide if update should be executed. +notes: + - tested against apcon iis+ii options: device: description: - - specify a device. Valid values are [all], [blade]. - Choosing [all] will sequentially update software on + - specify a device. Valid values are [all], [blade]. + Choosing [all] will sequentially update software on both controllers, all blades and the backplane from a tftp server. Choosing [blade] will download the software from the controller and update the software for any APCON blades. If an tftp server address is provided, [blade] option will downloads the software from a TFTP server for a special blade. - default: all - choices: ['all', 'blade'] + default: all + choices: ['all', 'blade'] blade_letter: description: - specify a blade letter. @@ -73,13 +89,22 @@ EXAMPLES = """ - name: Update APCON Devices - update_update: + apconos_update: device: all ipaddress: 10.0.0.100 filename: firmware_6.01_1550.pem """ RETURN = """ +stdout: + description: The set of response from the commands + returned: On success +current version: + description: Current version + returned: On success +stdout_lines: + description: The value of stdout split into a list + returned: On success """ import re @@ -88,13 +113,12 @@ from copy import deepcopy from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.parsing import Conditional from ansible.module_utils.network.common.utils import remove_default_spec -from ansible.module_utils.network.apconos.apconos import load_config, run_commands +from ansible.module_utils.network.apconos.apconos import load_config, run_commands from ansible.module_utils.network.apconos.apconos import apconos_argument_spec, check_args from ansible.module_utils.six import string_types -from ansible.utils.display import Display -display = Display() def to_lines(stdout): for item in stdout: @@ -102,6 +126,7 @@ def to_lines(stdout): item = str(item).split('\n') yield item + def check_version(module): """check if current version is higher. """ @@ -114,6 +139,7 @@ def check_version(module): else: return cur_version, (version[0]) > (cur_version) + def construct_update_command(module): """construct update command """ @@ -124,20 +150,14 @@ def construct_update_command(module): filename = module.params['filename'] if device[0] == 'all': command[0] = command[0] + ' ' + device[0] + ' ' \ - + ipaddress[0] + ' ' + filename[0] + ' false' - #elif device[0] == 'backplane': - # command[0] = command[0] + ' device backplane' + + ipaddress[0] + ' ' + filename[0] + ' false' elif device[0] == 'blade': - #if ipaddress == None: - # command[0] = command[0] + ' ' + ' device blade ' + blade_letter[0] + ' false' - #else: if ipaddress and blade_letter and filename: command[0] = command[0] + ' ' + ' device blade standalone ' + blade_letter[0] \ - + ' ' + ipaddress[0] + ' ' + filename[0] + ' false' - #elif device[0] == 'controller': - # command[0] = command[0] + ' standby controller' + + ' ' + ipaddress[0] + ' ' + filename[0] + ' false' + + return command - return command def main(): """ main entry point for module execution @@ -153,8 +173,7 @@ def main(): blade_letter=dict(default='A', type='list'), ipaddress=dict(type='list'), filename=dict(type='list'), - version=dict(type='list'), - ) + version=dict(type='list'),) spec.update(apconos_argument_spec) diff --git a/lib/ansible/plugins/action/apconos.py b/lib/ansible/plugins/action/apconos.py index 85f76670b37ef5..4a6c42348f38c1 100644 --- a/lib/ansible/plugins/action/apconos.py +++ b/lib/ansible/plugins/action/apconos.py @@ -25,9 +25,6 @@ from ansible.module_utils.network.common.utils import load_provider from ansible.module_utils.connection import Connection from ansible.module_utils._text import to_text -from ansible.utils.display import Display - -display = Display() class ActionModule(_ActionModule): @@ -51,12 +48,10 @@ def run(self, tmp=None, task_vars=None): pc.become_pass = provider['auth_pass'] pc.become_method = 'enable' - 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) connection.set_options(direct={'persistent_command_timeout': command_timeout}) 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: ' + @@ -72,7 +67,6 @@ def run(self, tmp=None, task_vars=None): conn = Connection(socket_path) out = conn.get_prompt() if to_text(out, errors='surrogate_then_replace').strip().endswith(')#'): - display.vvvv('In Config mode, sending exit to device', self._play_context.remote_addr) conn.send_command('exit') else: conn.send_command('enable') diff --git a/lib/ansible/plugins/cliconf/apconos.py b/lib/ansible/plugins/cliconf/apconos.py index 15ef336279f453..d1392b55b6d673 100644 --- a/lib/ansible/plugins/cliconf/apconos.py +++ b/lib/ansible/plugins/cliconf/apconos.py @@ -12,10 +12,21 @@ # # Contains CLIConf Plugin methods for apconos Modules # APCON Networking -# + from __future__ import (absolute_import, division, print_function) __metaclass__ = type +DOCUMENTATION = """ +--- +author: "David Lee (@davidlee-ap)" +cliconf: apconos +short_description: Use apconos cliconf to run command on APCON network devices +description: + - This apconos plugin provides low level abstraction apis for + sending and receiving CLI commands from APCON network devices. +version_added: "2.9" +""" + import re import json diff --git a/lib/ansible/plugins/doc_fragments/apconos.py b/lib/ansible/plugins/doc_fragments/apconos.py new file mode 100644 index 00000000000000..c98c4742c043bb --- /dev/null +++ b/lib/ansible/plugins/doc_fragments/apconos.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Peter Sprygada +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +class ModuleDocFragment(object): + + # Standard files documentation fragment + DOCUMENTATION = r''' +options: + authorize: + description: + - B(Deprecated) + - "Starting with Ansible 2.5 we recommend using C(connection: network_cli) and C(become: yes)." + - HORIZONTALLINE + - Instructs the module to enter privileged mode on the remote device + before sending any commands. If not specified, the device will + attempt to execute all commands in non-privileged mode. If the value + is not specified in the task, the value of environment variable + C(ANSIBLE_NET_AUTHORIZE) will be used instead. + type: bool + default: no + auth_pass: + description: + - B(Deprecated) + - "Starting with Ansible 2.5 we recommend using C(connection: network_cli) and C(become: yes) with C(become_pass)." + - HORIZONTALLINE + - Specifies the password to use if required to enter privileged mode + on the remote device. If I(authorize) is false, then this argument + does nothing. If the value is not specified in the task, the value of + environment variable C(ANSIBLE_NET_AUTH_PASS) will be used instead. + type: str + provider: + description: + - B(Deprecated) + - "Starting with Ansible 2.5 we recommend using C(connection: network_cli)." + - HORIZONTALLINE + - A dict object containing connection details. + type: dict + suboptions: + host: + description: + - Specifies the DNS host name or address for connecting to the remote + device over the specified transport. The value of host is used as + the destination address for the transport. + type: str + required: true + port: + description: + - Specifies the port to use when building the connection to the remote device. + type: int + default: 22 + username: + description: + - Configures the username to use to authenticate the connection to + the remote device. This value is used to authenticate + the SSH session. If the value is not specified in the task, the + value of environment variable C(ANSIBLE_NET_USERNAME) will be used instead. + type: str + password: + description: + - Specifies the password to use to authenticate the connection to + the remote device. This value is used to authenticate + the SSH session. If the value is not specified in the task, the + value of environment variable C(ANSIBLE_NET_PASSWORD) will be used instead. + type: str + timeout: + description: + - Specifies the timeout in seconds for communicating with the network device + for either connecting or sending commands. If the timeout is + exceeded before the operation is completed, the module will error. + type: int + default: 10 + ssh_keyfile: + description: + - Specifies the SSH key to use to authenticate the connection to + the remote device. This value is the path to the + key used to authenticate the SSH session. If the value is not specified + in the task, the value of environment variable C(ANSIBLE_NET_SSH_KEYFILE) + will be used instead. + type: path + authorize: + description: + - Instructs the module to enter privileged mode on the remote device + before sending any commands. If not specified, the device will + attempt to execute all commands in non-privileged mode. If the value + is not specified in the task, the value of environment variable + C(ANSIBLE_NET_AUTHORIZE) will be used instead. + type: bool + default: no + auth_pass: + description: + - Specifies the password to use if required to enter privileged mode + on the remote device. If I(authorize) is false, then this argument + does nothing. If the value is not specified in the task, the value of + environment variable C(ANSIBLE_NET_AUTH_PASS) will be used instead. + type: str +notes: + - For more information on using Ansible to manage network devices see the :ref:`Ansible Network Guide ` +''' diff --git a/lib/ansible/plugins/terminal/apconos.py b/lib/ansible/plugins/terminal/apconos.py index 84273358b76b54..62ac9d06086bbf 100644 --- a/lib/ansible/plugins/terminal/apconos.py +++ b/lib/ansible/plugins/terminal/apconos.py @@ -27,12 +27,9 @@ class TerminalModule(TerminalBase): terminal_stdout_re = [ - re.compile(br'>>\ |#\ |\$\ ') - - + re.compile(br'>>\ |#\ |\$\ ') ] terminal_stderr_re = [ re.compile(br"connection timed out", re.I), ] - diff --git a/test/sanity/ansible-doc/skip.txt b/test/sanity/ansible-doc/skip.txt index 99a8e526e7cf3e..a1a2c6b27eb86e 100644 --- a/test/sanity/ansible-doc/skip.txt +++ b/test/sanity/ansible-doc/skip.txt @@ -40,3 +40,4 @@ panos_security_policy s3 vsphere_guest win_msi +apconos_config diff --git a/test/sanity/code-smell/action-plugin-docs.py b/test/sanity/code-smell/action-plugin-docs.py index 76de98ea4d7f1b..cc1548a38b8f96 100755 --- a/test/sanity/code-smell/action-plugin-docs.py +++ b/test/sanity/code-smell/action-plugin-docs.py @@ -22,6 +22,7 @@ def main(): # The following action plugins provide base classes for network platform specific modules to support `connection: local`. # Once we fully deprecate the use of connection local, the base classes will go away. 'aireos', + 'apconos', 'aruba', 'asa', 'ce', From 474cc5ea244f9a749e3f8896614bf9aa479086f7 Mon Sep 17 00:00:00 2001 From: chunwei li Date: Thu, 30 May 2019 11:00:05 -0700 Subject: [PATCH 04/17] revise for passing sanity tests --- .../modules/network/apconos/apconos_cert.py | 12 ++++++++--- .../network/apconos/apconos_command.py | 18 ++++++----------- .../modules/network/apconos/apconos_config.py | 2 ++ .../modules/network/apconos/apconos_update.py | 20 ++++++++++++++----- test/sanity/ansible-doc/skip.txt | 1 - 5 files changed, 32 insertions(+), 21 deletions(-) diff --git a/lib/ansible/modules/network/apconos/apconos_cert.py b/lib/ansible/modules/network/apconos/apconos_cert.py index 127b2948a2d481..9ae46964fe9dcf 100644 --- a/lib/ansible/modules/network/apconos/apconos_cert.py +++ b/lib/ansible/modules/network/apconos/apconos_cert.py @@ -32,6 +32,12 @@ notes: - tested against apcon iis+ii options: + command: + description: + - currently it is not being used in apconos_cert module. + provider: + description: + - please use connection:network_cli. ipaddress: description: - specify a remote ip address at which tftp server resides. @@ -73,9 +79,9 @@ EXAMPLES = """ - name: Install SSL Certificate - apconos_cert: - ipaddress: 10.0.0.100 - filename: remoteSSLCert.pem + apconos_cert: + ipaddress: 10.0.0.100 + filename: remoteSSLCert.pem """ RETURN = """ diff --git a/lib/ansible/modules/network/apconos/apconos_command.py b/lib/ansible/modules/network/apconos/apconos_command.py index bc8ce936978a21..01a9d01c890929 100644 --- a/lib/ansible/modules/network/apconos/apconos_command.py +++ b/lib/ansible/modules/network/apconos/apconos_command.py @@ -44,6 +44,9 @@ module is not returned until the condition is satisfied or the number of retires as expired. required: true + provider: + description: + - Please use connetcion network_cli. wait_for: description: - List of conditions to evaluate against the output of the @@ -78,24 +81,15 @@ """ EXAMPLES = """ -vars: - cli: - host: "{{ inventory_hostname }}" - port: 22 - username: admin - password: admin - timeout: 30 - -- name: test contains operator +- name: basic configuration apconos_command: commands: - - show version + - show version register: result -- name: get output for single command +- name: get output from single command apconos_command: commands: ['show version'] - provider: "{{ cli }}" register: result """ diff --git a/lib/ansible/modules/network/apconos/apconos_config.py b/lib/ansible/modules/network/apconos/apconos_config.py index bed388b500a1ed..f5c00d0b1f36a9 100644 --- a/lib/ansible/modules/network/apconos/apconos_config.py +++ b/lib/ansible/modules/network/apconos/apconos_config.py @@ -27,6 +27,8 @@ version_added: "2.9.0" author: "David Lee (@davidlee-ap)" short_description: "Configure Apcon modules" +description: + - configure apcon modules for future """ EXAMPLES = """ diff --git a/lib/ansible/modules/network/apconos/apconos_update.py b/lib/ansible/modules/network/apconos/apconos_update.py index bc5244e2371907..8bafdaa599bfc8 100644 --- a/lib/ansible/modules/network/apconos/apconos_update.py +++ b/lib/ansible/modules/network/apconos/apconos_update.py @@ -45,9 +45,16 @@ a TFTP server for a special blade. default: all choices: ['all', 'blade'] + version: + description: + - specify a version that will be installed. + provider: + description: + - Please use connection network_cli. blade_letter: description: - specify a blade letter. + default: ['A'] ipaddress: description: - specify an ip address of tftp server. @@ -89,22 +96,25 @@ EXAMPLES = """ - name: Update APCON Devices - apconos_update: - device: all - ipaddress: 10.0.0.100 - filename: firmware_6.01_1550.pem + apconos_update: + device: all + ipaddress: 10.0.0.100 + filename: firmware_6.01_1550.pem """ RETURN = """ stdout: description: The set of response from the commands returned: On success + type: list current version: description: Current version returned: On success + type: list stdout_lines: description: The value of stdout split into a list returned: On success + type: list """ import re @@ -169,7 +179,7 @@ def main(): retries=dict(default=10, type='int'), interval=dict(default=1, type='int'), - device=dict(type='list'), + device=dict(default='all', choices=['all', 'blade'], type='list'), blade_letter=dict(default='A', type='list'), ipaddress=dict(type='list'), filename=dict(type='list'), diff --git a/test/sanity/ansible-doc/skip.txt b/test/sanity/ansible-doc/skip.txt index a1a2c6b27eb86e..99a8e526e7cf3e 100644 --- a/test/sanity/ansible-doc/skip.txt +++ b/test/sanity/ansible-doc/skip.txt @@ -40,4 +40,3 @@ panos_security_policy s3 vsphere_guest win_msi -apconos_config From 2f05ef79da04e9e8e8db2c36653bb9908a92a144 Mon Sep 17 00:00:00 2001 From: chunwei li Date: Thu, 1 Aug 2019 13:26:27 -0700 Subject: [PATCH 05/17] Add unit test for apcon_command module Remove apconos_config.py apconos_update.py apconos_cert.py for now --- .../module_utils/network/apconos/apconos.py | 2 +- .../modules/network/apconos/apconos_cert.py | 190 ------------- .../network/apconos/apconos_command.py | 50 ++-- .../modules/network/apconos/apconos_config.py | 38 --- .../modules/network/apconos/apconos_update.py | 256 ------------------ lib/ansible/plugins/action/apconos.py | 75 ----- lib/ansible/plugins/action/apconos_config.py | 108 -------- .../units/modules/network/apconos/__init__.py | 0 .../modules/network/apconos/apconos_module.py | 89 ++++++ .../network/apconos/fixtures/enable_ssh | 0 .../network/apconos/fixtures/show_version | 2 + .../network/apconos/test_apconos_command.py | 122 +++++++++ 12 files changed, 248 insertions(+), 684 deletions(-) delete mode 100644 lib/ansible/modules/network/apconos/apconos_cert.py delete mode 100644 lib/ansible/modules/network/apconos/apconos_config.py delete mode 100644 lib/ansible/modules/network/apconos/apconos_update.py delete mode 100644 lib/ansible/plugins/action/apconos.py delete mode 100644 lib/ansible/plugins/action/apconos_config.py create mode 100644 test/units/modules/network/apconos/__init__.py create mode 100644 test/units/modules/network/apconos/apconos_module.py create mode 100644 test/units/modules/network/apconos/fixtures/enable_ssh create mode 100644 test/units/modules/network/apconos/fixtures/show_version create mode 100644 test/units/modules/network/apconos/test_apconos_command.py diff --git a/lib/ansible/module_utils/network/apconos/apconos.py b/lib/ansible/module_utils/network/apconos/apconos.py index c5a54d44862487..0ef08d47287c24 100644 --- a/lib/ansible/module_utils/network/apconos/apconos.py +++ b/lib/ansible/module_utils/network/apconos/apconos.py @@ -4,7 +4,7 @@ # Ansible still belong to the author of the module, and may assign their own # license to the complete work. # -# Copyright (C) 2018 APCON, Inc. +# Copyright (C) 2019 APCON, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/lib/ansible/modules/network/apconos/apconos_cert.py b/lib/ansible/modules/network/apconos/apconos_cert.py deleted file mode 100644 index 9ae46964fe9dcf..00000000000000 --- a/lib/ansible/modules/network/apconos/apconos_cert.py +++ /dev/null @@ -1,190 +0,0 @@ -#!/usr/bin/python -# -# Copyright (C) 2018 Apcon. -# -# GNU General Public License v3.0+ -# -# This program 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. -# -# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -# -# Module to execute apconos Commands on Apcon Switches. -# Apcon Networking - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = """ ---- -module: apconos_cert -version_added: "2.9.0" -author: "David Lee (@davidlee-ap)" -short_description: install ssl ipv4 certificate on apcon network devices -description: - - import and install ssl ipv4 certificate with specifying remote filename -notes: - - tested against apcon iis+ii -options: - command: - description: - - currently it is not being used in apconos_cert module. - provider: - description: - - please use connection:network_cli. - ipaddress: - description: - - specify a remote ip address at which tftp server resides. - filename: - description: - - specify a remote file name which resides on a remote tftp server. - 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. - 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. - 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. - 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. - default: 1 -""" - -EXAMPLES = """ -- name: Install SSL Certificate - apconos_cert: - ipaddress: 10.0.0.100 - filename: remoteSSLCert.pem -""" - -RETURN = """ -""" - -import re -import time - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.network.common.parsing import Conditional -from ansible.module_utils.network.common.utils import remove_default_spec -from ansible.module_utils.network.apconos.apconos import load_config, run_commands -from ansible.module_utils.network.apconos.apconos import apconos_argument_spec, check_args -from ansible.module_utils.six import string_types - - -def to_lines(stdout): - for item in stdout: - if isinstance(item, string_types): - item = str(item).split('\n') - yield item - - -def construct_update_command(module): - """construct update command - """ - command = module.params['command'] - ipaddress = module.params['ipaddress'] - filename = module.params['filename'] - command[0] = 'tftp put ssl ipv4 ' + ipaddress[0] + ' ' + filename[0] + time.strftime("%d_%b+%H:%M:%S", time.gmtime()) - command.append('tftp get ssl ipv4 ' + ipaddress[0] + ' ' + filename[0]) - - return command - - -def main(): - """ main entry point for module execution - """ - spec = dict( - wait_for=dict(type='list'), - match=dict(default='all', choices=['all', 'any']), - - retries=dict(default=10, type='int'), - interval=dict(default=1, type='int'), - - ipaddress=dict(type='list'), - filename=dict(type='list'), - command=dict(type='list')) - - spec.update(apconos_argument_spec) - - module = AnsibleModule(argument_spec=spec, supports_check_mode=True) - result = {'changed': False} - - 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, construct_update_command(module)) - - 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) - - for item in responses: - if len(item) == 0: - result.update({ - 'changed': True, - 'stdout': responses, - 'stdout_lines': list(to_lines(responses)) - }) - elif 'ERROR' in item: - result.update({ - 'failed': True, - 'stdout': responses, - 'stdout_lines': list(to_lines(responses)) - }) - else: - result.update({ - 'stdout': item, - 'stdout_lines': list(to_lines(responses)) - }) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/network/apconos/apconos_command.py b/lib/ansible/modules/network/apconos/apconos_command.py index 01a9d01c890929..988c82c1590d61 100644 --- a/lib/ansible/modules/network/apconos/apconos_command.py +++ b/lib/ansible/modules/network/apconos/apconos_command.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -# Copyright (C) 2018 Apcon. +# Copyright (C) 2019 APCON. # # GNU General Public License v3.0+ # @@ -25,7 +25,7 @@ DOCUMENTATION = """ --- module: apconos_command -version_added: "2.9.0" +version_added: "2.9" author: "David Lee (@davidlee-ap)" short_description: Run arbitrary commands on APCON devices description: @@ -34,7 +34,7 @@ argument that will cause the module to wait for a specific condition before returning or timing out if the condition is not met. notes: - - tested against apcon iis+ii + - Tested against apcon iis+ii options: commands: description: @@ -81,13 +81,14 @@ """ EXAMPLES = """ -- name: basic configuration +- name: Basic Configuration apconos_command: commands: - show version + - enable ssh register: result -- name: get output from single command +- name: Get output from single command apconos_command: commands: ['show version'] register: result @@ -99,18 +100,26 @@ import time from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import transform_commands, to_lines from ansible.module_utils.network.apconos.apconos import run_commands, check_args from ansible.module_utils.network.apconos.apconos import apconos_argument_spec from ansible.module_utils.network.common.parsing import Conditional from ansible.module_utils.six import string_types +def parse_commands(module, warnings): + #commands = transform_commands(module) + commands = module.params['commands'] -def to_lines(stdout): - for item in stdout: - if isinstance(item, string_types): - item = str(item).split('\n') - yield item + if module.check_mode: + for item in list(commands): + if not item.startswith('show'): + warnings.append( + 'Only show commands are supported when using check mode, not ' + 'executing %s' % item + ) + commands.remove(item) + return commands def main(): spec = dict( @@ -126,11 +135,13 @@ def main(): spec.update(apconos_argument_spec) module = AnsibleModule(argument_spec=spec, supports_check_mode=True) - result = {'changed': False} + warnings = list() + result = {'changed': False, 'warnings': warnings} wait_for = module.params['wait_for'] or list() conditionals = [Conditional(c) for c in wait_for] + commands = parse_commands(module, warnings) commands = module.params['commands'] retries = module.params['retries'] interval = module.params['interval'] @@ -159,11 +170,18 @@ def main(): for item in responses: if len(item) == 0: - result.update({ - 'changed': True, - 'stdout': responses, - 'stdout_lines': list(to_lines(responses)) - }) + if module.check_mode: + result.update({ + 'changed': False, + 'stdout': responses, + 'stdout_lines': list(to_lines(responses)) + }) + else: + result.update({ + 'changed': True, + 'stdout': responses, + 'stdout_lines': list(to_lines(responses)) + }) elif 'ERROR' in item: result.update({ 'failed': True, diff --git a/lib/ansible/modules/network/apconos/apconos_config.py b/lib/ansible/modules/network/apconos/apconos_config.py deleted file mode 100644 index f5c00d0b1f36a9..00000000000000 --- a/lib/ansible/modules/network/apconos/apconos_config.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/python -# -# Copyright (C) 2018 Apcon. -# -# GNU General Public License v3.0+ -# -# This program 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. -# -# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -# -# Module to execute apconos Commands on Apcon Switches. -# Apcon Networking - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = """ ---- -module: apconos_config -version_added: "2.9.0" -author: "David Lee (@davidlee-ap)" -short_description: "Configure Apcon modules" -description: - - configure apcon modules for future -""" - -EXAMPLES = """ -""" - -RETURN = """ -""" diff --git a/lib/ansible/modules/network/apconos/apconos_update.py b/lib/ansible/modules/network/apconos/apconos_update.py deleted file mode 100644 index 8bafdaa599bfc8..00000000000000 --- a/lib/ansible/modules/network/apconos/apconos_update.py +++ /dev/null @@ -1,256 +0,0 @@ -#!/usr/bin/python -# -# Copyright (C) 2018 Apcon. -# -# GNU General Public License v3.0+ -# -# This program 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. -# -# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -# -# Module to execute apconos Commands on Apcon Switches. -# Apcon Networking - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = """ ---- -module: apconos_update -version_added: "2.9.0" -author: "David Lee (@davidlee-ap)" -short_description: update firmware on apcon network devices -description: - - Sends update commands to APCON Network devices. Commands check - current versions and statues on the devices to decide if update - should be executed. -notes: - - tested against apcon iis+ii -options: - device: - description: - - specify a device. Valid values are [all], [blade]. - Choosing [all] will sequentially update software on - both controllers, all blades and the backplane from a tftp server. - Choosing [blade] will download the software from the controller - and update the software for any APCON blades. If an tftp server - address is provided, [blade] option will downloads the software from - a TFTP server for a special blade. - default: all - choices: ['all', 'blade'] - version: - description: - - specify a version that will be installed. - provider: - description: - - Please use connection network_cli. - blade_letter: - description: - - specify a blade letter. - default: ['A'] - ipaddress: - description: - - specify an ip address of tftp server. - filename: - description: - - specify a file name of firmware. - 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. - 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. - 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. - 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. - default: 1 -""" - -EXAMPLES = """ -- name: Update APCON Devices - apconos_update: - device: all - ipaddress: 10.0.0.100 - filename: firmware_6.01_1550.pem -""" - -RETURN = """ -stdout: - description: The set of response from the commands - returned: On success - type: list -current version: - description: Current version - returned: On success - type: list -stdout_lines: - description: The value of stdout split into a list - returned: On success - type: list -""" - -import re -import time - -from copy import deepcopy - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.network.common.parsing import Conditional -from ansible.module_utils.network.common.utils import remove_default_spec -from ansible.module_utils.network.apconos.apconos import load_config, run_commands -from ansible.module_utils.network.apconos.apconos import apconos_argument_spec, check_args -from ansible.module_utils.six import string_types - - -def to_lines(stdout): - for item in stdout: - if isinstance(item, string_types): - item = str(item).split('\n') - yield item - - -def check_version(module): - """check if current version is higher. - """ - version = module.params['version'] - responses = run_commands(module, ['show version']) - cur_version = list(to_lines(responses))[0][1][-5:-1] - - if version is None: - return cur_version, True - else: - return cur_version, (version[0]) > (cur_version) - - -def construct_update_command(module): - """construct update command - """ - command = ['update'] - device = module.params['device'] - blade_letter = module.params['blade_letter'] - ipaddress = module.params['ipaddress'] - filename = module.params['filename'] - if device[0] == 'all': - command[0] = command[0] + ' ' + device[0] + ' ' \ - + ipaddress[0] + ' ' + filename[0] + ' false' - elif device[0] == 'blade': - if ipaddress and blade_letter and filename: - command[0] = command[0] + ' ' + ' device blade standalone ' + blade_letter[0] \ - + ' ' + ipaddress[0] + ' ' + filename[0] + ' false' - - return command - - -def main(): - """ main entry point for module execution - """ - spec = dict( - wait_for=dict(type='list'), - match=dict(default='all', choices=['all', 'any']), - - retries=dict(default=10, type='int'), - interval=dict(default=1, type='int'), - - device=dict(default='all', choices=['all', 'blade'], type='list'), - blade_letter=dict(default='A', type='list'), - ipaddress=dict(type='list'), - filename=dict(type='list'), - version=dict(type='list'),) - - spec.update(apconos_argument_spec) - - module = AnsibleModule(argument_spec=spec, supports_check_mode=True) - result = {'changed': False} - - 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'] - - responses = None - cur_version, cond = check_version(module) - while cond and retries > 0: - responses = run_commands(module, construct_update_command(module)) - - 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) - - if responses is not None: - for item in responses: - if len(item) == 0: - result.update({ - 'changed': True, - 'stdout': responses, - 'current version': cur_version, - 'stdout_lines': list(to_lines(responses)) - }) - elif 'ERROR' in item: - result.update({ - 'failed': True, - 'stdout': responses, - 'current version': cur_version, - 'stdout_lines': list(to_lines(responses)) - }) - else: - result.update({ - 'stdout': item, - 'current version': cur_version, - 'stdout_lines': list(to_lines(responses)) - }) - else: - msg = 'The current version is newer' - module.fail_json(msg=msg) - result.update({ - 'current version': cur_version, - }) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/plugins/action/apconos.py b/lib/ansible/plugins/action/apconos.py deleted file mode 100644 index 4a6c42348f38c1..00000000000000 --- a/lib/ansible/plugins/action/apconos.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright (C) 2018 APCON. -# -# GNU General Public License v3.0+ -# -# This program 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. -# -# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -# -# Contains Action Plugin methods for apconos Config Module -# APCON Networking -# - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import sys -import copy - -from ansible import constants as C -from ansible.plugins.action.normal import ActionModule as _ActionModule -from ansible.module_utils.network.apconos.apconos import apconos_provider_spec -from ansible.module_utils.network.common.utils import load_provider -from ansible.module_utils.connection import Connection -from ansible.module_utils._text import to_text - - -class ActionModule(_ActionModule): - - def run(self, tmp=None, task_vars=None): - del tmp # tmp no longer has any effect - - socket_path = None - if self._play_context.connection == 'local': - provider = load_provider(apconos_provider_spec, self._task.args) - pc = copy.deepcopy(self._play_context) - pc.connection = 'network_cli' - pc.network_os = 'apconos' - pc.remote_addr = provider['host'] or self._play_context.remote_addr - pc.port = provider['port'] or self._play_context.port or 22 - pc.remote_user = provider['username'] or self._play_context.connection_user - pc.password = provider['password'] or self._play_context.password - pc.private_key_file = provider['ssh_keyfile'] or self._play_context.private_key_file - command_timeout = int(provider['timeout'] or C.PERSISTENT_COMMAND_TIMEOUT) - pc.become = provider['authorize'] or True - pc.become_pass = provider['auth_pass'] - pc.become_method = 'enable' - - connection = self._shared_loader_obj.connection_loader.get('persistent', pc, sys.stdin) - connection.set_options(direct={'persistent_command_timeout': command_timeout}) - - socket_path = connection.run() - 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 or exec mode - if socket_path is None: - socket_path = self._connection.socket_path - - conn = Connection(socket_path) - out = conn.get_prompt() - if to_text(out, errors='surrogate_then_replace').strip().endswith(')#'): - conn.send_command('exit') - else: - conn.send_command('enable') - - result = super(ActionModule, self).run(task_vars=task_vars) - return result diff --git a/lib/ansible/plugins/action/apconos_config.py b/lib/ansible/plugins/action/apconos_config.py deleted file mode 100644 index 3f054a9e05ef1c..00000000000000 --- a/lib/ansible/plugins/action/apconos_config.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright (C) 2017 APCON. -# -# GNU General Public License v3.0+ -# -# This program 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. -# -# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -# -# Contains Action Plugin methods for apconos Config Module -# APCON Networking -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import os -import re -import time -import glob - -from ansible.plugins.action.apconos import ActionModule as _ActionModule -from ansible.module_utils._text import to_text -from ansible.module_utils.six.moves.urllib.parse import urlsplit -from ansible.utils.vars import merge_hash - -PRIVATE_KEYS_RE = re.compile('__.+__') - - -class ActionModule(_ActionModule): - - def run(self, tmp=None, task_vars=None): - - if self._task.args.get('src'): - try: - self._handle_template() - except ValueError as exc: - return dict(failed=True, msg=to_text(exc)) - - result = super(ActionModule, self).run(tmp, task_vars) - del tmp # tmp no longer has any effect - - if self._task.args.get('backup') and result.get('__backup__'): - # User requested backup and no error occurred in module. - # NOTE: If there is a parameter error, _backup key may not be in results. - filepath = self._write_backup(task_vars['inventory_hostname'], - result['__backup__']) - - result['backup_path'] = filepath - - # strip out any keys that have two leading and two trailing - # underscore characters - for key in list(result): - if PRIVATE_KEYS_RE.match(key): - del result[key] - - return result - - def _get_working_path(self): - cwd = self._loader.get_basedir() - if self._task._role is not None: - cwd = self._task._role._role_path - return cwd - - def _write_backup(self, host, contents): - backup_path = self._get_working_path() + '/backup' - if not os.path.exists(backup_path): - os.mkdir(backup_path) - for fn in glob.glob('%s/%s*' % (backup_path, host)): - os.remove(fn) - tstamp = time.strftime("%Y-%m-%d@%H:%M:%S", time.localtime(time.time())) - filename = '%s/%s_config.%s' % (backup_path, host, tstamp) - open(filename, 'w').write(contents) - return filename - - def _handle_template(self): - src = self._task.args.get('src') - working_path = self._get_working_path() - - if os.path.isabs(src) or urlsplit('src').scheme: - source = src - else: - source = self._loader.path_dwim_relative(working_path, 'templates', src) - if not source: - source = self._loader.path_dwim_relative(working_path, src) - - if not os.path.exists(source): - raise ValueError('path specified in src not found') - - try: - with open(source, 'r') as f: - template_data = to_text(f.read()) - except IOError: - return dict(failed=True, msg='unable to load src file') - - # Create a template search path in the following order: - # [working_path, self_role_path, dependent_role_paths, dirname(source)] - searchpath = [working_path] - if self._task._role is not None: - searchpath.append(self._task._role._role_path) - if hasattr(self._task, "_block:"): - dep_chain = self._task._block.get_dep_chain() - if dep_chain is not None: - for role in dep_chain: - searchpath.append(role._role_path) - searchpath.append(os.path.dirname(source)) - self._templar.environment.loader.searchpath = searchpath - self._task.args['src'] = self._templar.template(template_data) diff --git a/test/units/modules/network/apconos/__init__.py b/test/units/modules/network/apconos/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/test/units/modules/network/apconos/apconos_module.py b/test/units/modules/network/apconos/apconos_module.py new file mode 100644 index 00000000000000..2619767ee65fda --- /dev/null +++ b/test/units/modules/network/apconos/apconos_module.py @@ -0,0 +1,89 @@ +# (c) 2019 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 Exception: + pass + + fixture_data[path] = data + return data + + +class TestApconosModule(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/apconos/fixtures/enable_ssh b/test/units/modules/network/apconos/fixtures/enable_ssh new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/test/units/modules/network/apconos/fixtures/show_version b/test/units/modules/network/apconos/fixtures/show_version new file mode 100644 index 00000000000000..a541d9e97de7bc --- /dev/null +++ b/test/units/modules/network/apconos/fixtures/show_version @@ -0,0 +1,2 @@ +APCON +COMPONENT MODEL VERSION diff --git a/test/units/modules/network/apconos/test_apconos_command.py b/test/units/modules/network/apconos/test_apconos_command.py new file mode 100644 index 00000000000000..dac2e2fd33d757 --- /dev/null +++ b/test/units/modules/network/apconos/test_apconos_command.py @@ -0,0 +1,122 @@ +# (c) 2019 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 units.compat.mock import patch +from ansible.modules.network.apconos import apconos_command +from units.modules.utils import set_module_args +from .apconos_module import TestApconosModule, load_fixture + + +class TestApconosCommandModule(TestApconosModule): + + module = apconos_command + + def setUp(self): + super(TestApconosCommandModule, self).setUp() + + self.mock_run_commands = patch('ansible.modules.network.apconos.apconos_command.run_commands') + self.run_commands = self.mock_run_commands.start() + + def tearDown(self): + super(TestApconosCommandModule, 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: + filename = str(item).replace(' ', '_') + output.append(load_fixture(filename)) + return output + + self.run_commands.side_effect = load_from_file + + def test_apcon_command_simple(self): + set_module_args(dict(commands=['show version'])) + result = self.execute_module() + self.assertEqual(len(result['stdout_lines']), 1) + self.assertEqual(result['stdout_lines'][0][0], 'APCON') + + def test_apcon_command_multiple(self): + set_module_args(dict(commands=['show version', 'show version'])) + result = self.execute_module() + self.assertEqual(len(result['stdout_lines']), 2) + self.assertEqual(result['stdout_lines'][0][0], 'APCON') + self.assertEqual(result['stdout_lines'][1][0], 'APCON') + + def test_apcon_command_wait_for(self): + wait_for = 'result[0] contains "APCON"' + set_module_args(dict(commands=['show version'], wait_for=wait_for)) + self.execute_module() + + def test_apcon_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_apcon_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_apcon_command_match_any(self): + wait_for = ['result[0] contains "test string"', + 'result[0] contains "VERSION"'] + set_module_args(dict(commands=['show version'], wait_for=wait_for, match='any')) + self.execute_module() + + def test_apcon_command_match_all(self): + wait_for = ['result[0] contains "COMPONENT"', + 'result[0] contains "MODEL"', + 'result[0] contains "VERSION"'] + set_module_args(dict(commands=['show version'], wait_for=wait_for, match='all')) + self.execute_module() + + def test_apcon_command_match_all_failure(self): + wait_for = ['result[0] contains "APCON OS"', + '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_apcon_command_checkmode_warning(self): + commands = ['enable ssh'] + set_module_args({ + 'commands': commands, + '_ansible_check_mode': True, + }) + result = self.execute_module() + self.assertEqual( + result['warnings'], + ['Only show commands are supported when using check mode, not executing enable ssh'], + ) + + def test_apcon_command_checkmode_not_warning(self): + commands = ['enable ssh'] + set_module_args(dict(commands=commands, _ansible_check_mode=False)) + result = self.execute_module(changed=True) + self.assertEqual(result['warnings'], []) \ No newline at end of file From 20b5ff5ab7fdc1fca7c6c402f5447cf4cfdd9c33 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Thu, 1 Aug 2019 23:00:07 +0200 Subject: [PATCH 06/17] [s3_bucket] access the bucket encryption response gracefully (#59826) Signed-off-by: Jakob Ackermann --- lib/ansible/modules/cloud/amazon/s3_bucket.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/ansible/modules/cloud/amazon/s3_bucket.py b/lib/ansible/modules/cloud/amazon/s3_bucket.py index c59af28f50fe2e..48e5471142169d 100644 --- a/lib/ansible/modules/cloud/amazon/s3_bucket.py +++ b/lib/ansible/modules/cloud/amazon/s3_bucket.py @@ -419,12 +419,14 @@ def put_bucket_versioning(s3_client, bucket_name, required_versioning): def get_bucket_encryption(s3_client, bucket_name): try: result = s3_client.get_bucket_encryption(Bucket=bucket_name) - return result.get('ServerSideEncryptionConfiguration').get('Rules')[0].get('ApplyServerSideEncryptionByDefault') + return result.get('ServerSideEncryptionConfiguration', {}).get('Rules', [])[0].get('ApplyServerSideEncryptionByDefault') except ClientError as e: if e.response['Error']['Code'] == 'ServerSideEncryptionConfigurationNotFoundError': return None else: raise e + except (IndexError, KeyError): + return None @AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket']) From 88e34491895ec0f4241ef2284b5711d06b878499 Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Fri, 2 Aug 2019 07:31:28 +1000 Subject: [PATCH 07/17] Fix ansible-galaxy import command to use argspec instead (#59898) --- lib/ansible/cli/galaxy.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/ansible/cli/galaxy.py b/lib/ansible/cli/galaxy.py index 2c7b92add1d014..4ff114ec707e8e 100644 --- a/lib/ansible/cli/galaxy.py +++ b/lib/ansible/cli/galaxy.py @@ -913,11 +913,8 @@ def execute_import(self): 'FAILED': C.COLOR_ERROR, } - if len(context.CLIARGS['args']) < 2: - raise AnsibleError("Expected a github_username and github_repository. Use --help.") - - github_user = to_text(context.CLIARGS['args'][0], errors='surrogate_or_strict') - github_repo = to_text(context.CLIARGS['args'][1], errors='surrogate_or_strict') + github_user = to_text(context.CLIARGS['github_user'], errors='surrogate_or_strict') + github_repo = to_text(context.CLIARGS['github_repo'], errors='surrogate_or_strict') if context.CLIARGS['check_status']: task = self.api.get_import_task(github_user=github_user, github_repo=github_repo) From bb1256ca9aa4c22225dbeef0ef23a20fa9388b2f Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Thu, 1 Aug 2019 17:06:18 -0400 Subject: [PATCH 08/17] Disable mongodb_shard and mongodb_replicaset tests They are causing failures when run as part of the entire group shippable/posix/group1/ --- test/integration/targets/mongodb_replicaset/aliases | 1 + test/integration/targets/mongodb_shard/aliases | 1 + 2 files changed, 2 insertions(+) diff --git a/test/integration/targets/mongodb_replicaset/aliases b/test/integration/targets/mongodb_replicaset/aliases index e9fc78ee6de5df..e7ce95b73a029b 100644 --- a/test/integration/targets/mongodb_replicaset/aliases +++ b/test/integration/targets/mongodb_replicaset/aliases @@ -4,3 +4,4 @@ skip/osx skip/freebsd skip/rhel needs/root +disabled diff --git a/test/integration/targets/mongodb_shard/aliases b/test/integration/targets/mongodb_shard/aliases index e9fc78ee6de5df..e7ce95b73a029b 100644 --- a/test/integration/targets/mongodb_shard/aliases +++ b/test/integration/targets/mongodb_shard/aliases @@ -4,3 +4,4 @@ skip/osx skip/freebsd skip/rhel needs/root +disabled From 056aac1e3012d0e65991b73f2478b844b7cbdfc8 Mon Sep 17 00:00:00 2001 From: Abhijeet Kasurde Date: Fri, 2 Aug 2019 03:18:34 +0530 Subject: [PATCH 09/17] Minor fixes in galaxy command for collection (#59846) Signed-off-by: Abhijeet Kasurde --- lib/ansible/cli/galaxy.py | 6 +++--- test/units/cli/test_galaxy.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/ansible/cli/galaxy.py b/lib/ansible/cli/galaxy.py index 4ff114ec707e8e..66c45a723c9e79 100644 --- a/lib/ansible/cli/galaxy.py +++ b/lib/ansible/cli/galaxy.py @@ -55,7 +55,7 @@ def init_parser(self): ''' create an options parser for bin/ansible ''' super(GalaxyCLI, self).init_parser( - desc="Perform various Role related operations.", + desc="Perform various Role and Collection related operations.", ) # common @@ -413,7 +413,7 @@ def execute_init(self): obj_name = context.CLIARGS['{0}_name'.format(galaxy_type)] inject_data = dict( - description='your description', + description='your {0} description'.format(galaxy_type), ansible_plugin_list_dir=get_versioned_doclink('plugins/plugins.html'), ) if galaxy_type == 'role': @@ -525,7 +525,7 @@ def execute_init(self): if not os.path.exists(b_dir_path): os.makedirs(b_dir_path) - display.display("- %s was created successfully" % obj_name) + display.display("- %s %s was created successfully" % (galaxy_type.title(), obj_name)) def execute_info(self): """ diff --git a/test/units/cli/test_galaxy.py b/test/units/cli/test_galaxy.py index c63dc3e21fc482..e9ba9b8ee81bc2 100644 --- a/test/units/cli/test_galaxy.py +++ b/test/units/cli/test_galaxy.py @@ -494,7 +494,7 @@ def test_collection_default(collection_skeleton): assert metadata['authors'] == ['your name '] assert metadata['readme'] == 'README.md' assert metadata['version'] == '1.0.0' - assert metadata['description'] == 'your description' + assert metadata['description'] == 'your collection description' assert metadata['license'] == ['GPL-2.0-or-later'] assert metadata['tags'] == [] assert metadata['dependencies'] == {} @@ -637,7 +637,7 @@ def test_collection_build(collection_artifact): assert coll_info['authors'] == ['your name '] assert coll_info['readme'] == 'README.md' assert coll_info['tags'] == [] - assert coll_info['description'] == 'your description' + assert coll_info['description'] == 'your collection description' assert coll_info['license'] == ['GPL-2.0-or-later'] assert coll_info['license_file'] is None assert coll_info['dependencies'] == {} From d71f5f673e0de51cef9b8624fd24b3fa5cc58732 Mon Sep 17 00:00:00 2001 From: chunwei li Date: Thu, 1 Aug 2019 15:05:52 -0700 Subject: [PATCH 10/17] Fix for santiy test --- lib/ansible/module_utils/network/apconos/apconos.py | 2 ++ lib/ansible/modules/network/apconos/apconos_command.py | 4 +++- test/sanity/code-smell/action-plugin-docs.py | 1 - test/units/modules/network/apconos/apconos_module.py | 2 +- test/units/modules/network/apconos/test_apconos_command.py | 2 +- 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/ansible/module_utils/network/apconos/apconos.py b/lib/ansible/module_utils/network/apconos/apconos.py index 0ef08d47287c24..0d88baaf776bde 100644 --- a/lib/ansible/module_utils/network/apconos/apconos.py +++ b/lib/ansible/module_utils/network/apconos/apconos.py @@ -31,6 +31,8 @@ # Contains utility methods # APCON Networking +from __future__ import (absolute_import, division, print_function) + import time import socket import re diff --git a/lib/ansible/modules/network/apconos/apconos_command.py b/lib/ansible/modules/network/apconos/apconos_command.py index 988c82c1590d61..d1a7168c4e03ce 100644 --- a/lib/ansible/modules/network/apconos/apconos_command.py +++ b/lib/ansible/modules/network/apconos/apconos_command.py @@ -106,8 +106,9 @@ from ansible.module_utils.network.common.parsing import Conditional from ansible.module_utils.six import string_types + def parse_commands(module, warnings): - #commands = transform_commands(module) + commands = module.params['commands'] if module.check_mode: @@ -121,6 +122,7 @@ def parse_commands(module, warnings): return commands + def main(): spec = dict( commands=dict(type='list', required=True), diff --git a/test/sanity/code-smell/action-plugin-docs.py b/test/sanity/code-smell/action-plugin-docs.py index cc1548a38b8f96..76de98ea4d7f1b 100755 --- a/test/sanity/code-smell/action-plugin-docs.py +++ b/test/sanity/code-smell/action-plugin-docs.py @@ -22,7 +22,6 @@ def main(): # The following action plugins provide base classes for network platform specific modules to support `connection: local`. # Once we fully deprecate the use of connection local, the base classes will go away. 'aireos', - 'apconos', 'aruba', 'asa', 'ce', diff --git a/test/units/modules/network/apconos/apconos_module.py b/test/units/modules/network/apconos/apconos_module.py index 2619767ee65fda..712d3f18d8bb30 100644 --- a/test/units/modules/network/apconos/apconos_module.py +++ b/test/units/modules/network/apconos/apconos_module.py @@ -81,7 +81,7 @@ def changed(self, changed=False): self.module.main() result = exc.exception.args[0] - + self.assertEqual(result['changed'], changed, result) return result diff --git a/test/units/modules/network/apconos/test_apconos_command.py b/test/units/modules/network/apconos/test_apconos_command.py index dac2e2fd33d757..1a0663d42e71c2 100644 --- a/test/units/modules/network/apconos/test_apconos_command.py +++ b/test/units/modules/network/apconos/test_apconos_command.py @@ -119,4 +119,4 @@ def test_apcon_command_checkmode_not_warning(self): commands = ['enable ssh'] set_module_args(dict(commands=commands, _ansible_check_mode=False)) result = self.execute_module(changed=True) - self.assertEqual(result['warnings'], []) \ No newline at end of file + self.assertEqual(result['warnings'], []) From 97ba48d53b4771ff75d97f94edca1017ddc1f076 Mon Sep 17 00:00:00 2001 From: chunwei li Date: Thu, 14 Feb 2019 10:20:01 -0800 Subject: [PATCH 11/17] Add APCON modules --- .../module_utils/network/apconos/__init__.py | 0 .../module_utils/network/apconos/apconos.py | 156 ++++++++++++ .../modules/network/apconos/__init__.py | 0 .../modules/network/apconos/apconos_cert.py | 171 +++++++++++++ .../network/apconos/apconos_command.py | 193 +++++++++++++++ .../modules/network/apconos/apconos_update.py | 227 ++++++++++++++++++ lib/ansible/plugins/action/apconos.py | 81 +++++++ lib/ansible/plugins/action/apconos_config.py | 108 +++++++++ lib/ansible/plugins/cliconf/apconos.py | 64 +++++ lib/ansible/plugins/terminal/apconos.py | 40 +++ 10 files changed, 1040 insertions(+) create mode 100644 lib/ansible/module_utils/network/apconos/__init__.py create mode 100644 lib/ansible/module_utils/network/apconos/apconos.py create mode 100644 lib/ansible/modules/network/apconos/__init__.py create mode 100644 lib/ansible/modules/network/apconos/apconos_cert.py create mode 100644 lib/ansible/modules/network/apconos/apconos_command.py create mode 100644 lib/ansible/modules/network/apconos/apconos_update.py create mode 100644 lib/ansible/plugins/action/apconos.py create mode 100644 lib/ansible/plugins/action/apconos_config.py create mode 100644 lib/ansible/plugins/cliconf/apconos.py create mode 100644 lib/ansible/plugins/terminal/apconos.py diff --git a/lib/ansible/module_utils/network/apconos/__init__.py b/lib/ansible/module_utils/network/apconos/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/lib/ansible/module_utils/network/apconos/apconos.py b/lib/ansible/module_utils/network/apconos/apconos.py new file mode 100644 index 00000000000000..78a469a9d2c109 --- /dev/null +++ b/lib/ansible/module_utils/network/apconos/apconos.py @@ -0,0 +1,156 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by +# Ansible still belong to the author of the module, and may assign their own +# license to the complete work. +# +# Copyright (C) 2018 APCON, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# Contains utility methods +# APCON Networking + +import time +import socket +import re +from distutils.cmd import Command +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, EntityCollection +from ansible.module_utils.connection import Connection, exec_command +from ansible.module_utils.connection import ConnectionError + +_DEVICE_CONFIGS = {} +_CONNECTION = None + +apconos_provider_spec = { + 'host': dict(), + 'port': dict(type='int'), + 'username': dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + 'password': dict(fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD']), + no_log=True), + 'ssh_keyfile': dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), + type='path'), + 'authorize': dict(fallback=(env_fallback, ['ANSIBLE_NET_AUTHORIZE']), + type='bool'), + 'auth_pass': dict(fallback=(env_fallback, ['ANSIBLE_NET_AUTH_PASS']), + no_log=True), + 'timeout': dict(type='int'), + 'context': dict(), + 'passwords': dict() +} + +apconos_argument_spec = { + 'host': dict(), + 'port': dict(type='int'), + 'username': dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + 'password': dict(fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD']), + no_log=True), + 'ssh_keyfile': dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), + type='path'), + 'authorize': dict(fallback=(env_fallback, ['ANSIBLE_NET_AUTHORIZE']), + type='bool'), + 'auth_pass': dict(fallback=(env_fallback, ['ANSIBLE_NET_AUTH_PASS']), + no_log=True), + 'timeout': dict(type='int'), + 'context': dict(), + 'passwords': dict(), + 'provider': dict(type='dict', options=apconos_provider_spec), +} + +command_spec = { + 'command': dict(key=True), + 'prompt': dict(), + 'answer': dict(), + 'check_all': dict() +} + + +def get_provider_argspec(): + return apconos_provider_spec + + +def check_args(module, warnings): + pass + + +def get_connection(module): + global _CONNECTION + if _CONNECTION: + return _CONNECTION + _CONNECTION = Connection(module._socket_path) + + return _CONNECTION + + +def get_config(module, flags=None): + flags = [] if flags is None else flags + + cmd += ' '.join(flags) + cmd = cmd.strip() + + try: + return _DEVICE_CONFIGS[cmd] + except KeyError: + conn = get_connection(module) + out = conn.get(cmd) + cfg = to_text(out, errors='surrogate_then_replace').strip() + _DEVICE_CONFIGS[cmd] = cfg + return cfg + +def run_commands(module, commands, check_rc=True): + connection = get_connection(module) + transform = EntityCollection(module, command_spec) + commands = transform(commands) + + responses = list() + + for cmd in commands: + out = connection.get(**cmd) + responses.append(to_text(out, errors='surrogate_then_replace')) + + return responses + +def load_config(module, config): + try: + conn = get_connection(module) + conn.get('enable') + conn.edit_config(config) + except ConnectionError as exc: + module.fail_json(msg=to_text(exc)) + +def get_defaults_flag(module): + rc, out, err = exec_command(module, 'display running-config ?') + out = to_text(out, errors='surrogate_then_replace') + + commands = set() + for line in out.splitlines(): + if line: + commands.add(line.strip().split()[0]) + + if 'all' in commands: + return 'all' + else: + return 'full' + diff --git a/lib/ansible/modules/network/apconos/__init__.py b/lib/ansible/modules/network/apconos/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/lib/ansible/modules/network/apconos/apconos_cert.py b/lib/ansible/modules/network/apconos/apconos_cert.py new file mode 100644 index 00000000000000..c42ec194162184 --- /dev/null +++ b/lib/ansible/modules/network/apconos/apconos_cert.py @@ -0,0 +1,171 @@ +#!/usr/bin/python + +# (c) 2019, Ansible by Red Hat, inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'network'} + +DOCUMENTATION = """ +--- +module: apconos_cert +version_added: "" +author: "" +short_description: install ssl ipv4 certificate on apcon network devices +description: + - import and install ssl ipv4 certificate with specifying remote filename +notes: + - tested against apcon iis+ii +options: + ipaddress: + description: + - specify a remote ip address at which tftp server resides. + filename: + description: + - specify a remote file name which resides on a remote tftp server. + 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. + 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. + 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. + 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. + default: 1 +""" + +EXAMPLES = """ +- name: Install SSL Certificate + update_cert: + ipaddress: 10.0.0.100 + filename: remoteSSLCert.pem +""" + +RETURN = """ +""" + +import re +import time + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import remove_default_spec +from ansible.module_utils.network.apconos.apconos import load_config, run_commands +from ansible.module_utils.network.apconos.apconos import apconos_argument_spec, check_args +from ansible.module_utils.six import string_types +from ansible.utils.display import Display + +display = Display() + +def to_lines(stdout): + for item in stdout: + if isinstance(item, string_types): + item = str(item).split('\n') + yield item + +def construct_update_command(module): + """construct update command + """ + #command = ['update'] + command = module.params['command'] + ipaddress = module.params['ipaddress'] + filename = module.params['filename'] + command[0] = 'tftp put ssl ipv4 ' + ipaddress[0] + ' ' + filename[0]+time.strftime("%d_%b+%H:%M:%S", time.gmtime()) + command.append('tftp get ssl ipv4 ' + ipaddress[0] + ' ' + filename[0]) + + return command + +def main(): + """ main entry point for module execution + """ + spec = dict( + wait_for=dict(type='list'), + match=dict(default='all', choices=['all', 'any']), + + retries=dict(default=10, type='int'), + interval=dict(default=1, type='int'), + + ipaddress=dict(type='list'), + filename=dict(type='list'), + command=dict(type='list')) + + spec.update(apconos_argument_spec) + + module = AnsibleModule(argument_spec=spec, supports_check_mode=True) + result = {'changed': False} + + 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, construct_update_command(module)) + + 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) + + for item in responses: + if len(item) == 0: + result.update({ + 'changed': True, + 'stdout': responses, + 'stdout_lines': list(to_lines(responses)) + }) + elif 'ERROR' in item: + result.update({ + 'failed': True, + 'stdout': responses, + 'stdout_lines': list(to_lines(responses)) + }) + else: + result.update({ + 'stdout': item, + 'stdout_lines': list(to_lines(responses)) + }) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/network/apconos/apconos_command.py b/lib/ansible/modules/network/apconos/apconos_command.py new file mode 100644 index 00000000000000..ffb59e85e6e81f --- /dev/null +++ b/lib/ansible/modules/network/apconos/apconos_command.py @@ -0,0 +1,193 @@ +#!/usr/bin/python +# +# Copyright (C) 2018 Apcon. +# +# GNU General Public License v3.0+ +# +# This program 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. +# +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Module to execute apconos Commands on Apcon Switches. +# Apcon Networking + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = """ +--- +module: apconos_command +version_added: "" +author: "" +short_description: Run arbitrary commands on APCON devices +description: + - Sends arbitrary commands to an apcon device and returns the results + read from the device. The 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. +extends_documentation_fragment: apconos +options: + commands: + version_added: "" + description: + - List of commands to send to the remote 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 retires as expired. + required: true + wait_for: + version_added: "" + 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. + match: + version_added: "" + 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. + default: all + choices: ['any', 'all'] + retries: + version_added: "" + 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. + default: 10 + interval: + version_added: "" + 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. + default: 1 +""" + +EXAMPLES = """ +--- +vars: + cli: + host: "{{ inventory_hostname }}" + port: 22 + username: admin + password: admin + timeout: 30 + +--- +- name: test contains operator + apconos_command: + commands: + - show version + register: result + +- name: get output for single command + apconos_command: + commands: ['show version'] + provider: "{{ cli }}" + register: result +""" + +RETURN = """ +""" + +import time + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.apconos.apconos import run_commands, check_args +from ansible.module_utils.network.apconos.apconos import apconos_argument_spec +from ansible.module_utils.network.common.parsing import Conditional +from ansible.module_utils.six import string_types + + +def to_lines(stdout): + for item in stdout: + if isinstance(item, string_types): + item = str(item).split('\n') + yield item + + +def main(): + 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') + ) + + spec.update(apconos_argument_spec) + + module = AnsibleModule(argument_spec=spec, supports_check_mode=True) + result = {'changed': False} + + wait_for = module.params['wait_for'] or list() + conditionals = [Conditional(c) for c in wait_for] + + commands = module.params['commands'] + 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) + + for item in responses: + if len(item) == 0: + result.update({ + 'changed': True, + 'stdout': responses, + 'stdout_lines': list(to_lines(responses)) + }) + elif 'ERROR' in item: + result.update({ + 'failed': True, + 'stdout': responses, + 'stdout_lines': list(to_lines(responses)) + }) + else: + result.update({ + 'stdout': item, + 'stdout_lines': list(to_lines(responses)) + }) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/network/apconos/apconos_update.py b/lib/ansible/modules/network/apconos/apconos_update.py new file mode 100644 index 00000000000000..c8c789e3180c40 --- /dev/null +++ b/lib/ansible/modules/network/apconos/apconos_update.py @@ -0,0 +1,227 @@ +#!/usr/bin/python + +# (c) 2019, Ansible by Red Hat, inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'network'} + +DOCUMENTATION = """ +--- +module: apconos_update +version_added: "" +author: "" +short_description: update firmware on apcon network devices +description: + - Sends update commands to APCON Network devices. Commands check + current versions and statues on the devices to decide if update + should be executed. +options: + device: + description: + - specify a device. Valid values are [all], [blade]. + Choosing [all] will sequentially update software on + both controllers, all blades and the backplane from a tftp server. + Choosing [blade] will download the software from the controller + and update the software for any APCON blades. If an tftp server + address is provided, [blade] option will downloads the software from + a TFTP server for a special blade. + default: all + choices: ['all', 'blade'] + blade_letter: + description: + - specify a blade letter. + ipaddress: + description: + - specify an ip address of tftp server. + filename: + description: + - specify a file name of firmware. + 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. + 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. + 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. + 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. + default: 1 +""" + +EXAMPLES = """ +- name: Update APCON Devices + update_update: + device: all + ipaddress: 10.0.0.100 + filename: firmware_6.01_1550.pem +""" + +RETURN = """ +""" + +import re +import time + +from copy import deepcopy + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import remove_default_spec +from ansible.module_utils.network.apconos.apconos import load_config, run_commands +from ansible.module_utils.network.apconos.apconos import apconos_argument_spec, check_args +from ansible.module_utils.six import string_types +from ansible.utils.display import Display + +display = Display() + +def to_lines(stdout): + for item in stdout: + if isinstance(item, string_types): + item = str(item).split('\n') + yield item + +def check_version(module): + """check if current version is higher. + """ + version = module.params['version'] + responses = run_commands(module, ['show version']) + cur_version = list(to_lines(responses))[0][1][-5:-1] + + if version is None: + return cur_version, True + else: + return cur_version, (version[0]) > (cur_version) + +def construct_update_command(module): + """construct update command + """ + command = ['update'] + device = module.params['device'] + blade_letter = module.params['blade_letter'] + ipaddress = module.params['ipaddress'] + filename = module.params['filename'] + if device[0] == 'all': + command[0] = command[0] + ' ' + device[0] + ' ' \ + + ipaddress[0] + ' ' + filename[0] + ' false' + #elif device[0] == 'backplane': + # command[0] = command[0] + ' device backplane' + elif device[0] == 'blade': + #if ipaddress == None: + # command[0] = command[0] + ' ' + ' device blade ' + blade_letter[0] + ' false' + #else: + if ipaddress and blade_letter and filename: + command[0] = command[0] + ' ' + ' device blade standalone ' + blade_letter[0] \ + + ' ' + ipaddress[0] + ' ' + filename[0] + ' false' + #elif device[0] == 'controller': + # command[0] = command[0] + ' standby controller' + + return command + +def main(): + """ main entry point for module execution + """ + spec = dict( + wait_for=dict(type='list'), + match=dict(default='all', choices=['all', 'any']), + + retries=dict(default=10, type='int'), + interval=dict(default=1, type='int'), + + device=dict(type='list'), + blade_letter=dict(default='A', type='list'), + ipaddress=dict(type='list'), + filename=dict(type='list'), + version=dict(type='list'), + ) + + spec.update(apconos_argument_spec) + + module = AnsibleModule(argument_spec=spec, supports_check_mode=True) + result = {'changed': False} + + 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'] + + responses = None + cur_version, cond = check_version(module) + while cond and retries > 0: + responses = run_commands(module, construct_update_command(module)) + + 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) + + if responses is not None: + for item in responses: + if len(item) == 0: + result.update({ + 'changed': True, + 'stdout': responses, + 'current version': cur_version, + 'stdout_lines': list(to_lines(responses)) + }) + elif 'ERROR' in item: + result.update({ + 'failed': True, + 'stdout': responses, + 'current version': cur_version, + 'stdout_lines': list(to_lines(responses)) + }) + else: + result.update({ + 'stdout': item, + 'current version': cur_version, + 'stdout_lines': list(to_lines(responses)) + }) + else: + msg = 'The current version is newer' + module.fail_json(msg=msg) + result.update({ + 'current version': cur_version, + }) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/plugins/action/apconos.py b/lib/ansible/plugins/action/apconos.py new file mode 100644 index 00000000000000..85f76670b37ef5 --- /dev/null +++ b/lib/ansible/plugins/action/apconos.py @@ -0,0 +1,81 @@ +# Copyright (C) 2018 APCON. +# +# GNU General Public License v3.0+ +# +# This program 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. +# +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Contains Action Plugin methods for apconos Config Module +# APCON Networking +# + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import sys +import copy + +from ansible import constants as C +from ansible.plugins.action.normal import ActionModule as _ActionModule +from ansible.module_utils.network.apconos.apconos import apconos_provider_spec +from ansible.module_utils.network.common.utils import load_provider +from ansible.module_utils.connection import Connection +from ansible.module_utils._text import to_text +from ansible.utils.display import Display + +display = Display() + + +class ActionModule(_ActionModule): + + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + socket_path = None + if self._play_context.connection == 'local': + provider = load_provider(apconos_provider_spec, self._task.args) + pc = copy.deepcopy(self._play_context) + pc.connection = 'network_cli' + pc.network_os = 'apconos' + pc.remote_addr = provider['host'] or self._play_context.remote_addr + pc.port = provider['port'] or self._play_context.port or 22 + pc.remote_user = provider['username'] or self._play_context.connection_user + pc.password = provider['password'] or self._play_context.password + pc.private_key_file = provider['ssh_keyfile'] or self._play_context.private_key_file + command_timeout = int(provider['timeout'] or C.PERSISTENT_COMMAND_TIMEOUT) + pc.become = provider['authorize'] or True + pc.become_pass = provider['auth_pass'] + pc.become_method = 'enable' + + 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) + connection.set_options(direct={'persistent_command_timeout': command_timeout}) + + 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 or exec mode + if socket_path is None: + socket_path = self._connection.socket_path + + conn = Connection(socket_path) + out = conn.get_prompt() + if to_text(out, errors='surrogate_then_replace').strip().endswith(')#'): + display.vvvv('In Config mode, sending exit to device', self._play_context.remote_addr) + conn.send_command('exit') + else: + conn.send_command('enable') + + result = super(ActionModule, self).run(task_vars=task_vars) + return result diff --git a/lib/ansible/plugins/action/apconos_config.py b/lib/ansible/plugins/action/apconos_config.py new file mode 100644 index 00000000000000..3f054a9e05ef1c --- /dev/null +++ b/lib/ansible/plugins/action/apconos_config.py @@ -0,0 +1,108 @@ +# Copyright (C) 2017 APCON. +# +# GNU General Public License v3.0+ +# +# This program 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. +# +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Contains Action Plugin methods for apconos Config Module +# APCON Networking +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import re +import time +import glob + +from ansible.plugins.action.apconos import ActionModule as _ActionModule +from ansible.module_utils._text import to_text +from ansible.module_utils.six.moves.urllib.parse import urlsplit +from ansible.utils.vars import merge_hash + +PRIVATE_KEYS_RE = re.compile('__.+__') + + +class ActionModule(_ActionModule): + + def run(self, tmp=None, task_vars=None): + + if self._task.args.get('src'): + try: + self._handle_template() + except ValueError as exc: + return dict(failed=True, msg=to_text(exc)) + + result = super(ActionModule, self).run(tmp, task_vars) + del tmp # tmp no longer has any effect + + if self._task.args.get('backup') and result.get('__backup__'): + # User requested backup and no error occurred in module. + # NOTE: If there is a parameter error, _backup key may not be in results. + filepath = self._write_backup(task_vars['inventory_hostname'], + result['__backup__']) + + result['backup_path'] = filepath + + # strip out any keys that have two leading and two trailing + # underscore characters + for key in list(result): + if PRIVATE_KEYS_RE.match(key): + del result[key] + + return result + + def _get_working_path(self): + cwd = self._loader.get_basedir() + if self._task._role is not None: + cwd = self._task._role._role_path + return cwd + + def _write_backup(self, host, contents): + backup_path = self._get_working_path() + '/backup' + if not os.path.exists(backup_path): + os.mkdir(backup_path) + for fn in glob.glob('%s/%s*' % (backup_path, host)): + os.remove(fn) + tstamp = time.strftime("%Y-%m-%d@%H:%M:%S", time.localtime(time.time())) + filename = '%s/%s_config.%s' % (backup_path, host, tstamp) + open(filename, 'w').write(contents) + return filename + + def _handle_template(self): + src = self._task.args.get('src') + working_path = self._get_working_path() + + if os.path.isabs(src) or urlsplit('src').scheme: + source = src + else: + source = self._loader.path_dwim_relative(working_path, 'templates', src) + if not source: + source = self._loader.path_dwim_relative(working_path, src) + + if not os.path.exists(source): + raise ValueError('path specified in src not found') + + try: + with open(source, 'r') as f: + template_data = to_text(f.read()) + except IOError: + return dict(failed=True, msg='unable to load src file') + + # Create a template search path in the following order: + # [working_path, self_role_path, dependent_role_paths, dirname(source)] + searchpath = [working_path] + if self._task._role is not None: + searchpath.append(self._task._role._role_path) + if hasattr(self._task, "_block:"): + dep_chain = self._task._block.get_dep_chain() + if dep_chain is not None: + for role in dep_chain: + searchpath.append(role._role_path) + searchpath.append(os.path.dirname(source)) + self._templar.environment.loader.searchpath = searchpath + self._task.args['src'] = self._templar.template(template_data) diff --git a/lib/ansible/plugins/cliconf/apconos.py b/lib/ansible/plugins/cliconf/apconos.py new file mode 100644 index 00000000000000..15ef336279f453 --- /dev/null +++ b/lib/ansible/plugins/cliconf/apconos.py @@ -0,0 +1,64 @@ +# (C) 2018 Red Hat Inc. +# Copyright (C) 2018 APCON. +# +# GNU General Public License v3.0+ +# +# This program 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. +# +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Contains CLIConf Plugin methods for apconos Modules +# APCON Networking +# +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'] = 'apconos' + reply = self.get(b'show version') + data = to_text(reply, errors='surrogate_or_strict').strip() + if data: + device_info['network_os_version'] = self.parse_version(data) + device_info['network_os_model'] = self.parse_model(data) + + return device_info + + def parse_version(self, data): + return "" + + def parse_model(self, data): + return "" + + @enable_mode + def get_config(self, source='running', format='text'): + cmd = b'' + 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, check_all=False): + return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, check_all=check_all) + + def get_capabilities(self): + result = {} + return json.dumps(result) diff --git a/lib/ansible/plugins/terminal/apconos.py b/lib/ansible/plugins/terminal/apconos.py new file mode 100644 index 00000000000000..dc5c3aae9900b6 --- /dev/null +++ b/lib/ansible/plugins/terminal/apconos.py @@ -0,0 +1,40 @@ +# (C) 2017 Red Hat Inc. +# Copyright (C) 2018 APCON. +# +# GNU General Public License v3.0+ +# +# This program 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. +# +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Contains terminal Plugin methods for apconos Config Module +# Apcon Networking +# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import json +import re + +from ansible.errors import AnsibleConnectionFailure +from ansible.module_utils._text import to_text, to_bytes +from ansible.plugins.terminal import TerminalBase + + +class TerminalModule(TerminalBase): + + terminal_stdout_re = [ + # re.compile(br"[\r\n]?[\w+\-\.:\/\[\]]+(?:\([^\)]+\)){,3}(?:>|#) ?$"), + # re.compile(br"\[\w+\@[\w\-\.]+(?: [^\]])\] ?[>#\$] ?$"), + # re.compile(br">[\r\n]?") + re.compile(br".*>>") + + ] + + terminal_stderr_re = [ + re.compile(br"connection timed out", re.I), + ] + From 9318a5e48f3ac3b3b750421ef24997a8ec419591 Mon Sep 17 00:00:00 2001 From: chunwei li Date: Tue, 28 May 2019 10:41:26 -0700 Subject: [PATCH 12/17] Fix cli regex --- lib/ansible/plugins/terminal/apconos.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/ansible/plugins/terminal/apconos.py b/lib/ansible/plugins/terminal/apconos.py index dc5c3aae9900b6..84273358b76b54 100644 --- a/lib/ansible/plugins/terminal/apconos.py +++ b/lib/ansible/plugins/terminal/apconos.py @@ -27,10 +27,8 @@ class TerminalModule(TerminalBase): terminal_stdout_re = [ - # re.compile(br"[\r\n]?[\w+\-\.:\/\[\]]+(?:\([^\)]+\)){,3}(?:>|#) ?$"), - # re.compile(br"\[\w+\@[\w\-\.]+(?: [^\]])\] ?[>#\$] ?$"), - # re.compile(br">[\r\n]?") - re.compile(br".*>>") + re.compile(br'>>\ |#\ |\$\ ') + ] From d241bd397abbab6ce1548d0753fee3ab8fa8bc29 Mon Sep 17 00:00:00 2001 From: chunwei li Date: Wed, 29 May 2019 13:55:40 -0700 Subject: [PATCH 13/17] Pass Sanity Test --- .../module_utils/network/apconos/apconos.py | 9 +- .../modules/network/apconos/apconos_cert.py | 41 ++++--- .../network/apconos/apconos_command.py | 16 ++- .../modules/network/apconos/apconos_config.py | 36 +++++++ .../modules/network/apconos/apconos_update.py | 73 ++++++++----- lib/ansible/plugins/action/apconos.py | 6 -- lib/ansible/plugins/cliconf/apconos.py | 13 ++- lib/ansible/plugins/doc_fragments/apconos.py | 101 ++++++++++++++++++ lib/ansible/plugins/terminal/apconos.py | 5 +- test/sanity/code-smell/action-plugin-docs.py | 35 ++++++ 10 files changed, 269 insertions(+), 66 deletions(-) create mode 100644 lib/ansible/modules/network/apconos/apconos_config.py create mode 100644 lib/ansible/plugins/doc_fragments/apconos.py diff --git a/lib/ansible/module_utils/network/apconos/apconos.py b/lib/ansible/module_utils/network/apconos/apconos.py index 78a469a9d2c109..c5a54d44862487 100644 --- a/lib/ansible/module_utils/network/apconos/apconos.py +++ b/lib/ansible/module_utils/network/apconos/apconos.py @@ -36,7 +36,7 @@ import re from distutils.cmd import Command from ansible.module_utils._text import to_text -from ansible.module_utils.basic import env_fallback, return_values +from ansible.module_utils.basic import env_fallback from ansible.module_utils.network.common.utils import to_list, EntityCollection from ansible.module_utils.connection import Connection, exec_command from ansible.module_utils.connection import ConnectionError @@ -107,8 +107,7 @@ def get_connection(module): def get_config(module, flags=None): flags = [] if flags is None else flags - cmd += ' '.join(flags) - cmd = cmd.strip() + cmd = ' '.join(flags).strip() try: return _DEVICE_CONFIGS[cmd] @@ -119,6 +118,7 @@ def get_config(module, flags=None): _DEVICE_CONFIGS[cmd] = cfg return cfg + def run_commands(module, commands, check_rc=True): connection = get_connection(module) transform = EntityCollection(module, command_spec) @@ -132,6 +132,7 @@ def run_commands(module, commands, check_rc=True): return responses + def load_config(module, config): try: conn = get_connection(module) @@ -140,6 +141,7 @@ def load_config(module, config): except ConnectionError as exc: module.fail_json(msg=to_text(exc)) + def get_defaults_flag(module): rc, out, err = exec_command(module, 'display running-config ?') out = to_text(out, errors='surrogate_then_replace') @@ -153,4 +155,3 @@ def get_defaults_flag(module): return 'all' else: return 'full' - diff --git a/lib/ansible/modules/network/apconos/apconos_cert.py b/lib/ansible/modules/network/apconos/apconos_cert.py index c42ec194162184..127b2948a2d481 100644 --- a/lib/ansible/modules/network/apconos/apconos_cert.py +++ b/lib/ansible/modules/network/apconos/apconos_cert.py @@ -1,17 +1,31 @@ #!/usr/bin/python - -# (c) 2019, Ansible by Red Hat, inc -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Copyright (C) 2018 Apcon. +# +# GNU General Public License v3.0+ +# +# This program 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. +# +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Module to execute apconos Commands on Apcon Switches. +# Apcon Networking + +from __future__ import absolute_import, division, print_function +__metaclass__ = type ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], - 'supported_by': 'network'} + 'supported_by': 'community'} DOCUMENTATION = """ --- module: apconos_cert -version_added: "" -author: "" +version_added: "2.9.0" +author: "David Lee (@davidlee-ap)" short_description: install ssl ipv4 certificate on apcon network devices description: - import and install ssl ipv4 certificate with specifying remote filename @@ -59,7 +73,7 @@ EXAMPLES = """ - name: Install SSL Certificate - update_cert: + apconos_cert: ipaddress: 10.0.0.100 filename: remoteSSLCert.pem """ @@ -71,13 +85,12 @@ import time from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.parsing import Conditional from ansible.module_utils.network.common.utils import remove_default_spec -from ansible.module_utils.network.apconos.apconos import load_config, run_commands +from ansible.module_utils.network.apconos.apconos import load_config, run_commands from ansible.module_utils.network.apconos.apconos import apconos_argument_spec, check_args from ansible.module_utils.six import string_types -from ansible.utils.display import Display -display = Display() def to_lines(stdout): for item in stdout: @@ -85,17 +98,18 @@ def to_lines(stdout): item = str(item).split('\n') yield item + def construct_update_command(module): """construct update command """ - #command = ['update'] command = module.params['command'] ipaddress = module.params['ipaddress'] filename = module.params['filename'] - command[0] = 'tftp put ssl ipv4 ' + ipaddress[0] + ' ' + filename[0]+time.strftime("%d_%b+%H:%M:%S", time.gmtime()) + command[0] = 'tftp put ssl ipv4 ' + ipaddress[0] + ' ' + filename[0] + time.strftime("%d_%b+%H:%M:%S", time.gmtime()) command.append('tftp get ssl ipv4 ' + ipaddress[0] + ' ' + filename[0]) - return command + return command + def main(): """ main entry point for module execution @@ -123,7 +137,6 @@ def main(): interval = module.params['interval'] match = module.params['match'] - while retries > 0: responses = run_commands(module, construct_update_command(module)) diff --git a/lib/ansible/modules/network/apconos/apconos_command.py b/lib/ansible/modules/network/apconos/apconos_command.py index ffb59e85e6e81f..bc8ce936978a21 100644 --- a/lib/ansible/modules/network/apconos/apconos_command.py +++ b/lib/ansible/modules/network/apconos/apconos_command.py @@ -14,6 +14,8 @@ # Module to execute apconos Commands on Apcon Switches. # Apcon Networking +from __future__ import absolute_import, division, print_function +__metaclass__ = type ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], @@ -23,18 +25,18 @@ DOCUMENTATION = """ --- module: apconos_command -version_added: "" -author: "" +version_added: "2.9.0" +author: "David Lee (@davidlee-ap)" short_description: Run arbitrary commands on APCON devices description: - Sends arbitrary commands to an apcon device and returns the results read from the device. The 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. -extends_documentation_fragment: apconos +notes: + - tested against apcon iis+ii options: commands: - version_added: "" description: - List of commands to send to the remote device over the configured provider. The resulting output from the command @@ -43,7 +45,6 @@ the number of retires as expired. required: true wait_for: - version_added: "" description: - List of conditions to evaluate against the output of the command. The task will wait for each condition to be true @@ -51,7 +52,6 @@ within the configured number of retries, the task fails. See examples. match: - version_added: "" description: - The I(match) argument is used in conjunction with the I(wait_for) argument to specify the match policy. Valid @@ -62,7 +62,6 @@ default: all choices: ['any', 'all'] retries: - version_added: "" description: - Specifies the number of retries a command should by tried before it is considered failed. The command is run on the @@ -70,7 +69,6 @@ I(wait_for) conditions. default: 10 interval: - version_added: "" description: - Configures the interval in seconds to wait between retries of the command. If the command does not pass the specified @@ -80,7 +78,6 @@ """ EXAMPLES = """ ---- vars: cli: host: "{{ inventory_hostname }}" @@ -89,7 +86,6 @@ password: admin timeout: 30 ---- - name: test contains operator apconos_command: commands: diff --git a/lib/ansible/modules/network/apconos/apconos_config.py b/lib/ansible/modules/network/apconos/apconos_config.py new file mode 100644 index 00000000000000..bed388b500a1ed --- /dev/null +++ b/lib/ansible/modules/network/apconos/apconos_config.py @@ -0,0 +1,36 @@ +#!/usr/bin/python +# +# Copyright (C) 2018 Apcon. +# +# GNU General Public License v3.0+ +# +# This program 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. +# +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Module to execute apconos Commands on Apcon Switches. +# Apcon Networking + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: apconos_config +version_added: "2.9.0" +author: "David Lee (@davidlee-ap)" +short_description: "Configure Apcon modules" +""" + +EXAMPLES = """ +""" + +RETURN = """ +""" diff --git a/lib/ansible/modules/network/apconos/apconos_update.py b/lib/ansible/modules/network/apconos/apconos_update.py index c8c789e3180c40..bc5244e2371907 100644 --- a/lib/ansible/modules/network/apconos/apconos_update.py +++ b/lib/ansible/modules/network/apconos/apconos_update.py @@ -1,34 +1,50 @@ #!/usr/bin/python - -# (c) 2019, Ansible by Red Hat, inc -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Copyright (C) 2018 Apcon. +# +# GNU General Public License v3.0+ +# +# This program 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. +# +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Module to execute apconos Commands on Apcon Switches. +# Apcon Networking + +from __future__ import absolute_import, division, print_function +__metaclass__ = type ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], - 'supported_by': 'network'} + 'supported_by': 'community'} DOCUMENTATION = """ --- module: apconos_update -version_added: "" -author: "" +version_added: "2.9.0" +author: "David Lee (@davidlee-ap)" short_description: update firmware on apcon network devices description: - Sends update commands to APCON Network devices. Commands check - current versions and statues on the devices to decide if update + current versions and statues on the devices to decide if update should be executed. +notes: + - tested against apcon iis+ii options: device: description: - - specify a device. Valid values are [all], [blade]. - Choosing [all] will sequentially update software on + - specify a device. Valid values are [all], [blade]. + Choosing [all] will sequentially update software on both controllers, all blades and the backplane from a tftp server. Choosing [blade] will download the software from the controller and update the software for any APCON blades. If an tftp server address is provided, [blade] option will downloads the software from a TFTP server for a special blade. - default: all - choices: ['all', 'blade'] + default: all + choices: ['all', 'blade'] blade_letter: description: - specify a blade letter. @@ -73,13 +89,22 @@ EXAMPLES = """ - name: Update APCON Devices - update_update: + apconos_update: device: all ipaddress: 10.0.0.100 filename: firmware_6.01_1550.pem """ RETURN = """ +stdout: + description: The set of response from the commands + returned: On success +current version: + description: Current version + returned: On success +stdout_lines: + description: The value of stdout split into a list + returned: On success """ import re @@ -88,13 +113,12 @@ from copy import deepcopy from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.parsing import Conditional from ansible.module_utils.network.common.utils import remove_default_spec -from ansible.module_utils.network.apconos.apconos import load_config, run_commands +from ansible.module_utils.network.apconos.apconos import load_config, run_commands from ansible.module_utils.network.apconos.apconos import apconos_argument_spec, check_args from ansible.module_utils.six import string_types -from ansible.utils.display import Display -display = Display() def to_lines(stdout): for item in stdout: @@ -102,6 +126,7 @@ def to_lines(stdout): item = str(item).split('\n') yield item + def check_version(module): """check if current version is higher. """ @@ -114,6 +139,7 @@ def check_version(module): else: return cur_version, (version[0]) > (cur_version) + def construct_update_command(module): """construct update command """ @@ -124,20 +150,14 @@ def construct_update_command(module): filename = module.params['filename'] if device[0] == 'all': command[0] = command[0] + ' ' + device[0] + ' ' \ - + ipaddress[0] + ' ' + filename[0] + ' false' - #elif device[0] == 'backplane': - # command[0] = command[0] + ' device backplane' + + ipaddress[0] + ' ' + filename[0] + ' false' elif device[0] == 'blade': - #if ipaddress == None: - # command[0] = command[0] + ' ' + ' device blade ' + blade_letter[0] + ' false' - #else: if ipaddress and blade_letter and filename: command[0] = command[0] + ' ' + ' device blade standalone ' + blade_letter[0] \ - + ' ' + ipaddress[0] + ' ' + filename[0] + ' false' - #elif device[0] == 'controller': - # command[0] = command[0] + ' standby controller' + + ' ' + ipaddress[0] + ' ' + filename[0] + ' false' + + return command - return command def main(): """ main entry point for module execution @@ -153,8 +173,7 @@ def main(): blade_letter=dict(default='A', type='list'), ipaddress=dict(type='list'), filename=dict(type='list'), - version=dict(type='list'), - ) + version=dict(type='list'),) spec.update(apconos_argument_spec) diff --git a/lib/ansible/plugins/action/apconos.py b/lib/ansible/plugins/action/apconos.py index 85f76670b37ef5..4a6c42348f38c1 100644 --- a/lib/ansible/plugins/action/apconos.py +++ b/lib/ansible/plugins/action/apconos.py @@ -25,9 +25,6 @@ from ansible.module_utils.network.common.utils import load_provider from ansible.module_utils.connection import Connection from ansible.module_utils._text import to_text -from ansible.utils.display import Display - -display = Display() class ActionModule(_ActionModule): @@ -51,12 +48,10 @@ def run(self, tmp=None, task_vars=None): pc.become_pass = provider['auth_pass'] pc.become_method = 'enable' - 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) connection.set_options(direct={'persistent_command_timeout': command_timeout}) 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: ' + @@ -72,7 +67,6 @@ def run(self, tmp=None, task_vars=None): conn = Connection(socket_path) out = conn.get_prompt() if to_text(out, errors='surrogate_then_replace').strip().endswith(')#'): - display.vvvv('In Config mode, sending exit to device', self._play_context.remote_addr) conn.send_command('exit') else: conn.send_command('enable') diff --git a/lib/ansible/plugins/cliconf/apconos.py b/lib/ansible/plugins/cliconf/apconos.py index 15ef336279f453..d1392b55b6d673 100644 --- a/lib/ansible/plugins/cliconf/apconos.py +++ b/lib/ansible/plugins/cliconf/apconos.py @@ -12,10 +12,21 @@ # # Contains CLIConf Plugin methods for apconos Modules # APCON Networking -# + from __future__ import (absolute_import, division, print_function) __metaclass__ = type +DOCUMENTATION = """ +--- +author: "David Lee (@davidlee-ap)" +cliconf: apconos +short_description: Use apconos cliconf to run command on APCON network devices +description: + - This apconos plugin provides low level abstraction apis for + sending and receiving CLI commands from APCON network devices. +version_added: "2.9" +""" + import re import json diff --git a/lib/ansible/plugins/doc_fragments/apconos.py b/lib/ansible/plugins/doc_fragments/apconos.py new file mode 100644 index 00000000000000..c98c4742c043bb --- /dev/null +++ b/lib/ansible/plugins/doc_fragments/apconos.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Peter Sprygada +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +class ModuleDocFragment(object): + + # Standard files documentation fragment + DOCUMENTATION = r''' +options: + authorize: + description: + - B(Deprecated) + - "Starting with Ansible 2.5 we recommend using C(connection: network_cli) and C(become: yes)." + - HORIZONTALLINE + - Instructs the module to enter privileged mode on the remote device + before sending any commands. If not specified, the device will + attempt to execute all commands in non-privileged mode. If the value + is not specified in the task, the value of environment variable + C(ANSIBLE_NET_AUTHORIZE) will be used instead. + type: bool + default: no + auth_pass: + description: + - B(Deprecated) + - "Starting with Ansible 2.5 we recommend using C(connection: network_cli) and C(become: yes) with C(become_pass)." + - HORIZONTALLINE + - Specifies the password to use if required to enter privileged mode + on the remote device. If I(authorize) is false, then this argument + does nothing. If the value is not specified in the task, the value of + environment variable C(ANSIBLE_NET_AUTH_PASS) will be used instead. + type: str + provider: + description: + - B(Deprecated) + - "Starting with Ansible 2.5 we recommend using C(connection: network_cli)." + - HORIZONTALLINE + - A dict object containing connection details. + type: dict + suboptions: + host: + description: + - Specifies the DNS host name or address for connecting to the remote + device over the specified transport. The value of host is used as + the destination address for the transport. + type: str + required: true + port: + description: + - Specifies the port to use when building the connection to the remote device. + type: int + default: 22 + username: + description: + - Configures the username to use to authenticate the connection to + the remote device. This value is used to authenticate + the SSH session. If the value is not specified in the task, the + value of environment variable C(ANSIBLE_NET_USERNAME) will be used instead. + type: str + password: + description: + - Specifies the password to use to authenticate the connection to + the remote device. This value is used to authenticate + the SSH session. If the value is not specified in the task, the + value of environment variable C(ANSIBLE_NET_PASSWORD) will be used instead. + type: str + timeout: + description: + - Specifies the timeout in seconds for communicating with the network device + for either connecting or sending commands. If the timeout is + exceeded before the operation is completed, the module will error. + type: int + default: 10 + ssh_keyfile: + description: + - Specifies the SSH key to use to authenticate the connection to + the remote device. This value is the path to the + key used to authenticate the SSH session. If the value is not specified + in the task, the value of environment variable C(ANSIBLE_NET_SSH_KEYFILE) + will be used instead. + type: path + authorize: + description: + - Instructs the module to enter privileged mode on the remote device + before sending any commands. If not specified, the device will + attempt to execute all commands in non-privileged mode. If the value + is not specified in the task, the value of environment variable + C(ANSIBLE_NET_AUTHORIZE) will be used instead. + type: bool + default: no + auth_pass: + description: + - Specifies the password to use if required to enter privileged mode + on the remote device. If I(authorize) is false, then this argument + does nothing. If the value is not specified in the task, the value of + environment variable C(ANSIBLE_NET_AUTH_PASS) will be used instead. + type: str +notes: + - For more information on using Ansible to manage network devices see the :ref:`Ansible Network Guide ` +''' diff --git a/lib/ansible/plugins/terminal/apconos.py b/lib/ansible/plugins/terminal/apconos.py index 84273358b76b54..62ac9d06086bbf 100644 --- a/lib/ansible/plugins/terminal/apconos.py +++ b/lib/ansible/plugins/terminal/apconos.py @@ -27,12 +27,9 @@ class TerminalModule(TerminalBase): terminal_stdout_re = [ - re.compile(br'>>\ |#\ |\$\ ') - - + re.compile(br'>>\ |#\ |\$\ ') ] terminal_stderr_re = [ re.compile(br"connection timed out", re.I), ] - diff --git a/test/sanity/code-smell/action-plugin-docs.py b/test/sanity/code-smell/action-plugin-docs.py index e2a195685a17d8..7be1fa51ab2793 100755 --- a/test/sanity/code-smell/action-plugin-docs.py +++ b/test/sanity/code-smell/action-plugin-docs.py @@ -9,6 +9,41 @@ def main(): """Main entry point.""" + skip = set([ + '__init__', # action plugin base class, not an actual action plugin + 'net_base', # base class for other net_* action plugins which have a matching module + 'normal', # default action plugin for modules without a dedicated action plugin + 'network', # base class for network action plugins + + # The following action plugins existed without modules to document them before this test was put in place. + # They should either be removed, have a module added to document them, or have the exception documented here. + 'bigip', + 'bigiq', + 'ce_template', + + # The following action plugins provide base classes for network platform specific modules to support `connection: local`. + # Once we fully deprecate the use of connection local, the base classes will go away. + 'aireos', + 'apconos', + 'aruba', + 'asa', + 'ce', + 'cnos', + 'dellos10', + 'dellos6', + 'dellos9', + 'enos', + 'eos', + 'ios', + 'iosxr', + 'ironware', + 'junos', + 'netconf', + 'nxos', + 'sros', + 'vyos', + ]) + paths = sys.argv[1:] or sys.stdin.read().splitlines() module_names = set() From cac6df1b1e1865527593070a9bcf54a4b09a00ad Mon Sep 17 00:00:00 2001 From: chunwei li Date: Thu, 1 Aug 2019 16:26:17 -0700 Subject: [PATCH 14/17] rebase --- .../modules/network/apconos/apconos_cert.py | 12 ++++++++--- .../network/apconos/apconos_command.py | 18 ++++++----------- .../modules/network/apconos/apconos_config.py | 2 ++ .../modules/network/apconos/apconos_update.py | 20 ++++++++++++++----- 4 files changed, 32 insertions(+), 20 deletions(-) diff --git a/lib/ansible/modules/network/apconos/apconos_cert.py b/lib/ansible/modules/network/apconos/apconos_cert.py index 127b2948a2d481..9ae46964fe9dcf 100644 --- a/lib/ansible/modules/network/apconos/apconos_cert.py +++ b/lib/ansible/modules/network/apconos/apconos_cert.py @@ -32,6 +32,12 @@ notes: - tested against apcon iis+ii options: + command: + description: + - currently it is not being used in apconos_cert module. + provider: + description: + - please use connection:network_cli. ipaddress: description: - specify a remote ip address at which tftp server resides. @@ -73,9 +79,9 @@ EXAMPLES = """ - name: Install SSL Certificate - apconos_cert: - ipaddress: 10.0.0.100 - filename: remoteSSLCert.pem + apconos_cert: + ipaddress: 10.0.0.100 + filename: remoteSSLCert.pem """ RETURN = """ diff --git a/lib/ansible/modules/network/apconos/apconos_command.py b/lib/ansible/modules/network/apconos/apconos_command.py index bc8ce936978a21..01a9d01c890929 100644 --- a/lib/ansible/modules/network/apconos/apconos_command.py +++ b/lib/ansible/modules/network/apconos/apconos_command.py @@ -44,6 +44,9 @@ module is not returned until the condition is satisfied or the number of retires as expired. required: true + provider: + description: + - Please use connetcion network_cli. wait_for: description: - List of conditions to evaluate against the output of the @@ -78,24 +81,15 @@ """ EXAMPLES = """ -vars: - cli: - host: "{{ inventory_hostname }}" - port: 22 - username: admin - password: admin - timeout: 30 - -- name: test contains operator +- name: basic configuration apconos_command: commands: - - show version + - show version register: result -- name: get output for single command +- name: get output from single command apconos_command: commands: ['show version'] - provider: "{{ cli }}" register: result """ diff --git a/lib/ansible/modules/network/apconos/apconos_config.py b/lib/ansible/modules/network/apconos/apconos_config.py index bed388b500a1ed..f5c00d0b1f36a9 100644 --- a/lib/ansible/modules/network/apconos/apconos_config.py +++ b/lib/ansible/modules/network/apconos/apconos_config.py @@ -27,6 +27,8 @@ version_added: "2.9.0" author: "David Lee (@davidlee-ap)" short_description: "Configure Apcon modules" +description: + - configure apcon modules for future """ EXAMPLES = """ diff --git a/lib/ansible/modules/network/apconos/apconos_update.py b/lib/ansible/modules/network/apconos/apconos_update.py index bc5244e2371907..8bafdaa599bfc8 100644 --- a/lib/ansible/modules/network/apconos/apconos_update.py +++ b/lib/ansible/modules/network/apconos/apconos_update.py @@ -45,9 +45,16 @@ a TFTP server for a special blade. default: all choices: ['all', 'blade'] + version: + description: + - specify a version that will be installed. + provider: + description: + - Please use connection network_cli. blade_letter: description: - specify a blade letter. + default: ['A'] ipaddress: description: - specify an ip address of tftp server. @@ -89,22 +96,25 @@ EXAMPLES = """ - name: Update APCON Devices - apconos_update: - device: all - ipaddress: 10.0.0.100 - filename: firmware_6.01_1550.pem + apconos_update: + device: all + ipaddress: 10.0.0.100 + filename: firmware_6.01_1550.pem """ RETURN = """ stdout: description: The set of response from the commands returned: On success + type: list current version: description: Current version returned: On success + type: list stdout_lines: description: The value of stdout split into a list returned: On success + type: list """ import re @@ -169,7 +179,7 @@ def main(): retries=dict(default=10, type='int'), interval=dict(default=1, type='int'), - device=dict(type='list'), + device=dict(default='all', choices=['all', 'blade'], type='list'), blade_letter=dict(default='A', type='list'), ipaddress=dict(type='list'), filename=dict(type='list'), From 6f9337b14e326000ba9c3812452ef3f0fb7ada75 Mon Sep 17 00:00:00 2001 From: chunwei li Date: Thu, 1 Aug 2019 13:26:27 -0700 Subject: [PATCH 15/17] Add unit test for apcon_command module Remove apconos_config.py apconos_update.py apconos_cert.py for now --- .../module_utils/network/apconos/apconos.py | 2 +- .../modules/network/apconos/apconos_cert.py | 190 ------------- .../network/apconos/apconos_command.py | 50 ++-- .../modules/network/apconos/apconos_config.py | 38 --- .../modules/network/apconos/apconos_update.py | 256 ------------------ lib/ansible/plugins/action/apconos.py | 75 ----- lib/ansible/plugins/action/apconos_config.py | 108 -------- .../units/modules/network/apconos/__init__.py | 0 .../modules/network/apconos/apconos_module.py | 89 ++++++ .../network/apconos/fixtures/enable_ssh | 0 .../network/apconos/fixtures/show_version | 2 + .../network/apconos/test_apconos_command.py | 122 +++++++++ 12 files changed, 248 insertions(+), 684 deletions(-) delete mode 100644 lib/ansible/modules/network/apconos/apconos_cert.py delete mode 100644 lib/ansible/modules/network/apconos/apconos_config.py delete mode 100644 lib/ansible/modules/network/apconos/apconos_update.py delete mode 100644 lib/ansible/plugins/action/apconos.py delete mode 100644 lib/ansible/plugins/action/apconos_config.py create mode 100644 test/units/modules/network/apconos/__init__.py create mode 100644 test/units/modules/network/apconos/apconos_module.py create mode 100644 test/units/modules/network/apconos/fixtures/enable_ssh create mode 100644 test/units/modules/network/apconos/fixtures/show_version create mode 100644 test/units/modules/network/apconos/test_apconos_command.py diff --git a/lib/ansible/module_utils/network/apconos/apconos.py b/lib/ansible/module_utils/network/apconos/apconos.py index c5a54d44862487..0ef08d47287c24 100644 --- a/lib/ansible/module_utils/network/apconos/apconos.py +++ b/lib/ansible/module_utils/network/apconos/apconos.py @@ -4,7 +4,7 @@ # Ansible still belong to the author of the module, and may assign their own # license to the complete work. # -# Copyright (C) 2018 APCON, Inc. +# Copyright (C) 2019 APCON, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/lib/ansible/modules/network/apconos/apconos_cert.py b/lib/ansible/modules/network/apconos/apconos_cert.py deleted file mode 100644 index 9ae46964fe9dcf..00000000000000 --- a/lib/ansible/modules/network/apconos/apconos_cert.py +++ /dev/null @@ -1,190 +0,0 @@ -#!/usr/bin/python -# -# Copyright (C) 2018 Apcon. -# -# GNU General Public License v3.0+ -# -# This program 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. -# -# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -# -# Module to execute apconos Commands on Apcon Switches. -# Apcon Networking - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = """ ---- -module: apconos_cert -version_added: "2.9.0" -author: "David Lee (@davidlee-ap)" -short_description: install ssl ipv4 certificate on apcon network devices -description: - - import and install ssl ipv4 certificate with specifying remote filename -notes: - - tested against apcon iis+ii -options: - command: - description: - - currently it is not being used in apconos_cert module. - provider: - description: - - please use connection:network_cli. - ipaddress: - description: - - specify a remote ip address at which tftp server resides. - filename: - description: - - specify a remote file name which resides on a remote tftp server. - 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. - 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. - 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. - 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. - default: 1 -""" - -EXAMPLES = """ -- name: Install SSL Certificate - apconos_cert: - ipaddress: 10.0.0.100 - filename: remoteSSLCert.pem -""" - -RETURN = """ -""" - -import re -import time - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.network.common.parsing import Conditional -from ansible.module_utils.network.common.utils import remove_default_spec -from ansible.module_utils.network.apconos.apconos import load_config, run_commands -from ansible.module_utils.network.apconos.apconos import apconos_argument_spec, check_args -from ansible.module_utils.six import string_types - - -def to_lines(stdout): - for item in stdout: - if isinstance(item, string_types): - item = str(item).split('\n') - yield item - - -def construct_update_command(module): - """construct update command - """ - command = module.params['command'] - ipaddress = module.params['ipaddress'] - filename = module.params['filename'] - command[0] = 'tftp put ssl ipv4 ' + ipaddress[0] + ' ' + filename[0] + time.strftime("%d_%b+%H:%M:%S", time.gmtime()) - command.append('tftp get ssl ipv4 ' + ipaddress[0] + ' ' + filename[0]) - - return command - - -def main(): - """ main entry point for module execution - """ - spec = dict( - wait_for=dict(type='list'), - match=dict(default='all', choices=['all', 'any']), - - retries=dict(default=10, type='int'), - interval=dict(default=1, type='int'), - - ipaddress=dict(type='list'), - filename=dict(type='list'), - command=dict(type='list')) - - spec.update(apconos_argument_spec) - - module = AnsibleModule(argument_spec=spec, supports_check_mode=True) - result = {'changed': False} - - 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, construct_update_command(module)) - - 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) - - for item in responses: - if len(item) == 0: - result.update({ - 'changed': True, - 'stdout': responses, - 'stdout_lines': list(to_lines(responses)) - }) - elif 'ERROR' in item: - result.update({ - 'failed': True, - 'stdout': responses, - 'stdout_lines': list(to_lines(responses)) - }) - else: - result.update({ - 'stdout': item, - 'stdout_lines': list(to_lines(responses)) - }) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/network/apconos/apconos_command.py b/lib/ansible/modules/network/apconos/apconos_command.py index 01a9d01c890929..988c82c1590d61 100644 --- a/lib/ansible/modules/network/apconos/apconos_command.py +++ b/lib/ansible/modules/network/apconos/apconos_command.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -# Copyright (C) 2018 Apcon. +# Copyright (C) 2019 APCON. # # GNU General Public License v3.0+ # @@ -25,7 +25,7 @@ DOCUMENTATION = """ --- module: apconos_command -version_added: "2.9.0" +version_added: "2.9" author: "David Lee (@davidlee-ap)" short_description: Run arbitrary commands on APCON devices description: @@ -34,7 +34,7 @@ argument that will cause the module to wait for a specific condition before returning or timing out if the condition is not met. notes: - - tested against apcon iis+ii + - Tested against apcon iis+ii options: commands: description: @@ -81,13 +81,14 @@ """ EXAMPLES = """ -- name: basic configuration +- name: Basic Configuration apconos_command: commands: - show version + - enable ssh register: result -- name: get output from single command +- name: Get output from single command apconos_command: commands: ['show version'] register: result @@ -99,18 +100,26 @@ import time from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import transform_commands, to_lines from ansible.module_utils.network.apconos.apconos import run_commands, check_args from ansible.module_utils.network.apconos.apconos import apconos_argument_spec from ansible.module_utils.network.common.parsing import Conditional from ansible.module_utils.six import string_types +def parse_commands(module, warnings): + #commands = transform_commands(module) + commands = module.params['commands'] -def to_lines(stdout): - for item in stdout: - if isinstance(item, string_types): - item = str(item).split('\n') - yield item + if module.check_mode: + for item in list(commands): + if not item.startswith('show'): + warnings.append( + 'Only show commands are supported when using check mode, not ' + 'executing %s' % item + ) + commands.remove(item) + return commands def main(): spec = dict( @@ -126,11 +135,13 @@ def main(): spec.update(apconos_argument_spec) module = AnsibleModule(argument_spec=spec, supports_check_mode=True) - result = {'changed': False} + warnings = list() + result = {'changed': False, 'warnings': warnings} wait_for = module.params['wait_for'] or list() conditionals = [Conditional(c) for c in wait_for] + commands = parse_commands(module, warnings) commands = module.params['commands'] retries = module.params['retries'] interval = module.params['interval'] @@ -159,11 +170,18 @@ def main(): for item in responses: if len(item) == 0: - result.update({ - 'changed': True, - 'stdout': responses, - 'stdout_lines': list(to_lines(responses)) - }) + if module.check_mode: + result.update({ + 'changed': False, + 'stdout': responses, + 'stdout_lines': list(to_lines(responses)) + }) + else: + result.update({ + 'changed': True, + 'stdout': responses, + 'stdout_lines': list(to_lines(responses)) + }) elif 'ERROR' in item: result.update({ 'failed': True, diff --git a/lib/ansible/modules/network/apconos/apconos_config.py b/lib/ansible/modules/network/apconos/apconos_config.py deleted file mode 100644 index f5c00d0b1f36a9..00000000000000 --- a/lib/ansible/modules/network/apconos/apconos_config.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/python -# -# Copyright (C) 2018 Apcon. -# -# GNU General Public License v3.0+ -# -# This program 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. -# -# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -# -# Module to execute apconos Commands on Apcon Switches. -# Apcon Networking - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = """ ---- -module: apconos_config -version_added: "2.9.0" -author: "David Lee (@davidlee-ap)" -short_description: "Configure Apcon modules" -description: - - configure apcon modules for future -""" - -EXAMPLES = """ -""" - -RETURN = """ -""" diff --git a/lib/ansible/modules/network/apconos/apconos_update.py b/lib/ansible/modules/network/apconos/apconos_update.py deleted file mode 100644 index 8bafdaa599bfc8..00000000000000 --- a/lib/ansible/modules/network/apconos/apconos_update.py +++ /dev/null @@ -1,256 +0,0 @@ -#!/usr/bin/python -# -# Copyright (C) 2018 Apcon. -# -# GNU General Public License v3.0+ -# -# This program 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. -# -# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -# -# Module to execute apconos Commands on Apcon Switches. -# Apcon Networking - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = """ ---- -module: apconos_update -version_added: "2.9.0" -author: "David Lee (@davidlee-ap)" -short_description: update firmware on apcon network devices -description: - - Sends update commands to APCON Network devices. Commands check - current versions and statues on the devices to decide if update - should be executed. -notes: - - tested against apcon iis+ii -options: - device: - description: - - specify a device. Valid values are [all], [blade]. - Choosing [all] will sequentially update software on - both controllers, all blades and the backplane from a tftp server. - Choosing [blade] will download the software from the controller - and update the software for any APCON blades. If an tftp server - address is provided, [blade] option will downloads the software from - a TFTP server for a special blade. - default: all - choices: ['all', 'blade'] - version: - description: - - specify a version that will be installed. - provider: - description: - - Please use connection network_cli. - blade_letter: - description: - - specify a blade letter. - default: ['A'] - ipaddress: - description: - - specify an ip address of tftp server. - filename: - description: - - specify a file name of firmware. - 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. - 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. - 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. - 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. - default: 1 -""" - -EXAMPLES = """ -- name: Update APCON Devices - apconos_update: - device: all - ipaddress: 10.0.0.100 - filename: firmware_6.01_1550.pem -""" - -RETURN = """ -stdout: - description: The set of response from the commands - returned: On success - type: list -current version: - description: Current version - returned: On success - type: list -stdout_lines: - description: The value of stdout split into a list - returned: On success - type: list -""" - -import re -import time - -from copy import deepcopy - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.network.common.parsing import Conditional -from ansible.module_utils.network.common.utils import remove_default_spec -from ansible.module_utils.network.apconos.apconos import load_config, run_commands -from ansible.module_utils.network.apconos.apconos import apconos_argument_spec, check_args -from ansible.module_utils.six import string_types - - -def to_lines(stdout): - for item in stdout: - if isinstance(item, string_types): - item = str(item).split('\n') - yield item - - -def check_version(module): - """check if current version is higher. - """ - version = module.params['version'] - responses = run_commands(module, ['show version']) - cur_version = list(to_lines(responses))[0][1][-5:-1] - - if version is None: - return cur_version, True - else: - return cur_version, (version[0]) > (cur_version) - - -def construct_update_command(module): - """construct update command - """ - command = ['update'] - device = module.params['device'] - blade_letter = module.params['blade_letter'] - ipaddress = module.params['ipaddress'] - filename = module.params['filename'] - if device[0] == 'all': - command[0] = command[0] + ' ' + device[0] + ' ' \ - + ipaddress[0] + ' ' + filename[0] + ' false' - elif device[0] == 'blade': - if ipaddress and blade_letter and filename: - command[0] = command[0] + ' ' + ' device blade standalone ' + blade_letter[0] \ - + ' ' + ipaddress[0] + ' ' + filename[0] + ' false' - - return command - - -def main(): - """ main entry point for module execution - """ - spec = dict( - wait_for=dict(type='list'), - match=dict(default='all', choices=['all', 'any']), - - retries=dict(default=10, type='int'), - interval=dict(default=1, type='int'), - - device=dict(default='all', choices=['all', 'blade'], type='list'), - blade_letter=dict(default='A', type='list'), - ipaddress=dict(type='list'), - filename=dict(type='list'), - version=dict(type='list'),) - - spec.update(apconos_argument_spec) - - module = AnsibleModule(argument_spec=spec, supports_check_mode=True) - result = {'changed': False} - - 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'] - - responses = None - cur_version, cond = check_version(module) - while cond and retries > 0: - responses = run_commands(module, construct_update_command(module)) - - 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) - - if responses is not None: - for item in responses: - if len(item) == 0: - result.update({ - 'changed': True, - 'stdout': responses, - 'current version': cur_version, - 'stdout_lines': list(to_lines(responses)) - }) - elif 'ERROR' in item: - result.update({ - 'failed': True, - 'stdout': responses, - 'current version': cur_version, - 'stdout_lines': list(to_lines(responses)) - }) - else: - result.update({ - 'stdout': item, - 'current version': cur_version, - 'stdout_lines': list(to_lines(responses)) - }) - else: - msg = 'The current version is newer' - module.fail_json(msg=msg) - result.update({ - 'current version': cur_version, - }) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/plugins/action/apconos.py b/lib/ansible/plugins/action/apconos.py deleted file mode 100644 index 4a6c42348f38c1..00000000000000 --- a/lib/ansible/plugins/action/apconos.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright (C) 2018 APCON. -# -# GNU General Public License v3.0+ -# -# This program 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. -# -# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -# -# Contains Action Plugin methods for apconos Config Module -# APCON Networking -# - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import sys -import copy - -from ansible import constants as C -from ansible.plugins.action.normal import ActionModule as _ActionModule -from ansible.module_utils.network.apconos.apconos import apconos_provider_spec -from ansible.module_utils.network.common.utils import load_provider -from ansible.module_utils.connection import Connection -from ansible.module_utils._text import to_text - - -class ActionModule(_ActionModule): - - def run(self, tmp=None, task_vars=None): - del tmp # tmp no longer has any effect - - socket_path = None - if self._play_context.connection == 'local': - provider = load_provider(apconos_provider_spec, self._task.args) - pc = copy.deepcopy(self._play_context) - pc.connection = 'network_cli' - pc.network_os = 'apconos' - pc.remote_addr = provider['host'] or self._play_context.remote_addr - pc.port = provider['port'] or self._play_context.port or 22 - pc.remote_user = provider['username'] or self._play_context.connection_user - pc.password = provider['password'] or self._play_context.password - pc.private_key_file = provider['ssh_keyfile'] or self._play_context.private_key_file - command_timeout = int(provider['timeout'] or C.PERSISTENT_COMMAND_TIMEOUT) - pc.become = provider['authorize'] or True - pc.become_pass = provider['auth_pass'] - pc.become_method = 'enable' - - connection = self._shared_loader_obj.connection_loader.get('persistent', pc, sys.stdin) - connection.set_options(direct={'persistent_command_timeout': command_timeout}) - - socket_path = connection.run() - 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 or exec mode - if socket_path is None: - socket_path = self._connection.socket_path - - conn = Connection(socket_path) - out = conn.get_prompt() - if to_text(out, errors='surrogate_then_replace').strip().endswith(')#'): - conn.send_command('exit') - else: - conn.send_command('enable') - - result = super(ActionModule, self).run(task_vars=task_vars) - return result diff --git a/lib/ansible/plugins/action/apconos_config.py b/lib/ansible/plugins/action/apconos_config.py deleted file mode 100644 index 3f054a9e05ef1c..00000000000000 --- a/lib/ansible/plugins/action/apconos_config.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright (C) 2017 APCON. -# -# GNU General Public License v3.0+ -# -# This program 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. -# -# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -# -# Contains Action Plugin methods for apconos Config Module -# APCON Networking -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import os -import re -import time -import glob - -from ansible.plugins.action.apconos import ActionModule as _ActionModule -from ansible.module_utils._text import to_text -from ansible.module_utils.six.moves.urllib.parse import urlsplit -from ansible.utils.vars import merge_hash - -PRIVATE_KEYS_RE = re.compile('__.+__') - - -class ActionModule(_ActionModule): - - def run(self, tmp=None, task_vars=None): - - if self._task.args.get('src'): - try: - self._handle_template() - except ValueError as exc: - return dict(failed=True, msg=to_text(exc)) - - result = super(ActionModule, self).run(tmp, task_vars) - del tmp # tmp no longer has any effect - - if self._task.args.get('backup') and result.get('__backup__'): - # User requested backup and no error occurred in module. - # NOTE: If there is a parameter error, _backup key may not be in results. - filepath = self._write_backup(task_vars['inventory_hostname'], - result['__backup__']) - - result['backup_path'] = filepath - - # strip out any keys that have two leading and two trailing - # underscore characters - for key in list(result): - if PRIVATE_KEYS_RE.match(key): - del result[key] - - return result - - def _get_working_path(self): - cwd = self._loader.get_basedir() - if self._task._role is not None: - cwd = self._task._role._role_path - return cwd - - def _write_backup(self, host, contents): - backup_path = self._get_working_path() + '/backup' - if not os.path.exists(backup_path): - os.mkdir(backup_path) - for fn in glob.glob('%s/%s*' % (backup_path, host)): - os.remove(fn) - tstamp = time.strftime("%Y-%m-%d@%H:%M:%S", time.localtime(time.time())) - filename = '%s/%s_config.%s' % (backup_path, host, tstamp) - open(filename, 'w').write(contents) - return filename - - def _handle_template(self): - src = self._task.args.get('src') - working_path = self._get_working_path() - - if os.path.isabs(src) or urlsplit('src').scheme: - source = src - else: - source = self._loader.path_dwim_relative(working_path, 'templates', src) - if not source: - source = self._loader.path_dwim_relative(working_path, src) - - if not os.path.exists(source): - raise ValueError('path specified in src not found') - - try: - with open(source, 'r') as f: - template_data = to_text(f.read()) - except IOError: - return dict(failed=True, msg='unable to load src file') - - # Create a template search path in the following order: - # [working_path, self_role_path, dependent_role_paths, dirname(source)] - searchpath = [working_path] - if self._task._role is not None: - searchpath.append(self._task._role._role_path) - if hasattr(self._task, "_block:"): - dep_chain = self._task._block.get_dep_chain() - if dep_chain is not None: - for role in dep_chain: - searchpath.append(role._role_path) - searchpath.append(os.path.dirname(source)) - self._templar.environment.loader.searchpath = searchpath - self._task.args['src'] = self._templar.template(template_data) diff --git a/test/units/modules/network/apconos/__init__.py b/test/units/modules/network/apconos/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/test/units/modules/network/apconos/apconos_module.py b/test/units/modules/network/apconos/apconos_module.py new file mode 100644 index 00000000000000..2619767ee65fda --- /dev/null +++ b/test/units/modules/network/apconos/apconos_module.py @@ -0,0 +1,89 @@ +# (c) 2019 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 Exception: + pass + + fixture_data[path] = data + return data + + +class TestApconosModule(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/apconos/fixtures/enable_ssh b/test/units/modules/network/apconos/fixtures/enable_ssh new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/test/units/modules/network/apconos/fixtures/show_version b/test/units/modules/network/apconos/fixtures/show_version new file mode 100644 index 00000000000000..a541d9e97de7bc --- /dev/null +++ b/test/units/modules/network/apconos/fixtures/show_version @@ -0,0 +1,2 @@ +APCON +COMPONENT MODEL VERSION diff --git a/test/units/modules/network/apconos/test_apconos_command.py b/test/units/modules/network/apconos/test_apconos_command.py new file mode 100644 index 00000000000000..dac2e2fd33d757 --- /dev/null +++ b/test/units/modules/network/apconos/test_apconos_command.py @@ -0,0 +1,122 @@ +# (c) 2019 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 units.compat.mock import patch +from ansible.modules.network.apconos import apconos_command +from units.modules.utils import set_module_args +from .apconos_module import TestApconosModule, load_fixture + + +class TestApconosCommandModule(TestApconosModule): + + module = apconos_command + + def setUp(self): + super(TestApconosCommandModule, self).setUp() + + self.mock_run_commands = patch('ansible.modules.network.apconos.apconos_command.run_commands') + self.run_commands = self.mock_run_commands.start() + + def tearDown(self): + super(TestApconosCommandModule, 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: + filename = str(item).replace(' ', '_') + output.append(load_fixture(filename)) + return output + + self.run_commands.side_effect = load_from_file + + def test_apcon_command_simple(self): + set_module_args(dict(commands=['show version'])) + result = self.execute_module() + self.assertEqual(len(result['stdout_lines']), 1) + self.assertEqual(result['stdout_lines'][0][0], 'APCON') + + def test_apcon_command_multiple(self): + set_module_args(dict(commands=['show version', 'show version'])) + result = self.execute_module() + self.assertEqual(len(result['stdout_lines']), 2) + self.assertEqual(result['stdout_lines'][0][0], 'APCON') + self.assertEqual(result['stdout_lines'][1][0], 'APCON') + + def test_apcon_command_wait_for(self): + wait_for = 'result[0] contains "APCON"' + set_module_args(dict(commands=['show version'], wait_for=wait_for)) + self.execute_module() + + def test_apcon_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_apcon_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_apcon_command_match_any(self): + wait_for = ['result[0] contains "test string"', + 'result[0] contains "VERSION"'] + set_module_args(dict(commands=['show version'], wait_for=wait_for, match='any')) + self.execute_module() + + def test_apcon_command_match_all(self): + wait_for = ['result[0] contains "COMPONENT"', + 'result[0] contains "MODEL"', + 'result[0] contains "VERSION"'] + set_module_args(dict(commands=['show version'], wait_for=wait_for, match='all')) + self.execute_module() + + def test_apcon_command_match_all_failure(self): + wait_for = ['result[0] contains "APCON OS"', + '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_apcon_command_checkmode_warning(self): + commands = ['enable ssh'] + set_module_args({ + 'commands': commands, + '_ansible_check_mode': True, + }) + result = self.execute_module() + self.assertEqual( + result['warnings'], + ['Only show commands are supported when using check mode, not executing enable ssh'], + ) + + def test_apcon_command_checkmode_not_warning(self): + commands = ['enable ssh'] + set_module_args(dict(commands=commands, _ansible_check_mode=False)) + result = self.execute_module(changed=True) + self.assertEqual(result['warnings'], []) \ No newline at end of file From 809e419db1be62846e0fbbc70d4b89c01e58d252 Mon Sep 17 00:00:00 2001 From: chunwei li Date: Thu, 1 Aug 2019 15:05:52 -0700 Subject: [PATCH 16/17] Fix for santiy test --- lib/ansible/module_utils/network/apconos/apconos.py | 2 ++ lib/ansible/modules/network/apconos/apconos_command.py | 4 +++- test/sanity/code-smell/action-plugin-docs.py | 1 - test/units/modules/network/apconos/apconos_module.py | 2 +- test/units/modules/network/apconos/test_apconos_command.py | 2 +- 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/ansible/module_utils/network/apconos/apconos.py b/lib/ansible/module_utils/network/apconos/apconos.py index 0ef08d47287c24..0d88baaf776bde 100644 --- a/lib/ansible/module_utils/network/apconos/apconos.py +++ b/lib/ansible/module_utils/network/apconos/apconos.py @@ -31,6 +31,8 @@ # Contains utility methods # APCON Networking +from __future__ import (absolute_import, division, print_function) + import time import socket import re diff --git a/lib/ansible/modules/network/apconos/apconos_command.py b/lib/ansible/modules/network/apconos/apconos_command.py index 988c82c1590d61..d1a7168c4e03ce 100644 --- a/lib/ansible/modules/network/apconos/apconos_command.py +++ b/lib/ansible/modules/network/apconos/apconos_command.py @@ -106,8 +106,9 @@ from ansible.module_utils.network.common.parsing import Conditional from ansible.module_utils.six import string_types + def parse_commands(module, warnings): - #commands = transform_commands(module) + commands = module.params['commands'] if module.check_mode: @@ -121,6 +122,7 @@ def parse_commands(module, warnings): return commands + def main(): spec = dict( commands=dict(type='list', required=True), diff --git a/test/sanity/code-smell/action-plugin-docs.py b/test/sanity/code-smell/action-plugin-docs.py index 7be1fa51ab2793..869719f002ff74 100755 --- a/test/sanity/code-smell/action-plugin-docs.py +++ b/test/sanity/code-smell/action-plugin-docs.py @@ -24,7 +24,6 @@ def main(): # The following action plugins provide base classes for network platform specific modules to support `connection: local`. # Once we fully deprecate the use of connection local, the base classes will go away. 'aireos', - 'apconos', 'aruba', 'asa', 'ce', diff --git a/test/units/modules/network/apconos/apconos_module.py b/test/units/modules/network/apconos/apconos_module.py index 2619767ee65fda..712d3f18d8bb30 100644 --- a/test/units/modules/network/apconos/apconos_module.py +++ b/test/units/modules/network/apconos/apconos_module.py @@ -81,7 +81,7 @@ def changed(self, changed=False): self.module.main() result = exc.exception.args[0] - + self.assertEqual(result['changed'], changed, result) return result diff --git a/test/units/modules/network/apconos/test_apconos_command.py b/test/units/modules/network/apconos/test_apconos_command.py index dac2e2fd33d757..1a0663d42e71c2 100644 --- a/test/units/modules/network/apconos/test_apconos_command.py +++ b/test/units/modules/network/apconos/test_apconos_command.py @@ -119,4 +119,4 @@ def test_apcon_command_checkmode_not_warning(self): commands = ['enable ssh'] set_module_args(dict(commands=commands, _ansible_check_mode=False)) result = self.execute_module(changed=True) - self.assertEqual(result['warnings'], []) \ No newline at end of file + self.assertEqual(result['warnings'], []) From 63216751f947c10901900ac8b61b0e253859ae49 Mon Sep 17 00:00:00 2001 From: chunwei li Date: Thu, 1 Aug 2019 16:00:47 -0700 Subject: [PATCH 17/17] santiy --- lib/ansible/plugins/doc_fragments/apconos.py | 1 + test/units/modules/network/apconos/apconos_module.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/ansible/plugins/doc_fragments/apconos.py b/lib/ansible/plugins/doc_fragments/apconos.py index c98c4742c043bb..442c08cc98ec5a 100644 --- a/lib/ansible/plugins/doc_fragments/apconos.py +++ b/lib/ansible/plugins/doc_fragments/apconos.py @@ -2,6 +2,7 @@ # Copyright: (c) 2019, Peter Sprygada # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) class ModuleDocFragment(object): diff --git a/test/units/modules/network/apconos/apconos_module.py b/test/units/modules/network/apconos/apconos_module.py index 712d3f18d8bb30..03897ab67aa353 100644 --- a/test/units/modules/network/apconos/apconos_module.py +++ b/test/units/modules/network/apconos/apconos_module.py @@ -81,9 +81,9 @@ def changed(self, changed=False): self.module.main() result = exc.exception.args[0] - self.assertEqual(result['changed'], changed, result) return result def load_fixtures(self, commands=None): pass +