diff --git a/meta/runtime.yml b/meta/runtime.yml index 93a4ed9..f70cc61 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -4,6 +4,7 @@ action_groups: extend_group: - ipam - dns + - infra dns: - dns_view - dns_view_info @@ -21,6 +22,10 @@ action_groups: - ipam_address_block - ipam_address_block_info + infra: + - infra_join_token + - infra_join_token_info + plugin_routing: modules: b1_ipam_ip_space: diff --git a/plugins/modules/infra_join_token.py b/plugins/modules/infra_join_token.py new file mode 100644 index 0000000..8aa062f --- /dev/null +++ b/plugins/modules/infra_join_token.py @@ -0,0 +1,225 @@ +#!/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: infra_join_token +short_description: Manage JoinToken +description: + - Manage JoinToken +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 + - revoked + default: present + description: + description: + - Description of the Join Token + type: str + expires_at: + description: + - Expiration time of the Join Token + type: str + name: + description: + - Name of the Join Token + type: str + tags: + description: + - Tags of the Join Token + type: dict + +extends_documentation_fragment: + - infoblox.bloxone.common +""" # noqa: E501 + +EXAMPLES = r""" + - name: Create a Join token + infoblox.bloxone.infra_join_token: + name: "example_token" + state: "present" + + - name: Create a Join Token with Additional Fields + infoblox.bloxone.infra_join_token: + name: "example_token" + description: "Example Join Token" + tags: + location: "my-location" + + - name: Revoke a Join token + infoblox.bloxone.infra_join_token: + name: "example_token" + state: "revoked" +""" + +from ansible_collections.infoblox.bloxone.plugins.module_utils.modules import BloxoneAnsibleModule + +try: + from bloxone_client import ApiException, NotFoundException + from infra_provision import JoinToken, UIJoinTokenApi +except ImportError: + pass # Handled by BloxoneAnsibleModule + + +class JoinTokenModule(BloxoneAnsibleModule): + def __init__(self, *args, **kwargs): + super(JoinTokenModule, 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 = JoinToken.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 = UIJoinTokenApi(self.client).read(self.params["id"]) + return resp.result + except NotFoundException as e: + if self.params["state"] == "revoked": + return None + raise e + else: + filter = f"name=='{self.params['name']}'" + resp = UIJoinTokenApi(self.client).list(filter=filter) + + # If no results, set results to empty list + if not resp.results: + resp.results = [] + + if len(resp.results) == 1: + return resp.results[0] + if len(resp.results) > 1: + self.fail_json(msg=f"Found multiple View: {resp.results}") + if len(resp.results) == 0: + return None + + def create(self): + if self.check_mode: + return None + + resp = UIJoinTokenApi(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 + + update_body = self.payload + update_body = self.validate_readonly_on_update(self.existing, update_body, ["name", "description"]) + + resp = UIJoinTokenApi(self.client).update(id=self.existing.id, body=update_body) + return resp.result.model_dump(by_alias=True, exclude_none=True) + + def delete(self): + if self.check_mode: + return + + UIJoinTokenApi(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"] = "JoinToken created" + elif self.params["state"] == "present" and self.existing is not None: + if self.payload_changed(): + item = self.update() + result["changed"] = True + result["msg"] = "JoinToken updated" + elif self.params["state"] == "revoked" and self.existing is not None and self.existing.status != "REVOKED": + self.delete() + result["changed"] = True + result["msg"] = "JoinToken Revoked" + elif self.params["state"] == "revoked" and self.existing is not None and self.existing.status == "REVOKED": + result["changed"] = False + result["msg"] = "JoinToken Revoked" + + 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", "revoked"], default="present"), + description=dict(type="str"), + expires_at=dict(type="str"), + name=dict(type="str"), + tags=dict(type="dict"), + ) + + module = JoinTokenModule( + 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/infra_join_token_info.py b/plugins/modules/infra_join_token_info.py new file mode 100644 index 0000000..f524435 --- /dev/null +++ b/plugins/modules/infra_join_token_info.py @@ -0,0 +1,167 @@ +#!/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: infra_join_token_info +short_description: Manage JoinToken +description: + - Manage JoinToken +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 Join Token information by ID + infoblox.bloxone.infra_join_token_info: + id: "{{ join_token_id }}" + + - name: Get a Join Token information by filters (e.g. name) + infoblox.bloxone.infra_join_token_info: + filters: + name: "example_token" + + - name: Get a Join Token information by raw filter query + infoblox.bloxone.infra_join_token_info: + filter_query: "name=='example_token'" + + - name: Get a Join Token information by tag filters + infoblox.bloxone.infra_join_token_info: + tag_filters: + location: "site-1" +""" # noqa: E501 + +from ansible_collections.infoblox.bloxone.plugins.module_utils.modules import BloxoneAnsibleModule + +try: + from bloxone_client import ApiException, NotFoundException + from infra_provision import UIJoinTokenApi +except ImportError: + pass # Handled by BloxoneAnsibleModule + + +class JoinTokenInfoModule(BloxoneAnsibleModule): + def __init__(self, *args, **kwargs): + super(JoinTokenInfoModule, self).__init__(*args, **kwargs) + self._existing = None + self._limit = 1000 + + def find_by_id(self): + try: + resp = UIJoinTokenApi(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 = UIJoinTokenApi(self.client).list( + offset=offset, limit=self._limit, filter=filter_str, tfilter=tag_filter_str + ) + if not resp.results: + resp.results = [] + + 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 = JoinTokenInfoModule( + 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/infra_join_token/tasks/main.yml b/tests/integration/targets/infra_join_token/tasks/main.yml new file mode 100644 index 0000000..93fa0c1 --- /dev/null +++ b/tests/integration/targets/infra_join_token/tasks/main.yml @@ -0,0 +1,150 @@ +--- +- module_defaults: + group/infoblox.bloxone.all: + csp_url: "{{ csp_url }}" + api_key: "{{ api_key }}" + + block: + # Create a random name to avoid conflicts + - ansible.builtin.set_fact: + name: "test-join-token-{{ 999999 | random | string }}" + name2: "test-join-token-{{ 999999 | random | string }}" + + - name: Create a Join Token (check mode) + infoblox.bloxone.infra_join_token: + name: "{{ name }}" + state: present + check_mode: true + register: join_token + - name: Get Information about the Join Token + infoblox.bloxone.infra_join_token_info: + filters: + name: "{{ name }}" + register: join_token_info + - assert: + that: + - join_token is changed + - join_token_info is not failed + - join_token_info.objects | length == 0 + + - name: Create a Join Token + infoblox.bloxone.infra_join_token: + name: "{{ name }}" + state: present + register: join_token + - name: Get Information about the Join Token + infoblox.bloxone.infra_join_token_info: + filters: + name: "{{ name }}" + register: join_token_info + - assert: + that: + - join_token is changed + - join_token_info is not failed + - join_token_info.objects | length == 1 + + - name: Create a Join Token (idempotent) + infoblox.bloxone.infra_join_token: + name: "{{ name }}" + state: present + register: join_token + - assert: + that: + - join_token is not changed + - join_token is not failed + + - name: Revoke the Join Token (check mode) + infoblox.bloxone.infra_join_token: + name: "{{ name }}" + state: revoked + check_mode: true + register: join_token + - name: Get Information about the Join Token + infoblox.bloxone.infra_join_token_info: + filters: + name: "{{ name }}" + register: join_token_info + - assert: + that: + - join_token is changed + - join_token_info is not failed + - join_token_info.objects | length == 1 + - join_token_info.objects[0].status == "ACTIVE" + + - name: Revoke the Join Token + infoblox.bloxone.infra_join_token: + name: "{{ name }}" + state: revoked + register: join_token + - name: Get Information about the Join Token + infoblox.bloxone.infra_join_token_info: + filters: + name: "{{ name }}" + register: join_token_info + - assert: + that: + - join_token is changed + - join_token_info is not failed + # The join token is revoked , not deleted , hence the length would be 1 + - join_token_info.objects | length == 1 + - join_token_info.objects[0].status == "REVOKED" + + + - name: Revoke the Join Token (idempotent) + infoblox.bloxone.infra_join_token: + name: "{{ name }}" + state: revoked + register: join_token + - assert: + that: + - join_token is not changed + - join_token is not failed + + - name: Create a Join Token with Description + infoblox.bloxone.infra_join_token: + name: "{{ name2 }}" + description: "Test Join Token" + state: present + register: join_token + - name: Get Information about the Join Token + infoblox.bloxone.infra_join_token_info: + filters: + name: "{{ name2 }}" + register: join_token_info + - assert: + that: + - join_token is changed + - join_token_info is not failed + - join_token_info.objects | length == 1 + + - name: Create a Join Token with Tags + infoblox.bloxone.infra_join_token: + name: "{{ name2 }}" + description: "Test Join Token" + tags: + location: "site-1" + state: present + register: join_token + - name: Get Information about the Join Token + infoblox.bloxone.infra_join_token_info: + filters: + name: "{{ name2 }}" + register: join_token_info + - assert: + that: + - join_token is changed + - join_token_info is not failed + - join_token_info.objects | length == 1 + + always: + - name: "Clean up the second Join Token" + infoblox.bloxone.infra_join_token: + name: "{{ name2 }}" + state: revoked + ignore_errors: true + + - name: "Clean up the first Join Token" + infoblox.bloxone.infra_join_token: + name: "{{ name }}" + state: revoked + ignore_errors: true diff --git a/tests/integration/targets/infra_join_token_info/tasks/main.yml b/tests/integration/targets/infra_join_token_info/tasks/main.yml new file mode 100644 index 0000000..a516819 --- /dev/null +++ b/tests/integration/targets/infra_join_token_info/tasks/main.yml @@ -0,0 +1,54 @@ +--- +- module_defaults: + group/infoblox.bloxone.all: + csp_url: "{{ csp_url }}" + api_key: "{{ api_key }}" + + block: + - ansible.builtin.set_fact: + name: "test-join-token-{{ 999999 | random | string }}" + tag_value: "site-{{ 999999 | random | string }}" + + - name: Create a Join Token + infoblox.bloxone.infra_join_token: + name: "{{ name }}" + tags: + location: "{{ tag_value }}" + state: present + register: join_token + + - name: Get Information about the Join Token by filters (Name) + infoblox.bloxone.infra_join_token_info: + filters: + name: "{{ name }}" + register: join_token_info + - assert: + that: + - join_token_info.objects | length == 1 + - join_token_info.objects[0].id == join_token.id + + - name: Get Information about the Join Token by filter query + infoblox.bloxone.infra_join_token_info: + filter_query: "name=='{{ name }}'" + register: join_token_info + - assert: + that: + - join_token_info.objects | length == 1 + - join_token_info.objects[0].id == join_token.id + + - name: Get Information about the Join Token by a tag filter + infoblox.bloxone.infra_join_token_info: + tag_filters: + location: "{{ tag_value }}" + register: join_token_info + - assert: + that: + - join_token_info.objects | length == 1 + - join_token_info.objects[0].id == join_token.id + + always: + - name: "Clean up the Join Token" + infoblox.bloxone.infra_join_token: + name: "{{ name }}" + state: revoked + ignore_errors: true