diff --git a/lib/ansible/module_utils/junos.py b/lib/ansible/module_utils/junos.py index e160c05c858..e7d99c03bb6 100644 --- a/lib/ansible/module_utils/junos.py +++ b/lib/ansible/module_utils/junos.py @@ -48,6 +48,12 @@ ARGS_DEFAULT_VALUE = { 'timeout': 10 } +OPERATION_LOOK_UP = { + 'absent': 'delete', + 'active': 'active', + 'suspend': 'inactive' +} + def get_argspec(): return junos_argument_spec @@ -217,6 +223,25 @@ def get_param(module, key): def map_params_to_obj(module, param_to_xpath_map): + """ + Creates a new dictionary with key as xpath corresponding + to param and value is a dict with metadata and value for + the xpath. + Acceptable metadata keys: + 'xpath': Relative xpath corresponding to module param. + 'value': Value of param. + 'tag_only': Value is indicated by tag only in xml hierarchy. + 'leaf_only': If operation is to be added at leaf node only. + eg: Output + { + 'name': {'xpath': 'name', 'value': 'ge-0/0/1'} + 'disable': {'xpath': 'disable', 'tag_only': True} + } + + :param module: + :param param_to_xpath_map: Modules params to xpath map + :return: obj + """ obj = collections.OrderedDict() for key, attrib in param_to_xpath_map.items(): if key in module.params: @@ -236,30 +261,35 @@ def map_obj_to_ele(module, want, top, value_map=None): top_ele = top.split('/') root = Element(top_ele[0]) ele = root + oper = None if len(top_ele) > 1: for item in top_ele[1:-1]: ele = SubElement(ele, item) container = ele state = module.params.get('state') + # build xml subtree for obj in want: node = SubElement(container, top_ele[-1]) if state and state != 'present': - if state == 'absent': - node.set('operation', 'delete') - elif state == 'active': - node.set('active', 'active') - elif state == 'suspend': - node.set('inactive', 'inactive') + oper = OPERATION_LOOK_UP.get(state) + node.set(oper, oper) for xpath, attrib in obj.items(): tag_only = attrib.get('tag_only', False) + leaf_only = attrib.get('leaf_only', False) value = attrib.get('value') + # convert param value to device specific value if value_map and xpath in value_map: value = value_map[xpath].get(value) - if value or tag_only: + # for leaf only fields operation attributes should be at leaf level + # and not at node level. + if leaf_only and node.attrib.get(oper): + node.attrib.pop(oper) + + if value or tag_only or leaf_only: ele = node tags = xpath.split('/') @@ -269,6 +299,8 @@ def map_obj_to_ele(module, want, top, value_map=None): if tag_only: if not value: ele.set('delete', 'delete') + elif leaf_only and oper: + ele.set(oper, oper) else: ele.text = to_text(value, errors='surrogate_then_replace') diff --git a/lib/ansible/modules/network/eos/eos_banner.py b/lib/ansible/modules/network/eos/eos_banner.py index eb760218760..288bc70b5ad 100644 --- a/lib/ansible/modules/network/eos/eos_banner.py +++ b/lib/ansible/modules/network/eos/eos_banner.py @@ -39,7 +39,7 @@ options: configured on the remote device. required: true default: null - choices: ['login', 'banner'] + choices: ['login', 'motd'] text: description: - The banner text that should be diff --git a/lib/ansible/modules/network/ios/ios_banner.py b/lib/ansible/modules/network/ios/ios_banner.py index 8ef40b07222..876fd5acce5 100644 --- a/lib/ansible/modules/network/ios/ios_banner.py +++ b/lib/ansible/modules/network/ios/ios_banner.py @@ -39,7 +39,7 @@ options: configured on the remote device. required: true default: null - choices: ['login', 'banner'] + choices: ['login', 'motd'] text: description: - The banner text that should be diff --git a/lib/ansible/modules/network/iosxr/iosxr_banner.py b/lib/ansible/modules/network/iosxr/iosxr_banner.py index 24938662902..4e85cb3b088 100644 --- a/lib/ansible/modules/network/iosxr/iosxr_banner.py +++ b/lib/ansible/modules/network/iosxr/iosxr_banner.py @@ -40,7 +40,7 @@ options: configured on the remote device. required: true default: null - choices: ['login', 'banner'] + choices: ['login', 'motd'] text: description: - The banner text that should be diff --git a/lib/ansible/modules/network/junos/junos_banner.py b/lib/ansible/modules/network/junos/junos_banner.py new file mode 100644 index 00000000000..9c11819fcc0 --- /dev/null +++ b/lib/ansible/modules/network/junos/junos_banner.py @@ -0,0 +1,177 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# 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: junos_banner +version_added: "2.4" +author: "Ganesh Nalawade (@ganeshrn)" +short_description: Manage multiline banners on Juniper JUNOS devices +description: + - This will configure both login and motd banners on network devices. + It allows playbooks to add or remote + banner text from the active running configuration. +options: + banner: + description: + - Specifies which banner that should be + configured on the remote device. Value C(login) indicates + system login message prior to authenticating, C(motd) is login + announcement after successful authentication. + 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). + default: null + state: + description: + - Specifies whether or not the configuration is + present in the current devices active running configuration. + default: present + choices: ['present', 'absent', 'active', 'suspend'] +""" + +EXAMPLES = """ +- name: configure the login banner + junos_banner: + banner: login + text: | + this is my login banner + that contains a multiline + string + state: present + +- name: remove the motd banner + junos_banner: + banner: motd + state: absent + +- name: deactivate the motd banner + junos_banner: + banner: motd + state: suspend + +- name: activate the motd banner + junos_banner: + banner: motd + state: active + +- name: Configure banner from file + junos_banner: + banner: motd + text: "{{ lookup('file', './config_partial/raw_banner.cfg') }}" + state: present +""" + +RETURN = """ +rpc: + description: load-configuration RPC send to the device + returned: when configuration is changed on device + type: string + sample: > + + + this is my login banner + + " +""" +import collections + +from xml.etree.ElementTree import tostring + +from ansible.module_utils.junos import junos_argument_spec, check_args +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.junos import load_config, map_params_to_obj, map_obj_to_ele + +USE_PERSISTENT_CONNECTION = True + + +def validate_param_values(module, obj): + for key in obj: + # validate the param value (if validator func exists) + validator = globals().get('validate_%s' % key) + if callable(validator): + validator(module.params.get(key), module) + + +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', 'active', 'suspend']) + ) + + argument_spec.update(junos_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 + + top = 'system/login' + + param_to_xpath_map = collections.OrderedDict() + + param_to_xpath_map.update({ + 'text': {'xpath': 'message' if module.params['banner'] == 'login' else 'announcement', + 'leaf_only': True} + }) + + validate_param_values(module, param_to_xpath_map) + + want = list() + want.append(map_params_to_obj(module, param_to_xpath_map)) + ele = map_obj_to_ele(module, want, top) + + kwargs = {'commit': not module.check_mode} + kwargs['action'] = 'replace' + + diff = load_config(module, tostring(ele), warnings, **kwargs) + + if diff: + result.update({ + 'changed': True, + 'diff': {'prepared': diff}, + 'rpc': tostring(ele) + }) + + module.exit_json(**result) + +if __name__ == "__main__": + main() diff --git a/lib/ansible/modules/network/net_banner.py b/lib/ansible/modules/network/net_banner.py index bfe79a83c33..c1a2f40d578 100644 --- a/lib/ansible/modules/network/net_banner.py +++ b/lib/ansible/modules/network/net_banner.py @@ -37,7 +37,7 @@ options: - Specifies which banner that should be configured on the remote device. required: true - choices: ['login', 'banner'] + choices: ['login', 'motd'] text: description: - The banner text that should be @@ -85,4 +85,15 @@ commands: - this is my login banner - that contains a multiline - string + +rpc: + description: load-configuration RPC send to the device + returned: when configuration is changed on device + type: string + sample: > + + + this is my login banner + + " """ diff --git a/lib/ansible/modules/network/nxos/nxos_banner.py b/lib/ansible/modules/network/nxos/nxos_banner.py index a40407f7714..1ba31bcd423 100644 --- a/lib/ansible/modules/network/nxos/nxos_banner.py +++ b/lib/ansible/modules/network/nxos/nxos_banner.py @@ -40,7 +40,7 @@ options: configured on the remote device. required: true default: null - choices: ['exec', 'banner'] + choices: ['exec', 'motd'] text: description: - The banner text that should be diff --git a/test/integration/junos.yaml b/test/integration/junos.yaml index 6cf8e7135f5..81ae5c5cdad 100644 --- a/test/integration/junos.yaml +++ b/test/integration/junos.yaml @@ -16,3 +16,4 @@ - { role: junos_template, when: "limit_to in ['*', 'junos_template']" } - { role: junos_vlan, when: "limit_to in ['*', 'junos_vlan']" } - { role: junos_interface, when: "limit_to in ['*', 'junos_interface']" } + - { role: junos_banner, when: "limit_to in ['*', 'junos_banner']" } diff --git a/test/integration/targets/junos_banner/defaults/main.yaml b/test/integration/targets/junos_banner/defaults/main.yaml new file mode 100644 index 00000000000..9ef5ba51651 --- /dev/null +++ b/test/integration/targets/junos_banner/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "*" +test_items: [] diff --git a/test/integration/targets/junos_banner/tasks/main.yaml b/test/integration/targets/junos_banner/tasks/main.yaml new file mode 100644 index 00000000000..cc27f174fd8 --- /dev/null +++ b/test/integration/targets/junos_banner/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: netconf.yaml, tags: ['netconf'] } diff --git a/test/integration/targets/junos_banner/tasks/netconf.yaml b/test/integration/targets/junos_banner/tasks/netconf.yaml new file mode 100644 index 00000000000..c6a07db9a63 --- /dev/null +++ b/test/integration/targets/junos_banner/tasks/netconf.yaml @@ -0,0 +1,14 @@ +- name: collect netconf test cases + find: + paths: "{{ role_path }}/tests/netconf" + 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/junos_banner/tests/netconf/basic.yaml b/test/integration/targets/junos_banner/tests/netconf/basic.yaml new file mode 100644 index 00000000000..9ca9c7f332c --- /dev/null +++ b/test/integration/targets/junos_banner/tests/netconf/basic.yaml @@ -0,0 +1,141 @@ +--- +- debug: msg="START junos_banner netconf/basic.yaml" + +- name: setup - remove login banner + junos_banner: + banner: login + state: absent + provider: "{{ netconf }}" + +- name: Create login banner + junos_banner: + banner: login + text: this is my login banner + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'this is my login banner' in result.rpc" + +- name: Create login banner (idempotent) + junos_banner: + banner: login + text: this is my login banner + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == false" + +- name: Deactivate login banner + junos_banner: + banner: login + text: this is my login banner + state: suspend + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" + +- name: Activate login banner + junos_banner: + banner: login + text: this is my login banner + state: active + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" + +- name: delete login banner + junos_banner: + banner: login + state: absent + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" + +- name: setup - remove motd banner + junos_banner: + banner: motd + state: absent + provider: "{{ netconf }}" + +- name: Create motd banner + junos_banner: + banner: motd + text: this is my motd banner + state: present + provider: "{{ netconf }}" + register: result + +- debug: + msg: "{{ result }}" + +- assert: + that: + - "result.changed == true" + - "'this is my motd banner' in result.rpc" + +- name: Create motd banner (idempotent) + junos_banner: + banner: motd + text: this is my motd banner + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == false" + +- name: Deactivate motd banner + junos_banner: + banner: motd + state: suspend + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" + +- name: Activate motd banner + junos_banner: + banner: motd + state: active + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" + +- name: delete motd banner + junos_banner: + banner: motd + state: absent + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" diff --git a/test/integration/targets/junos_interface/tests/netconf/basic.yaml b/test/integration/targets/junos_interface/tests/netconf/basic.yaml index 306183a8be1..70f993ba058 100644 --- a/test/integration/targets/junos_interface/tests/netconf/basic.yaml +++ b/test/integration/targets/junos_interface/tests/netconf/basic.yaml @@ -126,5 +126,5 @@ - assert: that: - "result.changed == true" - - "'' in result.rpc" + - "'' in result.rpc" - "'ge-0/0/1' in result.rpc" diff --git a/test/integration/targets/junos_vlan/tests/netconf/basic.yaml b/test/integration/targets/junos_vlan/tests/netconf/basic.yaml index 0c4fed123dd..a090b84c051 100644 --- a/test/integration/targets/junos_vlan/tests/netconf/basic.yaml +++ b/test/integration/targets/junos_vlan/tests/netconf/basic.yaml @@ -79,5 +79,5 @@ - assert: that: - "result.changed == true" - - "'' in result.rpc" + - "'' in result.rpc" - "'test-vlan' in result.rpc" diff --git a/test/integration/targets/net_banner/tasks/main.yaml b/test/integration/targets/net_banner/tasks/main.yaml index 415c99d8b12..1bf87081a97 100644 --- a/test/integration/targets/net_banner/tasks/main.yaml +++ b/test/integration/targets/net_banner/tasks/main.yaml @@ -1,2 +1,3 @@ --- - { include: cli.yaml, tags: ['cli'] } +- { include: netconf.yaml, tags: ['netconf'] } \ No newline at end of file diff --git a/test/integration/targets/net_banner/tasks/netconf.yaml b/test/integration/targets/net_banner/tasks/netconf.yaml new file mode 100644 index 00000000000..1286b354228 --- /dev/null +++ b/test/integration/targets/net_banner/tasks/netconf.yaml @@ -0,0 +1,16 @@ +--- +- name: collect all netconf test cases + find: + paths: "{{ role_path }}/tests/netconf" + 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 case + include: "{{ test_case_to_run }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/net_banner/tests/junos/basic.yaml b/test/integration/targets/net_banner/tests/junos/basic.yaml new file mode 100644 index 00000000000..038982ebedd --- /dev/null +++ b/test/integration/targets/net_banner/tests/junos/basic.yaml @@ -0,0 +1,91 @@ +--- +- debug: msg="START net_banner junos/basic.yaml" + +- name: setup - remove login banner + net_banner: + banner: login + state: absent + provider: "{{ netconf }}" + +- name: Create login banner + net_banner: + banner: login + text: this is my login banner + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'this is my login banner' in result.rpc" + +- name: Create login banner (idempotent) + net_banner: + banner: login + text: this is my login banner + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == false" + +- name: delete login banner + net_banner: + banner: login + state: absent + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" + +- name: setup - remove motd banner + net_banner: + banner: motd + state: absent + provider: "{{ netconf }}" + +- name: Create motd banner + junos_banner: + banner: motd + text: this is my motd banner + state: present + provider: "{{ netconf }}" + register: result + +- debug: + msg: "{{ result }}" + +- assert: + that: + - "result.changed == true" + - "'this is my motd banner' in result.rpc" + +- name: Create motd banner (idempotent) + net_banner: + banner: motd + text: this is my motd banner + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == false" + +- name: delete motd banner + junos_banner: + banner: motd + state: absent + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" diff --git a/test/integration/targets/net_banner/tests/netconf/basic.yaml b/test/integration/targets/net_banner/tests/netconf/basic.yaml new file mode 100644 index 00000000000..5ff7cf5af8e --- /dev/null +++ b/test/integration/targets/net_banner/tests/netconf/basic.yaml @@ -0,0 +1,3 @@ +--- +- include: "{{ role_path }}/tests/junos/basic.yaml" + when: hostvars[inventory_hostname]['ansible_network_os'] == 'junos' diff --git a/test/integration/targets/net_interface/tests/junos/basic.yaml b/test/integration/targets/net_interface/tests/junos/basic.yaml index e36555028b7..ba30cc099fb 100644 --- a/test/integration/targets/net_interface/tests/junos/basic.yaml +++ b/test/integration/targets/net_interface/tests/junos/basic.yaml @@ -98,5 +98,5 @@ - assert: that: - "result.changed == true" - - "'' in result.rpc" + - "'' in result.rpc" - "'ge-0/0/1' in result.rpc" diff --git a/test/integration/targets/net_vlan/tests/junos/basic.yaml b/test/integration/targets/net_vlan/tests/junos/basic.yaml index 10646a68c95..697b50777ef 100644 --- a/test/integration/targets/net_vlan/tests/junos/basic.yaml +++ b/test/integration/targets/net_vlan/tests/junos/basic.yaml @@ -76,5 +76,5 @@ - assert: that: - "result.changed == true" - - "'' in result.rpc" + - "'' in result.rpc" - "'test-vlan' in result.rpc"