diff --git a/lib/ansible/module_utils/junos.py b/lib/ansible/module_utils/junos.py index 83813ace489..7bee49cf715 100644 --- a/lib/ansible/module_utils/junos.py +++ b/lib/ansible/module_utils/junos.py @@ -16,8 +16,9 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # -from contextlib import contextmanager +import collections +from contextlib import contextmanager from xml.etree.ElementTree import Element, SubElement, fromstring from ansible.module_utils.basic import env_fallback, return_values @@ -213,3 +214,45 @@ def load_config(module, candidate, warnings, action='merge', commit=False, forma def get_param(module, key): return module.params[key] or module.params['provider'].get(key) + + +def map_params_to_obj(module, param_xpath_map): + obj = collections.OrderedDict() + for key, value in param_xpath_map.items(): + if key in module.params: + obj.update({value: module.params[key]}) + return [obj] + + +def map_obj_to_ele(module, want, top): + top_ele = top.split('/') + root = Element(top_ele[0]) + ele = root + if len(top_ele) > 1: + for item in top_ele[1:-1]: + ele = SubElement(ele, item) + container = ele + state = module.params.get('state') + + 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') + + for key, value in obj.items(): + if value: + ele = node + tags = key.split('/') + for item in tags: + ele = SubElement(ele, item) + + ele.text = to_text(value, errors='surrogate_then_replace') + if state != 'present': + break + + return root diff --git a/lib/ansible/modules/network/junos/junos_vlan.py b/lib/ansible/modules/network/junos/junos_vlan.py new file mode 100644 index 00000000000..8bf70f0ed9b --- /dev/null +++ b/lib/ansible/modules/network/junos/junos_vlan.py @@ -0,0 +1,155 @@ +#!/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': 'core'} + + +DOCUMENTATION = """ +--- +module: junos_vlan +version_added: "2.4" +author: "Ganesh Nalawade (@ganeshrn)" +short_description: Manage VLANs on Juniper JUNOS network devices +description: + - This module provides declarative management of VLANs + on Juniper JUNOS network devices. +options: + name: + description: + - Name of the VLAN. + required: true + vlan_id: + description: + - ID of the VLAN. + required: true + description: + description: + - Text description of VLANs. + interfaces: + description: + - List of interfaces to check the VLAN has been + configured correctly. + collection: + description: List of VLANs definitions + purge: + description: + - Purge VLANs not defined in the collections parameter. + default: no + state: + description: + - State of the VLAN configuration. + default: present + choices: ['present', 'absent', 'active', 'suspend'] +""" + +EXAMPLES = """ +""" + +RETURN = """ +rpc: + description: load-configuration RPC send to the device + returned: when configuration is changed on device + type: string + sample: "test-vlan-4" +""" +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_vlan_id(value, module): + if not 1 <= value <= 4094: + module.fail_json(msg='vlan_id must be between 1 and 4094') + + +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( + name=dict(required=True), + vlan_id=dict(required=True, type='int'), + description=dict(), + interfaces=dict(), + collection=dict(), + purge=dict(default=False, type='bool'), + state=dict(default='present', + choices=['present', 'absent', 'active', 'suspend']) + ) + + argument_spec.update(junos_argument_spec) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) + + warnings = list() + check_args(module, warnings) + + result = {'changed': False} + + if warnings: + result['warnings'] = warnings + + top = 'vlans/vlan' + + param_xpath_map = collections.OrderedDict() + param_xpath_map.update({ + 'name': 'name', + 'vlan_id': 'vlan-id', + 'description': 'description' + }) + + validate_param_values(module, param_xpath_map) + + want = map_params_to_obj(module, param_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/plugins/action/net_base.py b/lib/ansible/plugins/action/net_base.py index 3bde543b864..3ca75f7476c 100644 --- a/lib/ansible/plugins/action/net_base.py +++ b/lib/ansible/plugins/action/net_base.py @@ -45,13 +45,17 @@ class ActionModule(ActionBase): play_context = copy.deepcopy(self._play_context) play_context.network_os = self._get_network_os(task_vars) - # TODO this can be netconf - play_context.connection = 'network_cli' self.provider = self._load_provider(play_context.network_os) + if play_context.network_os == 'junos': + play_context.connection = 'netconf' + play_context.port = self.provider['port'] or self._play_context.port or 830 + else: + play_context.connection = 'network_cli' + play_context.port = self.provider['port'] or self._play_context.port or 22 + play_context.remote_addr = self.provider['host'] or self._play_context.remote_addr - play_context.port = self.provider['port'] or self._play_context.port or 22 play_context.remote_user = self.provider['username'] or self._play_context.connection_user play_context.password = self.provider['password'] or self._play_context.password play_context.private_key_file = self.provider['ssh_keyfile'] or self._play_context.private_key_file diff --git a/test/integration/junos.yaml b/test/integration/junos.yaml index f2b1e80489e..6be7274cb59 100644 --- a/test/integration/junos.yaml +++ b/test/integration/junos.yaml @@ -14,3 +14,4 @@ - { role: junos_netconf, when: "limit_to in ['*', 'junos_netconf']" } - { role: junos_rpc, when: "limit_to in ['*', 'junos_rpc']" } - { role: junos_template, when: "limit_to in ['*', 'junos_template']" } + - { role: junos_vlan, when: "limit_to in ['*', 'junos_vlan']" } diff --git a/test/integration/targets/junos_vlan/defaults/main.yaml b/test/integration/targets/junos_vlan/defaults/main.yaml new file mode 100644 index 00000000000..9ef5ba51651 --- /dev/null +++ b/test/integration/targets/junos_vlan/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "*" +test_items: [] diff --git a/test/integration/targets/junos_vlan/tasks/main.yaml b/test/integration/targets/junos_vlan/tasks/main.yaml new file mode 100644 index 00000000000..cc27f174fd8 --- /dev/null +++ b/test/integration/targets/junos_vlan/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: netconf.yaml, tags: ['netconf'] } diff --git a/test/integration/targets/junos_vlan/tasks/netconf.yaml b/test/integration/targets/junos_vlan/tasks/netconf.yaml new file mode 100644 index 00000000000..c6a07db9a63 --- /dev/null +++ b/test/integration/targets/junos_vlan/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_vlan/tests/netconf/basic.yaml b/test/integration/targets/junos_vlan/tests/netconf/basic.yaml new file mode 100644 index 00000000000..0c4fed123dd --- /dev/null +++ b/test/integration/targets/junos_vlan/tests/netconf/basic.yaml @@ -0,0 +1,83 @@ +--- +- debug: msg="START junos_vlan netconf/basic.yaml" + +- name: setup - remove vlan + junos_vlan: + vlan_id: 100 + name: test-vlan + description: test vlan + state: absent + provider: "{{ netconf }}" + +- name: Create vlan + junos_vlan: + vlan_id: 100 + name: test-vlan + state: present + description: test vlan + provider: "{{ netconf }}" + register: result + +- debug: + msg: "{{ result }}" + +- assert: + that: + - "result.changed == true" + - "'test-vlan' in result.rpc" + - "'100' in result.rpc" + +- name: Create vlan again (idempotent) + junos_vlan: + vlan_id: 100 + name: test-vlan + state: present + description: test vlan + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == false" + +- name: Deactivate vlan + junos_vlan: + vlan_id: 100 + name: test-vlan + state: suspend + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" + - "'test-vlan' in result.rpc" + +- name: Activate vlan + junos_vlan: + vlan_id: 100 + name: test-vlan + state: active + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" + - "'test-vlan' in result.rpc" + +- name: Delete vlan + junos_vlan: + vlan_id: 100 + name: test-vlan + state: absent + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" + - "'test-vlan' in result.rpc" diff --git a/test/integration/targets/net_vlan/tasks/main.yaml b/test/integration/targets/net_vlan/tasks/main.yaml index 415c99d8b12..af08869c922 100644 --- a/test/integration/targets/net_vlan/tasks/main.yaml +++ b/test/integration/targets/net_vlan/tasks/main.yaml @@ -1,2 +1,3 @@ --- - { include: cli.yaml, tags: ['cli'] } +- { include: netconf.yaml, tags: ['netconf'] } diff --git a/test/integration/targets/net_vlan/tasks/netconf.yaml b/test/integration/targets/net_vlan/tasks/netconf.yaml new file mode 100644 index 00000000000..1286b354228 --- /dev/null +++ b/test/integration/targets/net_vlan/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_vlan/tests/junos/basic.yaml b/test/integration/targets/net_vlan/tests/junos/basic.yaml new file mode 100644 index 00000000000..13be53911c1 --- /dev/null +++ b/test/integration/targets/net_vlan/tests/junos/basic.yaml @@ -0,0 +1,80 @@ +--- +- debug: msg="START net_vlan netconf/basic.yaml" + +- name: setup - remove vlan + net_vlan: + vlan_id: 100 + name: test-vlan + state: absent + provider: "{{ netconf }}" + +- name: Create vlan + net_vlan: + vlan_id: 100 + name: test-vlan + state: present + provider: "{{ netconf }}" + register: result + +- debug: + msg: "{{ result }}" + +- assert: + that: + - "result.changed == true" + - "'test-vlan' in result.rpc" + - "'100' in result.rpc" + +- name: Create vlan again (idempotent) + net_vlan: + vlan_id: 100 + name: test-vlan + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == false" + +- name: Deactivate vlan + net_vlan: + vlan_id: 100 + name: test-vlan + state: suspend + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" + - "'test-vlan' in result.rpc" + +- name: Activate vlan + net_vlan: + vlan_id: 100 + name: test-vlan + state: active + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" + - "'test-vlan' in result.rpc" + +- name: Delete vlan + net_vlan: + vlan_id: 100 + name: test-vlan + state: absent + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - "'' in result.rpc" + - "'test-vlan' in result.rpc" diff --git a/test/integration/targets/net_vlan/tests/netconf/basic.yaml b/test/integration/targets/net_vlan/tests/netconf/basic.yaml new file mode 100644 index 00000000000..5ff7cf5af8e --- /dev/null +++ b/test/integration/targets/net_vlan/tests/netconf/basic.yaml @@ -0,0 +1,3 @@ +--- +- include: "{{ role_path }}/tests/junos/basic.yaml" + when: hostvars[inventory_hostname]['ansible_network_os'] == 'junos'