diff --git a/.config/dictionary.txt b/.config/dictionary.txt index 8dc43434..ce6203b0 100644 --- a/.config/dictionary.txt +++ b/.config/dictionary.txt @@ -62,3 +62,4 @@ alinabuzachis hdrs testuser testsecret +keygen diff --git a/meta/runtime.yml b/meta/runtime.yml index 13fef1cd..c7793370 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -4,6 +4,8 @@ requires_ansible: ">=2.15.0" # AAP 2.4 or newer action_groups: eda: + - activation + - activation_info - credential - credential_info - credential_type diff --git a/plugins/modules/activation.py b/plugins/modules/activation.py new file mode 100644 index 00000000..d50f2ab0 --- /dev/null +++ b/plugins/modules/activation.py @@ -0,0 +1,366 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# 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 = """ +--- +module: activation +author: + - "Nikhil Jain (@jainnikhil30)" + - "Alina Buzachis (@alinabuzachis)" +short_description: Manage rulebook activations in the EDA Controller +description: + - This module allows the user to create or delete rulebook activations in the EDA Controller. +options: + name: + description: + - The name of the rulebook activation. + type: str + required: true + description: + description: + - The description of the rulebook activation. + type: str + project_name: + description: + - The name of the project associated with the rulebook activation. + type: str + aliases: + - project + rulebook_name: + description: + - The name of the rulebook associated with the rulebook activation. + type: str + aliases: + - rulebook + extra_vars: + description: + - The extra variables for the rulebook activation. + type: str + restart_policy: + description: + - The restart policy for the rulebook activation. + default: "always" + choices: ["on-failure", "always", "never"] + type: str + enabled: + description: + - Whether the rulebook activation is enabled or not. + type: bool + default: true + decision_environment_name: + description: + - The name of the decision environment associated with the rulebook activation. + type: str + aliases: + - decision_environment + awx_token_name: + description: + - The token ID of the AWX controller. + type: str + aliases: + - awx_token + - token + organization_name: + description: + - The name of the organization. + type: str + aliases: + - organization + eda_credentials: + description: + - A list of IDs for EDA credentials used by the rulebook activation. + type: list + elements: str + aliases: + - credentials + k8s_service_name: + description: + - The name of the Kubernetes service associated with this rulebook activation. + type: str + webhooks: + description: + - A list of webhook IDs associated with the rulebook activation. + type: list + elements: str + swap_single_source: + description: + - Allow swapping of single sources in a rulebook without name match. + type: bool + default: true + event_streams: + description: + - A list of IDs representing the event streams that this rulebook activation listens to. + type: list + elements: int + log_level: + description: + - Allow setting the desired log level. + type: str + default: "debug" + choices: ["debug", "info", "error"] + state: + description: + - Desired state of the resource. + default: "present" + choices: ["present", "absent"] + type: str +extends_documentation_fragment: + - ansible.eda.eda_controller.auths +notes: + - Rulebook Activation API does not support PATCH method, due to this reason the module will + not perform any modification when an existing rulebook activation is found. +""" + +EXAMPLES = """ +- name: Create a rulebook activation + ansible.eda.activation: + name: "Example Rulebook Activation" + description: "Example Rulebook Activation description" + project_name: "Example Project" + rulebook_name: "hello_controller.yml" + decision_environment_name: "Example Decision Environment" + enabled: False + awx_token_name: "Example Token" + +- name: Delete a rulebook activation + ansible.eda.activation: + name: "Example Rulebook Activation" + state: absent +""" + + +RETURN = """ +id: + description: ID of the rulebook activation. + returned: when exists + type: int + sample: 37 +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.arguments import AUTH_ARGSPEC +from ..module_utils.client import Client +from ..module_utils.controller import Controller +from ..module_utils.errors import EDAError + + +def lookup(module, controller, endpoint, name): + result = None + try: + result = controller.resolve_name_to_id(endpoint, name) + except EDAError as e: + module.fail_json(msg=f"Failed to lookup resource: {e}") + return result + + +def create_params(module, controller): + activation_params = {} + + # Get the project id + project_id = None + if module.params.get("project_name"): + project_id = lookup( + module, controller, "projects", module.params["project_name"] + ) + if project_id is not None: + activation_params["project_id"] = project_id + + # Get the rulebook id + rulebook = None + params = {} + if project_id is not None: + params = {"data": {"project_id": project_id}} + if module.params.get("rulebook_name"): + try: + rulebook = controller.get_one_or_many( + "rulebooks", name=module.params["rulebook_name"], **params + ) + except EDAError as e: + module.fail_json(msg=f"Failed to lookup rulebook: {e}") + if rulebook is not None: + activation_params["rulebook_id"] = rulebook["id"] + + # Get the decision environment id + decision_environment_id = None + if module.params.get("decision_environment_name"): + decision_environment_id = lookup( + module, + controller, + "decision-environments", + module.params["decision_environment_name"], + ) + if decision_environment_id is not None: + activation_params["decision_environment_id"] = decision_environment_id + + # Get the organization id + organization_id = None + if module.params.get("organization_name"): + organization_id = lookup( + module, controller, "organizations", module.params["organization_name"] + ) + if organization_id is not None: + activation_params["organization_id"] = organization_id + + if module.params.get("description"): + activation_params["description"] = module.params["description"] + + if module.params.get("extra_vars"): + activation_params["extra_var"] = module.params["extra_vars"] + + # Get the AWX token id + awx_token_id = None + if module.params.get("awx_token_name"): + awx_token_id = lookup( + module, controller, "/users/me/awx-tokens/", module.params["awx_token_name"] + ) + if awx_token_id is not None: + activation_params["awx_token_id"] = awx_token_id + + if module.params.get("restart_policy"): + activation_params["restart_policy"] = module.params["restart_policy"] + + if module.params.get("enabled"): + activation_params["is_enabled"] = module.params["enabled"] + + if module.params.get("event_streams"): + activation_params["event_streams"] = module.params["event_streams"] + + # Get the eda credential ids + eda_credential_ids = None + if module.params.get("eda_credentials"): + eda_credential_ids = [] + for item in module.params["eda_credentials"]: + cred_id = lookup(module, controller, "eda-credentials", item) + if cred_id is not None: + eda_credential_ids.append(cred_id) + + if eda_credential_ids is not None: + activation_params["eda_credentials"] = eda_credential_ids + + if module.params.get("k8s_service_name"): + activation_params["k8s_service_name"] = module.params["k8s_service_name"] + + # Get the webhook ids + webhooks_ids = None + if module.params.get("webhooks"): + webhooks_ids = [] + for item in module.params["webhooks"]: + webhook_id = lookup(module, controller, "webhooks", item) + if webhook_id is not None: + webhooks_ids.append(webhook_id) + if webhooks_ids is not None: + activation_params["webhooks"] = webhooks_ids + + if module.params.get("log_level"): + activation_params["log_level"] = module.params["log_level"] + + if module.params.get("swap_single_source"): + activation_params["swap_single_source"] = module.params["swap_single_source"] + + return activation_params + + +def main(): + argument_spec = dict( + name=dict(type="str", required=True), + description=dict(type="str"), + project_name=dict(type="str", aliases=["project"]), + rulebook_name=dict(type="str", aliases=["rulebook"]), + extra_vars=dict(type="str"), + restart_policy=dict( + type="str", + default="always", + choices=[ + "on-failure", + "always", + "never", + ], + ), + enabled=dict(type="bool", default=True), + decision_environment_name=dict(type="str", aliases=["decision_environment"]), + awx_token_name=dict(type="str", aliases=["awx_token", "token"]), + organization_name=dict(type="str", aliases=["organization"]), + event_streams=dict(type="list", elements="int"), + eda_credentials=dict(type="list", elements="str", aliases=["credentials"]), + k8s_service_name=dict(type="str"), + webhooks=dict(type="list", elements="str"), + swap_single_source=dict(type="bool", default=True), + log_level=dict(type="str", choices=["debug", "info", "error"], default="debug"), + state=dict(choices=["present", "absent"], default="present"), + ) + + argument_spec.update(AUTH_ARGSPEC) + + required_if = [ + ("state", "present", ("name", "rulebook_name", "decision_environment_name")) + ] + + module = AnsibleModule( + argument_spec=argument_spec, required_if=required_if, supports_check_mode=True + ) + + client = Client( + host=module.params.get("controller_host"), + username=module.params.get("controller_username"), + password=module.params.get("controller_password"), + timeout=module.params.get("request_timeout"), + validate_certs=module.params.get("validate_certs"), + ) + + name = module.params.get("name") + state = module.params.get("state") + + controller = Controller(client, module) + + # Attempt to find rulebook activation based on the provided name + try: + activation = controller.get_one_or_many("activations", name=name) + except EDAError as e: + module.fail_json(msg=f"Failed to get rulebook activation: {e}") + + if state == "absent": + try: + result = controller.delete_if_needed(activation, endpoint="activations") + module.exit_json(**result) + except EDAError as e: + module.fail_json(msg=f"Failed to delete rulebook activation: {e}") + + if activation: + module.exit_json( + msg=f"A rulebook activation with name: {name} already exists. " + "The module does not support modifying a rulebook activation.", + **{"changed": False, "id": activation["id"]}, + ) + + # Activation Data that will be sent for create/update + activation_params = create_params(module, controller) + activation_params["name"] = ( + controller.get_item_name(activation) if activation else name + ) + + # If the state was present and we can let the module build or update the + # existing activation, this will return on its own + try: + result = controller.create_or_update_if_needed( + activation, + activation_params, + endpoint="activations", + item_type="activation", + ) + module.exit_json(**result) + except EDAError as e: + module.fail_json(msg=f"Failed to create/update rulebook activation: {e}") + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/activation_info.py b/plugins/modules/activation_info.py new file mode 100644 index 00000000..5eeb014d --- /dev/null +++ b/plugins/modules/activation_info.py @@ -0,0 +1,120 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: Contributors to the Ansible project +# 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 = """ +--- +module: activation_info +author: + - Alina Buzachis (@alinabuzachis) +short_description: List rulebook activations in the EDA Controller +description: + - List rulebook activations in the EDA controller. +version_added: 2.0.0 +options: + name: + description: + - The name of the rulebook activation. + type: str + required: false +extends_documentation_fragment: + - ansible.eda.eda_controller.auths +""" + + +EXAMPLES = """ + - name: Get information about a rulebook activation + ansible.eda.activation_info: + name: "Example Rulebook Activation" + + - name: List all rulebook activations + ansible.eda.activation_info: +""" + + +RETURN = """ +activations: + description: Information about rulebook activations. + returned: always + type: list + elements: dict + sample: [ + { + "id": 1, + "name": "Test activation", + "description": "A test activation", + "is_enabled": true, + "status": "running", + "extra_var": "", + "decision_environment_id": 1, + "project_id": 2, + "rulebook_id": 1, + "organization_id": 1, + "restart_policy": "on-failure", + "restart_count": 2, + "rulebook_name": "Test rulebook", + "current_job_id": "2", + "rules_count": 2, + "rules_fired_count": 2, + "created_at": "2024-08-10T14:22:30.123Z", + "modified_at": "2024-08-15T11:45:00.987Z", + "status_message": "Activation is running successfully.", + "awx_token_id": 1, + "event_streams": [], + "log_level": "info", + "eda_credentials": [], + "k8s_service_name": "", + "webhooks": [], + "swap_single_source": false + } + ] +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.arguments import AUTH_ARGSPEC +from ..module_utils.client import Client +from ..module_utils.common import to_list_of_dict +from ..module_utils.controller import Controller +from ..module_utils.errors import EDAError + + +def main(): + argument_spec = dict( + name=dict(type="str", required=False), + ) + + argument_spec.update(AUTH_ARGSPEC) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + client = Client( + host=module.params.get("controller_host"), + username=module.params.get("controller_username"), + password=module.params.get("controller_password"), + timeout=module.params.get("request_timeout"), + validate_certs=module.params.get("validate_certs"), + ) + + name = module.params.get("name") + controller = Controller(client, module) + + # Attempt to look up credential based on the provided name + try: + result = controller.get_one_or_many("activations", name=name, want_one=False) + except EDAError as e: + module.fail_json(msg=f"Failed to get rulebook activations: {e}") + + module.exit_json(activations=to_list_of_dict(result)) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/activation/tasks/main.yml b/tests/integration/targets/activation/tasks/main.yml new file mode 100644 index 00000000..76ac9d7f --- /dev/null +++ b/tests/integration/targets/activation/tasks/main.yml @@ -0,0 +1,268 @@ +--- +- block: + - set_fact: + credential_defaults: &credential_defaults + controller_username: "{{ controller_username }}" + controller_password: "{{ controller_password }}" + controller_host: "{{ controller_host }}" + validate_certs: false + + - name: Generate a random_string for the test + set_fact: + random_string: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: random_string is not defined + + - name: Generate a ID for the test + set_fact: + test_id: "{{ random_string | to_uuid }}" + when: test_id is not defined + + - name: Define variables for credential and project + set_fact: + credential_type_name: "Test_CredentialType_{{ test_id }}" + credential_name: "Test_Credential_{{ test_id }}" + decision_env_name: "Test_Decision_Env_{{ test_id }}" + activation_name: "Test_Activation_{{ test_id }}" + project_name: "Test_Project_{{ test_id }}" + awx_token_name: "Test_AWXToken_{{ test_id }}" + token_value: "your_private_access_token_name" + image_url: "quay.io/ansible/awx:latest" + scm_url: https://github.com/ansible/event-driven-ansible.git + rulebook_name: "demo_controller_rulebook.yml" + + - name: Create an AWX controller token + ansible.eda.controller_token: + <<: *credential_defaults + name: "{{ awx_token_name }}" + description: "A test AWX controller token description" + token: "{{ token_value }}" + state: present + + - name: Create a new credential type + ansible.eda.credential_type: + <<: *credential_defaults + name: "{{ credential_type_name }}" + state: present + description: "A test credential type" + inputs: + fields: + - id: "username" + label: "Username" + type: "string" + - id: "password" + label: "Password" + type: "string" + secret: true + - id: "ssh_key_data" + label: "SSH Private Key" + type: "string" + multiline: true + secret: true + - id: "ssh_key_unlock" + label: "SSH Key Passphrase" + type: "string" + secret: true + - id: "authorize" + label: "Authorize with SSL Certificate" + type: "boolean" + - id: "authorize_password" + label: "SSL Certificate Passphrase" + type: "string" + secret": true + injectors: + extra_vars: + username: joe + password: secret + register: credential_type_creation + + - name: Create a tempdir for an SSH key + local_action: shell mktemp -d + register: tempdir + + - name: Generate a local SSH key + local_action: "shell ssh-keygen -b 2048 -t rsa -f {{ tempdir.stdout }}/id_rsa -q -N 'passphrase'" + + - name: Read the generated key + set_fact: + ssh_key_data: "{{ lookup('file', tempdir.stdout + '/id_rsa') }}" + + - name: Create a new SCM credential + ansible.eda.credential: + <<: *credential_defaults + name: "{{ credential_name }}" + state: present + credential_type_name: Source Control + inputs: + username: joe + password: secret + ssh_key_data: "{{ ssh_key_data }}" + ssh_key_unlock: "passphrase" + register: credential_creation + + - name: Assert that the credential was created successfully + assert: + that: + - credential_creation is changed + - credential_creation is success + + - name: Create a new project + ansible.eda.project: + <<: *credential_defaults + name: "{{ project_name }}" + description: "Test Project Description" + url: "{{ scm_url }}" + credential: "{{ credential_name }}" + state: present + register: project_creation + + - name: Assert that the project was created successfully + assert: + that: + - project_creation is changed + - project_creation is success + + - name: Create a new decision environment + ansible.eda.decision_environment: + <<: *credential_defaults + name: "{{ decision_env_name }}" + description: "Test Decision Environment Description" + credential: "{{ credential_name }}" + image_url: "{{ image_url }}" + register: decision_environment_creation + + - name: Create a new rulebook activation in check mode + ansible.eda.activation: + <<: *credential_defaults + name: "{{ activation_name }}" + description: "Example Activation description" + project_name: "{{ project_name }}" + rulebook_name: "{{ rulebook_name }}" + decision_environment_name: "{{ decision_env_name }}" + enabled: False + awx_token_name: "{{ awx_token_name }}" + check_mode: true + register: _result + + - name: Check rulebook activation creation in check mode + assert: + that: + - _result.changed + + - name: Create a new rulebook activation + ansible.eda.activation: + <<: *credential_defaults + name: "{{ activation_name }}" + description: "Example Activation description" + project_name: "{{ project_name }}" + rulebook_name: "{{ rulebook_name }}" + decision_environment_name: "{{ decision_env_name }}" + enabled: False + awx_token_name: "{{ awx_token_name }}" + register: _result + + - name: Check rulebook activation creation + assert: + that: + - _result.changed + + - name: Create a new rulebook activation again + ansible.eda.activation: + <<: *credential_defaults + name: "{{ activation_name }}" + description: "Example Activation description" + project_name: "{{ project_name }}" + rulebook_name: "{{ rulebook_name }}" + decision_environment_name: "{{ decision_env_name }}" + enabled: False + awx_token_name: "{{ awx_token_name }}" + register: _result + + - name: Check rulebook activation creation + assert: + that: + - not _result.changed + - "'A rulebook activation with name: ' + activation_name + ' already exists. The module does not support modifying a rulebook activation.' in _result.msg" + + - name: Get information about the rulebook activation + ansible.eda.activation_info: + <<: *credential_defaults + name: "{{ activation_name }}" + + - name: List all the rulebook activations + ansible.eda.activation_info: + <<: *credential_defaults + + - name: Delete project + ansible.eda.project: + <<: *credential_defaults + name: "{{ project_name }}" + state: absent + register: project_deletion + + - name: Assert that the project was deleted successfully + assert: + that: + - project_deletion is changed + - project_deletion is success + + - name: Delete credential + ansible.eda.credential: + <<: *credential_defaults + name: "{{ credential_name }}" + state: absent + register: credential_deletion + + - name: Assert that the credential was created successfully + assert: + that: + - credential_deletion is changed + - credential_deletion is success + + - name: Delete rulebook activation + ansible.eda.activation: + <<: *credential_defaults + name: "{{ activation_name }}" + state: absent + + always: + - name: Delete AWX token + ansible.eda.controller_token: + <<: *credential_defaults + name: "{{ awx_token_name }}" + state: absent + ignore_errors: true + + - name: Delete project + ansible.eda.project: + <<: *credential_defaults + name: "{{ project_name }}" + state: absent + ignore_errors: true + + - name: Delete decision environment + ansible.eda.decision_environment: + <<: *credential_defaults + name: "{{ decision_env_name }}" + state: absent + ignore_errors: true + + - name: Delete credential + ansible.eda.credential: + <<: *credential_defaults + name: "{{ credential_name }}" + state: absent + ignore_errors: true + + - name: Delete credential type + ansible.eda.credential_type: + <<: *credential_defaults + name: "{{ credential_type_name }}" + state: absent + ignore_errors: true + + - name: Delete rulebook activation + ansible.eda.activation: + <<: *credential_defaults + name: "{{ activation_name }}" + state: absent + ignore_errors: true