From f5ebd9736c68f77c08c80f4d81d9fe517b067ada Mon Sep 17 00:00:00 2001 From: Anil Kumar Muraleedharan Date: Wed, 9 Jan 2019 21:13:57 +0530 Subject: [PATCH] Lenovo cnos banner new module (#50145) * Adding cnos_banner module to the list. --- .../modules/network/cnos/cnos_banner.py | 265 ++++++++++++++++++ .../modules/network/cnos/cnos_linkagg.py | 2 +- lib/ansible/modules/network/cnos/cnos_vlan.py | 2 +- test/integration/targets/cnos_banner/aliases | 2 + .../cnos_banner/cnos_banner_sample_hosts | 14 + .../targets/cnos_banner/defaults/main.yaml | 2 + .../targets/cnos_banner/tasks/cli.yaml | 22 ++ .../targets/cnos_banner/tasks/main.yaml | 2 + .../cnos_banner/tests/cli/basic-login.yaml | 43 +++ .../cnos_banner/tests/cli/basic-motd.yaml | 45 +++ .../cnos_banner/tests/cli/basic-no-login.yaml | 36 +++ .../targets/cnos_banner/vars/main.yaml | 9 + .../cnos/fixtures/cnos_banner_show_banner.txt | 3 + .../modules/network/cnos/test_cnos_banner.py | 62 ++++ 14 files changed, 507 insertions(+), 2 deletions(-) create mode 100644 lib/ansible/modules/network/cnos/cnos_banner.py create mode 100644 test/integration/targets/cnos_banner/aliases create mode 100644 test/integration/targets/cnos_banner/cnos_banner_sample_hosts create mode 100644 test/integration/targets/cnos_banner/defaults/main.yaml create mode 100644 test/integration/targets/cnos_banner/tasks/cli.yaml create mode 100644 test/integration/targets/cnos_banner/tasks/main.yaml create mode 100644 test/integration/targets/cnos_banner/tests/cli/basic-login.yaml create mode 100644 test/integration/targets/cnos_banner/tests/cli/basic-motd.yaml create mode 100644 test/integration/targets/cnos_banner/tests/cli/basic-no-login.yaml create mode 100644 test/integration/targets/cnos_banner/vars/main.yaml create mode 100644 test/units/modules/network/cnos/fixtures/cnos_banner_show_banner.txt create mode 100644 test/units/modules/network/cnos/test_cnos_banner.py diff --git a/lib/ansible/modules/network/cnos/cnos_banner.py b/lib/ansible/modules/network/cnos/cnos_banner.py new file mode 100644 index 00000000000..c7a2db0c1b1 --- /dev/null +++ b/lib/ansible/modules/network/cnos/cnos_banner.py @@ -0,0 +1,265 @@ +#!/usr/bin/python +# +# Copyright (C) 2017 Lenovo, Inc. +# (c) 2017, Ansible by 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 . +# +# Module to send banner commands to Lenovo Switches +# Two types of banners are supported login and motd +# Lenovo Networking +# + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = """ +--- +module: cnos_banner +version_added: "2.8" +author: "Anil Kumar Muraleedharan (@amuraleedhar)" +short_description: Manage multiline banners on Lenovo CNOS devices +description: + - This will configure both login and motd banners on remote devices + running Lenovo CNOS. It allows playbooks to add or remote + banner text from the active running configuration. +notes: + - Tested against CNOS 10.8.1 +options: + banner: + description: + - Specifies which banner should be configured on the remote device. + In Ansible 2.8 and earlier only I(login) and I(motd) were supported. + required: true + choices: ['login', 'motd'] + text: + description: + - The banner text that should be + present in the remote device running configuration. This argument + accepts a multiline string, with no empty lines. Requires + I(state=present). + state: + description: + - Specifies whether or not the configuration is + present in the current devices active running configuration. + default: present + choices: ['present', 'absent'] + provider: + description: + - B(Deprecated) + - "Starting with Ansible 2.5 we recommend using + C(connection: network_cli)." + - For more information please see the + L(CNOS Platform Options guide, ../network/user_guide/platform_cnos.html). + - HORIZONTALLINE + - A dict object containing connection details. + version_added: "2.8" + 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. + required: true + port: + description: + - Specifies the port to use when building the connection to the + remote device. + 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. + 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. + 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. + 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. + 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. +""" + +EXAMPLES = """ +- name: configure the login banner + cnos_banner: + banner: login + text: | + this is my login banner + that contains a multiline + string + state: present + +- name: remove the motd banner + cnos_banner: + banner: motd + state: absent + +- name: Configure banner from file + cnos_banner: + banner: motd + text: "{{ lookup('file', './config_partial/raw_banner.cfg') }}" + state: present + +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always + type: list + sample: + - banner login + - this is my login banner + - that contains a multiline + - string +""" +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.connection import exec_command +from ansible.module_utils.network.cnos.cnos import load_config, run_commands +from ansible.module_utils.network.cnos.cnos import check_args +from ansible.module_utils.network.cnos.cnos import cnos_argument_spec +from ansible.module_utils._text import to_text +import re + + +def map_obj_to_commands(updates, module): + commands = list() + want, have = updates + state = module.params['state'] + + if state == 'absent' and 'text' in have.keys() and have['text']: + commands.append('no banner %s' % module.params['banner']) + + elif state == 'present': + if want['text'] and (want['text'] != have.get('text')): + banner_cmd = 'banner %s ' % module.params['banner'] + for bline in want['text'].strip().splitlines(): + final_cmd = banner_cmd + bline.strip() + commands.append(final_cmd) + + return commands + + +def map_config_to_obj(module): + rc, out, err = exec_command(module, + 'show banner %s' % module.params['banner']) + if rc == 0: + output = out + else: + rc, out, err = exec_command(module, + 'show running-config | include banner %s' + % module.params['banner']) + if out: + output = re.search(r'\^C(.*)\^C', out, re.S).group(1).strip() + else: + output = None + obj = {'banner': module.params['banner'], 'state': 'absent'} + if output: + obj['text'] = output + obj['state'] = 'present' + return obj + + +def map_params_to_obj(module): + text = module.params['text'] + if text: + text = to_text(text).strip() + + return { + 'banner': module.params['banner'], + 'text': text, + 'state': module.params['state'] + } + + +def main(): + """ main entry point for module execution + """ + argument_spec = dict( + banner=dict(required=True, choices=['login', 'motd']), + text=dict(), + state=dict(default='present', choices=['present', 'absent']) + ) + + argument_spec.update(cnos_argument_spec) + + required_if = [('state', 'present', ('text',))] + + module = AnsibleModule(argument_spec=argument_spec, + required_if=required_if, + supports_check_mode=True) + + warnings = list() + check_args(module, warnings) + + result = {'changed': False} + if warnings: + result['warnings'] = warnings + want = map_params_to_obj(module) + have = map_config_to_obj(module) + + commands = map_obj_to_commands((want, have), module) + result['commands'] = commands + + if commands: + if not module.check_mode: + response = load_config(module, commands) + + result['changed'] = True + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/network/cnos/cnos_linkagg.py b/lib/ansible/modules/network/cnos/cnos_linkagg.py index 7a3222b0f2d..b6701acb211 100644 --- a/lib/ansible/modules/network/cnos/cnos_linkagg.py +++ b/lib/ansible/modules/network/cnos/cnos_linkagg.py @@ -66,7 +66,7 @@ options: description: - B(Deprecated) - "Starting with Ansible 2.5 we recommend using C(connection: network_cli)." - - For more information please see the L(IOS Platform Options guide, ../network/user_guide/platform_ios.html). + - For more information please see the L(CNOS Platform Options guide, ../network/user_guide/platform_cnos.html). - HORIZONTALLINE - A dict object containing connection details. version_added: "2.8" diff --git a/lib/ansible/modules/network/cnos/cnos_vlan.py b/lib/ansible/modules/network/cnos/cnos_vlan.py index 3c4aa70053d..67f13c01db7 100644 --- a/lib/ansible/modules/network/cnos/cnos_vlan.py +++ b/lib/ansible/modules/network/cnos/cnos_vlan.py @@ -89,7 +89,7 @@ options: description: - B(Deprecated) - "Starting with Ansible 2.5 we recommend using C(connection: network_cli)." - - For more information please see the L(IOS Platform Options guide, ../network/user_guide/platform_ios.html). + - For more information please see the L(CNOS Platform Options guide, ../network/user_guide/platform_cnos.html). - HORIZONTALLINE - A dict object containing connection details. version_added: "2.8" diff --git a/test/integration/targets/cnos_banner/aliases b/test/integration/targets/cnos_banner/aliases new file mode 100644 index 00000000000..be010d923f4 --- /dev/null +++ b/test/integration/targets/cnos_banner/aliases @@ -0,0 +1,2 @@ +# No Lenovo Switch simulator yet, so not enabled +unsupported diff --git a/test/integration/targets/cnos_banner/cnos_banner_sample_hosts b/test/integration/targets/cnos_banner/cnos_banner_sample_hosts new file mode 100644 index 00000000000..9e5a34d70f4 --- /dev/null +++ b/test/integration/targets/cnos_banner/cnos_banner_sample_hosts @@ -0,0 +1,14 @@ +# You have to paste this dummy information in /etc/ansible/hosts +# Notes: +# - Comments begin with the '#' character +# - Blank lines are ignored +# - Groups of hosts are delimited by [header] elements +# - You can enter hostnames or ip addresses +# - A hostname/ip can be a member of multiple groups +# +# In the /etc/ansible/hosts file u have to enter [cnos_banner_sample] tag +# Following you should specify IP Adresses details +# Please change and with appropriate value for your switch. + +[cnos_banner_sample] +10.241.107.39 ansible_network_os=cnos ansible_ssh_user= ansible_ssh_pass= diff --git a/test/integration/targets/cnos_banner/defaults/main.yaml b/test/integration/targets/cnos_banner/defaults/main.yaml new file mode 100644 index 00000000000..5f709c5aac1 --- /dev/null +++ b/test/integration/targets/cnos_banner/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" diff --git a/test/integration/targets/cnos_banner/tasks/cli.yaml b/test/integration/targets/cnos_banner/tasks/cli.yaml new file mode 100644 index 00000000000..303af407622 --- /dev/null +++ b/test/integration/targets/cnos_banner/tasks/cli.yaml @@ -0,0 +1,22 @@ +--- +- name: collect all cli test cases + find: + paths: "{{ role_path }}/tests/cli" + patterns: "{{ testcase }}.yaml" + register: test_cases + delegate_to: localhost + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test cases (connection=network_cli) + include: "{{ test_case_to_run }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run + +- name: run test case (connection=local) + include: "{{ test_case_to_run }} ansible_connection=local" + with_first_found: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/cnos_banner/tasks/main.yaml b/test/integration/targets/cnos_banner/tasks/main.yaml new file mode 100644 index 00000000000..415c99d8b12 --- /dev/null +++ b/test/integration/targets/cnos_banner/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/cnos_banner/tests/cli/basic-login.yaml b/test/integration/targets/cnos_banner/tests/cli/basic-login.yaml new file mode 100644 index 00000000000..0e1e8f97d61 --- /dev/null +++ b/test/integration/targets/cnos_banner/tests/cli/basic-login.yaml @@ -0,0 +1,43 @@ +--- + +- name: setup - remove login + cnos_banner: + banner: login + state: absent + provider: "{{ cli }}" + +- name: Set login + cnos_banner: + banner: login + text: | + this is my login banner + that has a multiline + string + state: present + provider: "{{ cli }}" + + register: result + +- debug: + msg: "{{ result }}" + +- assert: + that: + - "result.changed == true" + +- name: Set login again (idempotent) + cnos_banner: + banner: login + text: | + this is my login banner + that has a multiline + string + state: present + provider: "{{ cli }}" + + register: result + +- assert: + that: + - "result.changed == false" + - "result.commands | length == 0" diff --git a/test/integration/targets/cnos_banner/tests/cli/basic-motd.yaml b/test/integration/targets/cnos_banner/tests/cli/basic-motd.yaml new file mode 100644 index 00000000000..b7bcfc67105 --- /dev/null +++ b/test/integration/targets/cnos_banner/tests/cli/basic-motd.yaml @@ -0,0 +1,45 @@ +--- + +- name: setup - remove motd + cnos_banner: + banner: motd + state: absent + provider: "{{ cli }}" + + +- name: Set motd + cnos_banner: + banner: motd + text: | + this is my motd banner + that has a multiline + string + state: present + provider: "{{ cli }}" + + register: result + +- debug: + msg: "{{ result }}" + +- assert: + that: + - "result.changed == true" +# - "'banner motd @\nthis is my motd banner\nthat has a multiline\nstring\n@' in result.commands" + +- name: Set motd again (idempotent) + cnos_banner: + banner: motd + text: | + this is my motd banner + that has a multiline + string + state: present + provider: "{{ cli }}" + + register: result + +- assert: + that: + - "result.changed == false" + - "result.commands | length == 0" diff --git a/test/integration/targets/cnos_banner/tests/cli/basic-no-login.yaml b/test/integration/targets/cnos_banner/tests/cli/basic-no-login.yaml new file mode 100644 index 00000000000..b3d7d4b6715 --- /dev/null +++ b/test/integration/targets/cnos_banner/tests/cli/basic-no-login.yaml @@ -0,0 +1,36 @@ +--- +- name: Setup + cnos_banner: + banner: login + text: | + Junk login banner + over multiple lines + state: present + provider: "{{ cli }}" + +- name: remove login + cnos_banner: + banner: login + state: absent + provider: "{{ cli }}" + register: result + +- debug: + msg: "{{ result }}" + +- assert: + that: + - "result.changed == true" + - "'no banner login' in result.commands" # does this break due to "contains?" + +- name: remove login (idempotent) + cnos_banner: + banner: login + state: absent + provider: "{{ cli }}" + register: result + +- assert: + that: + - "result.changed == false" + - "result.commands | length == 0" diff --git a/test/integration/targets/cnos_banner/vars/main.yaml b/test/integration/targets/cnos_banner/vars/main.yaml new file mode 100644 index 00000000000..aa25153ec86 --- /dev/null +++ b/test/integration/targets/cnos_banner/vars/main.yaml @@ -0,0 +1,9 @@ +--- +cli: + host: "{{ inventory_hostname }}" + port: 22 + username: admin + password: admin + timeout: 30 + authorize: True + auth_pass: diff --git a/test/units/modules/network/cnos/fixtures/cnos_banner_show_banner.txt b/test/units/modules/network/cnos/fixtures/cnos_banner_show_banner.txt new file mode 100644 index 00000000000..a134a31753c --- /dev/null +++ b/test/units/modules/network/cnos/fixtures/cnos_banner_show_banner.txt @@ -0,0 +1,3 @@ +this is a sample +mulitline banner +used for testing diff --git a/test/units/modules/network/cnos/test_cnos_banner.py b/test/units/modules/network/cnos/test_cnos_banner.py new file mode 100644 index 00000000000..24a18cc97ed --- /dev/null +++ b/test/units/modules/network/cnos/test_cnos_banner.py @@ -0,0 +1,62 @@ +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from units.compat.mock import patch +from ansible.modules.network.cnos import cnos_banner +from units.modules.utils import set_module_args +from .cnos_module import TestCnosModule, load_fixture + + +class TestCnosBannerModule(TestCnosModule): + + module = cnos_banner + + def setUp(self): + super(TestCnosBannerModule, self).setUp() + + self.mock_exec_command = patch('ansible.modules.network.cnos.cnos_banner.exec_command') + self.exec_command = self.mock_exec_command.start() + + self.mock_load_config = patch('ansible.modules.network.cnos.cnos_banner.load_config') + self.load_config = self.mock_load_config.start() + + def tearDown(self): + super(TestCnosBannerModule, self).tearDown() + self.mock_exec_command.stop() + self.mock_load_config.stop() + + def load_fixtures(self, commands=None): + self.exec_command.return_value = (0, load_fixture('cnos_banner_show_banner.txt').strip(), None) + self.load_config.return_value = dict(diff=None, session='session') + + def test_cnos_banner_create(self): + for banner_type in ('login', 'motd'): + set_module_args(dict(banner=banner_type, text='test\nbanner\nstring')) + commands = ['banner {0} test'.format(banner_type), 'banner {0} banner'.format(banner_type), 'banner {0} string'.format(banner_type)] + self.execute_module(changed=True, commands=commands) + + def test_cnos_banner_remove(self): + set_module_args(dict(banner='login', state='absent')) + commands = ['no banner login'] + self.execute_module(changed=True, commands=commands) + + def test_cnos_banner_nochange(self): + banner_text = load_fixture('cnos_banner_show_banner.txt').strip() + set_module_args(dict(banner='login', text=banner_text)) + self.execute_module()