diff --git a/lib/ansible/modules/network/aci/aci_vlan_pool_encap_block.py b/lib/ansible/modules/network/aci/aci_vlan_pool_encap_block.py new file mode 100644 index 00000000000..3a248cb3e24 --- /dev/null +++ b/lib/ansible/modules/network/aci/aci_vlan_pool_encap_block.py @@ -0,0 +1,249 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Jacob McGill (jmcgill298) +# Copyright: (c) 2018, Dag Wieers (dagwieers) +# 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': 'community'} + +DOCUMENTATION = r''' +--- +module: aci_vlan_pool_encap_block +short_description: Manage encap blocks assigned to VLAN pools on Cisco ACI fabrics (fvns:EncapBlk) +description: +- Manage VLAN encap blocks that are assigned to VLAN pools on Cisco ACI fabrics. +- More information from the internal APIC class I(fvns:EncapBlk) at + U(https://developer.cisco.com/site/aci/docs/apis/apic-mim-ref/). +author: +- Jacob McGill (@jmcgill298) +- Dag Wieers (@dagwieers) +version_added: '2.5' +requirements: +- The C(pool) must exist in order to add or delete a encap block. +options: + allocation_mode: + description: + - The method used for allocating encaps to resources. + aliases: [ mode ] + choices: [ dynamic, inherit, static] + description: + description: + - Description for the pool encap block. + aliases: [ descr ] + pool: + description: + - The name of the pool that the encap block should be assigned to. + aliases: [ pool_name ] + block_end: + description: + - The end of encap block. + aliases: [ end ] + block_name: + description: + - The name to give to the encap block. + aliases: [ name, range ] + block_start: + description: + - The start of the encap block. + aliases: [ start ] + state: + description: + - Use C(present) or C(absent) for adding or removing. + - Use C(query) for listing an object or multiple objects. + choices: [ absent, present, query ] + default: present +extends_documentation_fragment: aci +''' + +EXAMPLES = r''' +- name: Add a new VLAN encap block + aci_vlan_pool_encap_block: + hostname: apic + username: admin + password: SomeSecretPassword + pool: production + block_start: 20 + block_end: 50 + state: present + +- name: Remove a VLAN encap block + aci_vlan_pool_encap_block: + hostname: apic + username: admin + password: SomeSecretPassword + pool: production + block_start: 20 + block_end: 50 + state: absent + +- name: Query a VLAN encap block + aci_vlan_pool_encap_block: + hostname: apic + username: admin + password: SomeSecretPassword + pool: production + block_start: 20 + block_end: 50 + state: query + +- name: Query a VLAN pool for encap blocks + aci_vlan_pool_encap_block: + hostname: apic + username: admin + password: SomeSecretPassword + pool: production + state: query + +- name: Query all VLAN encap blocks + aci_vlan_pool_encap_block: + hostname: apic + username: admin + password: SomeSecretPassword + state: query +''' + +RETURN = r''' +# +''' + +from ansible.module_utils.network.aci.aci import ACIModule, aci_argument_spec +from ansible.module_utils.basic import AnsibleModule + + +def main(): + argument_spec = aci_argument_spec + argument_spec.update( + allocation_mode=dict(type='str', aliases=['mode'], choices=['dynamic', 'inherit', 'static']), + description=dict(type='str', aliases=['descr']), + pool=dict(type='str', aliases=['pool_name']), + pool_allocation_mode=dict(type='str', aliases=['pool_mode'], choices=['dynamic', 'static']), + block_name=dict(type='str', aliases=["name"]), + block_end=dict(type='int', aliases=['end']), + block_start=dict(type='int', aliases=["start"]), + state=dict(type='str', default='present', choices=['absent', 'present', 'query']), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ['state', 'absent', ['pool', 'block_end', 'block_name', 'block_start']], + ['state', 'present', ['pool', 'block_end', 'block_name', 'block_start']], + ], + ) + + allocation_mode = module.params['allocation_mode'] + description = module.params['description'] + pool = module.params['pool'] + pool_allocation_mode = module.params['pool_allocation_mode'] + block_end = module.params['block_end'] + block_name = module.params['block_name'] + block_start = module.params['block_start'] + state = module.params['state'] + + if block_end is not None: + encap_end = 'vlan-{0}'.format(block_end) + else: + encap_end = None + + if block_start is not None: + encap_start = 'vlan-{0}'.format(block_start) + else: + encap_start = None + + # Collect proper mo information + aci_block_mo = 'from-[{0}]-to-[{1}]'.format(encap_start, encap_end) + pool_name = pool + + # Validate block_end and block_start are valid for its respective encap type + for encap_id in block_end, block_start: + if encap_id is not None: + if not 1 <= encap_id <= 4094: + module.fail_json(msg="vlan pools must have 'block_start' and 'block_end' values between 1 and 4094") + + # Build proper proper filter_target based on block_start, block_end, and block_name + if block_end is not None and block_start is not None: + # Validate block_start is less than block_end + if block_start > block_end: + module.fail_json(msg="The 'block_start' must be less than or equal to the 'block_end'") + + if block_name is None: + block_filter_target = 'and(eq({0}.from, "{1}"),eq({0}.to, "{2}"))'.format('fvnsEncapBlk', encap_start, encap_end) + else: + block_filter_target = 'and(eq({0}.from, "{1}"),eq({0}.to, "{2}"),eq({0}.name, "{3}"))'.format('fvnsEncapBlk', encap_start, encap_end, block_name) + elif block_end is None and block_start is None: + if block_name is None: + # Reset range managed object to None for aci util to properly handle query + aci_block_mo = None + block_filter_target = '' + else: + block_filter_target = 'eq({0}.name, "{1}")'.format('fvnsEncapBlk', block_name) + elif block_start is not None: + if block_name is None: + block_filter_target = 'eq({0}.from, "{1}")'.format('fvnsEncapBlk', encap_start) + else: + block_filter_target = 'and(eq({0}.from, "{1}"),eq({0}.name, "{2}"))'.format('fvnsEncapBlk', encap_start, block_name) + else: + if block_name is None: + block_filter_target = 'eq({0}.to, "{1}")'.format('fvnsEncapBlk', encap_end) + else: + block_filter_target = 'and(eq({0}.to, "{1}"),eq({0}.name, "{2}"))'.format('fvnsEncapBlk', encap_end, block_name) + + # ACI Pool URL requires the allocation mode (ex: uni/infra/vlanns-[poolname]-static) + if pool is not None: + if pool_allocation_mode is not None: + pool_name = '[{0}]-{1}'.format(pool, pool_allocation_mode) + else: + module.fail_json(msg="ACI requires the 'pool_allocation_mode' when 'pool' is provided") + + aci = ACIModule(module) + aci.construct_url( + root_class=dict( + aci_class='fvnsVlanInstP', + aci_rn='infra/vlanns-{0}'.format(pool_name), + filter_target='eq(fvnsVlanInstP.name, "{0}")'.format(pool), + module_object=pool, + ), + subclass_1=dict( + aci_class='fvnsEncapBlk', + aci_rn=aci_block_mo, + filter_target=block_filter_target, + module_object=aci_block_mo, + ), + ) + + aci.get_existing() + + if state == 'present': + # Filter out module parameters with null values + aci.payload( + aci_class='fvnsEncapBlk', + class_config={ + "allocMode": allocation_mode, + "descr": description, + "from": encap_start, + "name": block_name, + "to": encap_end, + } + ) + + # Generate config diff which will be used as POST request body + aci.get_diff(aci_class='fvnsEncapBlk') + + # Submit changes if module not in check_mode and the proposed is different than existing + aci.post_config() + + elif state == 'absent': + aci.delete_config() + + module.exit_json(**aci.result) + + +if __name__ == "__main__": + main() diff --git a/test/integration/targets/aci_vlan_pool_encap_block/aliases b/test/integration/targets/aci_vlan_pool_encap_block/aliases new file mode 100644 index 00000000000..b4e2520c177 --- /dev/null +++ b/test/integration/targets/aci_vlan_pool_encap_block/aliases @@ -0,0 +1 @@ +# No AcI Simulator yet, so not enabled diff --git a/test/integration/targets/aci_vlan_pool_encap_block/tasks/main.yml b/test/integration/targets/aci_vlan_pool_encap_block/tasks/main.yml new file mode 100644 index 00000000000..a954e110cf5 --- /dev/null +++ b/test/integration/targets/aci_vlan_pool_encap_block/tasks/main.yml @@ -0,0 +1,368 @@ +# Test code for the ACI modules + +# Copyright: (c) 2017, Jacob McGill (jmcgill298) +# Copyright: (c) 2018, Dag Wieers (dagwieers) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +- name: Test that we have an aci apic host, aci username and aci password + fail: + msg: 'Please define the following variables: aci_hostname, aci_username and aci_password.' + when: aci_hostname is not defined or aci_username is not defined or aci_password is not defined + +- name: Ensure vlan pool exists for tests to kick off + aci_vlan_pool: + host: "{{ aci_hostname }}" + username: "{{ aci_username }}" + password: "{{ aci_password }}" + validate_certs: no + state: absent + pool: anstest + allocation_mode: static + description: Ansible Test + +- name: Ensure vlan pool exists for tests to kick off + aci_vlan_pool: &aci_pool_present + host: "{{ aci_hostname }}" + username: "{{ aci_username }}" + password: "{{ aci_password }}" + validate_certs: no + state: present + pool: anstest + allocation_mode: static + description: Ansible Test + register: pool_present + +- name: Create vlan pool encap block - check mode works + aci_vlan_pool_encap_block: &aci_encap_block_present + <<: *aci_pool_present + block_name: anstest + block_start: 20 + block_end: 40 + pool: anstest + pool_allocation_mode: static + allocation_mode: inherit + description: Ansible Test + check_mode: yes + register: encap_block_present_check_mode + +- name: Present assertions + assert: + that: + - encap_block_present_check_mode.changed == true + - 'encap_block_present_check_mode.config == {"fvnsEncapBlk": {"attributes": {"allocMode": "inherit", "descr": "Ansible Test", "from": "vlan-20", "name": "anstest", "to": "vlan-40"}}}' + +- name: Create vlan pool encap_block - creation works + aci_vlan_pool_encap_block: + <<: *aci_encap_block_present + register: encap_block_present + +- name: Present assertions + assert: + that: + - encap_block_present.changed == true + - encap_block_present.existing == [] + - encap_block_present.config == encap_block_present_check_mode.config + - encap_block_present.config == encap_block_present.proposed + +- name: Create vlan pool range - idempotency works + aci_vlan_pool_encap_block: + <<: *aci_encap_block_present + register: encap_block_present_idempotent + +- name: Present assertions + assert: + that: + - encap_block_present_idempotent.changed == false + - 'encap_block_present_idempotent.existing.0.fvnsEncapBlk.attributes.name == "anstest"' + +- name: Update vlan pool range - update works + aci_vlan_pool_encap_block: + <<: *aci_encap_block_present + description: Ansible Test Update + allocation_mode: inherit + register: encap_block_present_update + +- name: Present assertions + assert: + that: + - encap_block_present_update.changed == true + - encap_block_present_update.existing != [] + - encap_block_present_update.config != encap_block_present.config + +- name: Create vlan pool range - used for query + aci_vlan_pool_encap_block: &aci_encap_block_present_2 + <<: *aci_encap_block_present + block_name: anstest_2 + block_start: 50 + block_end: 55 + register: encap_block_present_2 + +- name: Present assertions + assert: + that: + - encap_block_present_2.changed == true + - encap_block_present_2.existing == [] + +- name: Invalid encap_block_start - error message works + aci_vlan_pool_encap_block: + <<: *aci_encap_block_present + block_start: 0 + ignore_errors: yes + register: encap_block_start_low + +- name: Present assertions + assert: + that: + - encap_block_start_low.failed == true + - encap_block_start_low.msg == "vlan pools must have 'block_start' and 'block_end' values between 1 and 4094" + +- name: Invalid encap_block_start - error message works + aci_vlan_pool_encap_block: + <<: *aci_encap_block_present + block_start: 4096 + ignore_errors: yes + register: encap_block_start_high + +- name: Present assertions + assert: + that: + - encap_block_start_high.failed == true + - encap_block_start_high.msg == "vlan pools must have 'block_start' and 'block_end' values between 1 and 4094" + +- name: Invalid encap_block_end - error message works + aci_vlan_pool_encap_block: + <<: *aci_encap_block_present + block_end: 0 + ignore_errors: yes + register: encap_block_end_low + +- name: Present assertions + assert: + that: + - encap_block_end_low.failed == true + - encap_block_end_low.msg == "vlan pools must have 'block_start' and 'block_end' values between 1 and 4094" + +- name: Invalid encap_block_end - error message works + aci_vlan_pool_encap_block: + <<: *aci_encap_block_present + block_end: 4096 + ignore_errors: yes + register: encap_block_end_high + +- name: Present assertions + assert: + that: + - encap_block_end_high.failed == true + - encap_block_end_high.msg == "vlan pools must have 'block_start' and 'block_end' values between 1 and 4094" + +- name: Range start higher than range end - error message works + aci_vlan_pool_encap_block: + <<: *aci_encap_block_present + block_start: 1000 + ignore_errors: yes + register: encap_block_start_end + +- name: Present assertions + assert: + that: + - encap_block_start_end.failed == true + - encap_block_start_end.msg == "The 'block_start' must be less than or equal to the 'block_end'" + +- name: Missing required param - error message works + aci_vlan_pool_encap_block: + <<: *aci_pool_present + ignore_errors: yes + register: encap_block_present_missing_param + +- name: Present assertions + assert: + that: + - encap_block_present_missing_param.failed == true + - 'encap_block_present_missing_param.msg == "state is present but all of the following are missing: block_end, block_name, block_start"' + +- name: Missing required param - error message works + aci_vlan_pool_encap_block: + <<: *aci_encap_block_present + pool_allocation_mode: "{{ fake_var | default(omit) }}" + ignore_errors: yes + register: encap_block_present_allocation + +- name: Present assertions + assert: + that: + - encap_block_present_allocation.failed == true + - encap_block_present_allocation.msg == "ACI requires the 'pool_allocation_mode' when 'pool' is provided" + +- name: Query specific vlan pool range + aci_vlan_pool_encap_block: &aci_encap_block_query + <<: *aci_encap_block_present + state: query + register: encap_block_query + +- name: Query assertions + assert: + that: + - encap_block_query.changed == false + - encap_block_query.url.endswith("infra/vlanns-[anstest]-static/from-[vlan-20]-to-[vlan-40].json") + - encap_block_query.existing | length == 1 + - encap_block_query.existing.0.fvnsEncapBlk.attributes.name == "anstest" + +- name: Query vlan pool range - from, to, and name are filtered + aci_vlan_pool_encap_block: &aci_encap_block_query_filter + <<: *aci_encap_block_query + pool: "{{ fake_var | default(omit) }}" + register: encap_block_query_from_to_name + +- name: Query assertions + assert: + that: + - encap_block_query_from_to_name.changed == false + - encap_block_query_from_to_name.url.endswith("class/fvnsEncapBlk.json") + - '"query-target-filter=and(eq(fvnsEncapBlk.from, \"vlan-20\"),eq(fvnsEncapBlk.to, \"vlan-40\"),eq(fvnsEncapBlk.name, \"anstest\"))" in encap_block_query_from_to_name.filter_string' + - encap_block_query_from_to_name.existing.0.fvnsEncapBlk.attributes.name == "anstest" + - encap_block_query_from_to_name.existing.0.fvnsEncapBlk.attributes.from == "vlan-20" + - encap_block_query_from_to_name.existing.0.fvnsEncapBlk.attributes.to == "vlan-40" + +- name: Query vlan pool range - from and name are filtered + aci_vlan_pool_encap_block: + <<: *aci_encap_block_query_filter + block_end: "{{ fake_var | default(omit) }}" + register: encap_block_query_from_name + +- name: Query assertions + assert: + that: + - encap_block_query_from_name.changed == false + - encap_block_query_from_name.url.endswith("class/fvnsEncapBlk.json") + - '"query-target-filter=and(eq(fvnsEncapBlk.from, \"vlan-20\"),eq(fvnsEncapBlk.name, \"anstest\"))" in encap_block_query_from_name.filter_string' + - encap_block_query_from_name.existing.0.fvnsEncapBlk.attributes.name == "anstest" + - encap_block_query_from_name.existing.0.fvnsEncapBlk.attributes.from == "vlan-20" + +- name: Query vlan pool range - to and name are filtered + aci_vlan_pool_encap_block: + <<: *aci_encap_block_query_filter + block_start: "{{ fake_var | default(omit) }}" + register: encap_block_query_to_name + +- name: Query assertions + assert: + that: + - encap_block_query_to_name.changed == false + - encap_block_query_to_name.url.endswith("class/fvnsEncapBlk.json") + - '"query-target-filter=and(eq(fvnsEncapBlk.to, \"vlan-40\"),eq(fvnsEncapBlk.name, \"anstest\"))" in encap_block_query_to_name.filter_string' + - encap_block_query_to_name.existing.0.fvnsEncapBlk.attributes.name == "anstest" + - encap_block_query_to_name.existing.0.fvnsEncapBlk.attributes.to == "vlan-40" + +- name: Query vlan pool range - name is filtered + aci_vlan_pool_encap_block: + <<: *aci_encap_block_query_filter + block_start: "{{ fake_var | default(omit) }}" + block_end: "{{ fake_var | default(omit) }}" + register: encap_block_query_name + +- name: Query assertions + assert: + that: + - encap_block_query_name.changed == false + - encap_block_query_name.url.endswith("class/fvnsEncapBlk.json") + - '"query-target-filter=eq(fvnsEncapBlk.name, \"anstest\")" in encap_block_query_name.filter_string' + - encap_block_query_name.existing.0.fvnsEncapBlk.attributes.name == "anstest" + +- name: Query vlan pool range - from and to are filtered + aci_vlan_pool_encap_block: + <<: *aci_encap_block_query_filter + block_name: "{{ fake_var | default(omit) }}" + register: encap_block_query_from_to + +- name: Query assertions + assert: + that: + - encap_block_query_from_to.changed == false + - encap_block_query_from_to.url.endswith("class/fvnsEncapBlk.json") + - '"query-target-filter=and(eq(fvnsEncapBlk.from, \"vlan-20\"),eq(fvnsEncapBlk.to, \"vlan-40\"))" in encap_block_query_from_to.filter_string' + - encap_block_query_from_to.existing.0.fvnsEncapBlk.attributes.from == "vlan-20" + - encap_block_query_from_to.existing.0.fvnsEncapBlk.attributes.to == "vlan-40" + +- name: Query all ranges in a vlan pool + aci_vlan_pool_encap_block: + <<: *aci_pool_present + state: query + pool_allocation_mode: static + register: encap_block_query_pool + +- name: Query assertions + assert: + that: + - encap_block_query_pool.existing | length == 1 + - encap_block_query_pool.existing.0.fvnsVlanInstP.attributes.name == "anstest" + - encap_block_query_pool.existing.0.fvnsVlanInstP.children | length > 1 + - encap_block_query_pool.url.endswith("infra/vlanns-[anstest]-static.json") + +- name: Query all ranges + aci_vlan_pool_encap_block: + <<: *aci_pool_present + state: query + pool: "{{ fake_var | default(omit) }}" + register: encap_block_query_all + +- name: Query assertions + assert: + that: + - encap_block_query_all.changed == false + - encap_block_query_all.existing | length > 1 + - encap_block_query_all.existing.0.fvnsEncapBlk is defined + - encap_block_query_all.url.endswith("class/fvnsEncapBlk.json") + +- name: Delete vlan pool range - deletion works + aci_vlan_pool_encap_block: + <<: *aci_encap_block_present + state: absent + register: delete_range + +- name: Absent assertions + assert: + that: + - delete_range.changed == true + - delete_range.proposed == {} + - delete_range.existing.0.fvnsEncapBlk.attributes.name == "anstest" + +- name: Delete vlan pool range - check mode works + aci_vlan_pool_encap_block: &aci_encap_block_absent + <<: *aci_encap_block_present_2 + state: absent + check_mode: yes + register: delete_check_mode + +- name: Absent assertions + assert: + that: + - delete_check_mode.changed == true + - delete_check_mode.existing != [] + +- name: Delete vlan pool range - deletion works + aci_vlan_pool_encap_block: + <<: *aci_encap_block_absent + register: delete_encap_block_2 + +- name: Absent assertions + assert: + that: + - delete_encap_block_2.changed == true + - delete_encap_block_2.existing == delete_check_mode.existing + +- name: Delete vlan pool range again - idempotency works + aci_vlan_pool_encap_block: + <<: *aci_encap_block_absent + register: delete_idempotent + +- name: Absent assertions + assert: + that: + - delete_idempotent.changed == false + - delete_idempotent.existing == [] + +- name: Cleanup vlan pool + aci_vlan_pool: + <<: *aci_pool_present + state: absent + when: pool_present.changed == true