diff --git a/changelogs/fragments/55-ipam-address.yml b/changelogs/fragments/55-ipam-address.yml new file mode 100644 index 0000000..95ada8c --- /dev/null +++ b/changelogs/fragments/55-ipam-address.yml @@ -0,0 +1,3 @@ +deprecated_features: + - b1_ipam_ipv4_reservation - is deprecated in favor of 'ipam_address'. + - b1_ipam_ipv4_reservation_gather - is deprecated in favor of 'ipam_address_info'. diff --git a/meta/runtime.yml b/meta/runtime.yml index 93a4ed9..a9b37a9 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -20,6 +20,8 @@ action_groups: - ipam_subnet_info - ipam_address_block - ipam_address_block_info + - ipam_address + - ipam_address_info plugin_routing: modules: @@ -67,3 +69,12 @@ plugin_routing: deprecation: removal_version: 3.0.0 warning_text: Use infoblox.bloxone.dns_auth_zone_info instead. + + b1_ipam_ipv4_reservation: + deprecation: + removal_version: 3.0.0 + warning_text: Use infoblox.bloxone.ipam_address instead. + b1_ipam_ipv4_reservation_gather: + deprecation: + removal_version: 3.0.0 + warning_text: Use infoblox.bloxone.ipam_address_info instead. diff --git a/plugins/modules/b1_ipam_ipv4_reservation.py b/plugins/modules/b1_ipam_ipv4_reservation.py index 79d6788..1081e9f 100644 --- a/plugins/modules/b1_ipam_ipv4_reservation.py +++ b/plugins/modules/b1_ipam_ipv4_reservation.py @@ -14,6 +14,10 @@ author: "Amit Mishra (@amishra), Sriram Kannan(@kannans)" short_description: Configure IPv4 address reservation on Infoblox BloxOne DDI version_added: "1.0.1" +deprecated: + removed_in: 3.0.0 + why: This module is deprecated and will be removed in version 3.0.0. Use M(ipam_address) instead. + alternative: Use M(ipam_address) instead. description: - Get, Create, Update and Delete IPv4 address reservation on Infoblox BloxOne DDI. This module manages the IPAM IPv4 address reservation object using BloxOne REST APIs. requirements: diff --git a/plugins/modules/b1_ipam_ipv4_reservation_gather.py b/plugins/modules/b1_ipam_ipv4_reservation_gather.py index 82955f4..197929e 100644 --- a/plugins/modules/b1_ipam_ipv4_reservation_gather.py +++ b/plugins/modules/b1_ipam_ipv4_reservation_gather.py @@ -13,6 +13,10 @@ author: "Amit Mishra (@amishra), Sriram Kannan(@kannans)" short_description: Gather information about Address Block in B1DDI version_added: "1.0.1" +deprecated: + removed_in: 3.0.0 + why: This module is deprecated and will be removed in version 3.0.0. Use M(ipam_address_info) instead. + alternative: Use M(ipam_address_info) instead. description: - Gather information about Address Block object on Infoblox BloxOne DDI. This module gather information about address block object using BloxOne REST APIs. requirements: diff --git a/plugins/modules/ipam_address.py b/plugins/modules/ipam_address.py new file mode 100644 index 0000000..c01e688 --- /dev/null +++ b/plugins/modules/ipam_address.py @@ -0,0 +1,564 @@ +#!/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_address +short_description: Manage Address +description: + - Manage Address +version_added: 2.0.0 +author: Infoblox Inc. (@infobloxopen) +options: + id: + description: + - ID of the object + type: str + required: false + state: + description: + - Indicate desired state of the object + type: str + required: false + choices: + - present + - absent + default: present + address: + description: + - "The address in form \"a.b.c.d\"." + type: str + comment: + description: + - "The description for the address object. May contain 0 to 1024 characters. Can include UTF-8." + type: str + host: + description: + - "The resource identifier." + type: str + hwaddr: + description: + - "The hardware address associated with this IP address." + type: str + interface: + description: + - "The name of the network interface card (NIC) associated with the address, if any." + type: str + names: + description: + - "The list of all names associated with this address." + type: list + elements: dict + suboptions: + name: + description: + - "The name expressed as a single label or FQDN." + type: str + type: + description: + - "The origin of the name." + type: str + parent: + description: + - "The resource identifier." + type: str + range: + description: + - "The resource identifier." + type: str + space: + description: + - "The resource identifier." + type: str + tags: + description: + - "The tags for this address in JSON format." + type: dict + +extends_documentation_fragment: + - infoblox.bloxone.common +""" # noqa: E501 + +EXAMPLES = r""" + - name: "Create an IP space" + infoblox.bloxone.ipam_ip_space: + name: "{{ ip_name }}" + state: "present" + register: ip_space + + - name: "Create a Subnet" + infoblox.bloxone.ipam_subnet: + address: "10.0.0.0/16" + space: "{{ ip_space.id }}" + state: "present" + register: subnet + + - name: Create a Address + infoblox.bloxone.ipam_address: + address: "10.0.0.3" + space: "{{ ip_space.id }}" + state: "present" + register: address + - name: Get information about the Address + infoblox.bloxone.ipam_address_info: + filters: { + address: "10.0.0.3", + space: "{{ ip_space.id }}" + } + register: address_info + - assert: + that: + - address is changed + - address is not failed + - address_info.objects | length != 0 + + - name: Create the Address with Name + infoblox.bloxone.ipam_address: + address: "10.0.0.8" + names: [{"name": "test-1","type": "user"}] + space: "{{ ip_space.id }}" + state: "present" + register: address + - name: Get information about the Address + infoblox.bloxone.ipam_address_info: + filters: { + address: "10.0.0.8", + space: "{{ ip_space.id }}" + } + register: address_info + - assert: + that: + - address is changed + - address is not failed + - address_info.objects | length > 0 + + - name: Create the Address with hwaddr (check mode) + infoblox.bloxone.ipam_address: + address: "10.0.0.8" + hwaddr : "00:11:22:33:44:55" + space: "{{ ip_space.id }}" + state: "present" + check_mode: true + + - name: Create the Address with Interface (check mode) + infoblox.bloxone.ipam_address: + address: "10.0.0.8" + interface: "eth0" + space: "{{ ip_space.id }}" + state: "present" + register: address + check_mode: true + + - name: Create the Address with Tags (check mode) + infoblox.bloxone.ipam_address: + address: "10.0.0.8" + tags: { "tag1": "value1","tag2": "value2",} + space: "{{ ip_space.id }}" + state: "present" + check_mode: true + + - name: Delete a Address + infoblox.bloxone.ipam_address: + address: "10.0.0.3" + space: "{{ ip_space.id }}" + state: "absent" + + - name: Delete a Address + infoblox.bloxone.ipam_address: + address: "10.0.0.5" + space: "{{ ip_space.id }}" + state: "absent" + + - name: Delete a Address + infoblox.bloxone.ipam_address: + address: "10.0.0.8" + space: "{{ ip_space.id }}" + state: "absent" + + - name: Get information about the address + infoblox.bloxone.ipam_address_info: + filters: { + address: "10.0.0.3", + space: "{{ ip_space.id }}" + } + register: address_info + - assert: + that: + - address is changed + - address is not failed + - address_info.objects | length == 0 + + - name: Delete a Subnet + infoblox.bloxone.ipam_subnet: + address: "10.0.0.0/16" + space: "{{ ip_space.id }}" + state: "absent" + register: subnet + + - name: Delete IP Space + infoblox.bloxone.ipam_ip_space: + name: "{{ ip_name }}" + state: "absent" + register: ip_space +""" # noqa: E501 + +RETURN = r""" +id: + description: + - ID of the Address object + type: str + returned: Always +item: + description: + - Address object + type: complex + returned: Always + contains: + address: + description: + - "The address in form \"a.b.c.d\"." + type: str + returned: Always + comment: + description: + - "The description for the address object. May contain 0 to 1024 characters. Can include UTF-8." + type: str + returned: Always + compartment_id: + description: + - "The compartment associated with the object. If no compartment is associated with the object, the value defaults to empty." + type: str + returned: Always + created_at: + description: + - "Time when the object has been created." + type: str + returned: Always + dhcp_info: + description: + - "The DHCP information associated with this object." + type: dict + returned: Always + contains: + client_hostname: + description: + - "The DHCP host name associated with this client." + type: str + returned: Always + client_hwaddr: + description: + - "The hardware address associated with this client." + type: str + returned: Always + client_id: + description: + - "The ID associated with this client." + type: str + returned: Always + end: + description: + - "The timestamp at which the I(state), when set to I(leased), will be changed to I(free)." + type: str + returned: Always + fingerprint: + description: + - "The DHCP fingerprint for the associated lease." + type: str + returned: Always + iaid: + description: + - "Identity Association Identifier (IAID) of the lease. Applicable only for DHCPv6." + type: int + returned: Always + lease_type: + description: + - "Lease type. Applicable only for address under DHCP control. The value can be empty for address not under DHCP control." + - "Valid values are:" + - "* I(DHCPv6NonTemporaryAddress): DHCPv6 non-temporary address (NA)" + - "* I(DHCPv6TemporaryAddress): DHCPv6 temporary address (TA)" + - "* I(DHCPv6PrefixDelegation): DHCPv6 prefix delegation (PD)" + - "* I(DHCPv4): DHCPv4 lease" + type: str + returned: Always + preferred_lifetime: + description: + - "The length of time that a valid address is preferred (i.e., the time until deprecation). When the preferred lifetime expires, the address becomes deprecated on the client. It is still considered leased on the server. Applicable only for DHCPv6." + type: str + returned: Always + remain: + description: + - "The remaining time, in seconds, until which the I(state), when set to I(leased), will remain in that state." + type: int + returned: Always + start: + description: + - "The timestamp at which I(state) was first set to I(leased)." + type: str + returned: Always + state: + description: + - "Indicates the status of this IP address from a DHCP protocol standpoint as:" + - "* I(none): Address is not under DHCP control." + - "* I(free): Address is under DHCP control but has no lease currently assigned." + - "* I(leased): Address is under DHCP control and has a lease currently assigned. The lease details are contained in the matching I(dhcp/lease) resource." + type: str + returned: Always + state_ts: + description: + - "The timestamp at which the I(state) was last reported." + type: str + returned: Always + disable_dhcp: + description: + - "Read only. Represent the value of the same field in the associated I(dhcp/fixed_address) object." + type: bool + returned: Always + discovery_attrs: + description: + - "The discovery attributes for this address in JSON format." + type: dict + returned: Always + discovery_metadata: + description: + - "The discovery metadata for this address in JSON format." + type: dict + returned: Always + host: + description: + - "The resource identifier." + type: str + returned: Always + hwaddr: + description: + - "The hardware address associated with this IP address." + type: str + returned: Always + id: + description: + - "The resource identifier." + type: str + returned: Always + interface: + description: + - "The name of the network interface card (NIC) associated with the address, if any." + type: str + returned: Always + names: + description: + - "The list of all names associated with this address." + type: list + returned: Always + elements: dict + contains: + name: + description: + - "The name expressed as a single label or FQDN." + type: str + returned: Always + type: + description: + - "The origin of the name." + type: str + returned: Always + parent: + description: + - "The resource identifier." + type: str + returned: Always + protocol: + description: + - "The type of protocol (I(ip4) or I(ip6))." + type: str + returned: Always + range: + description: + - "The resource identifier." + type: str + returned: Always + space: + description: + - "The resource identifier." + type: str + returned: Always + state: + description: + - "The state of the address (I(used) or I(free))." + type: str + returned: Always + tags: + description: + - "The tags for this address in JSON format." + type: dict + returned: Always + updated_at: + description: + - "Time when the object has been updated. Equals to I(created_at) if not updated after creation." + type: str + returned: Always + usage: + description: + - "The usage is a combination of indicators, each tracking a specific associated use. Listed below are usage indicators with their meaning: usage indicator | description ---------------------- | -------------------------------- I(IPAM) | Address was created by the IPAM component. I(IPAM), I(RESERVED) | Address was created by the API call I(ipam/address) or I(ipam/host). I(IPAM), I(NETWORK) | Address was automatically created by the IPAM component and is the network address of the parent subnet. I(IPAM), I(BROADCAST) | Address was automatically created by the IPAM component and is the broadcast address of the parent subnet. I(DHCP) | Address was created by the DHCP component. I(DHCP), I(FIXEDADDRESS) | Address was created by the API call I(dhcp/fixed_address). I(DHCP), I(LEASED) | An active lease for that address was issued by a DHCP server. I(DHCP), I(DISABLED) | Address is disabled. I(DNS) | Address is used by one or more DNS records. I(DISCOVERED) | Address is discovered by some network discovery probe like Network Insight or NetMRI in NIOS." + type: list + returned: Always +""" # noqa: E501 + +from ansible_collections.infoblox.bloxone.plugins.module_utils.modules import BloxoneAnsibleModule + +try: + from bloxone_client import ApiException, NotFoundException + from ipam import Address, AddressApi +except ImportError: + pass # Handled by BloxoneAnsibleModule + + +class AddressModule(BloxoneAnsibleModule): + def __init__(self, *args, **kwargs): + super(AddressModule, self).__init__(*args, **kwargs) + + exclude = ["state", "csp_url", "api_key", "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 = Address.from_dict(self._payload_params) + self._existing = None + + @property + def existing(self): + return self._existing + + @existing.setter + def existing(self, value): + self._existing = value + + @property + def payload_params(self): + return self._payload_params + + @property + def payload(self): + return self._payload + + def payload_changed(self): + if self.existing is None: + # if existing is None, then it is a create operation + return True + + return self.is_changed(self.existing.model_dump(by_alias=True, exclude_none=True), self.payload_params) + + def find(self): + if self.params["id"] is not None: + try: + resp = AddressApi(self.client).read(self.params["id"]) + return resp.result + except NotFoundException as e: + if self.params["state"] == "absent": + return None + raise e + else: + filter = f"address=='{self.params['address']}' and space=='{self.params['space']}'" + resp = AddressApi(self.client).list(filter=filter) + if len(resp.results) == 1: + return resp.results[0] + if len(resp.results) > 1: + self.fail_json(msg=f"Found multiple Address: {resp.results}") + if len(resp.results) == 0: + return None + + def create(self): + if self.check_mode: + return None + + resp = AddressApi(self.client).create(body=self.payload) + return resp.result.model_dump(by_alias=True, exclude_none=True) + + def update(self): + if self.check_mode: + return None + + resp = AddressApi(self.client).update(id=self.existing.id, body=self.payload) + return resp.result.model_dump(by_alias=True, exclude_none=True) + + def delete(self): + if self.check_mode: + return + + AddressApi(self.client).delete(self.existing.id) + + def run_command(self): + result = dict(changed=False, object={}, id=None) + + # based on the state that is passed in, we will execute the appropriate + # functions + try: + self.existing = self.find() + item = {} + if self.params["state"] == "present" and self.existing is None: + item = self.create() + result["changed"] = True + result["msg"] = "Address created" + elif self.params["state"] == "present" and self.existing is not None: + if self.payload_changed(): + item = self.update() + result["changed"] = True + result["msg"] = "Address updated" + elif self.params["state"] == "absent" and self.existing is not None: + self.delete() + result["changed"] = True + result["msg"] = "Address deleted" + + if self.check_mode: + # if in check mode, do not update the result or the diff, just return the changed state + self.exit_json(**result) + + result["diff"] = dict( + before=self.existing.model_dump(by_alias=True, exclude_none=True) if self.existing is not None else {}, + after=item, + ) + result["object"] = item + result["id"] = ( + self.existing.id if self.existing is not None else item["id"] if (item and "id" in item) else None + ) + except ApiException as e: + self.fail_json(msg=f"Failed to execute command: {e.status} {e.reason} {e.body}") + + self.exit_json(**result) + + +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"), + comment=dict(type="str"), + host=dict(type="str"), + hwaddr=dict(type="str"), + interface=dict(type="str"), + names=dict( + type="list", + elements="dict", + options=dict( + name=dict(type="str"), + type=dict(type="str"), + ), + ), + parent=dict(type="str"), + range=dict(type="str"), + space=dict(type="str"), + tags=dict(type="dict"), + ) + + module = AddressModule( + argument_spec=module_args, + supports_check_mode=True, + required_if=[("state", "present", ["address", "space"])], + ) + + module.run_command() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ipam_address_info.py b/plugins/modules/ipam_address_info.py new file mode 100644 index 0000000..77a2b04 --- /dev/null +++ b/plugins/modules/ipam_address_info.py @@ -0,0 +1,423 @@ +#!/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_address_info +short_description: Manage Address +description: + - Manage Address +version_added: 2.0.0 +author: Infoblox Inc. (@infobloxopen) +options: + id: + description: + - ID of the object + type: str + required: false + filters: + description: + - Filter dict to filter objects + type: dict + required: false + filter_query: + description: + - Filter query to filter objects + type: str + required: false + tag_filters: + description: + - Filter dict to filter objects by tags + type: dict + required: false + tag_filter_query: + description: + - Filter query to filter objects by tags + type: str + required: false + +extends_documentation_fragment: + - infoblox.bloxone.common +""" # noqa: E501 + +EXAMPLES = r""" + - name: "Create an IP space" + infoblox.bloxone.ipam_ip_space: + name: "{{ ip_name }}" + state: "present" + register: ip_space + + - name: "Create a Subnet" + infoblox.bloxone.ipam_subnet: + address: "10.0.0.0/16" + space: "{{ ip_space.id }}" + state: "present" + register: subnet + + - name: Create A Address + infoblox.bloxone.ipam_address: + address: "10.0.0.3" + space: "{{ ip_space.id }}" + tags: + region: "eu" + state: "present" + register: address + + - name: Get information about the Address by ID + infoblox.bloxone.ipam_address_info: + id: "{{ address.id }}" + register: address_info + - assert: + that: + - address_info.objects | length != 0 + + - name: Get Address Block information by filters + infoblox.bloxone.ipam_address_info: + filters: + address: "10.0.0.3" + space: "{{ ip_space.id }}" + register: address_info + - assert: + that: + - address_info.objects | length == 1 + - address_info.objects[0].id == address.id + + - name: Get information about the Address by tag + infoblox.bloxone.ipam_address_info: + tag_filters: + region : "eu" + register: address_info + - assert: + that: + - address_info.objects | length != 0 + + - name: Get Address information by filter query + infoblox.bloxone.ipam_address_info: + filter_query: "address=='10.0.0.3' and space=='{{ ip_space.id }}'" + register: address_info + - assert: + that: + - address_info.objects | length != 0 + + - name: "Delete a Subnet" + infoblox.bloxone.ipam_subnet: + address: "10.0.0.0/16" + space: "{{ ip_space.id }}" + state: "absent" + register: subnet + + - name: "Delete IP Space" + infoblox.bloxone.ipam_ip_space: + name: "{{ ip_name }}" + state: "absent" + register: ip_space +""" # noqa: E501 + +RETURN = r""" +id: + description: + - ID of the Address object + type: str + returned: Always +objects: + description: + - Address object + type: list + elements: dict + returned: Always + contains: + address: + description: + - "The address in form \"a.b.c.d\"." + type: str + returned: Always + comment: + description: + - "The description for the address object. May contain 0 to 1024 characters. Can include UTF-8." + type: str + returned: Always + compartment_id: + description: + - "The compartment associated with the object. If no compartment is associated with the object, the value defaults to empty." + type: str + returned: Always + created_at: + description: + - "Time when the object has been created." + type: str + returned: Always + dhcp_info: + description: + - "The DHCP information associated with this object." + type: dict + returned: Always + contains: + client_hostname: + description: + - "The DHCP host name associated with this client." + type: str + returned: Always + client_hwaddr: + description: + - "The hardware address associated with this client." + type: str + returned: Always + client_id: + description: + - "The ID associated with this client." + type: str + returned: Always + end: + description: + - "The timestamp at which the I(state), when set to I(leased), will be changed to I(free)." + type: str + returned: Always + fingerprint: + description: + - "The DHCP fingerprint for the associated lease." + type: str + returned: Always + iaid: + description: + - "Identity Association Identifier (IAID) of the lease. Applicable only for DHCPv6." + type: int + returned: Always + lease_type: + description: + - "Lease type. Applicable only for address under DHCP control. The value can be empty for address not under DHCP control." + - "Valid values are:" + - "* I(DHCPv6NonTemporaryAddress): DHCPv6 non-temporary address (NA)" + - "* I(DHCPv6TemporaryAddress): DHCPv6 temporary address (TA)" + - "* I(DHCPv6PrefixDelegation): DHCPv6 prefix delegation (PD)" + - "* I(DHCPv4): DHCPv4 lease" + type: str + returned: Always + preferred_lifetime: + description: + - "The length of time that a valid address is preferred (i.e., the time until deprecation). When the preferred lifetime expires, the address becomes deprecated on the client. It is still considered leased on the server. Applicable only for DHCPv6." + type: str + returned: Always + remain: + description: + - "The remaining time, in seconds, until which the I(state), when set to I(leased), will remain in that state." + type: int + returned: Always + start: + description: + - "The timestamp at which I(state) was first set to I(leased)." + type: str + returned: Always + state: + description: + - "Indicates the status of this IP address from a DHCP protocol standpoint as:" + - "* I(none): Address is not under DHCP control." + - "* I(free): Address is under DHCP control but has no lease currently assigned." + - "* I(leased): Address is under DHCP control and has a lease currently assigned. The lease details are contained in the matching I(dhcp/lease) resource." + type: str + returned: Always + state_ts: + description: + - "The timestamp at which the I(state) was last reported." + type: str + returned: Always + disable_dhcp: + description: + - "Read only. Represent the value of the same field in the associated I(dhcp/fixed_address) object." + type: bool + returned: Always + discovery_attrs: + description: + - "The discovery attributes for this address in JSON format." + type: dict + returned: Always + discovery_metadata: + description: + - "The discovery metadata for this address in JSON format." + type: dict + returned: Always + external_keys: + description: + - "The external keys (source key) for this address in JSON format." + type: dict + returned: Always + host: + description: + - "The resource identifier." + type: str + returned: Always + hwaddr: + description: + - "The hardware address associated with this IP address." + type: str + returned: Always + id: + description: + - "The resource identifier." + type: str + returned: Always + interface: + description: + - "The name of the network interface card (NIC) associated with the address, if any." + type: str + returned: Always + names: + description: + - "The list of all names associated with this address." + type: list + returned: Always + elements: dict + contains: + name: + description: + - "The name expressed as a single label or FQDN." + type: str + returned: Always + type: + description: + - "The origin of the name." + type: str + returned: Always + parent: + description: + - "The resource identifier." + type: str + returned: Always + protocol: + description: + - "The type of protocol (I(ip4) or I(ip6))." + type: str + returned: Always + range: + description: + - "The resource identifier." + type: str + returned: Always + space: + description: + - "The resource identifier." + type: str + returned: Always + state: + description: + - "The state of the address (I(used) or I(free))." + type: str + returned: Always + tags: + description: + - "The tags for this address in JSON format." + type: dict + returned: Always + updated_at: + description: + - "Time when the object has been updated. Equals to I(created_at) if not updated after creation." + type: str + returned: Always + usage: + description: + - "The usage is a combination of indicators, each tracking a specific associated use. Listed below are usage indicators with their meaning: usage indicator | description ---------------------- | -------------------------------- I(IPAM) | Address was created by the IPAM component. I(IPAM), I(RESERVED) | Address was created by the API call I(ipam/address) or I(ipam/host). I(IPAM), I(NETWORK) | Address was automatically created by the IPAM component and is the network address of the parent subnet. I(IPAM), I(BROADCAST) | Address was automatically created by the IPAM component and is the broadcast address of the parent subnet. I(DHCP) | Address was created by the DHCP component. I(DHCP), I(FIXEDADDRESS) | Address was created by the API call I(dhcp/fixed_address). I(DHCP), I(LEASED) | An active lease for that address was issued by a DHCP server. I(DHCP), I(DISABLED) | Address is disabled. I(DNS) | Address is used by one or more DNS records. I(DISCOVERED) | Address is discovered by some network discovery probe like Network Insight or NetMRI in NIOS." + type: list + returned: Always +""" # noqa: E501 + +from ansible_collections.infoblox.bloxone.plugins.module_utils.modules import BloxoneAnsibleModule + +try: + from bloxone_client import ApiException, NotFoundException + from ipam import AddressApi +except ImportError: + pass # Handled by BloxoneAnsibleModule + + +class AddressInfoModule(BloxoneAnsibleModule): + def __init__(self, *args, **kwargs): + super(AddressInfoModule, self).__init__(*args, **kwargs) + self._existing = None + self._limit = 1000 + + def find_by_id(self): + try: + resp = AddressApi(self.client).read(self.params["id"]) + return [resp.result] + except NotFoundException as e: + return None + + def find(self): + if self.params["id"] is not None: + return self.find_by_id() + + filter_str = None + if self.params["filters"] is not None: + filter_str = " and ".join([f"{k}=='{v}'" for k, v in self.params["filters"].items()]) + elif self.params["filter_query"] is not None: + filter_str = self.params["filter_query"] + + tag_filter_str = None + if self.params["tag_filters"] is not None: + tag_filter_str = " and ".join([f"{k}=='{v}'" for k, v in self.params["tag_filters"].items()]) + elif self.params["tag_filter_query"] is not None: + tag_filter_str = self.params["tag_filter_query"] + + all_results = [] + offset = 0 + + while True: + try: + resp = AddressApi(self.client).list( + offset=offset, limit=self._limit, filter=filter_str, tfilter=tag_filter_str + ) + 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: + all_results.append(r.model_dump(by_alias=True, exclude_none=True)) + + 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=False), + filters=dict(type="dict", required=False), + filter_query=dict(type="str", required=False), + tag_filters=dict(type="dict", required=False), + tag_filter_query=dict(type="str", required=False), + ) + + module = AddressInfoModule( + argument_spec=module_args, + supports_check_mode=True, + mutually_exclusive=[ + ["id", "filters", "filter_query"], + ["id", "tag_filters", "tag_filter_query"], + ], + ) + module.run_command() + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/ipam_address/meta/main.yml b/tests/integration/targets/ipam_address/meta/main.yml new file mode 100644 index 0000000..973a9c7 --- /dev/null +++ b/tests/integration/targets/ipam_address/meta/main.yml @@ -0,0 +1,2 @@ +--- +dependencies: [setup_address] diff --git a/tests/integration/targets/ipam_address/tasks/main.yml b/tests/integration/targets/ipam_address/tasks/main.yml new file mode 100644 index 0000000..9a40cf4 --- /dev/null +++ b/tests/integration/targets/ipam_address/tasks/main.yml @@ -0,0 +1,225 @@ +--- +#TODO: add tests +# The following require additional plugins to be supported. +# - Address with NextAvailable ID count +# - Address with NextAvailable Subnet +# - Address with NextAvailable AddressBlock +# - Address with NextAvailable Range +- +- module_defaults: + group/infoblox.bloxone.all: + csp_url: "{{ csp_url }}" + api_key: "{{ api_key }}" + block: + + # Basic tests for Address + - name: Create a Address (check mode) + infoblox.bloxone.ipam_address: + address: "10.0.0.3" + space: "{{ ip_space.id }}" + state: "present" + check_mode: true + register: address + + - name: Create a Address + infoblox.bloxone.ipam_address: + address: "10.0.0.3" + space: "{{ ip_space.id }}" + state: "present" + register: address + - name: Get information about the Address + infoblox.bloxone.ipam_address_info: + filters: { + address: "10.0.0.3", + space: "{{ ip_space.id }}" + } + register: address_info + - assert: + that: + - address is changed + - address is not failed + - address_info.objects | length == 1 + - address_info.objects[0].id == address.id + + - name: Create a Address(idempotent) + infoblox.bloxone.ipam_address: + address: "10.0.0.3" + space: "{{ ip_space.id }}" + state: "present" + register: address + - assert: + that: + - address is not changed + - address is not failed + + - name: Delete a Address (check mode) + infoblox.bloxone.ipam_address: + address: "10.0.0.3" + space: "{{ ip_space.id }}" + state: "absent" + check_mode: true + + - name: Delete a Address + infoblox.bloxone.ipam_address: + address: "10.0.0.3" + space: "{{ ip_space.id }}" + state: "absent" + register: address + - name: Get information about the Address + infoblox.bloxone.ipam_address_info: + filters: { + address: "10.0.0.3", + space: "{{ ip_space.id }}" + } + register: address_info + - assert: + that: + - address is changed + - address is not failed + - address_info.objects | length == 0 + + - name: Delete a Address(idempotent) + infoblox.bloxone.ipam_address: + address: "10.0.0.3" + space: "{{ ip_space.id }}" + state: "absent" + register: address + - assert: + that: + - address is not changed + - address is not failed + + - name: Create the Address with Name + infoblox.bloxone.ipam_address: + address: "10.0.0.3" + names: + - name: "test-1" + type: "user" + space: "{{ ip_space.id }}" + state: "present" + register: address + - name: Get information about the Address + infoblox.bloxone.ipam_address_info: + filters: + address: "10.0.0.3" + space: "{{ ip_space.id }}" + register: address_info + - assert: + that: + - address is changed + - address is not failed + - address_info.objects | length == 1 + - address_info.objects[0].names[0].name == address.object.names[0].name + + - name: Create the Address with comment + infoblox.bloxone.ipam_address: + address: "10.0.0.5" + comment: "some comment" + space: "{{ ip_space.id }}" + state: "present" + register: address + - name: Get information about the Address + infoblox.bloxone.ipam_address_info: + filters: { + address: "10.0.0.5", + space: "{{ ip_space.id }}" + } + register: address_info + - assert: + that: + - address is changed + - address is not failed + - address_info.objects | length == 1 + - address_info.objects[0].id == address.id + - address_info.objects[0].comment == address.object.comment + + - name: Create the Address with hwaddr + infoblox.bloxone.ipam_address: + address: "10.0.0.6" + hwaddr: "00:11:22:33:44:55" + space: "{{ ip_space.id }}" + state: "present" + register: address + - name: Get information about the Address + infoblox.bloxone.ipam_address_info: + filters: { + address: "10.0.0.6", + space: "{{ ip_space.id }}", + hwaddr: "00:11:22:33:44:55" + } + register: address_info + - assert: + that: + - address is changed + - address is not failed + - address_info.objects | length == 1 + - address_info.objects[0].id == address.id + - address_info.objects[0].hwaddr == address.object.hwaddr + + - name: Create the Address with Interface + infoblox.bloxone.ipam_address: + address: "10.0.0.7" + interface: "eth0" + space: "{{ ip_space.id }}" + state: "present" + register: address + - name: Get information about the Address + infoblox.bloxone.ipam_address_info: + filters: { + address: "10.0.0.7", + space: "{{ ip_space.id }}", + interface: "eth0" + } + register: address_info + - assert: + that: + - address is changed + - address is not failed + - address_info.objects | length == 1 + - address_info.objects[0].id == address.id + - address_info.objects[0].interface == address.object.interface + + - name: Create the Address with Tags + infoblox.bloxone.ipam_address: + address: "10.0.0.8" + tags: {"tag1": "value1", "tag2": "value2"} + space: "{{ ip_space.id }}" + state: "present" + register: address + - name: Get information about the Address + infoblox.bloxone.ipam_address_info: + filters: { + address: "10.0.0.8", + space: "{{ ip_space.id }}", + } + register: address_info + - name: Debug address + debug: + var: address + + - name: Debug address_info + debug: + var: address_info + - assert: + that: + - address is changed + - address is not failed + - address_info.objects | length == 1 + - address_info.objects[0].id == address.id + - address_info.objects[0].tags.tag1 == "value1" + - address_info.objects[0].tags.tag2 == "value2" + + always: + # Cleanup if the test fails + - name: Delete a Subnet + infoblox.bloxone.ipam_subnet: + address: "10.0.0.0/16" + space: "{{ ip_space.id }}" + state: "absent" + register: subnet + + - name: Delete IP Space + infoblox.bloxone.ipam_ip_space: + name: "{{ ip_name }}" + state: "absent" + register: ip_space diff --git a/tests/integration/targets/ipam_address_info/meta/main.yml b/tests/integration/targets/ipam_address_info/meta/main.yml new file mode 100644 index 0000000..973a9c7 --- /dev/null +++ b/tests/integration/targets/ipam_address_info/meta/main.yml @@ -0,0 +1,2 @@ +--- +dependencies: [setup_address] diff --git a/tests/integration/targets/ipam_address_info/tasks/main.yml b/tests/integration/targets/ipam_address_info/tasks/main.yml new file mode 100644 index 0000000..a58efd8 --- /dev/null +++ b/tests/integration/targets/ipam_address_info/tasks/main.yml @@ -0,0 +1,84 @@ +--- + +- module_defaults: + group/infoblox.bloxone.all: + csp_url: "{{ csp_url }}" + api_key: "{{ api_key }}" + block: + # Create a random IP space name to avoid conflicts + - ansible.builtin.set_fact: + tag_value: "site-{{ 999999 | random | string }}" + + # Basic tests for Address Block + - name: Create A Address + infoblox.bloxone.ipam_address: + address: "10.0.0.3" + space: "{{ ip_space.id }}" + tags: + region: "eu" + state: "present" + register: address + + - name: Get information about the Address by ID + infoblox.bloxone.ipam_address_info: + id: "{{ address.id }}" + register: address_info + - assert: + that: + - address_info.objects | length != 0 + + - name: Get Address Block information by filters + infoblox.bloxone.ipam_address_info: + filters: + address: "10.0.0.3" + space: "{{ ip_space.id }}" + register: address_info + - assert: + that: + - address_info.objects | length == 1 + - address_info.objects[0].id == address.id + + - name: Get information about the Address by tag + infoblox.bloxone.ipam_address_info: + tag_filters: + region: "eu" + register: address_info + - assert: + that: + - address_info.objects | length != 0 + + - name: Get Address information by filter query + infoblox.bloxone.ipam_address_info: + filter_query: "address=='10.0.0.3' and space=='{{ ip_space.id }}'" + register: address_info + - assert: + that: + - address_info.objects | length != 0 + + - name: "Delete a Subnet" + infoblox.bloxone.ipam_subnet: + address: "10.0.0.0/16" + space: "{{ ip_space.id }}" + state: "absent" + register: subnet + + - name: "Delete IP Space" + infoblox.bloxone.ipam_ip_space: + name: "{{ ip_name }}" + state: "absent" + register: ip_space + + always: + # Cleanup if the test fails + - name: Delete a Subnet + infoblox.bloxone.ipam_subnet: + address: "10.0.0.0/16" + space: "{{ ip_space.id }}" + state: "absent" + register: subnet + + - name: Delete IP Space + infoblox.bloxone.ipam_ip_space: + name: "{{ ip_name }}" + state: "absent" + register: ip_space diff --git a/tests/integration/targets/setup_address/tasks/main.yml b/tests/integration/targets/setup_address/tasks/main.yml new file mode 100644 index 0000000..6e490d0 --- /dev/null +++ b/tests/integration/targets/setup_address/tasks/main.yml @@ -0,0 +1,23 @@ +--- +- module_defaults: + group/infoblox.bloxone.all: + csp_url: "{{ csp_url }}" + api_key: "{{ api_key }}" + block: + # Create a random IP space name to avoid conflicts + - ansible.builtin.set_fact: + ip_name: "test-ip-space1.-{{ 999999 | random | string }}" + + # Basic tests for Address + - name: Create an IP space + infoblox.bloxone.ipam_ip_space: + name: "{{ ip_name }}" + state: "present" + register: ip_space + + - name: Create a Subnet + infoblox.bloxone.ipam_subnet: + address: "10.0.0.0/16" + space: "{{ ip_space.id }}" + state: "present" + register: subnet