diff --git a/meta/runtime.yml b/meta/runtime.yml index 065089f2..970ae99b 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -4,6 +4,7 @@ action_groups: extend_group: - ipam - dns + - keys - infra dns: - dns_view @@ -35,6 +36,10 @@ action_groups: - ipam_address_info - ipam_next_available_ip_info + keys: + - tsig_key + - tsig_key_info + infra: - infra_join_token - infra_join_token_info diff --git a/plugins/modules/tsig_key.py b/plugins/modules/tsig_key.py new file mode 100644 index 00000000..41f1ae93 --- /dev/null +++ b/plugins/modules/tsig_key.py @@ -0,0 +1,317 @@ +#!/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: tsig_key +short_description: Manage TSIG Key +description: + - Manage TSIG Key +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 + algorithm: + description: + - "The TSIG key algorithm." + choices: + - hmac_sha1 + - hmac_sha224 + - hmac_sha256 + - hmac_sha384 + - hmac_sha512 + type: str + default: hmac_sha256 + comment: + description: + - "The description for the TSIG key. May contain 0 to 1024 characters. Can include UTF-8." + type: str + name: + description: + - "The TSIG key name in the absolute domain name format." + type: str + required: true + secret: + description: + - "The TSIG key secret as a Base64 encoded string." + type: str + tags: + description: + - "The tags for the TSIG key in JSON format." + type: dict + +extends_documentation_fragment: + - infoblox.bloxone.common +""" # noqa: E501 + +EXAMPLES = r""" +- name: Create TSIG Key + infoblox.bloxone.tsig_key: + name: "test-tsig-key" + secret: "rA+n89+aOCjFVNzBPbYkl+j3oQcl4U19JAkCIK9Ad8k=" + algorithm: "hmac_sha512" + state: present + +- name: Create TSIG Key with additional Fields + infoblox.bloxone.tsig_key: + name: "test-tsig-key" + secret: "fA+n89+aOCjFVNzBPbYkl+j3oQcl4U19JAkCIK9Ad8k=" + algorithm: "hmac_sha512" + state: present + tags: + location: "site-1" + +- name: Create TSIG Key with secret dynamically generated + infoblox.bloxone.tsig_key: + name: "test-tsig-key2" + algorithm: "hmac_sha512" + state: present + tags: + location: "site-1" + +- name: Delete TSIG Key + infoblox.bloxone.tsig_key: + name: "test-tsig-key" + state: absent +""" # noqa: E501 + +RETURN = r""" +id: + description: + - ID of the TSIG object + type: str + returned: Always +item: + description: + - Tsig object + type: complex + returned: Always + contains: + algorithm: + description: + - "The TSIG key algorithm." + - "Valid values are:" + - "* I(hmac_sha1)" + - "* I(hmac_sha224)" + - "* I(hmac_sha256)" + - "* I(hmac_sha384)" + - "* I(hmac_sha512)" + - "Defaults to I(hmac_sha256)." + type: str + returned: Always + comment: + description: + - "The description for the TSIG key. May contain 0 to 1024 characters. Can include UTF-8." + type: str + returned: Always + created_at: + description: + - "Time when the object has been created." + type: str + returned: Always + id: + description: + - "The resource identifier." + type: str + returned: Always + name: + description: + - "The TSIG key name in the absolute domain name format." + type: str + returned: Always + protocol_name: + description: + - "The TSIG key name supplied during a create/update operation that is converted to canonical form in punycode." + type: str + returned: Always + secret: + description: + - "The TSIG key secret as a Base64 encoded string." + type: str + returned: Always + tags: + description: + - "The tags for the TSIG key 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 +""" # noqa: E501 + +from ansible_collections.infoblox.bloxone.plugins.module_utils.modules import BloxoneAnsibleModule + +try: + from bloxone_client import ApiException, NotFoundException + from keys import GenerateTsigApi, TsigApi, TSIGKey +except ImportError: + pass # Handled by BloxoneAnsibleModule + + +class TsigKeyModule(BloxoneAnsibleModule): + def __init__(self, *args, **kwargs): + super(TsigKeyModule, 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 = TSIGKey.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 = TsigApi(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"name=='{self.params['name']}'" + resp = TsigApi(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 Tsig: {resp.results}") + if len(resp.results) == 0: + return None + + def create(self): + if self.check_mode: + return None + + if self.params["secret"] is None: + # Generate secret dynamically + api_res = GenerateTsigApi(self.client).generate_tsig(algorithm=self.params["algorithm"]) + if api_res is None: + self.fail_json(msg="Failed to generate TSIG secret.") + # Extract and set the secret + self.payload.secret = api_res.result.secret + + # Perform the create operation + resp = TsigApi(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 = TsigApi(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 + + TsigApi(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"] = "Tsig created" + elif self.params["state"] == "present" and self.existing is not None: + if self.payload_changed(): + item = self.update() + result["changed"] = True + result["msg"] = "Tsig updated" + elif self.params["state"] == "absent" and self.existing is not None: + self.delete() + result["changed"] = True + result["msg"] = "Tsig 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"), + algorithm=dict( + type="str", + choices=["hmac_sha1", "hmac_sha224", "hmac_sha256", "hmac_sha384", "hmac_sha512"], + default="hmac_sha256", + ), + comment=dict(type="str"), + name=dict(type="str", required=True), + secret=dict(type="str", no_log=True, required=False), + tags=dict(type="dict"), + ) + + module = TsigKeyModule( + argument_spec=module_args, + supports_check_mode=True, + required_if=[("state", "present", ["name"])], + ) + + module.run_command() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/tsig_key_info.py b/plugins/modules/tsig_key_info.py new file mode 100644 index 00000000..7cdfa951 --- /dev/null +++ b/plugins/modules/tsig_key_info.py @@ -0,0 +1,231 @@ +#!/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: tsig_key_info +short_description: Retrieves TSIG Keys +description: + - Retrieves information about existing TSIG Keys. +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: Get TSIG Key information by ID + infoblox.bloxone.tsig_key_info: + id: "{{ tsig_key_id }}" + +- name: Get TSIG Key information by filters (e.g. name) + infoblox.bloxone.tsig_key_info: + filters: + name: "tsig-name" + +- name: Get TSIG Key information by raw filter query + infoblox.bloxone.tsig_key_info: + filter_query: "name=='tsig-name'" + +- name: Get TSIG Key information by tag filters + infoblox.bloxone.tsig_key_info: + tag_filters: + location: "site-1" +""" # noqa: E501 + +RETURN = r""" +id: + description: + - ID of the TSIG object + type: str + returned: Always +objects: + description: + - TSIG object + type: list + elements: dict + returned: Always + contains: + algorithm: + description: + - "The TSIG key algorithm." + - "Valid values are:" + - "* I(hmac_sha1)" + - "* I(hmac_sha224)" + - "* I(hmac_sha256)" + - "* I(hmac_sha384)" + - "* I(hmac_sha512)" + - "Defaults to I(hmac_sha256)." + type: str + returned: Always + comment: + description: + - "The description for the TSIG key. May contain 0 to 1024 characters. Can include UTF-8." + type: str + returned: Always + created_at: + description: + - "Time when the object has been created." + type: str + returned: Always + id: + description: + - "The resource identifier." + type: str + returned: Always + name: + description: + - "The TSIG key name in the absolute domain name format." + type: str + returned: Always + protocol_name: + description: + - "The TSIG key name supplied during a create/update operation that is converted to canonical form in punycode." + type: str + returned: Always + secret: + description: + - "The TSIG key secret as a Base64 encoded string." + type: str + returned: Always + tags: + description: + - "The tags for the TSIG key 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 +""" # noqa: E501 + +from ansible_collections.infoblox.bloxone.plugins.module_utils.modules import BloxoneAnsibleModule + +try: + from bloxone_client import ApiException, NotFoundException + from keys import TsigApi +except ImportError: + pass # Handled by BloxoneAnsibleModule + + +class TsigKeyInfoModule(BloxoneAnsibleModule): + def __init__(self, *args, **kwargs): + super(TsigKeyInfoModule, self).__init__(*args, **kwargs) + self._existing = None + self._limit = 1000 + + def find_by_id(self): + try: + resp = TsigApi(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 = TsigApi(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 = TsigKeyInfoModule( + 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/keys_tsig/tasks/main.yml b/tests/integration/targets/keys_tsig/tasks/main.yml new file mode 100644 index 00000000..21882783 --- /dev/null +++ b/tests/integration/targets/keys_tsig/tasks/main.yml @@ -0,0 +1,239 @@ +--- +- module_defaults: + group/infoblox.bloxone.all: + csp_url: "{{ csp_url }}" + api_key: "{{ api_key }}" + block: + - ansible.builtin.set_fact: + tsig_key_name: "test-tsig-{{ 999999 | random | string }}." + tsig_key_secret: "rA+n89+aOCjFVNzBPbYkl+j3oQcl4U19JAkCIK9Ad8k=" + tsig_key_algorithm: "hmac_sha512" + + - name: Create a TSIG Key (check mode) + infoblox.bloxone.tsig_key: + name: "{{ tsig_key_name }}" + algorithm: "{{ tsig_key_algorithm }}" + secret: "{{ tsig_key_secret }}" + state: present + check_mode: true + register: tsig_key + - name: Get Information about the TSIG Key + infoblox.bloxone.tsig_key_info: + filters: + name: "{{ tsig_key_name }}" + register: tsig_key_info + - assert: + that: + - tsig_key is changed + - tsig_key_info is not failed + - tsig_key_info.objects | length == 0 + + - name: Create a TSIG Key + infoblox.bloxone.tsig_key: + name: "{{ tsig_key_name }}" + algorithm: "{{ tsig_key_algorithm }}" + secret: "{{ tsig_key_secret }}" + state: present + register: tsig_key + - name: Get Information about the TSIG Key + infoblox.bloxone.tsig_key_info: + filters: + name: "{{ tsig_key_name }}" + register: tsig_key_info + - assert: + that: + - tsig_key is changed + - tsig_key_info is not failed + - tsig_key_info.objects | length == 1 + + - name: Create the TSIG Key (idempotent) + infoblox.bloxone.tsig_key: + name: "{{ tsig_key_name }}" + secret: "{{ tsig_key_secret }}" + algorithm: "{{ tsig_key_algorithm }}" + state: present + register: tsig_key + - assert: + that: + - tsig_key is not changed + - tsig_key is not failed + + - name: Delete the TSIG Key (check mode) + infoblox.bloxone.tsig_key: + name: "{{ tsig_key_name }}" + secret: "{{ tsig_key_secret }}" + state: absent + check_mode: true + register: tsig_key + - name: Get Information about the TSIG Key after Deletion + infoblox.bloxone.tsig_key_info: + filters: + name: "{{ tsig_key_name }}" + register: tsig_key_info + - assert: + that: + - tsig_key is changed + - tsig_key_info is not failed + - tsig_key_info.objects | length == 1 + + - name: Delete the TSIG Key + infoblox.bloxone.tsig_key: + name: "{{ tsig_key_name }}" + secret: "{{ tsig_key_secret }}" + state: absent + register: tsig_key + - name: Get Information about the TSIG Key after Final Deletion + infoblox.bloxone.tsig_key_info: + filters: + name: "{{ tsig_key_name }}" + register: tsig_key_info + - assert: + that: + - tsig_key is changed + - tsig_key_info is not failed + - tsig_key_info.objects | length == 0 + + - name: Delete the TSIG Key (idempotent) + infoblox.bloxone.tsig_key: + name: "{{ tsig_key_name }}" + secret: "{{ tsig_key_secret }}" + state: absent + register: tsig_key + - assert: + that: + - tsig_key is not changed + - tsig_key is not failed + + - name: Create a TSIG Key with Algorithm type hmac_sha256 + infoblox.bloxone.tsig_key: + name: "{{ tsig_key_name }}" + algorithm: "hmac_sha256" + secret: "{{ tsig_key_secret }}" + state: present + register: tsig_key + - name: Get TSIG Key Information + infoblox.bloxone.tsig_key_info: + filters: + name: "{{ tsig_key_name }}" + register: tsig_key_info + - name: Assert TSIG Key Algorithm + assert: + that: + - tsig_key_info is not failed + - tsig_key_info.objects | length == 1 + - tsig_key_info.objects[0].name == tsig_key_name + - tsig_key_info.objects[0].algorithm == "hmac_sha256" + + - name: Create a TSIG Key with Algorithm type hmac_sha384 + infoblox.bloxone.tsig_key: + name: "{{ tsig_key_name }}" + algorithm: "hmac_sha384" + secret: "{{ tsig_key_secret }}" + state: present + register: tsig_key + - name: Get TSIG Key Information + infoblox.bloxone.tsig_key_info: + filters: + name: "{{ tsig_key_name }}" + register: tsig_key_info + - name: Assert TSIG Key Algorithm + assert: + that: + - tsig_key_info is not failed + - tsig_key_info.objects | length == 1 + - tsig_key_info.objects[0].name == tsig_key_name + - tsig_key_info.objects[0].algorithm == "hmac_sha384" + + - name: Create a TSIG Key with the secret dynamically generated + infoblox.bloxone.tsig_key: + name: "{{ tsig_key_name }}_generated." + algorithm: "hmac_sha384" + state: present + register: tsig_key + - name: Get TSIG Key Information + infoblox.bloxone.tsig_key_info: + filters: + name: "{{ tsig_key_name }}_generated." + register: tsig_key_info + - name: Assert TSIG Key Algorithm + assert: + that: + - tsig_key_info is not failed + - tsig_key_info.objects | length == 1 + - tsig_key_info.objects[0].name == "{{ tsig_key_name }}_generated." + - tsig_key_info.objects[0].algorithm == "hmac_sha384" + + + - name: Create a TSIG Key with a Comment + infoblox.bloxone.tsig_key: + name: "{{ tsig_key_name }}" + algorithm: "{{ tsig_key_algorithm }}" + secret: "{{ tsig_key_secret }}" + comment: "tsig_key_comment" + state: present + register: tsig_key + - name: Get TSIG Key Information + infoblox.bloxone.tsig_key_info: + filters: + name: "{{ tsig_key_name }}" + register: tsig_key_info + - name: Assert TSIG Key Comment and Existence + assert: + that: + - tsig_key_info is not failed + - tsig_key_info.objects | length == 1 + - tsig_key_info.objects[0].name == tsig_key_name + - tsig_key_info.objects[0].comment == "tsig_key_comment" + + - name: Create a TSIG Key with a Specific Secret + infoblox.bloxone.tsig_key: + name: "{{ tsig_key_name }}" + algorithm: "{{ tsig_key_algorithm }}" + secret: "wuQuR0A08ApqKT65yaGiqWHalHxS7Ie8LF2VTUFZFZo=" + state: present + register: tsig_key + - name: Get TSIG Key Information + infoblox.bloxone.tsig_key_info: + filters: + name: "{{ tsig_key_name }}" + register: tsig_key_info + - name: Assert TSIG Key Secret and Existence + assert: + that: + - tsig_key_info is not failed + - tsig_key_info.objects | length == 1 + - tsig_key_info.objects[0].secret == "wuQuR0A08ApqKT65yaGiqWHalHxS7Ie8LF2VTUFZFZo=" + + - name: Create a TSIG Key with Specific Tags + infoblox.bloxone.tsig_key: + name: "{{ tsig_key_name }}" + secret: "{{ tsig_key_secret }}" + algorithm: "{{ tsig_key_algorithm }}" + tags: + location: "site-1" + state: present + register: tsig_key + - name: Get TSIG Key Information + infoblox.bloxone.tsig_key_info: + filters: + name: "{{ tsig_key_name }}" + register: tsig_key_info + - name: Assert TSIG Key Tags and Existence + assert: + that: + - tsig_key_info is not failed + - tsig_key_info.objects | length == 1 + - tsig_key_info.objects[0].tags.location == "site-1" + + always: + - name: "Delete tsig keys" + infoblox.bloxone.tsig_key: + name: "{{ tsig_key_name }}" + state: "absent" + ignore_errors: true + + - name: "Delete tsig keys" + infoblox.bloxone.tsig_key: + name: "{{ tsig_key_name }}_generated." + state: "absent" + ignore_errors: true diff --git a/tests/integration/targets/keys_tsig_info/tasks/main.yml b/tests/integration/targets/keys_tsig_info/tasks/main.yml new file mode 100644 index 00000000..395d187f --- /dev/null +++ b/tests/integration/targets/keys_tsig_info/tasks/main.yml @@ -0,0 +1,68 @@ +--- +- module_defaults: + group/infoblox.bloxone.all: + csp_url: "{{ csp_url }}" + api_key: "{{ api_key }}" + block: + - ansible.builtin.set_fact: + tsig_key_name: "test-tsig-{{ 999999 | random | string }}." + tsig_key_secret: "rA+n89+aOCjFVNzBPbYkl+j3oQcl4U19JAkCIK9Ad8k=" + tsig_key_algorithm: "hmac_sha512" + tag_value: "site-{{ 999999 | random | string }}" + + - name: Create a TSIG Key + infoblox.bloxone.tsig_key: + name: "{{ tsig_key_name }}" + algorithm: "{{ tsig_key_algorithm }}" + secret: "{{ tsig_key_secret }}" + tags: + location: "{{ tag_value }}" + state: present + register: tsig_key + + - name: Get TSIG Key Information by ID + infoblox.bloxone.tsig_key_info: + filters: + id: "{{ tsig_key.id }}" + register: tsig_key_info + - assert: + that: + - tsig_key_info.objects | length == 1 + - tsig_key_info.objects[0].name == tsig_key.object.name + + - name: Get TSIG Key Information by Filters + infoblox.bloxone.tsig_key_info: + filters: + name: "{{ tsig_key_name }}" + register: tsig_key_info + - assert: + that: + - tsig_key_info.objects | length == 1 + - tsig_key_info.objects[0].id == tsig_key.id + + - name: Get TSIG Key Information by Filter Query + infoblox.bloxone.tsig_key_info: + filter_query: "name=='{{ tsig_key_name }}' and algorithm=='{{ tsig_key_algorithm }}'" + register: tsig_key_info + - assert: + that: + - tsig_key_info.objects | length == 1 + - tsig_key_info.objects[0].id == tsig_key.id + + - name: Get TSIG Key Information by Tag Filters + infoblox.bloxone.tsig_key_info: + tag_filters: + location: "{{ tag_value }}" + register: tsig_key_info + - assert: + that: + - tsig_key_info.objects | length == 1 + - tsig_key_info.objects[0].id == tsig_key.id + + always: + # Cleanup if the test fails + - name: "Delete TSIG Key" + infoblox.bloxone.tsig_key: + name: "{{ tsig_key_name }}" + state: "absent" + ignore_errors: true