diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 225b7a4a5..ab0725501 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -13,7 +13,25 @@ env: QUAY_USER: ansible+eda_gha jobs: + build-and-test-image: + if: github.repository == 'ansible/ansible-rulebook' + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Build local image + run: docker build -t localhost/ansible-rulebook:test . + + - name: Run tests + run: > + docker run --rm -u 0 localhost/ansible-rulebook:test bash -c ' + pip install -r requirements_test.txt && + pytest -m "e2e" -n auto' + build-and-push-image: + if: github.repository == 'ansible/ansible-rulebook' runs-on: ubuntu-latest permissions: contents: read diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7cbf129da..3d181701a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,6 @@ repos: additional_dependencies: - flake8-bugbear - repo: local - language: javascript hooks: - id: ajv name: ajv diff --git a/ansible_rulebook/schema/ruleset_schema.json b/ansible_rulebook/schema/ruleset_schema.json index 78e8d1ca0..e312b959f 100644 --- a/ansible_rulebook/schema/ruleset_schema.json +++ b/ansible_rulebook/schema/ruleset_schema.json @@ -56,6 +56,7 @@ } }, "required": [ + "name", "hosts", "sources", "rules" diff --git a/docs/actions.rst b/docs/actions.rst index 68c403c74..f11d1e2f5 100644 --- a/docs/actions.rst +++ b/docs/actions.rst @@ -62,6 +62,9 @@ Run an Ansible playbook. * - json_mode - Boolean, sends the playbook events data to the stdout as json strings as they are processed by ansible-runner - No + * - copy_files + - Boolean, copy the local playbook file to the ansible-runner project directory, this is not needed if you are running a playbook from an ansible collection. + - No run_module @@ -96,6 +99,21 @@ Run an Ansible module * - extra_vars - Additional vars to be passed into the playbook as extra vars. - No + * - json_mode + - Boolean, sends the playbook events data to the stdout as json strings as they are processed by ansible-runner + - No + * - set_facts + - Boolean, the artifacts from the module execution are inserted back into the rule set as facts + - No + * - post_events + - Boolean, the artifacts from the module execution are inserted back into the rule set as events + - No + * - ruleset + - The name of the ruleset to post the event or assert the fact to, default is current rule set. + - No + * - var_root + - If the event is a deeply nested dictionary, the var_root can specify the key name whose value should replace the matching event value. The var_root can take a dictionary to account for data when we have multiple matching events. + - No run_job_template **************** @@ -312,6 +330,9 @@ retract_fact * - ruleset - The name of the rule set to retract the fact, default is the current rule set name - No + * - partial + - The fact being requested to retracted is partial and doesn't have all the keys. Default is true + - No Example: diff --git a/docs/conditions.rst b/docs/conditions.rst index 1f128dda8..eda2cef7e 100644 --- a/docs/conditions.rst +++ b/docs/conditions.rst @@ -966,3 +966,30 @@ Example: | if an attribute exists before you use it in a condition. The rule engine | will check for the existence and only then compare it. If its missing, the | comparison fails. + + +| **Q:** If a condition string has an embedded colon followed by a space in it how do I escape it? + +| **Ans:** During the rulebook parsing you would see this error message: +| ERROR - Terminating mapping values are not allowed here. +| To resove this eror you would have to quote the whole condition string or use the > or | and +| move the entire condition to a separate line. + +Example: + .. code-block:: yaml + + name: rule1 + condition: 'event.abc == "test: 1"' + + + .. code-block:: yaml + + name: rule1 + condition: > + event.abc == "test: 1" + + .. code-block:: yaml + + name: rule1 + condition: | + event.abc == "test: 1" diff --git a/tests/e2e/files/rulebooks/82_non_alpha_keys.yml b/tests/e2e/files/rulebooks/82_non_alpha_keys.yml deleted file mode 100644 index a7fbf3547..000000000 --- a/tests/e2e/files/rulebooks/82_non_alpha_keys.yml +++ /dev/null @@ -1,26 +0,0 @@ ---- -- name: 82 non alpha keys - hosts: all - sources: - - ansible.eda.generic: - payload: - - "http://www.example.com": "down" - - urls: - "http://www.example.com": "up" - - नाम: മധു - - rules: - - name: r1 - condition: event["http://www.example.com"] == "down" - action: - debug: - msg: "First check worked" - - name: r2 - condition: event.urls["http://www.example.com"] == "up" - action: - debug: - msg: "Second check worked" - - name: r3 - condition: event["नाम"] is search("മധു", ignorecase=true) - action: - print_event: diff --git a/tests/e2e/test_non_alpha_keys.py b/tests/e2e/test_non_alpha_keys.py index f959e6a1e..1c0b0b65e 100644 --- a/tests/e2e/test_non_alpha_keys.py +++ b/tests/e2e/test_non_alpha_keys.py @@ -27,7 +27,7 @@ async def test_non_alpha_numeric_keys(): endpoint = "/api/ws2" proc_id = "42" port = 31415 - rulebook = utils.BASE_DATA_PATH / "rulebooks/82_non_alpha_keys.yml" + rulebook = utils.EXAMPLES_PATH / "82_non_alpha_keys.yml" websocket_address = f"ws://localhost:{port}{endpoint}" cmd = utils.Command( rulebook=rulebook, diff --git a/tests/e2e/test_run_module_output.py b/tests/e2e/test_run_module_output.py new file mode 100644 index 000000000..9a7048aca --- /dev/null +++ b/tests/e2e/test_run_module_output.py @@ -0,0 +1,108 @@ +""" +Module with tests for websockets +""" +import asyncio +import logging +from functools import partial + +import dpath +import pytest +import websockets.server as ws_server + +from . import utils + +LOGGER = logging.getLogger(__name__) +DEFAULT_TIMEOUT = 15 + + +@pytest.mark.e2e +@pytest.mark.asyncio +async def test_run_module_output(): + """ + Verify that ansible-rulebook can handle output of + run_module and then used in a condition + """ + # variables + host = "localhost" + endpoint = "/api/ws2" + proc_id = "42" + port = 31415 + rulebook = utils.EXAMPLES_PATH / "29_run_module.yml" + websocket_address = f"ws://localhost:{port}{endpoint}" + cmd = utils.Command( + rulebook=rulebook, + websocket=websocket_address, + proc_id=proc_id, + heartbeat=2, + ) + + # run server and ansible-rulebook + queue = asyncio.Queue() + handler = partial(utils.msg_handler, queue=queue) + async with ws_server.serve(handler, host, port): + LOGGER.info(f"Running command: {cmd}") + proc = await asyncio.create_subprocess_shell( + str(cmd), + cwd=utils.BASE_DATA_PATH, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + await asyncio.wait_for(proc.wait(), timeout=DEFAULT_TIMEOUT) + assert proc.returncode == 0 + + # Verify data + assert not queue.empty() + + action_counter = 0 + session_stats_counter = 0 + stats = None + rule_matches = { + "r1": { + "action": "run_module", + "event_key": "m/i", + "event_value": 1, + }, + "r2": { + "action": "print_event", + "event_key": "m/message", + "event_value": "FRED FLINTSTONE", + }, + } + while not queue.empty(): + data = await queue.get() + assert data["path"] == endpoint + data = data["payload"] + + if data["type"] == "Action": + action_counter += 1 + assert data["action_uuid"] is not None + assert data["ruleset_uuid"] is not None + assert data["rule_uuid"] is not None + assert data["status"] == "successful" + rule_name = data["rule"] + assert rule_name in rule_matches.keys() + + matching_events = data["matching_events"] + assert ( + dpath.get( + matching_events, rule_matches[rule_name]["event_key"] + ) + == rule_matches[rule_name]["event_value"] + ) + assert data["action"] == rule_matches[rule_name]["action"] + + if data["type"] == "SessionStats": + session_stats_counter += 1 + stats = data["stats"] + assert stats["ruleSetName"] == "29 run module" + assert stats["numberOfRules"] == 2 + assert stats["numberOfDisabledRules"] == 0 + assert data["activation_id"] == proc_id + + assert stats["rulesTriggered"] == 2 + assert stats["eventsProcessed"] == 6 + assert stats["eventsMatched"] == 2 + + assert session_stats_counter >= 2 + assert action_counter == 2 diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index 358d82297..93eaf669d 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -11,6 +11,7 @@ BASE_DATA_PATH = Path(f"{__file__}").parent / Path("files") DEFAULT_SOURCES = Path(f"{__file__}").parent / Path("../sources") +EXAMPLES_PATH = Path(f"{__file__}").parent / Path("../examples") DEFAULT_INVENTORY = BASE_DATA_PATH / "inventories/default_inventory.yml"