diff --git a/.config/dictionary.txt b/.config/dictionary.txt index 9887a27b..92c1acaf 100644 --- a/.config/dictionary.txt +++ b/.config/dictionary.txt @@ -60,10 +60,12 @@ refspec rulebook rulebooks ruleset +rulesets servicebus skipsdist snakeoil storepass +suboptions testenv testpass testsecret diff --git a/.config/manifest.txt b/.config/manifest.txt index ae6f9a10..c1ef1da2 100644 --- a/.config/manifest.txt +++ b/.config/manifest.txt @@ -87,6 +87,7 @@ plugins/modules/event_stream.py plugins/modules/event_stream_info.py plugins/modules/project.py plugins/modules/project_info.py +plugins/modules/rulebook_info.py plugins/modules/user.py plugins/modules/__init__.py plugins/module_utils/ diff --git a/meta/runtime.yml b/meta/runtime.yml index e5ab435c..97cb3c02 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -17,4 +17,5 @@ action_groups: - event_stream_info - project - project_info + - rulebook_info - user diff --git a/plugins/modules/activation.py b/plugins/modules/activation.py index 1740664d..e42edbb0 100644 --- a/plugins/modules/activation.py +++ b/plugins/modules/activation.py @@ -91,13 +91,6 @@ - This parameter is supported in AAP 2.5 and onwards. If specified for AAP 2.4, value will be ignored. type: str - webhooks: - description: - - A list of webhook IDs associated with the rulebook activation. - - This parameter is supported in AAP 2.5 and onwards. - If specified for AAP 2.4, value will be ignored. - type: list - elements: str swap_single_source: description: - Allow swapping of single sources in a rulebook without name match. @@ -107,11 +100,28 @@ default: true event_streams: description: - - A list of IDs representing the event streams that this rulebook activation listens to. + - A list of event stream names that this rulebook activation listens to. - This parameter is supported in AAP 2.5 and onwards. If specified for AAP 2.4, value will be ignored. type: list - elements: int + elements: dict + suboptions: + event_stream: + description: + - The name of the event stream. + type: str + source_name: + description: + - The name of the source. It can be the name defined in the rulebook or the generated one by + the API if the rulebook has not defined one, in this case the name can be retrieved with + M(ansible.eda.rulebook_info) module. + - O(event_streams.source_name) and O(event_streams.source_index) are mutually exclusive. + type: str + source_index: + description: + - The index of the source. + - O(event_streams.source_name) and O(event_streams.source_index) are mutually exclusive. + type: int log_level: description: - Allow setting the desired log level. @@ -144,6 +154,20 @@ enabled: False awx_token_name: "Example Token" +- name: Create a rulebook activation with event_streams option + ansible.eda.activation: + name: "Example Rulebook Activation" + description: "Example Activation description" + project_name: "Example Project" + rulebook_name: "hello_controller.yml" + decision_environment_name: "Example Decision Environment" + enabled: False + awx_token_name: "Example Token" + organization_name: "Default" + event_streams: + - event_stream: "Example Event Stream" + source_name: "Sample source" + - name: Delete a rulebook activation ansible.eda.activation: name: "Example Rulebook Activation" @@ -159,9 +183,20 @@ sample: 37 """ -from typing import Any -from ansible.module_utils.basic import AnsibleModule +import traceback +from typing import Any, Dict, List + +try: + import yaml +except ImportError: + HAS_YAML = False + YAML_IMPORT_ERROR = traceback.format_exc() +else: + HAS_YAML = True + YAML_IMPORT_ERROR = "" + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ..module_utils.arguments import AUTH_ARGSPEC from ..module_utils.client import Client @@ -170,10 +205,116 @@ from ..module_utils.errors import EDAError +def find_matching_source( + event: Dict[str, Any], sources: List[Dict[str, Any]], module: AnsibleModule +) -> Dict[str, Any]: + """ + Finds a matching source based on the source_name in the event. + Raises an error if no match is found. + """ + # Get the source_name from the event + source_name = event.get("source_name") + + # Search for the matching source in the list of sources + for source in sources: + if source["name"] == source_name: + return source # Return the matching source if found + + # If no match is found, raise an error + module.fail_json(msg=f"The specified source_name {source_name} does not exist.") + + return {} # Explicit return to satisfy mypy + + +def process_event_streams( + rulebook_id: int, + controller: Controller, + module: AnsibleModule, +) -> List[Dict[str, Any]]: + """ + Processes event streams and updates activation_params with source mappings. + + Args: + rulebook_id: The ID of the rulebook. + controller: The controller object used for API calls. + module: The module object, typically for error handling. + + Returns: + List source mappings. + """ + + source_mappings = [] + + try: + sources = controller.get_one_or_many( + f"rulebooks/{rulebook_id}/sources", name=module.params["rulebook_name"] + ) + except EDAError as e: + module.fail_json(msg=f"Failed to get rulebook source list: {e}") + + # Process each event_stream + for event in module.params.get("event_streams"): + source_mapping = {} + + # Check mutually exclusive conditions + if event.get("source_index") and event.get("source_name"): + module.fail_json( + msg="source_index and source_name options are mutually exclusive." + ) + + if event.get("source_index") is None and event.get("source_name") is None: + module.fail_json( + msg="You must specify one of the options: source_index or source_name." + ) + + # Handle source_index + if event.get("source_index") is not None: + try: + source_mapping["source_name"] = sources[event["source_index"]].get( + "name" + ) + source_mapping["rulebook_hash"] = sources[event["source_index"]].get( + "rulebook_hash" + ) + except IndexError as e: + module.fail_json( + msg=f"The specified source_index {event['source_index']} is out of range: {e}" + ) + + # Handle source_name + elif event.get("source_name"): + matching_source = find_matching_source(event, sources, module) + source_mapping["source_name"] = matching_source.get("name") + source_mapping["rulebook_hash"] = matching_source.get("rulebook_hash") + + if event.get("event_stream") is None: + module.fail_json(msg="You must specify an event stream name.") + + # Lookup event_stream_id + event_stream_id = lookup_resource_id( + module, + controller, + "event-streams", + event["event_stream"], + ) + + if event_stream_id is None: + module.fail_json( + msg=f"The event stream {event['event_stream']} does not exist." + ) + + # Add the event stream to the source mapping + source_mapping["event_stream_name"] = event["event_stream"] + source_mapping["event_stream_id"] = event_stream_id + source_mappings.append(source_mapping) + + return source_mappings + + def create_params( module: AnsibleModule, controller: Controller, is_aap_24: bool -) -> dict[str, Any]: - activation_params: dict[str, Any] = {} +) -> Dict[str, Any]: + activation_params: Dict[str, Any] = {} # Get the project id project_id = None @@ -242,9 +383,6 @@ def create_params( if module.params.get("enabled"): activation_params["is_enabled"] = module.params["enabled"] - if not is_aap_24 and module.params.get("event_streams"): - activation_params["event_streams"] = module.params["event_streams"] - # Get the eda credential ids eda_credential_ids = None if not is_aap_24 and module.params.get("eda_credentials"): @@ -260,16 +398,15 @@ def create_params( if not is_aap_24 and module.params.get("k8s_service_name"): activation_params["k8s_service_name"] = module.params["k8s_service_name"] - # Get the webhook ids - webhooks_ids = None - if not is_aap_24 and module.params.get("webhooks"): - webhooks_ids = [] - for item in module.params["webhooks"]: - webhook_id = lookup_resource_id(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 not is_aap_24 and module.params.get("event_streams"): + # Process event streams and source mappings + activation_params["source_mappings"] = yaml.dump( + process_event_streams( + rulebook_id=rulebook_id, + controller=controller, + module=module, + ) + ) if not is_aap_24 and module.params.get("log_level"): activation_params["log_level"] = module.params["log_level"] @@ -300,10 +437,17 @@ def main() -> None: 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"), + event_streams=dict( + type="list", + elements="dict", + options=dict( + event_stream=dict(type="str"), + source_index=dict(type="int"), + source_name=dict(type="str"), + ), + ), swap_single_source=dict(type="bool", default=True), log_level=dict(type="str", choices=["debug", "info", "error"], default="error"), state=dict(choices=["present", "absent"], default="present"), @@ -323,6 +467,11 @@ def main() -> None: argument_spec=argument_spec, required_if=required_if, supports_check_mode=True ) + if not HAS_YAML: + module.fail_json( + msg=missing_required_lib("pyyaml"), exception=YAML_IMPORT_ERROR + ) + client = Client( host=module.params.get("controller_host"), username=module.params.get("controller_username"), diff --git a/plugins/modules/activation_info.py b/plugins/modules/activation_info.py index d440dd0d..fb64fe5a 100644 --- a/plugins/modules/activation_info.py +++ b/plugins/modules/activation_info.py @@ -67,11 +67,10 @@ "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": [], + "event_streams": [], "swap_single_source": false } ] @@ -106,7 +105,7 @@ def main() -> None: name = module.params.get("name") controller = Controller(client, module) - # Attempt to look up credential based on the provided name + # Attempt to look up rulebook activation based on the provided name try: result = controller.get_one_or_many("activations", name=name) except EDAError as e: diff --git a/plugins/modules/rulebook_info.py b/plugins/modules/rulebook_info.py new file mode 100644 index 00000000..f4c25ae4 --- /dev/null +++ b/plugins/modules/rulebook_info.py @@ -0,0 +1,145 @@ +#!/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: rulebook_info +author: + - Alina Buzachis (@alinabuzachis) +short_description: List all rulebooks +description: + - List all rulebooks. +version_added: 2.1.0 +options: + name: + description: + - The name of the rulebook. + - If only O(name) is set, all rulebooks with the specific name will be listed in different projects. + For a granular filtering, use O(name) in combination with O(project). + type: str + required: false + project_name: + description: + - The name of the project. + type: str + required: false + aliases: + - project +extends_documentation_fragment: + - ansible.eda.eda_controller.auths +""" + + +EXAMPLES = """ + - name: Get information about a rulebook + ansible.eda.rulebook_info: + name: "Example Rulebook Activation" + project_name: "Example Project" + + - name: List all rulebooks + ansible.eda.rulebook_info: +""" + + +RETURN = """ +rulebooks: + description: Information about rulebooks. + returned: always + type: list + elements: dict + sample: [ + { + "created_at": "2024-09-13T16:13:28.268659Z", + "description": "", + "id": 893, + "modified_at": "2024-09-13T16:13:28.268712Z", + "name": "demo_controller_rulebook.yml", + "organization_id": 1, + "project_id": 177, + "rulesets": "", + "sources": [ + { + "name": "__SOURCE_1", + "rulebook_hash": "1e0f22025ab0a4e729fb68bcb9497412c3d9f477ce5a8cb91cc2ef15e35c4dc6", + "source_info": "ansible.eda.range:\n limit: 5\n" + } + ] + } +] + +""" + + +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 lookup_resource_id +from ..module_utils.controller import Controller +from ..module_utils.errors import EDAError + + +def main() -> None: + argument_spec = dict( + name=dict(type="str", required=False), + project_name=dict(type="str", required=False, aliases=["project"]), + ) + + 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"), + ) + + params = {} + result = [] + name = module.params.get("name") + + controller = Controller(client, module) + + # Get the project id + project_id = None + if module.params.get("project_name"): + project_id = lookup_resource_id( + module, controller, "projects", module.params["project_name"] + ) + if project_id is not None: + params = {"data": {"project_id": project_id}} + + # Attempt to look up rulebook + try: + rulebooks = controller.get_one_or_many("rulebooks", name=name, **params) + except EDAError as e: + module.fail_json(msg=f"Failed to get rulebook: {e}") + + if len(rulebooks) > 0: + for rulebook in rulebooks: + item = rulebook.copy() + try: + sources = controller.get_one_or_many( + f"rulebooks/{item['id']}/sources", name=item["name"] + ) + item["sources"] = sources + except EDAError as e: + module.fail_json(msg=f"Failed to get rulebook source list: {e}") + result.append(item) + + module.exit_json(rulebooks=result) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/activation/tasks/main.yml b/tests/integration/targets/activation/tasks/main.yml index cc68c315..d0725f72 100644 --- a/tests/integration/targets/activation/tasks/main.yml +++ b/tests/integration/targets/activation/tasks/main.yml @@ -23,12 +23,17 @@ credential_name: "Test_Credential_{{ test_id }}" decision_env_name: "Test_Decision_Env_{{ test_id }}" activation_name: "Test_Activation_{{ test_id }}" + activation_name_source_name: "Test_ActivationSourceName_{{ test_id }}" + activation_name_source_index: "Test_ActivationSourceIndex_{{ test_id }}" + activation_name_wrong_source: "Test_ActivationWrongSource_{{ 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" + event_stream_name: "Test_EvenStream_{{ test_id }}" + credential_name_basic: "Test_CredentialBasic_{{ test_id }}" - name: Create an AWX controller token ansible.eda.controller_token: @@ -120,6 +125,9 @@ - project_creation is changed - project_creation is success + - name: List all rulebooks + ansible.eda.rulebook_info: + - name: Create a new decision environment ansible.eda.decision_environment: name: "{{ decision_env_name }}" @@ -189,6 +197,137 @@ - name: List all the rulebook activations ansible.eda.activation_info: + - name: Delete rulebook activation + ansible.eda.activation: + name: "{{ activation_name }}" + state: absent + + - name: Get information about the rulebook activation + ansible.eda.activation_info: + name: "{{ activation_name }}" + + # Test event_streams option + - name: Create a new credential + ansible.eda.credential: + state: present + name: "{{ credential_name_basic }}" + description: "This is a test credential" + credential_type_name: "Basic Event Stream" + inputs: + username: "test" + password: "test" + organization_name: Default + register: _result + + - name: Create an event stream + ansible.eda.event_stream: + state: present + name: "{{ event_stream_name }}" + credential_name: "{{ credential_name_basic }}" + organization_name: Default + register: _result + + - name: Get information about a rulebook + ansible.eda.rulebook_info: + name: "{{ rulebook_name }}" + project_name: "{{ project_name }}" + register: _result_rulebook_info + + - name: Create a rulebook activation (wrong source_name) + ansible.eda.activation: + name: "{{ activation_name_wrong_source }}" + 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 }}" + organization_name: Default + event_streams: + - event_stream: "{{ event_stream_name }}" + source_name: "Test source name" + register: _result + ignore_errors: true + + - name: Check rulebook activation creation + assert: + that: + - not _result.changed + - '"The specified source_name Test source name does not exist." in _result.msg' + + - name: Create a rulebook activation + ansible.eda.activation: + name: "{{ activation_name_source_index }}" + 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 }}" + organization_name: Default + event_streams: + - event_stream: "{{ event_stream_name }}" + source_index: 0 + register: _result + + - name: Check rulebook activation creation + assert: + that: + - _result.changed + + - name: Get information about the rulebook activation + ansible.eda.activation_info: + name: "{{ activation_name_source_index }}" + register: _result_activation_info + + - name: Parse the source mappings from YAML to dictionary + set_fact: + source_mappings_dict: "{{ _result_activation_info.activations[0].source_mappings | from_yaml }}" + + - name: Assert that rulebook activation has been created and it mapped to an event stream + assert: + that: + - event_stream_name in (source_mappings_dict | map(attribute='event_stream_name') | list) + + - name: Delete rulebook activation + ansible.eda.activation: + name: "{{ activation_name_source_index }}" + state: absent + + - name: Create a new rulebook activation + ansible.eda.activation: + name: "{{ activation_name_source_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 }}" + organization_name: Default + event_streams: + - event_stream: "{{ event_stream_name }}" + source_name: "{{ _result_rulebook_info.rulebooks[0].sources[0].name }}" + register: _result + + - name: Check rulebook activation creation + assert: + that: + - _result.changed + + - name: Get information about the rulebook activation + ansible.eda.activation_info: + name: "{{ activation_name_source_name }}" + register: _result_activation_info + + - name: Parse the source mappings from YAML to dictionary + set_fact: + source_mappings_dict: "{{ _result_activation_info.activations[0].source_mappings | from_yaml }}" + + - name: Assert that rulebook activation has been created and it mapped to an event stream + assert: + that: + - event_stream_name in (source_mappings_dict | map(attribute='event_stream_name') | list) + - name: Delete project ansible.eda.project: name: "{{ project_name }}" @@ -231,26 +370,40 @@ state: absent ignore_errors: true - - name: Delete credential - ansible.eda.credential: - name: "{{ credential_name }}" + - name: Delete rulebook activations + ansible.eda.activation: + name: "{{ item }}" state: absent + loop: + - "{{ activation_name }}" + - "{{ activation_name_source_name }}" + - "{{ activation_name_source_index }}" + - "{{ activation_name_wrong_source }}" ignore_errors: true - - name: Delete credential type - ansible.eda.credential_type: - name: "{{ credential_type_name }}" + - name: Delete decision environment + ansible.eda.decision_environment: + name: "{{ decision_env_name }}" state: absent ignore_errors: true - - name: Delete rulebook activation - ansible.eda.activation: - name: "{{ activation_name }}" + - name: Delete event stream + ansible.eda.event_stream: + name: "{{ event_stream_name }}" state: absent ignore_errors: true - - name: Delete decision environment - ansible.eda.decision_environment: - name: "{{ decision_env_name }}" + - name: Delete credentials + ansible.eda.credential: + name: "{{ item }}" + state: absent + loop: + - "{{ credential_name }}" + - "{{ credential_name_basic }}" + ignore_errors: true + + - name: Delete credential type + ansible.eda.credential_type: + name: "{{ credential_type_name }}" state: absent ignore_errors: true