From e8dd01fa5b03ddad84b911f84099a7b16c712fa8 Mon Sep 17 00:00:00 2001 From: Ganesh Nalawade Date: Mon, 22 Jan 2018 20:38:07 +0530 Subject: [PATCH] Add junos_l2_interface module (#35123) * Add junos_l2_interface module * Implementation for junos_l2_interface module * Integration test * Fix CI issues * Fix review comments --- .../network/junos/junos_l2_interface.py | 264 +++++++++++++++ .../modules/network/junos/junos_vlan.py | 1 + test/integration/junos.yaml | 9 + .../junos_l2_interface/defaults/main.yaml | 2 + .../targets/junos_l2_interface/meta/main.yml | 2 + .../junos_l2_interface/tasks/main.yaml | 2 + .../junos_l2_interface/tasks/netconf.yaml | 22 ++ .../tests/netconf/basic.yaml | 311 ++++++++++++++++++ .../tests/netconf/net_l2_interface.yaml | 57 ++++ 9 files changed, 670 insertions(+) create mode 100644 lib/ansible/modules/network/junos/junos_l2_interface.py create mode 100644 test/integration/targets/junos_l2_interface/defaults/main.yaml create mode 100644 test/integration/targets/junos_l2_interface/meta/main.yml create mode 100644 test/integration/targets/junos_l2_interface/tasks/main.yaml create mode 100644 test/integration/targets/junos_l2_interface/tasks/netconf.yaml create mode 100644 test/integration/targets/junos_l2_interface/tests/netconf/basic.yaml create mode 100644 test/integration/targets/junos_l2_interface/tests/netconf/net_l2_interface.yaml diff --git a/lib/ansible/modules/network/junos/junos_l2_interface.py b/lib/ansible/modules/network/junos/junos_l2_interface.py new file mode 100644 index 0000000000..53ae7689d3 --- /dev/null +++ b/lib/ansible/modules/network/junos/junos_l2_interface.py @@ -0,0 +1,264 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'network'} + + +DOCUMENTATION = """ +--- +module: junos_l2_interface +version_added: "2.5" +author: "Ganesh Nalawade (@ganeshrn)" +short_description: Manage Layer-2 interface on Juniper JUNOS network devices +description: + - This module provides declarative management of Layer-2 interface + on Juniper JUNOS network devices. +options: + name: + description: + - Name of the interface excluding any logical unit number. + description: + description: + - Description of Interface. + aggregate: + description: + - List of Layer-2 interface definitions. + mode: + description: + - Mode in which interface needs to be configured. + choices: ['access', 'trunk'] + access_vlan: + description: + - Configure given VLAN in access port. The value of C(access_vlan) should + be vlan name. + trunk_vlans: + description: + - List of VLAN names to be configured in trunk port. The value of C(trunk_vlans) should + be list of vlan names. + native_vlan: + description: + - Native VLAN to be configured in trunk port. The value of C(native_vlan) + should be vlan id. + unit: + description: + - Logical interface number. Value of C(unit) should be of type + integer. + default: 0 + state: + description: + - State of the Layer-2 Interface configuration. + default: present + choices: ['present', 'absent',] + active: + description: + - Specifies whether or not the configuration is active or deactivated + default: True + choices: [True, False] +requirements: + - ncclient (>=v0.5.2) +notes: + - This module requires the netconf system service be enabled on + the remote device being managed. + - Tested against vqfx-10000 JUNOS Version 15.1X53-D60.4. +extends_documentation_fragment: junos +""" + +EXAMPLES = """ +- name: Configure interface in access mode + junos_l2_interface: + name: ge-0/0/1 + description: interface-access + mode: access + access_vlan: red + active: True + state: present + +- name: Configure interface in trunk mode + junos_l2_interface: + name: ge-0/0/1 + description: interface-trunk + mode: trunk + trunk_vlans: + - blue + - green + native_vlan: 100 + active: True + state: present + +- name: Configure interface in access and trunk mode using aggregate + junos_l2_interface: + aggregate: + - name: ge-0/0/1 + description: test-interface-access + mode: access + access_vlan: red + - name: ge-0/0/2 + description: test-interface-trunk + mode: trunk + trunk_vlans: + - blue + - green + native_vlan: 100 + active: True + state: present +""" + +RETURN = """ +diff: + description: Configuration difference before and after applying change. + returned: when configuration is changed and diff option is enabled. + type: string + sample: > + [edit interfaces] + + ge-0/0/1 { + + description "l2 interface configured by Ansible"; + + unit 0 { + + family ethernet-switching { + + interface-mode access; + + vlan { + + members red; + + } + + } + + } + + } +""" +import collections + +from copy import deepcopy + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import remove_default_spec +from ansible.module_utils.network.junos.junos import junos_argument_spec +from ansible.module_utils.network.junos.junos import load_config, map_params_to_obj, map_obj_to_ele +from ansible.module_utils.network.junos.junos import commit_configuration, discard_changes, locked_config, to_param_list + +try: + from lxml.etree import tostring +except ImportError: + from xml.etree.ElementTree import tostring + +USE_PERSISTENT_CONNECTION = True + + +def validate_vlan_id(value, module): + if value and not 0 <= value <= 4094: + module.fail_json(msg='vlan_id must be between 1 and 4094') + + +def validate_param_values(module, obj, param=None): + if not param: + param = module.params + for key in obj: + # validate the param value (if validator func exists) + validator = globals().get('validate_%s' % key) + if callable(validator): + validator(param.get(key), module) + + +def main(): + """ main entry point for module execution + """ + element_spec = dict( + name=dict(), + mode=dict(choices=['access', 'trunk']), + access_vlan=dict(), + native_vlan=dict(type='int'), + trunk_vlans=dict(type='list'), + unit=dict(default=0, type='int'), + description=dict(), + state=dict(default='present', choices=['present', 'absent']), + active=dict(default=True, type='bool') + ) + + aggregate_spec = deepcopy(element_spec) + aggregate_spec['name'] = dict(required=True) + + # remove default in aggregate spec, to handle common arguments + remove_default_spec(aggregate_spec) + + required_one_of = [['name', 'aggregate']] + mutually_exclusive = [['name', 'aggregate'], + ['access_vlan', 'trunk_vlans'], + ['access_vlan', 'native_vlan']] + + required_if = [('mode', 'access', ('access_vlan',)), + ('mode', 'trunk', ('trunk_vlans',))] + + argument_spec = dict( + aggregate=dict(type='list', elements='dict', options=aggregate_spec, mutually_exclusive=mutually_exclusive, required_if=required_if), + ) + + argument_spec.update(element_spec) + argument_spec.update(junos_argument_spec) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True, + mutually_exclusive=mutually_exclusive, + required_one_of=required_one_of, + required_if=required_if) + + warnings = list() + result = {'changed': False} + + if warnings: + result['warnings'] = warnings + + top = 'interfaces/interface' + + param_to_xpath_map = collections.OrderedDict() + param_to_xpath_map.update([ + ('name', {'xpath': 'name', 'is_key': True}), + ('unit', {'xpath': 'name', 'top': 'unit', 'is_key': True}), + ('mode', {'xpath': 'interface-mode', 'top': 'unit/family/ethernet-switching'}), + ('access_vlan', {'xpath': 'members', 'top': 'unit/family/ethernet-switching/vlan'}), + ('trunk_vlans', {'xpath': 'members', 'top': 'unit/family/ethernet-switching/vlan'}), + ('native_vlan', {'xpath': 'native-vlan-id'}), + ('description', 'description') + ]) + + params = to_param_list(module) + + requests = list() + for param in params: + # if key doesn't exist in the item, get it from module.params + for key in param: + if param.get(key) is None: + param[key] = module.params[key] + + item = param.copy() + + validate_param_values(module, param_to_xpath_map, param=item) + + want = map_params_to_obj(module, param_to_xpath_map, param=item) + requests.append(map_obj_to_ele(module, want, top, param=item)) + + diff = None + with locked_config(module): + for req in requests: + diff = load_config(module, tostring(req), warnings, action='replace') + + commit = not module.check_mode + if diff: + if commit: + commit_configuration(module) + else: + discard_changes(module) + result['changed'] = True + + if module._diff: + result['diff'] = {'prepared': diff} + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/lib/ansible/modules/network/junos/junos_vlan.py b/lib/ansible/modules/network/junos/junos_vlan.py index c0d74301f1..5bf2e1447c 100644 --- a/lib/ansible/modules/network/junos/junos_vlan.py +++ b/lib/ansible/modules/network/junos/junos_vlan.py @@ -204,6 +204,7 @@ def main(): want = map_params_to_obj(module, param_to_xpath_map, param=item) requests.append(map_obj_to_ele(module, want, top, param=item)) + diff = None with locked_config(module): for req in requests: diff = load_config(module, tostring(req), warnings, action='merge') diff --git a/test/integration/junos.yaml b/test/integration/junos.yaml index 32c63917e2..c6e32bcf8b 100644 --- a/test/integration/junos.yaml +++ b/test/integration/junos.yaml @@ -167,6 +167,15 @@ failed_modules: "{{ failed_modules }} + [ 'junos_vrf' ]" test_failed: true + - block: + - include_role: + name: junos_l2_interface + when: "limit_to in ['*', 'junos_l2_interface']" + rescue: + - set_fact: + failed_modules: "{{ failed_modules }} + [ 'junos_l2_interface' ]" + test_failed: true + ########### - debug: var=failed_modules when: test_failed diff --git a/test/integration/targets/junos_l2_interface/defaults/main.yaml b/test/integration/targets/junos_l2_interface/defaults/main.yaml new file mode 100644 index 0000000000..5f709c5aac --- /dev/null +++ b/test/integration/targets/junos_l2_interface/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" diff --git a/test/integration/targets/junos_l2_interface/meta/main.yml b/test/integration/targets/junos_l2_interface/meta/main.yml new file mode 100644 index 0000000000..191a0f2ea1 --- /dev/null +++ b/test/integration/targets/junos_l2_interface/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - prepare_junos_tests diff --git a/test/integration/targets/junos_l2_interface/tasks/main.yaml b/test/integration/targets/junos_l2_interface/tasks/main.yaml new file mode 100644 index 0000000000..cc27f174fd --- /dev/null +++ b/test/integration/targets/junos_l2_interface/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: netconf.yaml, tags: ['netconf'] } diff --git a/test/integration/targets/junos_l2_interface/tasks/netconf.yaml b/test/integration/targets/junos_l2_interface/tasks/netconf.yaml new file mode 100644 index 0000000000..fc9c0e248d --- /dev/null +++ b/test/integration/targets/junos_l2_interface/tasks/netconf.yaml @@ -0,0 +1,22 @@ +--- +- name: collect all netconf test cases + find: + paths: "{{ role_path }}/tests/netconf" + patterns: "{{ testcase }}.yaml" + connection: local + register: test_cases + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test case (connection=netconf) + include: "{{ test_case_to_run }} ansible_connection=netconf" + 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/junos_l2_interface/tests/netconf/basic.yaml b/test/integration/targets/junos_l2_interface/tests/netconf/basic.yaml new file mode 100644 index 0000000000..c922596fef --- /dev/null +++ b/test/integration/targets/junos_l2_interface/tests/netconf/basic.yaml @@ -0,0 +1,311 @@ +--- +- debug: msg="START junos_l2_interface netconf/basic.yaml on connection={{ ansible_connection }}" + +- name: setup - remove interface address + junos_l2_interface: + name: ge-0/0/1 + state: absent + provider: "{{ netconf }}" + +- name: setup - remove interface address + junos_l2_interface: + name: ge-0/0/2 + state: absent + provider: "{{ netconf }}" + +- name: Setup create vlans + junos_vlan: + vlan_id: "{{ item.vlan_id }}" + name: "{{ item.name }}" + state: present + provider: "{{ netconf }}" + with_items: + - { vlan_id: 100, name: red } + - { vlan_id: 200, name: blue } + - { vlan_id: 300, name: green } + +- name: Configure interface in access mode + junos_l2_interface: + name: ge-0/0/1 + description: test-interface-access + mode: access + access_vlan: red + active: True + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - result.diff.prepared is search("\+ *ge-0/0/1") + - result.diff.prepared is search("\+ *description test-interface-access") + - result.diff.prepared is search("\+ *unit 0") + - result.diff.prepared is search("\+ *family ethernet-switching") + - result.diff.prepared is search("\+ *interface-mode access") + - result.diff.prepared is search("\+ *members red") + +- name: Configure interface in access mode (idempotent) + junos_l2_interface: + name: ge-0/0/1 + description: test-interface-access + mode: access + access_vlan: red + active: True + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == false" + +- name: Deactivate interface in access mode + junos_l2_interface: + name: ge-0/0/1 + description: test-interface-access + mode: access + access_vlan: red + active: False + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - result.diff.prepared is search("! *inactive[:] ge-0/0/1") + - result.diff.prepared is search("! *inactive[:] unit 0") + - result.diff.prepared is search("! *inactive[:] family ethernet-switching") + - result.diff.prepared is search("! *inactive[:] vlan") + +- name: Activate interface in access mode + junos_l2_interface: + name: ge-0/0/1 + description: test-interface-access + mode: access + access_vlan: red + active: True + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == true" + - result.diff.prepared is search("! *active[:] ge-0/0/1") + - result.diff.prepared is search("! *active[:] unit 0") + - result.diff.prepared is search("! *active[:] family ethernet-switching") + - result.diff.prepared is search("! *active[:] vlan") + +- name: Change interface to trunk mode + junos_l2_interface: + name: ge-0/0/1 + description: test-interface-trunk + mode: trunk + trunk_vlans: + - blue + - green + native_vlan: 100 + active: True + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - 'result.changed == true' + - result.diff.prepared is search("\- *description test-interface-access") + - result.diff.prepared is search("\+ *description test-interface-trunk") + - result.diff.prepared is search("\+ *native-vlan-id 100") + - result.diff.prepared is search("\- *interface-mode access") + - result.diff.prepared is search("\+ *interface-mode trunk") + - result.diff.prepared is search("\- *members red") + - result.diff.prepared is search("\+ *members \[ blue green \]") + +- name: Change interface to trunk mode (idempotent) + junos_l2_interface: + name: ge-0/0/1 + description: test-interface-trunk + mode: trunk + trunk_vlans: + - blue + - green + native_vlan: 100 + active: True + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == false" + +- name: Delete l2 interface + junos_l2_interface: + name: ge-0/0/1 + description: test-interface-trunk + mode: trunk + trunk_vlans: + - blue + - green + native_vlan: 100 + active: True + state: absent + provider: "{{ netconf }}" + register: result + +- assert: + that: + - 'result.changed == true' + - result.diff.prepared is search("\- *description test-interface-trunk") + - result.diff.prepared is search("\- *native-vlan-id 100") + - result.diff.prepared is search("\- *unit 0") + - result.diff.prepared is search("\- *family ethernet-switching") + - result.diff.prepared is search("\- *interface-mode trunk") + - result.diff.prepared is search("\- *members \[ blue green \]") + +- name: Delete l2 interface (idempotent) + junos_l2_interface: + name: ge-0/0/1 + description: test-interface-trunk + mode: trunk + trunk_vlans: + - blue + - green + native_vlan: 100 + active: True + state: absent + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == false" + +- name: Configure interface in access and trunk mode using aggregate + junos_l2_interface: + aggregate: + - name: ge-0/0/1 + description: test-interface-access + mode: access + access_vlan: red + - name: ge-0/0/2 + description: test-interface-trunk + mode: trunk + trunk_vlans: + - blue + - green + native_vlan: 100 + active: True + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - 'result.changed == true' + - result.diff.prepared is search("\+ *ge-0/0/1") + - result.diff.prepared is search("\+ *description test-interface-access") + - result.diff.prepared is search("\+ *unit 0") + - result.diff.prepared is search("\+ *family ethernet-switching") + - result.diff.prepared is search("\+ *interface-mode access") + - result.diff.prepared is search("\+ *members red") + - result.diff.prepared is search("\+ *ge-0/0/2") + - result.diff.prepared is search("\+ *description test-interface-trunk") + - result.diff.prepared is search("\+ *native-vlan-id 100") + - result.diff.prepared is search("\+ *interface-mode trunk") + - result.diff.prepared is search("\+ *members \[ blue green \]") + +- name: Configure interface in access and trunk mode using aggregate (idempotent) + junos_l2_interface: + aggregate: + - name: ge-0/0/1 + description: test-interface-access + mode: access + access_vlan: red + - name: ge-0/0/2 + description: test-interface-trunk + mode: trunk + trunk_vlans: + - blue + - green + native_vlan: 100 + active: True + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == false" + +- name: Delete interface in access and trunk mode using aggregate + junos_l2_interface: + aggregate: + - name: ge-0/0/1 + description: test-interface-access + mode: access + access_vlan: red + - name: ge-0/0/2 + description: test-interface-trunk + mode: trunk + trunk_vlans: + - blue + - green + native_vlan: 100 + active: True + state: absent + provider: "{{ netconf }}" + register: result + + +- assert: + that: + - 'result.changed == true' + - result.diff.prepared is search("\- *ge-0/0/1") + - result.diff.prepared is search("\- *description test-interface-access") + - result.diff.prepared is search("\- *unit 0") + - result.diff.prepared is search("\- *family ethernet-switching") + - result.diff.prepared is search("\- *interface-mode access") + - result.diff.prepared is search("\- *members red") + - result.diff.prepared is search("\- *ge-0/0/2") + - result.diff.prepared is search("\- *description test-interface-trunk") + - result.diff.prepared is search("\- *native-vlan-id 100") + - result.diff.prepared is search("\- *interface-mode trunk") + - result.diff.prepared is search("\- *members \[ blue green \]") + +- name: Delete interface in access and trunk mode using aggregate (idempotent) + junos_l2_interface: + aggregate: + - name: ge-0/0/1 + description: test-interface-access + mode: access + access_vlan: red + - name: ge-0/0/2 + description: test-interface-trunk + mode: trunk + trunk_vlans: + - blue + - green + native_vlan: 100 + active: True + state: absent + provider: "{{ netconf }}" + register: result + +- assert: + that: + - "result.changed == false" + +- name: Teradown delete vlans + junos_vlan: + vlan_id: "{{ item.vlan_id }}" + name: "{{ item.name }}" + state: absent + provider: "{{ netconf }}" + with_items: + - { vlan_id: 100, name: red } + - { vlan_id: 200, name: blue } + - { vlan_id: 300, name: green } + +- debug: msg="END junos_l2_interface netconf/basic.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/junos_l2_interface/tests/netconf/net_l2_interface.yaml b/test/integration/targets/junos_l2_interface/tests/netconf/net_l2_interface.yaml new file mode 100644 index 0000000000..f2f0d7b285 --- /dev/null +++ b/test/integration/targets/junos_l2_interface/tests/netconf/net_l2_interface.yaml @@ -0,0 +1,57 @@ +--- +- debug: msg="START junos netconf/net_l2_interface.yaml on connection={{ ansible_connection }}" + +# Add minimal testcase to check args are passed correctly to +# implementation module and module run is successful. + +- name: setup - remove interface address + net_l2_interface: + name: ge-0/0/1 + state: absent + provider: "{{ netconf }}" + +- name: Setup create vlan 100 + junos_vlan: + vlan_id: 100 + name: red + state: present + provider: "{{ netconf }}" + register: result + +- name: Configure interface in access mode using platform agnostic module + net_l2_interface: + name: ge-0/0/1 + description: l2 interface configured by Ansible + mode: access + access_vlan: red + active: True + state: present + provider: "{{ netconf }}" + register: result + +- assert: + that: + - result.diff.prepared is search("\+ *ge-0/0/1") + - result.diff.prepared is search("\+ *description \"l2 interface configured by Ansible\"") + - result.diff.prepared is search("\+ *unit 0") + - result.diff.prepared is search("\+ *family ethernet-switching") + - result.diff.prepared is search("\+ *interface-mode access") + - result.diff.prepared is search("\+ *members red") + +- name: teardown - remove interface address + net_l2_interface: + name: ge-0/0/1 + state: absent + provider: "{{ netconf }}" + + +- name: teardown delete vlan 100 + junos_vlan: + vlan_id: 100 + name: red + state: absent + provider: "{{ netconf }}" + register: result + + +- debug: msg="END junos netconf/net_l3_interface.yaml on connection={{ ansible_connection }}"