diff --git a/changelogs/fragments/48-address-block.yml b/changelogs/fragments/48-address-block.yml new file mode 100644 index 00000000..a15bac58 --- /dev/null +++ b/changelogs/fragments/48-address-block.yml @@ -0,0 +1,2 @@ +major_changes: + - added support for creating and retrieving next available address blocks. \ No newline at end of file diff --git a/meta/runtime.yml b/meta/runtime.yml index 83d7e6ff..05965dea 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -27,6 +27,7 @@ action_groups: - ipam_address_block_info - ipam_host - ipam_host_info + - ipam_next_available_address_block_info infra: - infra_join_token diff --git a/plugins/modules/ipam_address_block.py b/plugins/modules/ipam_address_block.py index 58d0bf20..08a6a8d0 100644 --- a/plugins/modules/ipam_address_block.py +++ b/plugins/modules/ipam_address_block.py @@ -76,7 +76,7 @@ - "The minimum percentage of addresses that must be available outside of the DHCP ranges and fixed addresses when making a suggested change.." type: int reenable_date: - description: "" + description: "The date at which notifications will be re-enabled automatically." type: str cidr: description: @@ -727,42 +727,42 @@ description: - "The low threshold value for the percentage of used IP addresses relative to the total IP addresses available in the scope of the object. Thresholds are inclusive in the comparison test." type: int + next_available_id: + description: + - "The resource identifier for the address block where the next available address block should be generated." + type: str extends_documentation_fragment: - infoblox.bloxone.common """ # noqa: E501 EXAMPLES = r""" - - name: "Create an ip space" + - name: "Create an IP space (required as parent)" infoblox.bloxone.ipam_ip_space: name: "my-ip-space" state: "present" - register: ip_space + register: ip_space - name: "Create an address block" infoblox.bloxone.ipam_address_block: - address: "10.0.0.0/24" + address: "10.0.0.0/16" space: "{{ ip_space.id }}" state: "present" + register: address_block - - name: "Delete an Address Block" + - name: "Create Next Available Address Block" infoblox.bloxone.ipam_address_block: - address: "10.0.0.0/16" space: "{{ ip_space.id }}" - state: "absent" + cidr: 20 + next_available_id: "{{ address_block.id }}" + state: "present" - - name: "Create an Address Block with separate cidr" + - name: "Create an Address Block with Additional Fields" infoblox.bloxone.ipam_address_block: address: "10.0.0.0" cidr: 16 space: "{{ ip_space.id }}" state: "present" - - - name: "Create an Address Block with DHCP config overridden" - infoblox.bloxone.ipam_address_block: - address: "10.0.0.0/16" - space: "{{ ip_space.id }}" - state: "present" dhcp_config: lease_time: 3600 inheritance_sources: @@ -789,14 +789,14 @@ action: inherit lease_time_v6: action: inherit + tags: + location: "site-1" - - name: "Create an Address Block with tags" + - name: "Delete an Address Block" infoblox.bloxone.ipam_address_block: address: "10.0.0.0/16" space: "{{ ip_space.id }}" - state: "present" - tags: - location: "site-1" + state: "absent" """ RETURN = r""" @@ -868,7 +868,7 @@ type: int returned: Always reenable_date: - description: "" + description: "The date at which notifications will be re-enabled automatically." type: str returned: Always asm_scope_flag: @@ -2316,23 +2316,23 @@ returned: Always contains: abandoned: - description: "" + description: "The number of IP addresses in the scope of the object which are in the abandoned state (issued by a DHCP server and then declined by the client)." type: str returned: Always dynamic: - description: "" + description: "The number of IP addresses handed out by DHCP in the scope of the object. This includes all leased addresses, fixed addresses that are defined but not currently leased and abandoned leases." type: str returned: Always static: - description: "" + description: "The number of defined IP addresses such as reservations or DNS records. It can be computed as _static_ = _used_ - _dynamic_." type: str returned: Always total: - description: "" + description: "The total number of IP addresses available in the scope of the object." type: str returned: Always used: - description: "" + description: "The number of IP addresses used in the scope of the object." type: str returned: Always """ # noqa: E501 @@ -2349,12 +2349,15 @@ class AddressBlockModule(BloxoneAnsibleModule): def __init__(self, *args, **kwargs): super(AddressBlockModule, self).__init__(*args, **kwargs) + self.next_available_id = self.params.get("next_available_id") - if "/" in self.params["address"]: - self.params["address"], netmask = self.params["address"].split("/") - self.params["cidr"] = int(netmask) + # If address is None, next_available_id will be utilized along with a separately provided CIDR value + if self.params["address"] is not None: + if "/" in self.params["address"]: + self.params["address"], netmask = self.params["address"].split("/") + self.params["cidr"] = int(netmask) - exclude = ["state", "csp_url", "api_key", "id"] + exclude = ["state", "csp_url", "api_key", "id", "next_available_id"] self._payload_params = {k: v for k, v in self.params.items() if v is not None and k not in exclude} self._payload = AddressBlock.from_dict(self._payload_params) @@ -2399,6 +2402,9 @@ def find(self): return None raise e else: + # If address is None, return None, indicating next_available_address block should be created + if self.params["address"] is None: + return None filter = f"address=='{self.params['address']}' and space=='{self.params['space']}' and cidr=={self.params['cidr']}" resp = AddressBlockApi(self.client).list(filter=filter, inherit="full") if len(resp.results) == 1: @@ -2412,6 +2418,11 @@ def create(self): if self.check_mode: return None + # If next_available_id is not None, set the address to the next available ID. + if self.next_available_id is not None: + naId = f"{self.next_available_id}/nextavailableaddressblock" + self._payload.address = naId + resp = AddressBlockApi(self.client).create(body=self.payload, inherit="full") return resp.result.model_dump(by_alias=True, exclude_none=True) @@ -2475,7 +2486,8 @@ def main(): module_args = dict( id=dict(type="str", required=False), state=dict(type="str", required=False, choices=["present", "absent"], default="present"), - address=dict(type="str"), + address=dict(type="str", required=False), + next_available_id=dict(type="str", required=False), asm_config=dict( type="dict", options=dict( @@ -2766,7 +2778,10 @@ def main(): module = AddressBlockModule( argument_spec=module_args, supports_check_mode=True, - required_if=[("state", "present", ["address", "space"])], + mutually_exclusive=[["address", "next_available_id"]], + required_if=[("state", "present", ["space"])], + required_one_of=[["address", "next_available_id"]], + required_together=[["cidr", "next_available_id"]], ) module.run_command() diff --git a/plugins/modules/ipam_address_block_info.py b/plugins/modules/ipam_address_block_info.py index 26cc459c..a03e9d92 100644 --- a/plugins/modules/ipam_address_block_info.py +++ b/plugins/modules/ipam_address_block_info.py @@ -62,9 +62,8 @@ address: "10.0.0.0/16" space: "{{ ip_space.id }}" tags: - location: "{{ tag_value }}" + location: "site-1" state: "present" - register: address_block - name: Get Address Block information by ID infoblox.bloxone.ipam_address_block_info: @@ -83,7 +82,7 @@ - name: Get Address Block information by tag filters infoblox.bloxone.ipam_address_block_info: tag_filters: - location: "{{ tag_value }}" + location: "site-1" """ RETURN = r""" @@ -156,7 +155,7 @@ type: int returned: Always reenable_date: - description: "" + description: "The date at which notifications will be re-enabled automatically." type: str returned: Always asm_scope_flag: @@ -1619,23 +1618,23 @@ returned: Always contains: abandoned: - description: "" + description: "The number of IP addresses in the scope of the object which are in the abandoned state (issued by a DHCP server and then declined by the client)." type: str returned: Always dynamic: - description: "" + description: "The number of IP addresses handed out by DHCP in the scope of the object. This includes all leased addresses, fixed addresses that are defined but not currently leased and abandoned leases." type: str returned: Always static: - description: "" + description: "The number of defined IP addresses such as reservations or DNS records. It can be computed as _static_ = _used_ - _dynamic_." type: str returned: Always total: - description: "" + description: "The total number of IP addresses available in the scope of the object." type: str returned: Always used: - description: "" + description: "The number of IP addresses used in the scope of the object." type: str returned: Always """ # noqa: E501 diff --git a/plugins/modules/ipam_next_available_address_block_info.py b/plugins/modules/ipam_next_available_address_block_info.py new file mode 100644 index 00000000..014f25da --- /dev/null +++ b/plugins/modules/ipam_next_available_address_block_info.py @@ -0,0 +1,139 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: Infoblox 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 + +DOCUMENTATION = r""" +--- +module: ipam_next_available_address_block_info +short_description: Manage NextAvailableAddressBlock +description: + - Manage NextAvailableAddressBlock +version_added: 2.0.0 +author: Infoblox Inc. (@infobloxopen) +options: + id: + description: + - ID of the object + type: str + required: true + cidr: + description: + - The CIDR value of the object + type: int + required: true + count: + description: + - Number of objects to generate. Default 1 if not set + type: int + required: false +extends_documentation_fragment: + - infoblox.bloxone.common +""" # noqa: E501 + +EXAMPLES = r""" + - name: "Create an Address Block" + infoblox.bloxone.ipam_address_block: + address: "10.0.0.0/16" + space: "{{ ip_space.id }}" + state: "present" + + - name: Get Next Available Address Block Information by ID + infoblox.bloxone.ipam_next_available_address_block_info: + id: "{{ address_block.id }}" + cidr: 20 + + - name: Get Next Available Address Block Information by ID and Count + infoblox.bloxone.ipam_next_available_address_block_info: + id: "{{ address_block.id }}" + cidr: 24 + count: 5 +""" + +RETURN = r""" +id: + description: + - ID of the AddressBlock object. + type: str + returned: Always +objects: + description: + - List of next available address block's addresses. + type: list + elements: str + returned: Always +""" + +from ansible_collections.infoblox.bloxone.plugins.module_utils.modules import BloxoneAnsibleModule + +try: + from bloxone_client import ApiException + from ipam import AddressBlockApi +except ImportError: + pass # Handled by BloxoneAnsibleModule + + +class NextAvailableAddressBlockInfoModule(BloxoneAnsibleModule): + def __init__(self, *args, **kwargs): + super(NextAvailableAddressBlockInfoModule, self).__init__(*args, **kwargs) + self._existing = None + self._limit = 1000 + + def find(self): + all_results = [] + offset = 0 + while True: + try: + resp = AddressBlockApi(self.client).list_next_available_ab( + id=self.params["id"], cidr=self.params["cidr"], count=self.params["count"] + ) + all_results.extend(resp.results) + + if len(resp.results) < self._limit: + break + offset += self._limit + + except ApiException as e: + self.fail_json(msg=f"Failed to execute command: {e.status} {e.reason} {e.body}") + + return all_results + + def run_command(self): + result = dict(objects=[]) + + if self.check_mode: + self.exit_json(**result) + + find_results = self.find() + + all_results = [] + for r in find_results: + # The expected output is a list of addresses as strings. + # Therefore, we extract only the 'address' field from each object in the results. + all_results.append(r.address) + + result["objects"] = all_results + self.exit_json(**result) + + +def main(): + # define available arguments/parameters a user can pass to the module + module_args = dict( + id=dict(type="str", required=True), + cidr=dict(type="int", required=True), + count=dict(type="int", required=False), + ) + + module = NextAvailableAddressBlockInfoModule( + argument_spec=module_args, + supports_check_mode=True, + ) + module.run_command() + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/ipam_address_block/meta/main.yml b/tests/integration/targets/ipam_address_block/meta/main.yml new file mode 100644 index 00000000..32c7cbc6 --- /dev/null +++ b/tests/integration/targets/ipam_address_block/meta/main.yml @@ -0,0 +1,2 @@ +--- +dependencies: [setup_ip_space] diff --git a/tests/integration/targets/ipam_address_block/tasks/main.yml b/tests/integration/targets/ipam_address_block/tasks/main.yml index d4ad00cc..4fc24274 100644 --- a/tests/integration/targets/ipam_address_block/tasks/main.yml +++ b/tests/integration/targets/ipam_address_block/tasks/main.yml @@ -6,20 +6,12 @@ block: # Create a random IP space name to avoid conflicts - ansible.builtin.set_fact: - name: "test-ip-space-{{ 999999 | random | string }}" tag_value: "site-{{ 999999 | random | string }}" - # Basic tests for Address Block - - name: "Create an IP space" - infoblox.bloxone.ipam_ip_space: - name: "{{ name }}" - state: "present" - register: ip_space - - name: "Create an Address Block (check mode)" infoblox.bloxone.ipam_address_block: address: "10.0.0.0/16" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" state: "present" check_mode: true register: address_block @@ -27,7 +19,7 @@ infoblox.bloxone.ipam_address_block_info: filters: address: "10.0.0.0" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" cidr: 16 register: address_block_info - assert: @@ -39,14 +31,14 @@ - name: "Create an Address Block" infoblox.bloxone.ipam_address_block: address: "10.0.0.0/16" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" state: "present" register: address_block - name: Get information about the Address Block infoblox.bloxone.ipam_address_block_info: filters: address: "10.0.0.0" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" cidr: 16 register: address_block_info - assert: @@ -58,7 +50,7 @@ - name: "Create an Address Block (idempotent)" infoblox.bloxone.ipam_address_block: address: "10.0.0.0/16" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" state: "present" register: address_block - assert: @@ -66,10 +58,27 @@ - address_block is not changed - address_block is not failed + - name: "Create Next Available Address Block" + infoblox.bloxone.ipam_address_block: + space: "{{ _ip_space.id }}" + cidr: 20 + next_available_id: "{{ address_block.id }}" + state: "present" + register: next_available_address_block + - name: Get Address Block Information by ID + infoblox.bloxone.ipam_address_block_info: + id: "{{ next_available_address_block.id }}" + register: next_available_address_block_info + - assert: + that: + - next_available_address_block is changed + - next_available_address_block is not failed + - next_available_address_block_info.objects | length == 1 + - name: "Delete an Address Block (check mode)" infoblox.bloxone.ipam_address_block: address: "10.0.0.0/16" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" state: "absent" check_mode: true register: address_block @@ -77,7 +86,7 @@ infoblox.bloxone.ipam_address_block_info: filters: address: "10.0.0.0" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" cidr: 16 register: address_block_info - assert: @@ -89,14 +98,14 @@ - name: "Delete an Address Block" infoblox.bloxone.ipam_address_block: address: "10.0.0.0/16" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" state: "absent" register: address_block - name: Get information about the Address Block infoblox.bloxone.ipam_address_block_info: filters: address: "10.0.0.0" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" cidr: 16 register: address_block_info - assert: @@ -108,7 +117,7 @@ - name: "Delete an Address Block (idempotent)" infoblox.bloxone.ipam_address_block: address: "10.0.0.0/16" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" state: "absent" register: address_block - assert: @@ -120,14 +129,14 @@ infoblox.bloxone.ipam_address_block: address: "10.0.0.0" cidr: 16 - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" state: "present" register: address_block - name: Get information about the Address Block infoblox.bloxone.ipam_address_block_info: filters: address: "10.0.0.0" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" cidr: 16 register: address_block_info - assert: @@ -139,7 +148,7 @@ - name: "Create an Address Block with ASM config overridden" infoblox.bloxone.ipam_address_block: address: "10.0.0.0/16" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" asm_config: asm_threshold: 70 enable: true @@ -172,7 +181,7 @@ infoblox.bloxone.ipam_address_block_info: filters: address: "10.0.0.0" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" cidr: 16 register: address_block_info - assert: @@ -188,7 +197,7 @@ - name: "Create an Address Block with comment" infoblox.bloxone.ipam_address_block: address: "10.0.0.0/16" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" state: "present" comment: "Comment." register: address_block @@ -196,7 +205,7 @@ infoblox.bloxone.ipam_address_block_info: filters: address: "10.0.0.0" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" cidr: 16 register: address_block_info - assert: @@ -208,7 +217,7 @@ - name: "Create an Address Block with ddns_client_update set to server" infoblox.bloxone.ipam_address_block: address: "10.0.0.0/16" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" state: "present" ddns_client_update: "server" register: address_block @@ -216,7 +225,7 @@ infoblox.bloxone.ipam_address_block_info: filters: address: "10.0.0.0" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" cidr: 16 register: address_block_info - assert: @@ -228,7 +237,7 @@ - name: "Create an Address Block with ddns_use_conflict_resolution set to false" infoblox.bloxone.ipam_address_block: address: "10.0.0.0/16" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" state: "present" ddns_use_conflict_resolution: "false" register: address_block @@ -236,7 +245,7 @@ infoblox.bloxone.ipam_address_block_info: filters: address: "10.0.0.0" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" cidr: 16 register: address_block_info - assert: @@ -248,7 +257,7 @@ - name: "Create an Address Block with DHCP config overridden" infoblox.bloxone.ipam_address_block: address: "10.0.0.0/16" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" dhcp_config: lease_time: 3600 inheritance_sources: @@ -281,7 +290,7 @@ infoblox.bloxone.ipam_address_block_info: filters: address: "10.0.0.0" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" cidr: 16 register: address_block_info - assert: @@ -294,7 +303,7 @@ - name: "Create an Address Block with hostname_rewrite_enabled set to true" infoblox.bloxone.ipam_address_block: address: "10.0.0.0/16" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" state: "present" hostname_rewrite_enabled: "true" register: address_block @@ -302,7 +311,7 @@ infoblox.bloxone.ipam_address_block_info: filters: address: "10.0.0.0" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" cidr: 16 register: address_block_info - assert: @@ -314,7 +323,7 @@ - name: "Create an Address Block with tags" infoblox.bloxone.ipam_address_block: address: "10.0.0.0/16" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" tags: location: "{{ tag_value }}" state: "present" @@ -323,7 +332,7 @@ infoblox.bloxone.ipam_address_block_info: filters: address: "10.0.0.0" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" cidr: 16 register: address_block_info - assert: @@ -334,8 +343,6 @@ always: # Cleanup if the test fails - - name: "Delete IP Space" - infoblox.bloxone.ipam_ip_space: - name: "{{ name }}" - state: "absent" - ignore_errors: true + - ansible.builtin.include_role: + name: setup_ip_space + tasks_from: cleanup.yml diff --git a/tests/integration/targets/ipam_address_block_info/meta/main.yml b/tests/integration/targets/ipam_address_block_info/meta/main.yml new file mode 100644 index 00000000..32c7cbc6 --- /dev/null +++ b/tests/integration/targets/ipam_address_block_info/meta/main.yml @@ -0,0 +1,2 @@ +--- +dependencies: [setup_ip_space] diff --git a/tests/integration/targets/ipam_address_block_info/tasks/main.yml b/tests/integration/targets/ipam_address_block_info/tasks/main.yml index ad4b1570..28a88003 100644 --- a/tests/integration/targets/ipam_address_block_info/tasks/main.yml +++ b/tests/integration/targets/ipam_address_block_info/tasks/main.yml @@ -9,16 +9,10 @@ name: "test-ip-space-{{ 999999 | random | string }}" tag_value: "site-{{ 999999 | random | string }}" - - name: "Create an IP space" - infoblox.bloxone.ipam_ip_space: - name: "{{ name }}" - state: "present" - register: ip_space - - name: "Create an Address Block" infoblox.bloxone.ipam_address_block: address: "10.0.0.0/16" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" tags: location: "{{ tag_value }}" state: "present" @@ -37,7 +31,7 @@ infoblox.bloxone.ipam_address_block_info: filters: address: "10.0.0.0" - space: "{{ ip_space.id }}" + space: "{{ _ip_space.id }}" cidr: 16 register: address_block_info - assert: @@ -47,7 +41,7 @@ - name: Get Address Block information by filter query infoblox.bloxone.ipam_address_block_info: - filter_query: "address=='10.0.0.0' and space=='{{ ip_space.id }}' and cidr==16" + filter_query: "address=='10.0.0.0' and space=='{{ _ip_space.id }}' and cidr==16" register: address_block_info - assert: that: @@ -62,4 +56,9 @@ - assert: that: - address_block_info.objects | length == 1 - - address_block_info.objects[0].id == address_block.id \ No newline at end of file + - address_block_info.objects[0].id == address_block.id + always: + # Cleanup if the test fails + - ansible.builtin.include_role: + name: setup_ip_space + tasks_from: cleanup.yml diff --git a/tests/integration/targets/ipam_next_available_address_block_info/meta/main.yml b/tests/integration/targets/ipam_next_available_address_block_info/meta/main.yml new file mode 100644 index 00000000..32c7cbc6 --- /dev/null +++ b/tests/integration/targets/ipam_next_available_address_block_info/meta/main.yml @@ -0,0 +1,2 @@ +--- +dependencies: [setup_ip_space] diff --git a/tests/integration/targets/ipam_next_available_address_block_info/tasks/main.yml b/tests/integration/targets/ipam_next_available_address_block_info/tasks/main.yml new file mode 100644 index 00000000..92e4a06e --- /dev/null +++ b/tests/integration/targets/ipam_next_available_address_block_info/tasks/main.yml @@ -0,0 +1,39 @@ +--- +- module_defaults: + group/infoblox.bloxone.all: + csp_url: "{{ csp_url }}" + api_key: "{{ api_key }}" + block: + - name: "Create an Address Block" + infoblox.bloxone.ipam_address_block: + address: "10.0.0.0/16" + space: "{{ _ip_space.id }}" + state: "present" + register: address_block + + - name: Get Next Available Address Block Information by ID and default Count + infoblox.bloxone.ipam_next_available_address_block_info: + id: "{{ address_block.id }}" + cidr: 20 + register: next_available_address_block_info + - assert: + that: + - next_available_address_block_info is not failed + - next_available_address_block_info.objects | length == 1 + + - name: Get Next Available Address Block Information by ID and Count + infoblox.bloxone.ipam_next_available_address_block_info: + id: "{{ address_block.id }}" + cidr: 24 + count: 5 + register: next_available_address_block_info + - assert: + that: + - next_available_address_block_info is not failed + - next_available_address_block_info.objects | length == 5 + + always: + # Cleanup if the test fails + - ansible.builtin.include_role: + name: setup_ip_space + tasks_from: cleanup.yml