From 6e56a615351c1c38793f5cc1697622400a9e975f Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Thu, 16 Mar 2017 16:35:44 +0100 Subject: [PATCH] Add ios_banner module (#22687) With unit and integration tests. --- CHANGELOG.md | 1 + lib/ansible/modules/network/ios/ios_banner.py | 163 ++++++++++++++++++ test/integration/ios.yaml | 1 + test/integration/targets/ios_banner/aliases | 1 + .../targets/ios_banner/defaults/main.yaml | 2 + .../targets/ios_banner/meta/main.yml | 2 + .../targets/ios_banner/tasks/cli.yaml | 15 ++ .../targets/ios_banner/tasks/main.yaml | 2 + .../ios_banner/tests/cli/basic-login.yaml | 51 ++++++ .../ios_banner/tests/cli/basic-motd.yaml | 50 ++++++ .../ios_banner/tests/cli/basic-no-login.yaml | 44 +++++ .../ios/fixtures/ios_banner_show_banner.txt | 3 + .../modules/network/ios/test_ios_banner.py | 59 +++++++ 13 files changed, 394 insertions(+) create mode 100644 lib/ansible/modules/network/ios/ios_banner.py create mode 100644 test/integration/targets/ios_banner/aliases create mode 100644 test/integration/targets/ios_banner/defaults/main.yaml create mode 100644 test/integration/targets/ios_banner/meta/main.yml create mode 100644 test/integration/targets/ios_banner/tasks/cli.yaml create mode 100644 test/integration/targets/ios_banner/tasks/main.yaml create mode 100644 test/integration/targets/ios_banner/tests/cli/basic-login.yaml create mode 100644 test/integration/targets/ios_banner/tests/cli/basic-motd.yaml create mode 100644 test/integration/targets/ios_banner/tests/cli/basic-no-login.yaml create mode 100644 test/units/modules/network/ios/fixtures/ios_banner_show_banner.txt create mode 100644 test/units/modules/network/ios/test_ios_banner.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a322007e68..4a29d6141a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -171,6 +171,7 @@ Ansible Changes By Release * ipa_user - ipinfoio_facts - ios: + * ios_banner * ios_system * ios_vrf - iosxr_system diff --git a/lib/ansible/modules/network/ios/ios_banner.py b/lib/ansible/modules/network/ios/ios_banner.py new file mode 100644 index 0000000000..4d32fe3215 --- /dev/null +++ b/lib/ansible/modules/network/ios/ios_banner.py @@ -0,0 +1,163 @@ +#!/usr/bin/python +# +# 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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = """ +--- +module: ios_banner +version_added: "2.3" +author: "Ricardo Carrillo Cruz (@rcarrillocruz)" +short_description: Manage multiline banners on Cisco IOS devices +description: + - This will configure both login and motd banners on remote devices + running Cisco IOS. It allows playbooks to add or remote + banner text from the active running configuration. +extends_documentation_fragment: ios +options: + banner: + description: + - Specifies which banner that should be + configured on the remote device. + required: true + default: null + choices: ['login', 'banner'] + text: + description: + - The banner text that should be + present in the remote device running configuration. This argument + accepts a multiline string. Requires I(state=present). + default: null + state: + description: + - Specifies whether or not the configuration is + present in the current devices active running configuration. + default: present + choices: ['present', 'absent'] +""" + +EXAMPLES = """ +- name: configure the login banner + ios_banner: + banner: login + text: | + this is my login banner + that contains a multiline + string + state: present + +- name: remove the motd banner + ios_banner: + banner: motd + state: absent +""" + +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.ios import load_config, run_commands +from ansible.module_utils.ios import ios_argument_spec, check_args + +def map_obj_to_commands(updates, module): + commands = list() + want, have = updates + state = module.params['state'] + + if state == 'absent' 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'] + banner_cmd += ' @\n' + banner_cmd += want['text'].strip() + banner_cmd += '\n@' + commands.append(banner_cmd) + + return commands + +def map_config_to_obj(module): + output = run_commands(module, ['show banner %s' % module.params['banner']]) + obj = {'banner': module.params['banner'], 'state': 'absent'} + if output: + obj['text'] = output[0] + obj['state'] = 'present' + return obj + +def map_params_to_obj(module): + text = module.params['text'] + if text: + text = str(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(ios_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/test/integration/ios.yaml b/test/integration/ios.yaml index 7e0bc264bd..ea4d97e2dd 100644 --- a/test/integration/ios.yaml +++ b/test/integration/ios.yaml @@ -8,6 +8,7 @@ debug: false roles: + - { role: ios_banner, when: "limit_to in ['*', 'ios_banner']" } - { role: ios_command, when: "limit_to in ['*', 'ios_command']" } - { role: ios_config, when: "limit_to in ['*', 'ios_config']" } - { role: ios_facts, when: "limit_to in ['*', 'ios_facts']" } diff --git a/test/integration/targets/ios_banner/aliases b/test/integration/targets/ios_banner/aliases new file mode 100644 index 0000000000..93151a8d9d --- /dev/null +++ b/test/integration/targets/ios_banner/aliases @@ -0,0 +1 @@ +network/ci diff --git a/test/integration/targets/ios_banner/defaults/main.yaml b/test/integration/targets/ios_banner/defaults/main.yaml new file mode 100644 index 0000000000..5f709c5aac --- /dev/null +++ b/test/integration/targets/ios_banner/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" diff --git a/test/integration/targets/ios_banner/meta/main.yml b/test/integration/targets/ios_banner/meta/main.yml new file mode 100644 index 0000000000..159cea8d38 --- /dev/null +++ b/test/integration/targets/ios_banner/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - prepare_ios_tests diff --git a/test/integration/targets/ios_banner/tasks/cli.yaml b/test/integration/targets/ios_banner/tasks/cli.yaml new file mode 100644 index 0000000000..d675462dd0 --- /dev/null +++ b/test/integration/targets/ios_banner/tasks/cli.yaml @@ -0,0 +1,15 @@ +--- +- name: collect all cli test cases + find: + paths: "{{ role_path }}/tests/cli" + patterns: "{{ testcase }}.yaml" + register: test_cases + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test case + include: "{{ test_case_to_run }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/ios_banner/tasks/main.yaml b/test/integration/targets/ios_banner/tasks/main.yaml new file mode 100644 index 0000000000..415c99d8b1 --- /dev/null +++ b/test/integration/targets/ios_banner/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/ios_banner/tests/cli/basic-login.yaml b/test/integration/targets/ios_banner/tests/cli/basic-login.yaml new file mode 100644 index 0000000000..9e86324cf2 --- /dev/null +++ b/test/integration/targets/ios_banner/tests/cli/basic-login.yaml @@ -0,0 +1,51 @@ +--- + +- name: setup - remove login + ios_banner: + banner: login + state: absent + authorize: yes + provider: "{{ cli }}" + +- name: Set login + ios_banner: + banner: login + text: | + this is my login banner + that has a multiline + string + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- debug: + msg: "{{ result }}" + +- assert: + that: + - "result.changed == true" + - "'this is my login banner' in result.commands" + - "'that has a multiline' in result.commands" + +- name: Set login again (idempotent) + ios_banner: + banner: login + text: | + this is my login banner + that has a multiline + string + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - "result.changed == false" + - "result.commands | length == 0" + + +# FIXME add in tests for everything defined in docs +# FIXME Test state:absent + test: +# FIXME Without powers ensure "privileged mode required" diff --git a/test/integration/targets/ios_banner/tests/cli/basic-motd.yaml b/test/integration/targets/ios_banner/tests/cli/basic-motd.yaml new file mode 100644 index 0000000000..d71ab1a369 --- /dev/null +++ b/test/integration/targets/ios_banner/tests/cli/basic-motd.yaml @@ -0,0 +1,50 @@ +--- + +- name: setup - remove motd + ios_banner: + banner: motd + state: absent + authorize: yes + +- name: Set motd + ios_banner: + banner: motd + text: | + this is my motd banner + that has a multiline + string + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- debug: + msg: "{{ result }}" + +- assert: + that: + - "result.changed == true" + - "'this is my motd banner' in result.commands" + - "'that has a multiline' in result.commands" + +- name: Set motd again (idempotent) + ios_banner: + banner: motd + text: | + this is my motd banner + that has a multiline + string + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - "result.changed == false" + - "result.commands | length == 0" + + +# FIXME add in tests for everything defined in docs +# FIXME Test state:absent + test: +# FIXME Without powers ensure "privileged mode required" diff --git a/test/integration/targets/ios_banner/tests/cli/basic-no-login.yaml b/test/integration/targets/ios_banner/tests/cli/basic-no-login.yaml new file mode 100644 index 0000000000..9b0f618382 --- /dev/null +++ b/test/integration/targets/ios_banner/tests/cli/basic-no-login.yaml @@ -0,0 +1,44 @@ +--- +- name: Setup + ios_banner: + banner: login + text: | + Junk login banner + over multiple lines + state: present + authorize: yes + provider: "{{ cli }}" + +- name: remove login + ios_banner: + banner: login + state: absent + authorize: yes + 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) + ios_banner: + banner: login + state: absent + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - "result.changed == false" + - "result.commands | length == 0" + + +# FIXME add in tests for everything defined in docs +# FIXME Test state:absent + test: +# FIXME Without powers ensure "privileged mode required" diff --git a/test/units/modules/network/ios/fixtures/ios_banner_show_banner.txt b/test/units/modules/network/ios/fixtures/ios_banner_show_banner.txt new file mode 100644 index 0000000000..a134a31753 --- /dev/null +++ b/test/units/modules/network/ios/fixtures/ios_banner_show_banner.txt @@ -0,0 +1,3 @@ +this is a sample +mulitline banner +used for testing diff --git a/test/units/modules/network/ios/test_ios_banner.py b/test/units/modules/network/ios/test_ios_banner.py new file mode 100644 index 0000000000..46fd9aca53 --- /dev/null +++ b/test/units/modules/network/ios/test_ios_banner.py @@ -0,0 +1,59 @@ +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import json + +from ansible.compat.tests.mock import patch +from ansible.modules.network.ios import ios_banner +from .ios_module import TestIosModule, load_fixture, set_module_args + + +class TestIosBannerModule(TestIosModule): + + module = ios_banner + + def setUp(self): + self.mock_run_commands = patch('ansible.modules.network.ios.ios_banner.run_commands') + self.run_commands = self.mock_run_commands.start() + + self.mock_load_config = patch('ansible.modules.network.ios.ios_banner.load_config') + self.load_config = self.mock_load_config.start() + + def tearDown(self): + self.mock_run_commands.stop() + self.mock_load_config.stop() + + def load_fixtures(self, commands=None): + self.run_commands.return_value = [load_fixture('ios_banner_show_banner.txt').strip()] + self.load_config.return_value = dict(diff=None, session='session') + + def test_ios_banner_create(self): + set_module_args(dict(banner='login', text='test\nbanner\nstring')) + commands = ['banner login @\ntest\nbanner\nstring\n@'] + self.execute_module(changed=True, commands=commands) + + def test_ios_banner_remove(self): + set_module_args(dict(banner='login', state='absent')) + commands = ['no banner login'] + self.execute_module(changed=True, commands=commands) + + def test_ios_banner_nochange(self): + banner_text = load_fixture('ios_banner_show_banner.txt').strip() + set_module_args(dict(banner='login', text=banner_text)) + self.execute_module()