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'