├── .ansible-lint ├── .config ├── README.md ├── constraints.txt ├── dictionary.txt ├── galaxy-importer.cfg ├── manifest.txt ├── requirements-docs.in ├── requirements-test.in └── requirements.in ├── .flake8 ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── check-coverage.sh ├── dependabot.yml ├── release-drafter.yml └── workflows │ ├── ack.yml │ ├── push.yml │ └── tox.yml ├── .gitignore ├── .gitleaks.toml ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── .tool-versions ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bindep.txt ├── codecov.yml ├── cspell.config.yaml ├── demos └── dynatrace-demo │ ├── fake_app.py │ ├── instructions.md │ ├── inventory.yml │ ├── playbook.yml │ └── rulebook.yml ├── docs ├── .changelog.yml ├── .gitignore ├── antsibull-docs.cfg ├── build.sh ├── conf.py └── docsite │ └── links.yml ├── examples └── eda-setup.yml ├── extensions └── eda │ ├── plugins │ ├── event_filter │ │ ├── README.md │ │ ├── dashes_to_underscores.py │ │ ├── insert_hosts_to_meta.py │ │ ├── json_filter.py │ │ ├── noop.py │ │ └── normalize_keys.py │ └── event_source │ │ ├── README.md │ │ ├── __init__.py │ │ ├── alertmanager.py │ │ ├── aws_cloudtrail.py │ │ ├── aws_sqs_queue.py │ │ ├── azure_service_bus.py │ │ ├── file.py │ │ ├── file_watch.py │ │ ├── generic.py │ │ ├── journald.py │ │ ├── kafka.py │ │ ├── pg_listener.py │ │ ├── range.py │ │ ├── schemas │ │ ├── alertmanager.json │ │ ├── aws_cloudtrail.json │ │ ├── aws_sqs_queue.json │ │ ├── azure_service_bus.json │ │ ├── file.json │ │ ├── file_watch.json │ │ ├── generic.json │ │ ├── journald.json │ │ ├── kafka.json │ │ ├── pg_listener.json │ │ ├── range.json │ │ ├── url_check.json │ │ └── webhook.json │ │ ├── tick.py │ │ ├── url_check.py │ │ └── webhook.py │ └── rulebooks │ ├── demo_controller_rulebook.yml │ ├── demo_rulebook.yml │ ├── demo_webhook_rulebook.yml │ ├── git-hook-deploy-rules.yml │ ├── git-hook-test-rules.yml │ ├── github-ci-cd-rules.yml │ ├── hello_events.yml │ ├── journald_events.yml │ ├── kafka-test-rules.yml │ ├── local-test-rules.yml │ └── process_down.yml ├── galaxy.yml ├── meta ├── extensions.yml └── runtime.yml ├── playbooks └── hello.yml ├── plugins ├── README.md ├── doc_fragments │ ├── __init__.py │ └── eda_controller.py ├── module_utils │ ├── __init__.py │ ├── arguments.py │ ├── client.py │ ├── common.py │ ├── controller.py │ └── errors.py └── modules │ ├── __init__.py │ ├── controller_token.py │ ├── credential.py │ ├── credential_info.py │ ├── credential_type.py │ ├── credential_type_info.py │ ├── decision_environment.py │ ├── decision_environment_info.py │ ├── event_stream.py │ ├── event_stream_info.py │ ├── project.py │ ├── project_info.py │ ├── rulebook_activation.py │ ├── rulebook_activation_copy.py │ ├── rulebook_activation_info.py │ ├── rulebook_info.py │ └── user.py ├── pyproject.toml ├── requirements.txt ├── schemas ├── README.md └── event │ ├── README.md │ └── range.json ├── tests ├── __init__.py ├── config.yml ├── conftest.py ├── integration │ ├── __init__.py │ ├── conftest.py │ ├── default_inventory.yml │ ├── event_source_aws_cloudtrail │ │ ├── __init__.py │ │ └── test_awscloudtrail_rules.yml │ ├── event_source_kafka │ │ ├── __init__.py │ │ ├── ansible │ │ ├── broker_jaas.conf │ │ ├── certs-clean.sh │ │ ├── certs-create.sh │ │ ├── docker-compose.yml │ │ ├── test_kafka_rules_headers.yml │ │ ├── test_kafka_rules_plaintext.yml │ │ ├── test_kafka_rules_sasl_plaintext.yml │ │ ├── test_kafka_rules_sasl_ssl.yml │ │ ├── test_kafka_rules_ssl.yml │ │ └── test_kafka_source.py │ ├── event_source_url_check │ │ ├── __init__.py │ │ ├── test_url_check_rules.yml │ │ ├── test_url_check_rules_urls.yml │ │ ├── test_url_check_source.py │ │ └── webserver_files │ │ │ └── index.html │ ├── event_source_webhook │ │ ├── __init__.py │ │ ├── test_webhook_hmac_rules.yml │ │ ├── test_webhook_rules.yml │ │ └── test_webhook_source.py │ ├── integration_config.yaml.template │ ├── targets │ │ ├── activation │ │ │ └── tasks │ │ │ │ ├── activation_lifecycle.yml │ │ │ │ └── main.yml │ │ ├── controller_token │ │ │ └── tasks │ │ │ │ ├── create.yml │ │ │ │ └── main.yml │ │ ├── credential │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── credential_type │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── decision_environment │ │ │ └── tasks │ │ │ │ ├── create.yml │ │ │ │ ├── delete.yml │ │ │ │ ├── main.yml │ │ │ │ └── update.yml │ │ ├── decision_environment_info │ │ │ └── tasks │ │ │ │ ├── list.yml │ │ │ │ └── main.yml │ │ ├── event_stream │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── project │ │ │ └── tasks │ │ │ │ ├── create.yml │ │ │ │ ├── delete.yml │ │ │ │ ├── main.yml │ │ │ │ ├── scm_branch.yml │ │ │ │ ├── sync.yml │ │ │ │ └── update.yml │ │ ├── project_info │ │ │ └── tasks │ │ │ │ ├── create.yml │ │ │ │ └── main.yml │ │ └── user │ │ │ └── tasks │ │ │ ├── create.yml │ │ │ ├── main.yml │ │ │ └── update.yml │ └── utils.py ├── run-staging └── unit │ ├── __init__.py │ ├── event_filter │ ├── test_insert_hosts_to_meta.py │ └── test_normalize_keys.py │ ├── event_source │ ├── __init__.py │ ├── test_alertmanager.py │ ├── test_aws_cloudtrail.py │ ├── test_aws_sqs_queue.py │ ├── test_azure_service_bus.py │ ├── test_generic.py │ ├── test_kafka.py │ ├── test_pg_listener.py │ └── test_webhook.py │ ├── plugins │ └── module_utils │ │ ├── test_client.py │ │ ├── test_common.py │ │ └── test_controller.py │ └── requirements.txt └── tox.ini /.ansible-lint: -------------------------------------------------------------------------------- 1 | warn_list: 2 | - experimental # all rules tagged as experimental 3 | -------------------------------------------------------------------------------- /.config/README.md: -------------------------------------------------------------------------------- 1 | `manifest.txt` contains the exact list of files we do expect to be inside 2 | the built collection .tar.gz file. This is used to verify that the build 3 | process has not missed any files or included any extra files. 4 | 5 | If you add new files, you might need to also update this file or the CI/CD 6 | pipeline will complain about it being out-of sync. 7 | 8 | # Requirements 9 | 10 | All `requirements*.in` files need this names in order to allow dependabot 11 | to [function correctly](https://github.com/dependabot/dependabot-core/issues/10007). 12 | -------------------------------------------------------------------------------- /.config/dictionary.txt: -------------------------------------------------------------------------------- 1 | -CAcreateserial 2 | -Djava 3 | ARGSPEC 4 | AUTHS 5 | Abhijeet 6 | Alina 7 | Buzachis 8 | EDAHTTP 9 | FQCN 10 | GSSAPI 11 | Kaio 12 | Kasurde 13 | Nikhil 14 | OAUTHBEARER 15 | Oliveira 16 | PYTHONUNBUFFERED 17 | Passw 18 | akasurde 19 | alertmanager 20 | alinabuzachis 21 | antsibull 22 | auths 23 | basepython 24 | benthomasosn 25 | bindep 26 | buildx 27 | cafile 28 | capath 29 | caroot 30 | cliconf 31 | confluentinc 32 | conftest 33 | conninfo 34 | containerd 35 | dbname 36 | deadsnakes 37 | devel 38 | dict2items 39 | digestmod 40 | docsite 41 | dynatrace 42 | envlist 43 | equalto 44 | extfile 45 | finito 46 | hdrs 47 | healthcheck 48 | httpapi 49 | items2dict 50 | jaas 51 | jainnikhil 52 | kaiokmo 53 | keygen 54 | keypass 55 | keytool 56 | krb5-user 57 | libkrb5-dev 58 | libpq 59 | libsystemd 60 | mydb 61 | myqueue 62 | nada 63 | netconf 64 | noprompt 65 | parseable 66 | posargs 67 | pythonmain 68 | realworld 69 | refspec 70 | rejectattr 71 | rulebook 72 | rulebooks 73 | ruleset 74 | rulesets 75 | selectattr 76 | servicebus 77 | skipsdist 78 | snakeoil 79 | sscg 80 | storepass 81 | suboptions 82 | testenv 83 | testpass 84 | testsecret 85 | testuser 86 | toctree 87 | toxinidir 88 | trustore 89 | truststore 90 | unshallow 91 | whitt 92 | -------------------------------------------------------------------------------- /.config/galaxy-importer.cfg: -------------------------------------------------------------------------------- 1 | # https://github.com/ansible/galaxy-importer/tree/master 2 | [galaxy-importer] 3 | ; LOG_LEVEL_MAIN = DEBUG 4 | RUN_ANSIBLE_TEST = False 5 | ; ANSIBLE_LOCAL_TMP = '~/.ansible/tmp' 6 | CHECK_REQUIRED_TAGS = True 7 | REQUIRE_V1_OR_LATER = True 8 | RUN_ANSIBLE_LINT = False 9 | -------------------------------------------------------------------------------- /.config/manifest.txt: -------------------------------------------------------------------------------- 1 | .ansible-lint 2 | .flake8 3 | .gitignore 4 | bindep.txt 5 | CONTRIBUTING.md 6 | docs/ 7 | docs/CHANGELOG.md 8 | docs/README.md 9 | examples/ 10 | examples/eda-setup.yml 11 | extensions/ 12 | extensions/eda/ 13 | extensions/eda/plugins/ 14 | extensions/eda/plugins/event_filter/ 15 | extensions/eda/plugins/event_filter/dashes_to_underscores.py 16 | extensions/eda/plugins/event_filter/insert_hosts_to_meta.py 17 | extensions/eda/plugins/event_filter/json_filter.py 18 | extensions/eda/plugins/event_filter/noop.py 19 | extensions/eda/plugins/event_filter/normalize_keys.py 20 | extensions/eda/plugins/event_filter/README.md 21 | extensions/eda/plugins/event_source/ 22 | extensions/eda/plugins/event_source/alertmanager.py 23 | extensions/eda/plugins/event_source/aws_cloudtrail.py 24 | extensions/eda/plugins/event_source/aws_sqs_queue.py 25 | extensions/eda/plugins/event_source/azure_service_bus.py 26 | extensions/eda/plugins/event_source/file.py 27 | extensions/eda/plugins/event_source/file_watch.py 28 | extensions/eda/plugins/event_source/generic.py 29 | extensions/eda/plugins/event_source/journald.py 30 | extensions/eda/plugins/event_source/kafka.py 31 | extensions/eda/plugins/event_source/pg_listener.py 32 | extensions/eda/plugins/event_source/range.py 33 | extensions/eda/plugins/event_source/README.md 34 | extensions/eda/plugins/event_source/schemas/ 35 | extensions/eda/plugins/event_source/schemas/alertmanager.json 36 | extensions/eda/plugins/event_source/schemas/aws_cloudtrail.json 37 | extensions/eda/plugins/event_source/schemas/aws_sqs_queue.json 38 | extensions/eda/plugins/event_source/schemas/azure_service_bus.json 39 | extensions/eda/plugins/event_source/schemas/file.json 40 | extensions/eda/plugins/event_source/schemas/file_watch.json 41 | extensions/eda/plugins/event_source/schemas/generic.json 42 | extensions/eda/plugins/event_source/schemas/journald.json 43 | extensions/eda/plugins/event_source/schemas/kafka.json 44 | extensions/eda/plugins/event_source/schemas/pg_listener.json 45 | extensions/eda/plugins/event_source/schemas/range.json 46 | extensions/eda/plugins/event_source/schemas/url_check.json 47 | extensions/eda/plugins/event_source/schemas/webhook.json 48 | extensions/eda/plugins/event_source/tick.py 49 | extensions/eda/plugins/event_source/url_check.py 50 | extensions/eda/plugins/event_source/webhook.py 51 | extensions/eda/plugins/event_source/__init__.py 52 | extensions/eda/rulebooks/ 53 | extensions/eda/rulebooks/demo_controller_rulebook.yml 54 | extensions/eda/rulebooks/demo_rulebook.yml 55 | extensions/eda/rulebooks/demo_webhook_rulebook.yml 56 | extensions/eda/rulebooks/git-hook-deploy-rules.yml 57 | extensions/eda/rulebooks/git-hook-test-rules.yml 58 | extensions/eda/rulebooks/github-ci-cd-rules.yml 59 | extensions/eda/rulebooks/hello_events.yml 60 | extensions/eda/rulebooks/journald_events.yml 61 | extensions/eda/rulebooks/kafka-test-rules.yml 62 | extensions/eda/rulebooks/local-test-rules.yml 63 | extensions/eda/rulebooks/process_down.yml 64 | FILES.json 65 | LICENSE 66 | MANIFEST.json 67 | meta/ 68 | meta/extensions.yml 69 | meta/runtime.yml 70 | playbooks/ 71 | playbooks/hello.yml 72 | plugins/ 73 | plugins/doc_fragments/ 74 | plugins/doc_fragments/eda_controller.py 75 | plugins/doc_fragments/__init__.py 76 | plugins/modules/ 77 | plugins/modules/controller_token.py 78 | plugins/modules/credential.py 79 | plugins/modules/credential_info.py 80 | plugins/modules/credential_type.py 81 | plugins/modules/credential_type_info.py 82 | plugins/modules/decision_environment.py 83 | plugins/modules/decision_environment_info.py 84 | plugins/modules/event_stream.py 85 | plugins/modules/event_stream_info.py 86 | plugins/modules/project.py 87 | plugins/modules/project_info.py 88 | plugins/modules/rulebook_activation.py 89 | plugins/modules/rulebook_activation_copy.py 90 | plugins/modules/rulebook_activation_info.py 91 | plugins/modules/rulebook_info.py 92 | plugins/modules/user.py 93 | plugins/modules/__init__.py 94 | plugins/module_utils/ 95 | plugins/module_utils/arguments.py 96 | plugins/module_utils/client.py 97 | plugins/module_utils/common.py 98 | plugins/module_utils/controller.py 99 | plugins/module_utils/errors.py 100 | plugins/module_utils/__init__.py 101 | plugins/README.md 102 | README.md 103 | requirements.txt 104 | schemas/ 105 | schemas/event/ 106 | schemas/event/range.json 107 | schemas/event/README.md 108 | schemas/README.md 109 | tests/ 110 | tests/config.yml 111 | tests/integration/ 112 | tests/__init__.py 113 | -------------------------------------------------------------------------------- /.config/requirements-docs.in: -------------------------------------------------------------------------------- 1 | ansible-pygments 2 | antsibull-docs>=2.13.1,<3.0.0 3 | attrs 4 | galaxy-importer>=0.4.24 5 | mk>=2.6.1 6 | myst-parser 7 | setuptools # https://issues.redhat.com/browse/AAH-3375 8 | sphinx 9 | sphinx-ansible-theme>=0.10.3 10 | -------------------------------------------------------------------------------- /.config/requirements-test.in: -------------------------------------------------------------------------------- 1 | -r requirements.in 2 | coverage-enable-subprocess>=1.0 # see https://github.com/nedbat/coveragepy/issues/1341#issuecomment-1228942657 3 | coverage==6.5.0; python_version < "3.10" # ansible-core will complain 4 | coverage==7.3.2; python_version == "3.10" # ansible-core will complain 5 | coverage[toml] == 7.6.1; python_version >= "3.11" # ansible-core will complain 6 | pytest>=7.0.0 7 | asyncmock>=0.4.2 8 | pytest-asyncio>=0.20.0 9 | pytest-timeout>=2.0.1 10 | requests>=2.31.0 11 | ansible-rulebook>=1.0.0 12 | tox>=4.15.1 13 | podman-compose 14 | -------------------------------------------------------------------------------- /.config/requirements.in: -------------------------------------------------------------------------------- 1 | ../requirements.txt -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # matches black's default 3 | max-line-length = 88 4 | extend-ignore = E203 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | description: "🐞 Create a bug report to help us improve" 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Bug Report issues are for **concrete, actionable bugs** only. 9 | For debugging help or technical support, please see [our ways of contact](https://ansible-rulebook.readthedocs.io/en/latest/contributing.html) 10 | 11 | - type: checkboxes 12 | id: terms 13 | attributes: 14 | label: Please confirm the following 15 | options: 16 | - label: I agree to follow this project's [code of conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html). 17 | required: true 18 | - label: I have checked the [current issues](https://github.com/ansible/ansible-rulebook/issues) for duplicates. 19 | required: true 20 | - label: I understand that event-driven-ansible collection is open source software provided for free and that I might not receive a timely response. 21 | required: true 22 | 23 | - type: textarea 24 | id: summary 25 | attributes: 26 | label: Bug Summary 27 | description: Briefly describe the problem. 28 | validations: 29 | required: true 30 | 31 | - type: textarea 32 | id: environment 33 | attributes: 34 | label: Environment 35 | description: What is your OS, python, etc? Share the output of `ansible-rulebook --version` command. 36 | validations: 37 | required: true 38 | 39 | - type: textarea 40 | id: steps-to-reproduce 41 | attributes: 42 | label: Steps to reproduce 43 | description: >- 44 | Describe exactly how a developer or quality engineer can reproduce the bug. 45 | Usually the content of the rulebook and examples of the incoming events are useful. 46 | validations: 47 | required: true 48 | 49 | - type: textarea 50 | id: actual-results 51 | attributes: 52 | label: Actual results 53 | description: What actually happened? 54 | validations: 55 | required: true 56 | 57 | - type: textarea 58 | id: expected-results 59 | attributes: 60 | label: Expected results 61 | description: What did you expect to happen when running the steps above? 62 | validations: 63 | required: true 64 | 65 | - type: textarea 66 | id: additional-information 67 | attributes: 68 | label: Additional information 69 | description: Include any other relevant information. 70 | validations: 71 | required: false 72 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: For debugging help or technical support 4 | url: https://ansible-rulebook.readthedocs.io/en/latest/contributing.html 5 | about: For general debugging or technical support please see the linked section of our docs. 6 | 7 | - name: 📝 Ansible Code of Conduct 8 | url: https://docs.ansible.com/ansible/latest/community/code_of_conduct.html?utm_medium=github&utm_source=issue_template_chooser 9 | about: EDA-Controller uses the Ansible Code of Conduct; ❤ Be nice to other members of the community. ☮ Behave. 10 | 11 | - name: 💼 For Enterprise 12 | url: https://www.ansible.com/use-cases/event-driven-automation 13 | about: Red Hat offers support for the Ansible Automation Platform 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ✨ Feature request 3 | description: Suggest an idea for this project 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Feature Request issues are for **feature requests** only. 9 | For debugging help or technical support, please see the [our ways of contact](https://ansible-rulebook.readthedocs.io/en/latest/contributing.html) 10 | 11 | - type: checkboxes 12 | id: terms 13 | attributes: 14 | label: Please confirm the following 15 | options: 16 | - label: I agree to follow this project's [code of conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html). 17 | required: true 18 | - label: I have checked the [current issues](https://github.com/ansible/ansible-rulebook/issues) for duplicates. 19 | required: true 20 | - label: I understand that ansible-rulebook is open source software provided for free and that I might not receive a timely response. 21 | required: true 22 | 23 | - type: dropdown 24 | id: feature-type 25 | attributes: 26 | label: Feature type 27 | description: >- 28 | What kind of feature is this? 29 | multiple: false 30 | options: 31 | - "New Feature" 32 | - "Enhancement to Existing Feature" 33 | validations: 34 | required: true 35 | 36 | - type: textarea 37 | id: summary 38 | attributes: 39 | label: Feature Summary 40 | description: Briefly describe the desired enhancement. 41 | validations: 42 | required: true 43 | 44 | - type: textarea 45 | id: steps-to-reproduce 46 | attributes: 47 | label: Steps to reproduce 48 | description: >- 49 | Describe the necessary steps to understand the scenario of the requested enhancement. 50 | Include all the steps that will help the developer and QE team understand what you are requesting. 51 | validations: 52 | required: true 53 | 54 | - type: textarea 55 | id: current-results 56 | attributes: 57 | label: Current results 58 | description: What is currently happening on the scenario? 59 | validations: 60 | required: true 61 | 62 | - type: textarea 63 | id: suggested-results 64 | attributes: 65 | label: Suggested feature result 66 | description: What is the result this new feature will bring? 67 | validations: 68 | required: true 69 | 70 | - type: textarea 71 | id: additional-information 72 | attributes: 73 | label: Additional information 74 | description: Please provide any other information you think is relevant that could help us understand your feature request. 75 | validations: 76 | required: false 77 | -------------------------------------------------------------------------------- /.github/check-coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | JOBS_PRODUCING_COVERAGE=7 3 | declare -a FOUND_FILES 4 | # IFS=$'\n' FOUND_FILES=( $(find . -name coverage.xml) ) 5 | while IFS='' read -r line; do FOUND_FILES+=("$line"); done < <(find . -name coverage.xml) 6 | 7 | if [ "${#FOUND_FILES[@]}" -ne "${JOBS_PRODUCING_COVERAGE}" ]; then 8 | echo "::error::Broken CI/CD setup, found ${#FOUND_FILES[@]} coverage.xml file(s) instead of expected ${JOBS_PRODUCING_COVERAGE}. Found: ${FOUND_FILES[*]}" 9 | exit 1 10 | fi 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: pip 5 | directory: /.config/ 6 | schedule: 7 | day: sunday 8 | interval: weekly 9 | labels: 10 | - dependabot-deps-updates 11 | - skip-changelog 12 | groups: 13 | dependencies: 14 | patterns: 15 | - "*" 16 | exclude-patterns: 17 | # galaxy-importer ceiling confuses dependabot 18 | - attrs 19 | - bleach 20 | - flake8 21 | ignore: 22 | - dependency-name: attrs 23 | - dependency-name: bleach 24 | - dependency-name: flake8 25 | - package-ecosystem: "github-actions" 26 | directory: "/" 27 | schedule: 28 | interval: daily 29 | labels: 30 | - "dependencies" 31 | - "skip-changelog" 32 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # see https://github.com/ansible/team-devtools 3 | _extends: ansible/team-devtools 4 | -------------------------------------------------------------------------------- /.github/workflows/ack.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # See https://github.com/ansible/team-devtools/blob/main/.github/workflows/ack.yml 3 | name: ack 4 | on: 5 | pull_request_target: 6 | types: [opened, labeled, unlabeled, synchronize] 7 | 8 | permissions: 9 | checks: write 10 | contents: write # needed to update release 11 | pull-requests: write # pr approval and merge 12 | 13 | jobs: 14 | ack: 15 | uses: ansible/team-devtools/.github/workflows/ack.yml@main 16 | secrets: inherit 17 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # See https://github.com/ansible/team-devtools/blob/main/.github/workflows/push.yml 3 | name: push 4 | "on": 5 | push: 6 | branches: 7 | - main 8 | - "releases/**" 9 | - "stable/**" 10 | 11 | jobs: 12 | ack: 13 | uses: ansible/team-devtools/.github/workflows/push.yml@main 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # python 2 | *.pyc 3 | 4 | # venv 5 | venv 6 | .venv/ 7 | 8 | # tests 9 | tests/output 10 | .pytest_cache/ 11 | .tox 12 | .ruff* 13 | tests/integration/event_source_kafka/*.crt 14 | tests/integration/event_source_kafka/*.key 15 | tests/integration/event_source_kafka/*.csr 16 | tests/integration/event_source_kafka/*.jks 17 | tests/integration/integration_config.yml 18 | 19 | # Ide's 20 | .vscode 21 | .idea 22 | 23 | # Builds 24 | *.tar.gz 25 | 26 | # Coverage 27 | .coverage* 28 | *coverage.combined 29 | 30 | # Generated with antsibull-docs 31 | docs/build/ 32 | docs/rst/*.rst 33 | docs/temp-rst 34 | 35 | # Other 36 | tests/integration/inventory 37 | **/.DS_Store 38 | .ansible/ 39 | 40 | # Collection specific 41 | importer_result.json 42 | docs/rst/CHANGELOG.md 43 | CHANGELOG.md 44 | docs/README.md 45 | -------------------------------------------------------------------------------- /.gitleaks.toml: -------------------------------------------------------------------------------- 1 | [allowlist] 2 | description = "Global Allowlist" 3 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the OS, Python version and other tools you might need 9 | build: 10 | os: ubuntu-24.04 11 | tools: 12 | python: "3.12" 13 | commands: 14 | # Copy executable in the first directory in path, which happens to be writable on RTD 15 | - asdf --version 16 | - asdf plugin add github-cli 17 | - asdf install 18 | - git fetch --unshallow || true 19 | - git fetch --tags 20 | - pip install --user tox 21 | - python3 -m tox -e docs 22 | - mkdir -p $READTHEDOCS_OUTPUT 23 | - cp -r docs/build/html ${READTHEDOCS_OUTPUT}/ 24 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | github-cli 2.55.0 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. 4 | Every new feature should be tested and documented. 5 | New source plugins or source filters will be evaluated for inclusion in the collection and might be rejected. Please consider the option of creating a new collection for your plugins if it is not a good fit for this collection. 6 | 7 | If you are new here, read the [Quick-start development guide first](https://docs.ansible.com/ansible/devel/community/create_pr_quick_start.html). 8 | 9 | ## Code of Conduct 10 | 11 | The ansible.eda collection follows the Ansible project's [Code of Conduct](https://docs.ansible.com/ansible/devel/community/code_of_conduct.html). Please read and familiarize yourself with this document. 12 | 13 | ## Submitting Issues 14 | 15 | All software has bugs, and the amazon.aws collection is no exception. When you find a bug, you can help tremendously by telling us [about it](https://github.com/ansible/event-driven-ansible/issues/new/choose). 16 | 17 | If you should discover that the bug you're trying to file already exists in an issue, you can help by verifying the behavior of the reported bug with a comment in that issue, or by reporting any additional information 18 | 19 | ## Writing New Code 20 | 21 | ## Cloning 22 | 23 | Due to ansible-test own requirements, you must clone the repository into 24 | a directory structure such `ansible_collections/ansible/eda`. This will allow 25 | ansible-test to test your code without having to install the collection. 26 | 27 | ## Pre-commit 28 | 29 | We recommend running pre-commit prior to submitting pull requests. A [pre-commit config](.pre-commit-config.yaml) file is included in this repository and the following steps will get you up and running with pre-commit quickly: 30 | 31 | 1. Install pre-commit: 32 | 33 | pip install pre-commit 34 | 35 | 2. Deploy the pre-commit config: 36 | 37 | pre-commit install 38 | 39 | Pre-commit is now set up to run each time you create a new commit. If you wish to run pre-commit against all tracked files in the repository without performing a commit, you can run: 40 | 41 | ```shell 42 | pre-commit run --all 43 | ``` 44 | 45 | # CI testing 46 | 47 | This collection uses GitHub Actions to run Sanity, Integration and Units to validate its content. 48 | 49 | # Adding tests 50 | 51 | When fixing a bug, first reproduce it by adding a task as reported to a suitable file under the tests/integration/targets//tasks/ directory and run the integration tests as described below. The same is relevant for new features. 52 | 53 | It is not necessary but if you want you can also add unit tests to a suitable file under the tests/units/ directory and run them as described below. 54 | 55 | # Checking your code locally 56 | 57 | It will make your and other people's life a bit easier if you run the tests locally and fix all failures before pushing. If you're unable to run the tests locally, please create your PR as a draft to avoid reviewers being added automatically. 58 | 59 | ## Running tests for source plugins 60 | 61 | Running the tests requires `ansible-rulebook` to be installed. Please review the [ansible-rulebook requirements](https://ansible.readthedocs.io/projects/rulebook/en/latest/installation.html#requirements), but do not install `ansible-rulebook` manually. It will be installed via the test requirements. 62 | 63 | We recommend setting up a Python virtual environment to install the test dependencies into: 64 | 65 | 1. Initiate the virtual environment: 66 | 67 | python -m venv venv 68 | source venv/bin/activate 69 | 70 | 2. Export the `JAVA_HOME` environment variable required by `ansible-rulebook`: 71 | 72 | export JAVA_HOME=/usr/lib/jvm/java-17-openjdk 73 | 74 | 3. Install the test requirements: 75 | 76 | pip install -r .config/requirements.in 77 | 78 | ## Sanity and Unit tests 79 | 80 | Sanity and unity tests can easily be run via tox: 81 | 82 | ```shell 83 | tox -e py 84 | ``` 85 | 86 | ## Integration tests 87 | 88 | Integration tests require the addition of [docker](https://docs.docker.com/engine/install/) or [podman](https://podman.io/getting-started/installation). 89 | 90 | Then install the collection directly from your local repo and execute the tests: 91 | 92 | ```shell 93 | tox -e integration 94 | ``` 95 | -------------------------------------------------------------------------------- /bindep.txt: -------------------------------------------------------------------------------- 1 | # ubuntu: 22.04 (default py312, others from deadsnakes) 2 | libsystemd0 [test platform:debian] 3 | libsystemd-dev [test platform:debian] 4 | pkg-config [test platform:debian] 5 | 6 | shellcheck [lint platform:ubuntu-noble] 7 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | --- 2 | comment: false 3 | coverage: 4 | status: 5 | patch: true 6 | project: 7 | default: 8 | threshold: 0.5% 9 | -------------------------------------------------------------------------------- /cspell.config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | dictionaryDefinitions: 3 | - name: words 4 | path: .config/dictionary.txt 5 | addWords: true 6 | dictionaries: 7 | # Use `cspell-cli trace word` to check where a work is defined 8 | - en_US 9 | - bash 10 | - words 11 | - python 12 | ignorePaths: 13 | - cspell.config.yaml 14 | # The requirements file 15 | - requirements.txt 16 | - .config/constraints.txt 17 | - .config/manifest.txt 18 | - docs/requirements.txt 19 | - docs/requirements.in 20 | # Test fixtures generated from outside 21 | - test/**/*.result 22 | - src/ansiblelint/schemas/*.json 23 | # Other 24 | - "*.svg" 25 | # Changelog specific 26 | - changelogs/config.yaml 27 | -------------------------------------------------------------------------------- /demos/dynatrace-demo/fake_app.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from os import environ 3 | 4 | from aiohttp import web 5 | from aiohttp.web_request import Request 6 | from aiohttp.web_response import Response 7 | 8 | routes = web.RouteTableDef() 9 | 10 | 11 | @routes.get("/health") 12 | async def health(request: Request) -> Response: 13 | return web.json_response({"status": "RUNNING"}) 14 | 15 | 16 | @routes.get("/down") 17 | async def down(request: Request) -> Response: 18 | # simulate program crashing 19 | sys.exit(1) 20 | 21 | 22 | if __name__ == "__main__": 23 | app = web.Application() 24 | app.add_routes(routes) 25 | port = int(environ.get("HTTP_PORT", 5080)) 26 | web.run_app(app=app, port=port) 27 | -------------------------------------------------------------------------------- /demos/dynatrace-demo/instructions.md: -------------------------------------------------------------------------------- 1 | # Ansible Rulebook + Dynatrace DEMO 2 | 3 | ## Description 4 | In this demo we will register a host to Dynatrace and set up a webhook to send 5 | problem notifications to ansible-rulebook CLI. Dynatrace monitors the 6 | availability of a process. Upon receiving a problem notification the ansible- 7 | rulebook CLI runs a remedy playbook to restart the process. 8 | 9 | ## Instructions 10 | ### Nodes or instances 11 | * Rulebook Node: The node where the ansible-rulebook CLI is running 12 | * Client Node: The node where the monitored process is running 13 | * Dynatrace Console: An active Dynatrace tenant and its web console 14 | 15 | ### Set up client node 16 | 1. Prepare a VM with host name called `lamp` 17 | 2. Install Dynatrace OneAgent. Host `lamp` then should appear in Dynatrace 18 | console 19 | 3. Have python3 installed 20 | 4. Copy `fake_app.py` under path `/root/fake-app`. Run `python3 fake_app.py`. 21 | `fake_app.py` is a web app with two GET endpoints running on port 5080. 22 | `:5080/health` will return `{"status": "RUNNING"}`. 23 | `:5080/down` will force the app to exit, simulating a process 24 | crashing. 25 | 5. Make the client node ansible accessible from the rulebook node. 26 | 27 | ### Set up the rulebook node 28 | 1. Have ansible-rulebook CLI and its dependencies installed 29 | 2. Have ansible.eda collection installed by ansible-galaxy 30 | 3. Update `inventory.yml` with correct ip and user to access the client node 31 | 4. Start the rulebook CLI: 32 | ```shell 33 | ansible-rulebook -i demos/dynatrace-demo/inventory.yml --rules demos/dynatrace-demo/rulebook.yml 34 | ``` 35 | This rulebook starts an alertmanager source that listens on port 5050 36 | 37 | ### Configure Dynatrace 38 | 1. On the console window go to `Settings/Integrations/Problem notifications`. 39 | Add a new notification with type `Custom Integration`. Set the webhook URL to 40 | `:5050/dynatrace`. Make sure the URL is publicly accessible. 41 | Otherwise use a reverse proxy. 42 | 2. Go to `Settings/Processes and containers/Process availability`. Add a new 43 | monitoring rule. Add a new detection rule with process property = `Command line` 44 | and Condition = `$suffix(fake_app.py)`. Save the monitored rule name as 45 | `pythonmain` 46 | 47 | ### Test 48 | 1. Use a curl or similar command to request `http://:5080/health`. It 49 | should response properly 50 | 2. Make request `http://:5080/down` to shutdown the fake_app 51 | 3. Wait a few minutes and observe Dynatrace console window. The Problems page 52 | should report a problem 53 | 4. The rulebook CLI should respond to the problem and run a playbook 54 | 5. Dynatrace should then change the problem status to `Closed` 55 | 6. Verify `http://:5080/health` is responding again 56 | -------------------------------------------------------------------------------- /demos/dynatrace-demo/inventory.yml: -------------------------------------------------------------------------------- 1 | all: 2 | hosts: 3 | lamp: 4 | ansible_host: 192.168.1.171 5 | ansible_user: root 6 | -------------------------------------------------------------------------------- /demos/dynatrace-demo/playbook.yml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | gather_facts: false 3 | tasks: 4 | - name: Restart main 5 | shell: "(cd /root/fake-app; python3 fake_app.py >/dev/null 2>&1 &)" 6 | async: 10 7 | poll: 0 8 | -------------------------------------------------------------------------------- /demos/dynatrace-demo/rulebook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: dynatrace demo 3 | hosts: all 4 | sources: 5 | - name: alertmanager 6 | ansible.eda.alertmanager: 7 | host: 0.0.0.0 8 | port: 5050 9 | data_alerts_path: 10 | data_host_path: ImpactedEntityNames 11 | skip_original_data: true 12 | filters: 13 | - ansible.eda.json_filter: 14 | exclude_keys: 15 | - ProblemDetailsMarkdown 16 | - ProblemDetailsText 17 | - ProblemDetailsJSON 18 | rules: 19 | - name: restart process 20 | condition: event.alert.ProblemTitle == "No process found for rule pythonmain" and event.alert.State == "OPEN" 21 | action: 22 | run_playbook: 23 | name: demos/dynatrace-demo/playbook.yml 24 | -------------------------------------------------------------------------------- /docs/.changelog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | file_name: rst/CHANGELOG.md 3 | excluded_labels: 4 | - maintenance 5 | - dependencies 6 | - skip-changelog 7 | sections: 8 | added: 9 | - feature 10 | - enhancement 11 | changed: 12 | - backwards-incompatible 13 | fixed: 14 | - bug 15 | - bugfix 16 | - documentation 17 | skip_entries_without_label: false 18 | show_unreleased: true 19 | check_for_updates: true 20 | logger: spinner 21 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Copyright (c) Ansible Project 2 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | # Created with antsibull-docs 2.13.1 6 | 7 | /build 8 | /rst/ 9 | -------------------------------------------------------------------------------- /docs/antsibull-docs.cfg: -------------------------------------------------------------------------------- 1 | # Copyright (c) Ansible Project 2 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | # Created with antsibull-docs 2.13.1 6 | 7 | breadcrumbs = true 8 | indexes = true 9 | use_html_blobs = false 10 | add_antsibull_docs_version = true 11 | 12 | # You can specify ways to convert a collection name (.) to an URL here. 13 | # You can replace either of or by "*" to match all values in that place, 14 | # or use "*" for the collection name to match all collections. In the URL, you can use 15 | # {namespace} and {name} for the two components of the collection name. If you want to use 16 | # "{" or "}" in the URL, write "{{" or "}}" instead. Basically these are Python format 17 | # strings (https://docs.python.org/3.8/library/string.html#formatstrings). 18 | collection_url = { 19 | * = "https://galaxy.ansible.com/ui/repo/published/{namespace}/{name}/" 20 | } 21 | 22 | # The same wildcard rules and formatting rules as for collection_url apply. 23 | collection_install = { 24 | * = "ansible-galaxy collection install {namespace}.{name}" 25 | } 26 | -------------------------------------------------------------------------------- /docs/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | 4 | pushd "$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 5 | trap "{ popd; }" EXIT 6 | 7 | GALAXY_VERSION=$(python -c "import yaml; print(yaml.safe_load(open('../galaxy.yml'))['version'])") 8 | # Determine the collection version, if current commit is tagged, use it. Otherwise, generate pre-release version. 9 | 10 | # this is for inclusion in collection archive (galaxy renders docs/*.md in the web UI) 11 | cp -f ../README.md ./README.md 12 | 13 | # Create collection documentation 14 | mkdir -p rst 15 | chmod og-w rst # antsibull-docs wants that directory only readable by itself 16 | antsibull-docs \ 17 | --config-file antsibull-docs.cfg \ 18 | collection \ 19 | --cleanup everything \ 20 | --fail-on-error \ 21 | --use-current \ 22 | --squash-hierarchy \ 23 | --dest-dir rst \ 24 | ansible.eda 25 | 26 | 27 | # To be included inside the built collection archive 28 | pushd .. 29 | mk -v changelog 30 | cp -f CHANGELOG.md ./docs/CHANGELOG.md 31 | 32 | # that is for our sphinx build site: 33 | cp -f CHANGELOG.md ./docs/rst/changelog.md 34 | popd 35 | 36 | CHANGELOG_VERSION=$(grep -m 1 '^#' rst/changelog.md | awk '/^#/ {print $2; exit}' | sed 's/v//') 37 | 38 | if [[ "${GALAXY_VERSION}" != "${CHANGELOG_VERSION}" ]]; then 39 | echo "WARN: galaxy.yaml collection version ${GALAXY_VERSION} does not match the changelog version ${CHANGELOG_VERSION}. Please update the galaxy.yaml version to match the changelog version." 40 | fi 41 | 42 | cat >rst/_.rst < .config/manifest.txt 82 | sync 83 | 84 | echo "INFO: Restore temporary modified galaxy.yml file." 85 | git checkout HEAD -- galaxy.yml >/dev/null 86 | 87 | git --no-pager diff -U0 --minimal || { 88 | echo "ERROR: Manifest at .config/manifest.txt changed, please update it." 89 | exit 3 90 | } 91 | echo "INFO: Galaxy importer check passed." 92 | 93 | echo "INFO: Determined collection version ${GALAXY_VERSION}" 94 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Ansible Project 2 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | # This file only contains a selection of the most common options. For a full list see the 6 | # documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # cspell: ignore devel nitpicky intersphinx modindex 10 | project = "Event Driven Ansible Collection" 11 | # pylint: disable=redefined-builtin 12 | copyright = "Ansible contributors" 13 | 14 | title = "Event Driven Ansible Collection" 15 | html_short_title = "ansible.eda" 16 | 17 | extensions = [ 18 | "sphinx.ext.autodoc", 19 | "sphinx.ext.intersphinx", 20 | "sphinx_antsibull_ext", 21 | "myst_parser", 22 | ] 23 | source_suffix = [".rst", ".md"] 24 | 25 | pygments_style = "ansible" 26 | 27 | highlight_language = "YAML+Jinja" 28 | 29 | html_theme = "sphinx_ansible_theme" 30 | html_show_sphinx = False 31 | 32 | display_version = False 33 | 34 | html_use_smartypants = True 35 | html_use_modindex = False 36 | html_use_index = False 37 | html_copy_source = False 38 | 39 | # See https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#confval-intersphinx_mapping for the syntax 40 | intersphinx_mapping = { 41 | "python": ("https://docs.python.org/2/", (None, "../python2.inv")), 42 | "python3": ("https://docs.python.org/3/", (None, "../python3.inv")), 43 | "jinja2": ("http://jinja.palletsprojects.com/", (None, "../jinja2.inv")), 44 | "ansible_devel": ( 45 | "https://docs.ansible.com/ansible/devel/", 46 | (None, "../ansible_devel.inv"), 47 | ), 48 | } 49 | 50 | default_role = "any" 51 | 52 | nitpicky = True 53 | -------------------------------------------------------------------------------- /docs/docsite/links.yml: -------------------------------------------------------------------------------- 1 | extra_links: 2 | - description: Changelog 3 | url: changelog.html 4 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_filter/README.md: -------------------------------------------------------------------------------- 1 | 2 | # EDA Event Filters 3 | 4 | Event filters are plugins that filter events once they have been received. 5 | Examples of these plugins are filters that remove unnecessary fields from events, 6 | or change dashes to underscores in key names. 7 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_filter/dashes_to_underscores.py: -------------------------------------------------------------------------------- 1 | import multiprocessing as mp 2 | from typing import Any 3 | 4 | DOCUMENTATION = r""" 5 | --- 6 | short_description: Change dashes to underscores. 7 | description: 8 | - An event filter that changes dashes in keys to underscores. For instance, the key X-Y becomes the new key X_Y. 9 | options: 10 | overwrite: 11 | description: 12 | - Overwrite the values if there is a collision with a new key. 13 | type: bool 14 | default: true 15 | """ 16 | 17 | EXAMPLES = r""" 18 | - ansible.eda.alertmanager: 19 | host: 0.0.0.0 20 | port: 5050 21 | filters: 22 | - ansible.eda.dashes_to_underscores: 23 | overwrite: false 24 | """ 25 | 26 | 27 | def main( 28 | event: dict[str, Any], 29 | overwrite: bool = True, # noqa: FBT001, FBT002 30 | ) -> dict[str, Any]: 31 | """Change dashes in keys to underscores.""" 32 | logger = mp.get_logger() 33 | logger.info("dashes_to_underscores") 34 | queue = [event] 35 | while queue: 36 | obj = queue.pop() 37 | if isinstance(obj, dict): 38 | for key in list(obj.keys()): 39 | value = obj[key] 40 | queue.append(value) 41 | if "-" in key: 42 | new_key = key.replace("-", "_") 43 | del obj[key] 44 | if (new_key in obj and overwrite) or (new_key not in obj): 45 | obj[new_key] = value 46 | logger.info("Replacing %s with %s", key, new_key) 47 | 48 | return event 49 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_filter/insert_hosts_to_meta.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from typing import Any 5 | 6 | import dpath 7 | 8 | DOCUMENTATION = r""" 9 | --- 10 | short_description: Extract hosts from the event data and insert them to the meta dict. 11 | description: 12 | - An ansible-rulebook event filter that extracts hosts from the event data and 13 | inserts them to the meta dict. In ansible-rulebook, this will limit an action 14 | running on hosts in the meta dict. 15 | options: 16 | host_path: 17 | description: 18 | - The json path inside the event data to find hosts. 19 | - Do nothing if the key is not present or does exist in event. 20 | type: str 21 | default: null 22 | path_separator: 23 | description: 24 | - The separator to interpret host_path. 25 | type: str 26 | default: "." 27 | host_separator: 28 | description: 29 | - The separator to interpret host string. 30 | - host_path can point to a string or a list. If it is a single 31 | string but contains multiple hosts, use this parameter to 32 | delimits the hosts. Treat the value as a single host if the 33 | parameter is not present. 34 | type: str 35 | default: null 36 | raise_error: 37 | description: 38 | - Whether raise PathNotExistError if host_path does not 39 | exist in the event. 40 | - It is recommended to turn it on during the rulebook 41 | development time. You can then turn it off for production. 42 | type: bool 43 | default: false 44 | log_error: 45 | description: 46 | - Whether log an error message if host_path does not 47 | exist in the event. 48 | - You can turn if off if it is expected to have events not 49 | having the host_path to avoid noises in the log. 50 | type: bool 51 | default: true 52 | """ 53 | 54 | EXAMPLES = r""" 55 | - ansible.eda.alertmanager: 56 | host: 0.0.0.0 57 | port: 5050 58 | filters: 59 | - ansible.eda.insert_hosts_to_meta: 60 | host_path: "app.target" 61 | path_separator: "." 62 | host_separator: ";" 63 | raise_error: true 64 | log_error: true 65 | """ 66 | 67 | LOGGER = logging.getLogger(__name__) 68 | 69 | 70 | class PathNotExistError(Exception): 71 | """Cannot find the path in the event.""" 72 | 73 | 74 | # pylint: disable=too-many-arguments 75 | def main( 76 | event: dict[str, Any], 77 | host_path: str | None = None, 78 | host_separator: str | None = None, 79 | path_separator: str = ".", 80 | *, 81 | raise_error: bool = False, 82 | log_error: bool = True, 83 | ) -> dict[str, Any]: 84 | """Extract hosts from event data and insert into meta dict.""" 85 | if not host_path: 86 | return event 87 | 88 | try: 89 | hosts = dpath.get(event, host_path, path_separator) 90 | except KeyError as error: 91 | # does not contain host 92 | msg = f"Event {event} does not contain {host_path}" 93 | if log_error: 94 | LOGGER.error(msg) # noqa: TRY400 95 | if raise_error: 96 | raise PathNotExistError(msg) from error 97 | return event 98 | 99 | if isinstance(hosts, str): 100 | hosts = hosts.split(host_separator) if host_separator else [hosts] 101 | elif isinstance(hosts, (list, tuple)): 102 | for host in hosts: 103 | if not isinstance(host, str): 104 | msg = f"{host} is not a valid hostname" 105 | raise TypeError(msg) 106 | else: 107 | msg = f"{hosts} is not a valid hostname" 108 | raise TypeError(msg) 109 | 110 | if "meta" not in event: 111 | event["meta"] = {} 112 | event["meta"]["hosts"] = hosts 113 | return event 114 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_filter/json_filter.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import fnmatch 4 | from typing import Any, Optional 5 | 6 | DOCUMENTATION = r""" 7 | --- 8 | short_description: Filter keys out of events. 9 | description: 10 | - An event filter that filters keys out of events. 11 | Includes override excludes. 12 | This is useful to exclude information from events that is unneeded by the rule engine. 13 | options: 14 | exclude_keys: 15 | description: 16 | - A list of strings or patterns to remove. 17 | type: list 18 | elements: str 19 | default: null 20 | include_keys: 21 | description: 22 | - A list of strings or patterns to keep even if it matches exclude_keys patterns. 23 | type: list 24 | elements: str 25 | default: null 26 | notes: 27 | - The values in both parameters - include_keys and exclude_keys, must be a full path in 28 | top to bottom order to the keys to be filtered (or left to right order if it is given 29 | as a list), as shown in the examples below. 30 | """ 31 | 32 | EXAMPLES = r""" 33 | - ansible.eda.generic: 34 | payload: 35 | key1: 36 | key2: 37 | f_ignore_1: 1 38 | f_ignore_2: 2 39 | key3: 40 | key4: 41 | f_use_1: 42 42 | f_use_2: 45 43 | filters: 44 | - ansible.eda.json_filter: 45 | include_keys: 46 | - key3 47 | - key4 48 | - f_use* 49 | exclude_keys: ['key1', 'key2', 'f_ignore_1'] 50 | """ 51 | 52 | 53 | def _matches_include_keys(include_keys: list[str], string: str) -> bool: 54 | return any(fnmatch.fnmatch(string, pattern) for pattern in include_keys) 55 | 56 | 57 | def _matches_exclude_keys(exclude_keys: list[str], string: str) -> bool: 58 | return any(fnmatch.fnmatch(string, pattern) for pattern in exclude_keys) 59 | 60 | 61 | def main( 62 | event: dict[str, Any], 63 | exclude_keys: Optional[list[str]] = None, # noqa: UP007 64 | include_keys: Optional[list[str]] = None, # noqa: UP007 65 | ) -> dict[str, Any]: 66 | """Filter keys out of events.""" 67 | if exclude_keys is None: 68 | exclude_keys = [] 69 | 70 | if include_keys is None: 71 | include_keys = [] 72 | 73 | queue = [] 74 | queue.append(event) 75 | while queue: 76 | obj = queue.pop() 77 | if isinstance(obj, dict): 78 | for item in list(obj.keys()): 79 | if (item in include_keys) or _matches_include_keys(include_keys, item): 80 | queue.append(obj[item]) 81 | elif (item in exclude_keys) or _matches_exclude_keys( 82 | exclude_keys, 83 | item, 84 | ): 85 | del obj[item] 86 | else: 87 | queue.append(obj[item]) 88 | 89 | return event 90 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_filter/noop.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | DOCUMENTATION = r""" 4 | --- 5 | short_description: Do nothing. 6 | description: 7 | - An event filter that does nothing to the input. 8 | """ 9 | 10 | 11 | def main(event: dict[str, Any]) -> dict[str, Any]: 12 | """Return the input.""" 13 | return event 14 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_filter/normalize_keys.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import multiprocessing as mp 3 | import re 4 | from typing import Any 5 | 6 | DOCUMENTATION = r""" 7 | --- 8 | short_description: Change keys that contain non-alpha numeric or underscore to underscores. 9 | description: | 10 | - An event filter that changes keys that contain non alpha numeric or 11 | underscore to underscores. 12 | - For instance, the key server-name becomes the new key server_name. 13 | If there are consecutive non alpha numeric or under score, they would 14 | be coalesced into a single underscore. 15 | For instance the key server.com/&abc becomes server_com_abc 16 | instead of server_com__abc. 17 | - If there is a existing key with the normalized name, it will get overwritten 18 | by default. If you don't want to over write it you can pass in "overwrite: false" 19 | The default value of overwrite is true. 20 | options: 21 | overwrite: 22 | description: 23 | - Overwrite the values if there is a collision with a new key. 24 | type: bool 25 | default: true 26 | """ 27 | 28 | EXAMPLES = r""" 29 | - ansible.eda.alertmanager: 30 | host: 0.0.0.0 31 | port: 5050 32 | filters: 33 | ansible.eda.normalize_keys: 34 | 35 | - ansible.eda.alertmanager: 36 | host: 0.0.0.0 37 | port: 5050 38 | filters: 39 | ansible.eda.normalize_keys: 40 | overwrite: false 41 | """ 42 | 43 | normalize_regex = re.compile("[^0-9a-zA-Z_]+") 44 | 45 | 46 | def main( 47 | event: dict[str, Any], 48 | overwrite: bool = True, # noqa: FBT001, FBT002 49 | ) -> dict[str, Any]: 50 | """Change keys that contain non-alphanumeric characters to underscores.""" 51 | logger = mp.get_logger() 52 | logger.info("normalize_keys") 53 | return _normalize_embedded_keys(event, overwrite, logger) 54 | 55 | 56 | def _normalize_embedded_keys( 57 | obj: dict[str, Any], 58 | overwrite: bool, # noqa: FBT001 59 | logger: logging.Logger, 60 | ) -> dict[str, Any]: 61 | if isinstance(obj, dict): 62 | new_dict = {} 63 | original_keys = list(obj.keys()) 64 | for key in original_keys: 65 | new_key = normalize_regex.sub("_", key) 66 | if new_key == key or new_key not in original_keys: 67 | new_dict[new_key] = _normalize_embedded_keys( 68 | obj[key], 69 | overwrite, 70 | logger, 71 | ) 72 | elif new_key in original_keys and overwrite: 73 | new_dict[new_key] = _normalize_embedded_keys( 74 | obj[key], 75 | overwrite, 76 | logger, 77 | ) 78 | logger.warning("Replacing existing key %s", new_key) 79 | return new_dict 80 | if isinstance(obj, list): 81 | return [_normalize_embedded_keys(item, overwrite, logger) for item in obj] 82 | return obj 83 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_source/README.md: -------------------------------------------------------------------------------- 1 | 2 | # EDA Event Source Plugin 3 | 4 | Event source plugins describe the source of an event stream. Event sources are the 5 | main plugin that produces events for EDA. 6 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_source/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/event-driven-ansible/fc4e83305b1c1ae32c9374f5bb7aec6876d9733f/extensions/eda/plugins/event_source/__init__.py -------------------------------------------------------------------------------- /extensions/eda/plugins/event_source/azure_service_bus.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import concurrent.futures 3 | import contextlib 4 | import json 5 | from typing import Any 6 | 7 | from azure.servicebus import ServiceBusClient 8 | 9 | DOCUMENTATION = r""" 10 | --- 11 | short_description: Receive events from an Azure service bus. 12 | description: 13 | - An ansible-rulebook event source module for receiving events from an Azure service bus. 14 | - In order to get the service bus and the connection string, refer to 15 | https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-python-how-to-use-queues?tabs=passwordless 16 | options: 17 | conn_str: 18 | description: 19 | - The connection string to connect to the Azure service bus. 20 | type: str 21 | required: true 22 | queue_name: 23 | description: 24 | - The name of the queue to pull messages from. 25 | type: str 26 | required: true 27 | logging_enable: 28 | description: 29 | - Whether to turn on logging. 30 | type: bool 31 | default: true 32 | """ 33 | 34 | EXAMPLES = r""" 35 | - ansible.eda.azure_service_bus: 36 | conn_str: "{{connection_str}}" 37 | queue_name: "{{queue_name}}" 38 | """ 39 | 40 | 41 | def receive_events( 42 | loop: asyncio.events.AbstractEventLoop, 43 | queue: asyncio.Queue[Any], 44 | args: dict[str, Any], # pylint: disable=W0621 45 | ) -> None: 46 | """Receive events from service bus.""" 47 | servicebus_client = ServiceBusClient.from_connection_string( 48 | conn_str=args["conn_str"], 49 | logging_enable=bool(args.get("logging_enable", True)), 50 | ) 51 | 52 | with servicebus_client: 53 | receiver = servicebus_client.get_queue_receiver(queue_name=args["queue_name"]) 54 | with receiver: 55 | for msg in receiver: 56 | meta = {"message_id": msg.message_id} 57 | body = str(msg) 58 | with contextlib.suppress(json.JSONDecodeError): 59 | body = json.loads(body) 60 | 61 | loop.call_soon_threadsafe( 62 | queue.put_nowait, 63 | {"body": body, "meta": meta}, 64 | ) 65 | receiver.complete_message(msg) 66 | 67 | 68 | async def main( 69 | queue: asyncio.Queue[Any], 70 | args: dict[str, Any], # pylint: disable=W0621 71 | ) -> None: 72 | """Receive events from service bus in a loop.""" 73 | loop = asyncio.get_running_loop() 74 | 75 | with concurrent.futures.ThreadPoolExecutor(max_workers=1) as task_pool: 76 | await loop.run_in_executor(task_pool, receive_events, loop, queue, args) 77 | 78 | 79 | if __name__ == "__main__": 80 | """MockQueue if running directly.""" 81 | 82 | class MockQueue(asyncio.Queue[Any]): 83 | """A fake queue.""" 84 | 85 | def put_nowait(self: "MockQueue", event: dict[str, Any]) -> None: 86 | """Print the event.""" 87 | print(event) # noqa: T201 88 | 89 | args = { 90 | "conn_str": "Endpoint=sb://foo.servicebus.windows.net/", 91 | "queue_name": "eda-queue", 92 | } 93 | asyncio.run(main(MockQueue(), args)) 94 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_source/file.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from asyncio import Queue 3 | from typing import Any, Union 4 | 5 | import yaml 6 | from watchdog.events import FileSystemEvent, RegexMatchingEventHandler 7 | from watchdog.observers import Observer 8 | 9 | DOCUMENTATION = r""" 10 | --- 11 | short_description: Load facts from YAML files initially and when the file changes. 12 | description: 13 | - An ansible-rulebook event source plugin for loading facts from YAML files 14 | initially and when the file changes. 15 | options: 16 | files: 17 | description: 18 | - A list of file paths pointing to YAML files. 19 | type: list 20 | elements: str 21 | """ 22 | 23 | EXAMPLES = r""" 24 | - ansible.eda.file: 25 | files: 26 | - /path/to/fact.yml 27 | """ 28 | 29 | 30 | def send_facts(queue: Queue[Any], filename: Union[str, bytes]) -> None: 31 | """Send facts to the queue.""" 32 | if isinstance(filename, bytes): 33 | filename = str(filename, "utf-8") 34 | with pathlib.Path(filename).open(encoding="utf-8") as file: 35 | data = yaml.safe_load(file.read()) 36 | if data is None: 37 | return 38 | if isinstance(data, dict): 39 | # pylint: disable=unused-variable 40 | coroutine = queue.put(data) 41 | else: 42 | if not isinstance(data, list): 43 | msg = ( 44 | "Unsupported facts type, expects a list of dicts found " 45 | f"{type(data)}" 46 | ) 47 | raise TypeError(msg) 48 | if not all(bool(isinstance(item, dict)) for item in data): 49 | msg = f"Unsupported facts type, expects a list of dicts found {data}" 50 | raise TypeError(msg) 51 | for item in data: 52 | # pylint: disable=unused-variable 53 | coroutine = queue.put(item) # noqa: F841 54 | 55 | 56 | def main(queue: Queue[Any], args: dict[str, Any]) -> None: 57 | """Load facts from YAML files initially and when the file changes.""" 58 | files = [pathlib.Path(f).resolve().as_posix() for f in args.get("files", [])] 59 | 60 | if not files: 61 | return 62 | 63 | for filename in files: 64 | send_facts(queue, filename) 65 | _observe_files(queue, files) 66 | 67 | 68 | def _observe_files(queue: Queue[Any], files: list[str]) -> None: 69 | class Handler(RegexMatchingEventHandler): 70 | """A handler for file events.""" 71 | 72 | def __init__(self, **kwargs: Any) -> None: # noqa: ANN401 73 | RegexMatchingEventHandler.__init__(self, **kwargs) 74 | 75 | def on_created(self, event: FileSystemEvent) -> None: 76 | if event.src_path in files: 77 | send_facts(queue, event.src_path) 78 | 79 | def on_deleted(self: "Handler", event: FileSystemEvent) -> None: 80 | pass 81 | 82 | def on_modified(self: "Handler", event: FileSystemEvent) -> None: 83 | if event.src_path in files: 84 | send_facts(queue, event.src_path) 85 | 86 | def on_moved(self: "Handler", event: FileSystemEvent) -> None: 87 | pass 88 | 89 | observer = Observer() 90 | handler = Handler() 91 | 92 | for filename in files: 93 | directory = pathlib.Path(filename).parent 94 | observer.schedule(handler, directory.absolute().as_posix(), recursive=False) 95 | 96 | observer.start() 97 | 98 | try: 99 | observer.join() 100 | finally: 101 | observer.stop() 102 | 103 | 104 | if __name__ == "__main__": 105 | """MockQueue if running directly.""" 106 | 107 | class MockQueue(Queue[Any]): 108 | """A fake queue.""" 109 | 110 | async def put(self: "MockQueue", event: dict[str, Any]) -> None: 111 | """Print the event.""" 112 | print(event) # noqa: T201 113 | 114 | main(MockQueue(), {"files": ["facts.yml"]}) 115 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_source/journald.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Any 3 | 4 | # https://github.com/pylint-dev/pylint/issues/7240 5 | # Also systemd-python fails to install on pre-commit.ci due to: 6 | # No such file or directory: 'pkg-config' 7 | # pylint: disable=import-error 8 | from systemd import journal # type: ignore # noqa: PGH003 9 | 10 | DOCUMENTATION = r""" 11 | --- 12 | short_description: Tail systemd journald logs. 13 | description: 14 | - An ansible-rulebook event source plugin that tails systemd journald logs. 15 | options: 16 | match: 17 | description: 18 | - A string used to match messages and return them, see 19 | https://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html. 20 | type: str 21 | required: true 22 | delay: 23 | description: 24 | - The delay (in seconds) between messages. 25 | type: int 26 | default: 0 27 | """ 28 | 29 | EXAMPLES = r""" 30 | - name: Return severity 6 messages 31 | ansible.eda.journald: 32 | match: "PRIORITY=6" 33 | 34 | - name: Return messages when sudo is used 35 | ansible.eda.journald: 36 | match: "_EXE=/usr/bin/sudo" 37 | 38 | - name: Return all messages 39 | ansible.eda.journald: 40 | match: "ALL" 41 | """ 42 | 43 | 44 | async def main(queue: asyncio.Queue[Any], args: dict[str, Any]) -> None: 45 | """Read journal entries and add them to the provided queue. 46 | 47 | Args: 48 | ---- 49 | queue (asyncio.Queue): The queue to which journal entries will be added. 50 | args (dict[str, Any]): Additional arguments: 51 | - delay (int): The delay in seconds. Defaults to 0. 52 | - match (str): A string to match. 53 | 54 | Returns: 55 | ------- 56 | None 57 | 58 | """ 59 | delay = args.get("delay", 0) 60 | match = args.get("match", []) 61 | 62 | if not match: 63 | return 64 | 65 | journal_stream = journal.Reader() 66 | journal_stream.seek_tail() 67 | 68 | while True: 69 | if match != "ALL": 70 | journal_stream.add_match(match) 71 | for entry in journal_stream: 72 | stream_dict = {} 73 | for field in entry: 74 | stream_dict[field.lower()] = f"{entry[field]}" 75 | 76 | await queue.put({"journald": stream_dict}) 77 | await asyncio.sleep(delay) 78 | 79 | stream_dict.clear() 80 | 81 | 82 | if __name__ == "__main__": 83 | """ 84 | Entry point of the program. 85 | """ 86 | 87 | class MockQueue(asyncio.Queue[Any]): 88 | """A mock implementation of a queue that prints the event.""" 89 | 90 | async def put(self, event: str) -> None: 91 | """Add the event to the queue and print it.""" 92 | print(event) # noqa: T201 93 | 94 | asyncio.run(main(MockQueue(), {"match": "ALL"})) 95 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_source/range.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Any 3 | 4 | DOCUMENTATION = r""" 5 | --- 6 | short_description: Generate events with an increasing index i. 7 | description: 8 | - An ansible-rulebook event source plugin for generating events with an increasing index i. 9 | options: 10 | limit: 11 | description: 12 | - The upper limit of the range of the index. 13 | type: int 14 | default: 0 15 | """ 16 | 17 | EXAMPLES = r""" 18 | - ansible.eda.range: 19 | limit: 5 20 | """ 21 | 22 | 23 | async def main(queue: asyncio.Queue[Any], args: dict[str, Any]) -> None: 24 | """Generate events with an increasing index i with a limit.""" 25 | delay = args.get("delay", 0) 26 | 27 | for i in range(int(args["limit"])): 28 | await queue.put({"i": i}) 29 | await asyncio.sleep(delay) 30 | 31 | 32 | if __name__ == "__main__": 33 | # MockQueue if running directly 34 | 35 | class MockQueue(asyncio.Queue[Any]): 36 | """A fake queue.""" 37 | 38 | async def put(self: "MockQueue", event: dict[str, Any]) -> None: 39 | """Print the event.""" 40 | print(event) # noqa: T201 41 | 42 | asyncio.run(main(MockQueue(), {"limit": 5})) 43 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_source/schemas/alertmanager.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://redhat.com/ansible_events/sources/alertmanager.json", 4 | "title": "Alert manager Source Plugin", 5 | "description": "A plugin for Alert Manager", 6 | "type": "object", 7 | "properties": { 8 | "host": { 9 | "description": "The webserver hostname to listen to. Set to 0.0.0.0 to listen on all interfaces. Defaults to 127.0.0.1", 10 | "title": "Host", 11 | "type": "string", 12 | "default": "127.0.0.1" 13 | }, 14 | "port": { 15 | "description": "The TCP port to listen to. Defaults to 5000", 16 | "title": "Port", 17 | "type": "integer", 18 | "default": 5000 19 | }, 20 | "data_alerts_path": { 21 | "description": "The json path to find alert data. Default to alerts Use empty string to treat the whole payload data as one alert.", 22 | "title": "Alerts Path", 23 | "type": "string", 24 | "default": "alerts" 25 | }, 26 | "data_host_path": { 27 | "description": "The json path inside the alert data to find alerting host. Use empty string if there is no need to find host. Default to labels.instance.", 28 | "title": "Host Path", 29 | "type": "string", 30 | "default": "labels.instance" 31 | }, 32 | "data_path_separator": { 33 | "description": "The separator to interpret data_host_path and data_alerts_path. Default is . (dot or period)", 34 | "title": "Path Separator", 35 | "type": "string", 36 | "default": "." 37 | }, 38 | "skip_original_data": { 39 | "description": "If enabled only the alert data will be put in queue, else put sequentially both the received original data and each parsed alert item to the queue.", 40 | "title": "Skip Original Data", 41 | "type": "boolean", 42 | "default": false 43 | } 44 | }, 45 | "required": [ 46 | "host", 47 | "port" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_source/schemas/aws_cloudtrail.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://redhat.com/ansible_events/sources/aws_cloudtrail.json", 4 | "title": "Source Plugin for AWS CloudTrail", 5 | "description": "An ansible-rulebook event source module for getting events from an AWS CloudTrail", 6 | "type": "object", 7 | "properties": { 8 | "access_key": { 9 | "description": "AWS access key ID", 10 | "title": "Access Key", 11 | "type": "string", 12 | "format": "password" 13 | }, 14 | "secret_key": { 15 | "description": "AWS secret key", 16 | "title": "Secret Key", 17 | "type": "string", 18 | "format": "password" 19 | }, 20 | "session_token": { 21 | "description": "STS session token for use with temporary credentials", 22 | "title": "Session Token", 23 | "type": "string", 24 | "format": "password" 25 | }, 26 | "endpoint_url": { 27 | "description": "URL to connect to instead of the default AWS endpoints", 28 | "title": "Endpoint URL", 29 | "type": "string" 30 | }, 31 | "region": { 32 | "description": "AWS region to use", 33 | "title": "Region", 34 | "type": "string" 35 | }, 36 | "delay_seconds": { 37 | "description": "The number of seconds to wait between polling", 38 | "title": "Poll Delay", 39 | "type": "integer", 40 | "default": 10 41 | }, 42 | "lookup_attributes": { 43 | "description": "Lookup attributes", 44 | "title": "Filters", 45 | "type": "array", 46 | "items": { 47 | "$ref": "#/$defs/lookup" 48 | } 49 | }, 50 | "event_category": { 51 | "description": "Event Category", 52 | "title": "Event Category", 53 | "type": "string" 54 | } 55 | }, 56 | "$defs": { 57 | "lookup": { 58 | "type": "object", 59 | "required": [ 60 | "AttributeKey", 61 | "AttributeValue" 62 | ], 63 | "properties": { 64 | "AttributeKey": { 65 | "type": "string", 66 | "title": "Key", 67 | "description": "Specifies an attribute on which to filter the events" 68 | }, 69 | "AttributeValue": { 70 | "type": "string", 71 | "title": "Value", 72 | "description": "Specifies a value for the specified AttributeKey" 73 | } 74 | } 75 | } 76 | }, 77 | "required": [ 78 | "lookup_attributes", 79 | "event_category" 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_source/schemas/aws_sqs_queue.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://redhat.com/ansible_events/sources/aws_sqs_queue.json", 4 | "title": "Source Plugin for AWS SQS Queue", 5 | "description": "An ansible-rulebook event source plugin for receiving events via an AWS SQS queue.", 6 | "type": "object", 7 | "properties": { 8 | "access_key": { 9 | "description": "AWS access key ID", 10 | "type": "string", 11 | "title": "Access Key", 12 | "format": "password" 13 | }, 14 | "secret_key": { 15 | "description": "AWS secret key", 16 | "type": "string", 17 | "title": "Secret Key", 18 | "format": "password" 19 | }, 20 | "session_token": { 21 | "description": "STS session token for use with temporary credentials", 22 | "title": "Session Token", 23 | "type": "string", 24 | "format": "password" 25 | }, 26 | "endpoint_url": { 27 | "description": "URL to connect to instead of the default AWS endpoints", 28 | "type": "string", 29 | "title": "End Point URL" 30 | }, 31 | "region": { 32 | "description": "AWS region to use", 33 | "type": "string", 34 | "title": "Region" 35 | }, 36 | "name": { 37 | "description": "The name of the queue", 38 | "type": "string", 39 | "title": "Queue Name" 40 | }, 41 | "delay_seconds": { 42 | "description": "The SQS long polling duration. Set to 0 to disable", 43 | "title": "Polling Interval", 44 | "type": "integer", 45 | "default": 2 46 | } 47 | }, 48 | "required": [ 49 | "name" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_source/schemas/azure_service_bus.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://redhat.com/ansible_events/sources/azure_service_bus.json", 4 | "title": "Azure Service Bus plugin for EDA", 5 | "description": "An ansible-rulebook event source module for receiving events from an Azure service bus", 6 | "type": "object", 7 | "properties": { 8 | "conn_str": { 9 | "description": "The connection string", 10 | "type": "string", 11 | "title": "Connection String" 12 | }, 13 | "queue_name": { 14 | "description": "The queue name", 15 | "type": "string", 16 | "title": "Queue Name" 17 | }, 18 | "logging_enable": { 19 | "description": "Turn on logging", 20 | "type": "boolean", 21 | "default": true, 22 | "title": "Enable Logging" 23 | } 24 | }, 25 | "required": [ 26 | "conn_str", 27 | "queue_name" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_source/schemas/file.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://redhat.com/ansible_events/sources/file.json", 4 | "title": "YAML File monitor plugin for EDA", 5 | "description": "An ansible-rulebook event source plugin for loading facts from YAML files initially and when the file changes.", 6 | "type": "object", 7 | "properties": { 8 | "files": { 9 | "description": "An array of YAML files to monitor", 10 | "title": "YAML Files", 11 | "type": "array", 12 | "items": { 13 | "type": "string" 14 | } 15 | } 16 | }, 17 | "required": [ 18 | "files" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_source/schemas/file_watch.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://redhat.com/ansible_events/sources/file_watch.json", 4 | "title": "File Watcher Source plugin", 5 | "description": "An ansible-rulebook event source plugin for watching file system changes.", 6 | "type": "object", 7 | "properties": { 8 | "path": { 9 | "description": "The directory to watch for changes.", 10 | "type": "string", 11 | "title": "Path" 12 | }, 13 | "recursive": { 14 | "description": "Recursively watch the path if true", 15 | "type": "boolean", 16 | "title": "Recursive" 17 | }, 18 | "ignore_regexes": { 19 | "description": "A list of regular expressions to ignore changes", 20 | "title": "Ignore Regexes", 21 | "type": "array", 22 | "items": { 23 | "type": "string" 24 | } 25 | } 26 | }, 27 | "required": [ 28 | "path", 29 | "recursive" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_source/schemas/journald.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://redhat.com/ansible_events/sources/journald.json", 4 | "title": "A journald source plugin", 5 | "description": "An ansible-events event source plugin that tails systemd journald logs.", 6 | "type": "object", 7 | "properties": { 8 | "match": { 9 | "description": "Events that matches, see see https://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html ALL matches all events", 10 | "title": "Match", 11 | "type": "string", 12 | "examples": [ 13 | "PRIORITY=6", 14 | "_EXE=/usr/bin/sudo", 15 | "ALL" 16 | ] 17 | } 18 | }, 19 | "required": [ 20 | "match" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_source/schemas/pg_listener.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://redhat.com/ansible_events/sources/pg_listener.json", 4 | "title": "Postgres Listener Source Plugin", 5 | "description": "An event source plugin for reading events from Postgres Notify/Listen", 6 | "type": "object", 7 | "properties": { 8 | "dsn": { 9 | "description": "The connection string Data Source Name", 10 | "type": "string", 11 | "title": "Data Source Name" 12 | }, 13 | "channels": { 14 | "description": "The channels to listen on", 15 | "title": "Channels", 16 | "type": "array", 17 | "items": { 18 | "type": "string" 19 | } 20 | } 21 | }, 22 | "required": [ 23 | "dsn", 24 | "channels" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_source/schemas/range.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://redhat.com/ansible_events/sources/range.json", 4 | "title": "Event Source Range Plugin", 5 | "description": "A simple plugin to generate integer values given a limit", 6 | "type": "object", 7 | "properties": { 8 | "limit": { 9 | "description": "The max integer value", 10 | "title": "Limit", 11 | "type": "integer" 12 | }, 13 | "delay": { 14 | "description": "The number of seconds to wait between events", 15 | "title": "Delay", 16 | "type": "integer" 17 | } 18 | }, 19 | "required": [ 20 | "limit" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_source/schemas/url_check.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://redhat.com/ansible_events/sources/url_check.json", 4 | "title": "URL Checker source plugin", 5 | "description": "An ansible-rulebook event source plugin that polls a set of URLs and sends events with their status.", 6 | "type": "object", 7 | "properties": { 8 | "urls": { 9 | "description": "A list of URL's to poll", 10 | "title": "URLs", 11 | "type": "array", 12 | "items": { 13 | "type": "string" 14 | } 15 | }, 16 | "delay": { 17 | "description": "Number of seconds to wait between polling", 18 | "title": "Delay", 19 | "type": "integer", 20 | "default": 1 21 | }, 22 | "verify_ssl": { 23 | "description": "Verify SSL Certificate", 24 | "title": "Verify Server Certificate", 25 | "type": "boolean", 26 | "default": true 27 | } 28 | }, 29 | "required": [ 30 | "urls" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_source/schemas/webhook.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://redhat.com/ansible_events/sources/webhook.json", 4 | "title": "Webhook Source Plugin", 5 | "description": "Webhook plugins can receive events from external servers", 6 | "type": "object", 7 | "properties": { 8 | "port": { 9 | "description": "The port number to listen for incoming events", 10 | "type": "integer", 11 | "title": "Port" 12 | }, 13 | "host": { 14 | "description": "The host address to listen on, default is 0.0.0.0", 15 | "type": "string", 16 | "title": "Host", 17 | "default": "0.0.0.0" 18 | }, 19 | "token": { 20 | "description": "Authentication token in header", 21 | "type": "string", 22 | "title": "Authentication Token", 23 | "default": "" 24 | }, 25 | "certfile": { 26 | "description": "The certfile to use", 27 | "type": "string", 28 | "title": "Certificate File", 29 | "default": "" 30 | }, 31 | "keyfile": { 32 | "description": "The keyfile to use", 33 | "type": "string", 34 | "title": "Key File", 35 | "default": "" 36 | }, 37 | "password": { 38 | "description": "The password to use", 39 | "type": "string", 40 | "title": "Password", 41 | "default": "" 42 | }, 43 | "hmac_secret": { 44 | "description": "The HMAC Secret to use", 45 | "type": "string", 46 | "title": "HMAC Secret", 47 | "default": "" 48 | }, 49 | "hmac_algo": { 50 | "description": "The HMAC Algorithm to use", 51 | "type": "string", 52 | "title": "HMAC Algorithm", 53 | "default": "sha256" 54 | }, 55 | "hmac_header": { 56 | "description": "The HTTP header which will contain the payload signature", 57 | "type": "string", 58 | "title": "HMAC Header", 59 | "default": "x-hub-signature-256" 60 | }, 61 | "hmac_format": { 62 | "description": "The format of the payload signature, hex or base64", 63 | "type": "string", 64 | "enum": [ 65 | "hex", 66 | "base64" 67 | ], 68 | "title": "HMAC Format", 69 | "default": "hex" 70 | } 71 | }, 72 | "required": [ 73 | "port" 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_source/tick.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import itertools 3 | from typing import Any 4 | 5 | DOCUMENTATION = r""" 6 | --- 7 | short_description: Generate events with an increasing index i that never ends. 8 | description: 9 | - An ansible-rulebook event source plugin for generating events with an increasing index i that never ends. 10 | options: 11 | delay: 12 | description: 13 | - The delay (in seconds) between ticks. 14 | type: int 15 | default: 1 16 | """ 17 | 18 | EXAMPLES = r""" 19 | - ansible.eda.tick: 20 | delay: 5 21 | """ 22 | 23 | 24 | async def main(queue: asyncio.Queue[Any], args: dict[str, Any]) -> None: 25 | """Generate events with an increasing index i and a time between ticks.""" 26 | delay = args.get("delay", 1) 27 | 28 | for i in itertools.count(start=1): 29 | await queue.put({"i": i}) 30 | await asyncio.sleep(delay) 31 | 32 | 33 | if __name__ == "__main__": 34 | """MockQueue if running directly.""" 35 | 36 | class MockQueue(asyncio.Queue[Any]): 37 | """A fake queue.""" 38 | 39 | async def put(self: "MockQueue", event: dict[str, Any]) -> None: 40 | """Print the event.""" 41 | print(event) # noqa: T201 42 | 43 | asyncio.run(main(MockQueue(), {"delay": 1})) 44 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_source/url_check.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Any 3 | 4 | import aiohttp 5 | 6 | DOCUMENTATION = r""" 7 | --- 8 | short_description: Poll a set of URLs and sends events with their status. 9 | description: 10 | - An ansible-rulebook event source plugin that polls a set of URLs and sends events with their status. 11 | options: 12 | urls: 13 | description: 14 | - A list of urls to poll. 15 | type: list 16 | elements: str 17 | required: true 18 | delay: 19 | description: 20 | - The delay (in seconds) between polling. 21 | type: int 22 | default: 1 23 | verify_ssl: 24 | description: 25 | - Verify SSL certificate. 26 | type: bool 27 | default: true 28 | """ 29 | 30 | EXAMPLES = r""" 31 | - ansible.eda.url_check: 32 | urls: 33 | - http://44.201.5.56:8000/docs 34 | delay: 10 35 | """ 36 | 37 | OK = 200 38 | 39 | 40 | async def main(queue: asyncio.Queue[Any], args: dict[str, Any]) -> None: 41 | """Poll a set of URLs and send events with status.""" 42 | urls = args.get("urls", []) 43 | delay = int(args.get("delay", 1)) 44 | verify_ssl = args.get("verify_ssl", True) 45 | 46 | if not urls: 47 | return 48 | 49 | while True: 50 | for url in urls: 51 | try: 52 | async with aiohttp.ClientSession() as session: # noqa: SIM117 53 | async with session.get(url, verify_ssl=verify_ssl) as resp: 54 | await queue.put( 55 | { 56 | "url_check": { 57 | "url": url, 58 | "status": "up" if resp.status == OK else "down", 59 | "status_code": resp.status, 60 | }, 61 | }, 62 | ) 63 | 64 | except aiohttp.ClientError as exc: # noqa: PERF203 65 | client_error = str(exc) 66 | await queue.put( 67 | { 68 | "url_check": { 69 | "url": url, 70 | "status": "down", 71 | "error_msg": client_error, 72 | }, 73 | }, 74 | ) 75 | 76 | await asyncio.sleep(delay) 77 | 78 | 79 | if __name__ == "__main__": 80 | """MockQueue if running directly.""" 81 | 82 | class MockQueue(asyncio.Queue[Any]): 83 | """A fake queue.""" 84 | 85 | async def put(self: "MockQueue", event: dict[str, Any]) -> None: 86 | """Print the event.""" 87 | print(event) # noqa: T201 88 | 89 | asyncio.run(main(MockQueue(), {"urls": ["http://redhat.com"]})) 90 | -------------------------------------------------------------------------------- /extensions/eda/rulebooks/demo_controller_rulebook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Controller Demo 3 | hosts: all 4 | sources: 5 | - ansible.eda.range: 6 | limit: 5 7 | rules: 8 | - name: Controller Rule 9 | condition: event.i == 1 10 | action: 11 | run_job_template: 12 | name: Demo Job Template 13 | organization: Default 14 | ... 15 | -------------------------------------------------------------------------------- /extensions/eda/rulebooks/demo_rulebook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This rulebook is for demo purposes only 3 | # Typically the events come from an external source 4 | # and you might have to configure the external source 5 | # and have it generate events on demand. 6 | # In this simple rulebook the events to be generated 7 | # is directly defined in the source definition using 8 | # the ansible.eda.generic source plugin which defines 9 | # a collection of events under the payload key. 10 | # 11 | - name: Simple Rulebook for Demo 12 | hosts: all 13 | sources: 14 | - name: Demo Source 15 | ansible.eda.generic: 16 | payload: 17 | - msg: "Welcome" 18 | - msg: "Adios" 19 | rules: 20 | - name: Say Hello 21 | condition: event.msg == "Welcome" 22 | action: 23 | debug: 24 | msg: "Hello from EDA" 25 | ... 26 | -------------------------------------------------------------------------------- /extensions/eda/rulebooks/demo_webhook_rulebook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This rulebook is for demo purposes only 3 | # This webhook listens on port 5555 by default, 4 | # you can override it by passing in an variable 5 | # called port. If you have other webhook sources 6 | # running on the default port 5555 you might see 7 | # port conflicts. 8 | # It logs every event received so it should work 9 | # with any vendors payload. 10 | # It doesn't use authentication to keep things simple 11 | # 12 | - name: Webhook demo prints all incoming events 13 | hosts: all 14 | sources: 15 | - ansible.eda.webhook: 16 | port: "{{ port | default(5555) }}" 17 | host: 0.0.0.0 18 | rules: 19 | - name: Webhook rule 20 | condition: true 21 | action: 22 | print_event: 23 | pretty: true 24 | ... 25 | -------------------------------------------------------------------------------- /extensions/eda/rulebooks/git-hook-deploy-rules.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Local Deploy Git Hook Rules 3 | hosts: all 4 | sources: 5 | - ansible.eda.webhook: 6 | port: 5001 7 | rules: 8 | - name: run tests 1 9 | condition: event.payload.repo == vars.repo 10 | action: 11 | run_playbook: 12 | name: ansible.eda.continuous_integration 13 | var_root: payload 14 | post_events: true 15 | - name: print output 16 | condition: event.output is defined 17 | action: 18 | print_event: 19 | var_root: output 20 | ... 21 | -------------------------------------------------------------------------------- /extensions/eda/rulebooks/git-hook-test-rules.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Local Test Runner Git Hook Rules 3 | hosts: all 4 | sources: 5 | - ansible.eda.webhook: 6 | rules: 7 | - name: run tests 1 8 | condition: event.payload.src_path == vars.src_path 9 | action: 10 | run_playbook: 11 | name: ansible.eda.run_pytest 12 | var_root: payload 13 | post_events: true 14 | - name: print output 15 | condition: event.output is defined 16 | action: 17 | print_event: 18 | var_root: output 19 | ... 20 | -------------------------------------------------------------------------------- /extensions/eda/rulebooks/github-ci-cd-rules.yml: -------------------------------------------------------------------------------- 1 | - name: GitHub Deploy Git Hook Rules 2 | hosts: all 3 | sources: 4 | - ansible.eda.azure_service_bus: 5 | conn_str: "{{connection_str}}" 6 | queue_name: "{{queue_name}}" 7 | filters: 8 | - benthomasosn.eda.json_filter: 9 | include_keys: ['clone_url'] 10 | exclude_keys: ['*_url', '_links', 'base', 'sender', 'owner', 'user'] 11 | - benthomasosn.eda.dashes_to_underscores: 12 | rules: 13 | - name: Pull Request Open 14 | condition: event.meta.headers.X_Github_Event == "pull_request" and event.payload.action == "opened" 15 | action: 16 | post_event: 17 | event: 18 | repository_name: "{{event.payload.repository.full_name}}" 19 | repo: "{{event.payload.pull_request.head.repo.clone_url}}" 20 | ref: "{{event.payload.pull_request.head.ref}}" 21 | pr: "{{event.payload.number}}" 22 | sha: "{{event.payload.pull_request.head.sha}}" 23 | - name: Pull Request Reopen 24 | condition: event.meta.headers.X_Github_Event == "pull_request" and event.payload.action == "reopened" 25 | action: 26 | post_event: 27 | event: 28 | repository_name: "{{event.payload.repository.full_name}}" 29 | repo: "{{event.payload.pull_request.head.repo.clone_url}}" 30 | ref: "{{event.payload.pull_request.head.ref}}" 31 | pr: "{{event.payload.number}}" 32 | sha: "{{event.payload.pull_request.head.sha}}" 33 | - name: Push to PR 34 | condition: event.meta.headers.X_Github_Event == "pull_request" and event.payload.action == "synchronize" 35 | action: 36 | post_event: 37 | event: 38 | repository_name: "{{event.payload.repository.full_name}}" 39 | repo: "{{event.payload.repository.clone_url}}" 40 | ref: "{{event.payload.pull_request.head.ref}}" 41 | pr: "{{event.payload.number}}" 42 | sha: "{{event.payload.after}}" 43 | - name: run tests 44 | condition: event.repository_name == vars.repo_name and event.pr is defined 45 | action: 46 | run_playbook: 47 | name: ansible.eda.continuous_integration 48 | post_events: true 49 | - name: print output 50 | condition: event.output is defined 51 | action: 52 | print_event: 53 | var_root: output 54 | - name: debug 55 | enabled: false 56 | condition: event.payload is defined 57 | action: 58 | debug: 59 | ... 60 | -------------------------------------------------------------------------------- /extensions/eda/rulebooks/hello_events.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Hello Events 3 | hosts: all 4 | sources: 5 | - ansible.eda.range: 6 | limit: 5 7 | rules: 8 | - name: Say Hello 9 | condition: event.i == 1 10 | action: 11 | run_playbook: 12 | name: ansible.eda.hello 13 | ... 14 | -------------------------------------------------------------------------------- /extensions/eda/rulebooks/journald_events.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Journald events 3 | hosts: all 4 | sources: 5 | - name: Match all messages 6 | ansible.eda.journald: 7 | match: "ALL" 8 | rules: 9 | - name: Print sudo journald event message 10 | condition: event.journald._comm == 'sudo' 11 | action: 12 | print_event: 13 | pretty: true 14 | var_root: 15 | journald.message: journald.message 16 | ... 17 | -------------------------------------------------------------------------------- /extensions/eda/rulebooks/kafka-test-rules.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Demo rules with kafka as source 3 | hosts: localhost 4 | sources: 5 | - name: kafka 6 | kafka: 7 | topic: eda 8 | host: localhost 9 | port: 9092 10 | group_id: testing 11 | rules: 12 | - name: Check defined 13 | condition: event.body.i is defined 14 | action: 15 | debug: 16 | - name: Stop 17 | condition: event.body == stop 18 | action: 19 | shutdown: 20 | -------------------------------------------------------------------------------- /extensions/eda/rulebooks/local-test-rules.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Local Test Runner Rules 3 | hosts: all 4 | sources: 5 | - name: watchdog 6 | ansible.eda.watchdog: 7 | path: "{{src_path}}" 8 | recursive: true 9 | ignore_regexes: ['.*\.pytest.*', '.*__pycache__.*', '.*/.git.*'] 10 | rules: 11 | - name: run tests 1 12 | condition: event.type == "DirModifiedEvent" 13 | action: 14 | run_playbook: 15 | name: ansible.eda.run_pytest 16 | post_events: true 17 | - name: run tests 2 18 | condition: event.type == "FileModifiedEvent" 19 | action: 20 | run_playbook: 21 | name: ansible.eda.run_pytest 22 | post_events: true 23 | - name: print output 24 | condition: event.output is defined 25 | action: 26 | print_event: 27 | var_root: output 28 | ... 29 | -------------------------------------------------------------------------------- /extensions/eda/rulebooks/process_down.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Automatic Remediation of a webserver 3 | hosts: all 4 | sources: 5 | - name: listen for alerts 6 | ansible.eda.alertmanager: 7 | host: 0.0.0.0 8 | port: 8000 9 | rules: 10 | - name: restart web server 11 | condition: event.alert.labels.job == "fastapi" and event.alert.status == "firing" 12 | action: 13 | run_playbook: 14 | name: ansible.eda.start_app 15 | copy_files: true 16 | post_events: true 17 | - name: debug 18 | condition: event.alert.labels.job == "fastapi" 19 | action: 20 | debug: 21 | -------------------------------------------------------------------------------- /galaxy.yml: -------------------------------------------------------------------------------- 1 | ### REQUIRED 2 | # The namespace of the collection. This can be a company/brand/organization or product namespace under which all 3 | # content lives. May only contain alphanumeric lowercase characters and underscores. Namespaces cannot start with 4 | # underscores or numbers and cannot contain consecutive underscores 5 | namespace: ansible 6 | 7 | # The name of the collection. Has the same character restrictions as 'namespace' 8 | name: eda 9 | 10 | # The version of the collection. Must be compatible with semantic versioning 11 | version: 2.8.0 12 | 13 | # The path to the Markdown (.md) readme file. This path is relative to the root of the collection 14 | readme: README.md 15 | 16 | # A list of the collection's content authors. Can be just the name or in the format 'Full Name (url) 17 | # @nicks:irc/im.site#channel' 18 | authors: 19 | - bthomass@redhat.com 20 | - jpisciot@redhat.com 21 | 22 | ### OPTIONAL but strongly recommended 23 | # A short summary description of the collection 24 | description: Event-Driven Ansible 25 | 26 | # Either a single license or a list of licenses for content inside of a collection. Ansible Galaxy currently only 27 | # accepts L(SPDX,https://spdx.org/licenses/) licenses. This key is mutually exclusive with 'license_file' 28 | license: 29 | - Apache-2.0 30 | 31 | # The path to the license file for the collection. This path is relative to the root of the collection. This key is 32 | # mutually exclusive with 'license' 33 | license_file: "" 34 | 35 | # A list of tags you want to associate with the collection for indexing/searching. A tag name has the same character 36 | # requirements as 'namespace' and 'name' 37 | tags: 38 | - infrastructure 39 | - tools 40 | - eda 41 | 42 | # Collections that this collection requires to be installed for it to be usable. The key of the dict is the 43 | # collection label 'namespace.name'. The value is a version range 44 | # L(specifiers,https://python-semanticversion.readthedocs.io/en/latest/#requirement-specification). Multiple version 45 | # range specifiers can be set and are separated by ',' 46 | dependencies: {} 47 | 48 | # The URL of the originating SCM repository 49 | repository: https://github.com/ansible/event-driven-ansible 50 | 51 | # The URL to any online docs 52 | documentation: https://ansible.readthedocs.io/projects/ansible-eda/ 53 | 54 | # The URL to the homepage of the collection/project 55 | homepage: https://www.redhat.com/en/technologies/management/ansible/event-driven-ansible 56 | 57 | # The URL to the collection issue tracker 58 | issues: https://github.com/ansible/event-driven-ansible/issues 59 | 60 | # A list of file glob-like patterns used to filter any files or directories that should not be included in the build 61 | # artifact. A pattern is matched from the relative path of the file or directory of the collection directory. This 62 | # uses 'fnmatch' to match the files or directories. Some directories and files like 'galaxy.yml', '*.pyc', '*.retry', 63 | # and '.git' are always filtered 64 | build_ignore: 65 | - "*.tar.gz" 66 | - .ansible 67 | - .benchmarks 68 | - .config 69 | - .github 70 | - .gitleaks.toml 71 | - .idea 72 | - .mypy_cache 73 | - .pre-commit-config.yaml 74 | - .pytest_cache 75 | - .readthedocs.yaml 76 | - .ruff_cache 77 | - .tool-versions 78 | - .tox 79 | - .venv 80 | - CHANGELOG.md 81 | - ansible_collections 82 | - changelogs/.plugin-cache.yaml 83 | - codecov.yml 84 | - collections 85 | - cspell.config.yaml 86 | - demos 87 | - docs/.changelog.yml 88 | - docs/.gitignore 89 | - docs/antsibull-docs.cfg 90 | - docs/build 91 | - docs/build.sh 92 | - docs/conf.py 93 | - docs/docsite 94 | - docs/rst 95 | - docs/temp-rst 96 | - pyproject.toml 97 | - run_pytest.yml 98 | - start_app.yml 99 | - tests/conftest.py 100 | - tests/integration/* 101 | - tests/run-staging 102 | - tests/unit 103 | - tox.ini 104 | - venv 105 | -------------------------------------------------------------------------------- /meta/extensions.yml: -------------------------------------------------------------------------------- 1 | extensions: 2 | - args: 3 | ext_dir: eda/plugins/event_filter 4 | - args: 5 | ext_dir: eda/plugins/event_source 6 | -------------------------------------------------------------------------------- /meta/runtime.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://access.redhat.com/support/policy/updates/ansible-automation-platform 3 | # https://docs.ansible.com/ansible/latest/reference_appendices/release_and_maintenance.html#ansible-core-support-matrix 4 | requires_ansible: ">=2.15.0" # AAP 2.4 or newer 5 | action_groups: 6 | eda: 7 | - activation 8 | - activation_info 9 | - controller_token 10 | - credential 11 | - credential_info 12 | - credential_type 13 | - credential_type_info 14 | - decision_environment 15 | - decision_environment_info 16 | - event_stream 17 | - event_stream_info 18 | - project 19 | - project_info 20 | - rulebook_info 21 | - rulebook_activation 22 | - rulebook_activation_copy 23 | - rulebook_activation_info 24 | - user 25 | plugin_routing: 26 | modules: 27 | activation: 28 | redirect: ansible.eda.rulebook_activation 29 | deprecation: 30 | removal_version: 4.0.0 31 | warning_text: >- 32 | activation has been renamed to rulebook_activation. 33 | Please update your tasks. 34 | activation_info: 35 | redirect: ansible.eda.rulebook_activation_info 36 | deprecation: 37 | removal_version: 4.0.0 38 | warning_text: >- 39 | activation_info has been renamed to rulebook_activation_info. 40 | Please update your tasks. 41 | -------------------------------------------------------------------------------- /playbooks/hello.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Hello 3 | hosts: localhost 4 | gather_facts: false 5 | tasks: 6 | - name: Print hello 7 | ansible.builtin.debug: 8 | msg: hello 9 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # Collections Plugins Directory 2 | 3 | This directory can be used to ship various plugins inside an Ansible collection. Each plugin is placed in a folder that 4 | is named after the type of plugin it is in. It can also include the `module_utils` and `modules` directory that 5 | would contain module utils and modules respectively. 6 | 7 | Here is an example directory of the majority of plugins currently supported by Ansible: 8 | 9 | ``` 10 | └── plugins 11 | ├── action 12 | ├── become 13 | ├── cache 14 | ├── callback 15 | ├── cliconf 16 | ├── connection 17 | ├── filter 18 | ├── httpapi 19 | ├── inventory 20 | ├── lookup 21 | ├── module_utils 22 | ├── modules 23 | ├── netconf 24 | ├── shell 25 | ├── strategy 26 | ├── terminal 27 | ├── test 28 | └── vars 29 | ``` 30 | 31 | A full list of plugin types can be found at [Working With Plugins](https://docs.ansible.com/ansible/latest/plugins/plugins.html). 32 | -------------------------------------------------------------------------------- /plugins/doc_fragments/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/event-driven-ansible/fc4e83305b1c1ae32c9374f5bb7aec6876d9733f/plugins/doc_fragments/__init__.py -------------------------------------------------------------------------------- /plugins/doc_fragments/eda_controller.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright: Contributors to the Ansible project 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | 7 | class ModuleDocFragment: 8 | AUTHS = """ 9 | options: 10 | controller_host: 11 | description: 12 | - The URL of the EDA controller. 13 | - If not set, the value of C(CONTROLLER_HOST), or E(AAP_HOSTNAME) environment variables will be used. 14 | - Support for E(AAP_HOSTNAME) has been added in version 2.7. 15 | required: true 16 | type: str 17 | version_added: '2.0.0' 18 | aliases: [ aap_hostname ] 19 | controller_username: 20 | description: 21 | - Username used for authentication. 22 | - If not set, the value of C(CONTROLLER_USERNAME), or E(AAP_USERNAME) environment variables will be used. 23 | - Support for E(AAP_USERNAME) has been added in version 2.7. 24 | type: str 25 | version_added: '2.0.0' 26 | aliases: [ aap_username ] 27 | controller_password: 28 | description: 29 | - Password used for authentication. 30 | - If not set, the value of C(CONTROLLER_PASSWORD), or E(AAP_PASSWORD) environment variables will be used. 31 | - Support for E(AAP_PASSWORD) has been added in version 2.7. 32 | type: str 33 | version_added: '2.0.0' 34 | aliases: [ aap_password ] 35 | request_timeout: 36 | description: 37 | - Timeout in seconds for the connection with the EDA controller. 38 | - If not set, the value of C(CONTROLLER_TIMEOUT), E(AAP_REQUEST_TIMEOUT) environment variables 39 | - will be used. 40 | - Support for E(AAP_REQUEST_TIMEOUT) has been added in version 2.7. 41 | type: float 42 | default: 10 43 | version_added: '2.0.0' 44 | aliases: [ aap_request_timeout ] 45 | validate_certs: 46 | description: 47 | - Whether to allow insecure connections to Ansible Automation Platform EDA 48 | Controller instance. 49 | - If C(no), SSL certificates will not be validated. 50 | - This should only be used on personally controlled sites using self-signed certificates. 51 | - If value not set, will try environment variable C(CONTROLLER_VERIFY_SSL), or E(AAP_VALIDATE_CERTS). 52 | - Support for E(AAP_VALIDATE_CERTS) has been added in version 2.7. 53 | default: True 54 | type: bool 55 | version_added: '2.0.0' 56 | aliases: [ aap_validate_certs ] 57 | """ 58 | -------------------------------------------------------------------------------- /plugins/module_utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/event-driven-ansible/fc4e83305b1c1ae32c9374f5bb7aec6876d9733f/plugins/module_utils/__init__.py -------------------------------------------------------------------------------- /plugins/module_utils/arguments.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright: Contributors to the Ansible project 4 | # Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) 5 | 6 | from __future__ import absolute_import, annotations, division, print_function 7 | 8 | __metaclass__ = type 9 | 10 | from ansible.module_utils.basic import env_fallback 11 | 12 | AUTH_ARGSPEC: dict[str, dict[str, object]] = { 13 | "controller_host": { 14 | "fallback": (env_fallback, ["CONTROLLER_HOST", "AAP_HOSTNAME"]), 15 | "required": True, 16 | "aliases": ["aap_hostname"], 17 | }, 18 | "controller_username": { 19 | "required": False, 20 | "fallback": (env_fallback, ["CONTROLLER_USERNAME", "AAP_USERNAME"]), 21 | "aliases": ["aap_username"], 22 | }, 23 | "controller_password": { 24 | "required": False, 25 | "fallback": (env_fallback, ["CONTROLLER_PASSWORD", "AAP_PASSWORD"]), 26 | "no_log": True, 27 | "aliases": ["aap_password"], 28 | }, 29 | "validate_certs": { 30 | "type": "bool", 31 | "default": True, 32 | "required": False, 33 | "fallback": (env_fallback, ["CONTROLLER_VERIFY_SSL", "AAP_VALIDATE_CERTS"]), 34 | "aliases": ["aap_validate_certs"], 35 | }, 36 | "request_timeout": { 37 | "type": "float", 38 | "default": 10.0, 39 | "required": False, 40 | "fallback": ( 41 | env_fallback, 42 | ["CONTROLLER_REQUEST_TIMEOUT", "AAP_REQUEST_TIMEOUT"], 43 | ), 44 | "aliases": ["aap_request_timeout"], 45 | }, 46 | } 47 | -------------------------------------------------------------------------------- /plugins/module_utils/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright: Contributors to the Ansible project 4 | # Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | 8 | __metaclass__ = type 9 | 10 | 11 | from typing import Any, Optional 12 | 13 | from ansible.module_utils.basic import AnsibleModule 14 | 15 | from .controller import Controller 16 | from .errors import EDAError 17 | 18 | 19 | def lookup_resource_id( 20 | module: AnsibleModule, 21 | controller: Controller, 22 | endpoint: str, 23 | name: str, 24 | params: Optional[dict[str, Any]] = None, 25 | ) -> Optional[int]: 26 | result = None 27 | 28 | try: 29 | result = controller.resolve_name_to_id( 30 | endpoint, name, **params if params is not None else {} 31 | ) 32 | except EDAError as e: 33 | module.fail_json(msg=f"Failed to lookup resource: {e}") 34 | return result 35 | -------------------------------------------------------------------------------- /plugins/module_utils/errors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright: Contributors to the Ansible project 4 | # Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) 5 | from __future__ import absolute_import, division, print_function 6 | 7 | __metaclass__ = type 8 | 9 | 10 | class EDAError(Exception): 11 | pass 12 | 13 | 14 | class EDAHTTPError(EDAError): 15 | pass 16 | 17 | 18 | class AuthError(EDAError): 19 | pass 20 | -------------------------------------------------------------------------------- /plugins/modules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/event-driven-ansible/fc4e83305b1c1ae32c9374f5bb7aec6876d9733f/plugins/modules/__init__.py -------------------------------------------------------------------------------- /plugins/modules/credential_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: Contributors to the Ansible project 5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | 12 | DOCUMENTATION = r""" 13 | --- 14 | module: credential_info 15 | author: 16 | - Alina Buzachis (@alinabuzachis) 17 | short_description: List credentials in the EDA Controller 18 | description: 19 | - List credentials in the EDA controller. 20 | version_added: 2.0.0 21 | options: 22 | name: 23 | description: 24 | - The name of the credential. 25 | type: str 26 | required: false 27 | extends_documentation_fragment: 28 | - ansible.eda.eda_controller.auths 29 | notes: 30 | - M(ansible.eda.credential_info) supports AAP 2.5 and onwards. 31 | """ 32 | 33 | 34 | EXAMPLES = r""" 35 | - name: Get information about a credential 36 | ansible.eda.credential_info: 37 | name: "Test" 38 | 39 | - name: List all credentials 40 | ansible.eda.credential_info: 41 | """ 42 | 43 | 44 | RETURN = r""" 45 | credentials: 46 | description: Information about credentials. 47 | returned: always 48 | type: list 49 | elements: dict 50 | sample: [ 51 | { 52 | "created_at": "2024-08-14T08:57:55.151787Z", 53 | "credential_type": { 54 | "id": 1, 55 | "kind": "scm", 56 | "name": "Source Control", 57 | "namespace": "scm" 58 | }, 59 | "description": "This is a test credential", 60 | "id": 24, 61 | "inputs": { 62 | "password": "$encrypted$", 63 | "username": "testuser" 64 | }, 65 | "managed": false, 66 | "modified_at": "2024-08-14T08:57:56.324925Z", 67 | "name": "New Test Credential", 68 | "organization": { 69 | "description": "The default organization", 70 | "id": 1, 71 | "name": "Default" 72 | }, 73 | "references": null 74 | } 75 | ] 76 | """ 77 | 78 | 79 | from ansible.module_utils.basic import AnsibleModule 80 | 81 | from ..module_utils.arguments import AUTH_ARGSPEC 82 | from ..module_utils.client import Client 83 | from ..module_utils.controller import Controller 84 | from ..module_utils.errors import EDAError 85 | 86 | 87 | def main() -> None: 88 | argument_spec = dict( 89 | name=dict(type="str", required=False), 90 | ) 91 | 92 | argument_spec.update(AUTH_ARGSPEC) 93 | 94 | module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) 95 | 96 | client = Client( 97 | host=module.params.get("controller_host"), 98 | username=module.params.get("controller_username"), 99 | password=module.params.get("controller_password"), 100 | timeout=module.params.get("request_timeout"), 101 | validate_certs=module.params.get("validate_certs"), 102 | ) 103 | 104 | name = module.params.get("name") 105 | controller = Controller(client, module) 106 | credential_endpoint = "eda-credentials" 107 | credential_path = controller.get_endpoint(credential_endpoint) 108 | if credential_path.status in (404,): 109 | module.fail_json( 110 | msg="Module ansible.eda.credential_info supports AAP 2.5 and onwards" 111 | ) 112 | 113 | # Attempt to look up credential based on the provided name 114 | try: 115 | result = controller.get_one_or_many( 116 | credential_endpoint, name=name, want_one=False 117 | ) 118 | except EDAError as e: 119 | module.fail_json(msg=f"Failed to get credential: {e}") 120 | 121 | module.exit_json(credentials=result) 122 | 123 | 124 | if __name__ == "__main__": 125 | main() 126 | -------------------------------------------------------------------------------- /plugins/modules/credential_type_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: Contributors to the Ansible project 5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, annotations, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | 12 | DOCUMENTATION = r""" 13 | --- 14 | module: credential_type_info 15 | author: 16 | - Alina Buzachis (@alinabuzachis) 17 | short_description: List credential types in EDA Controller 18 | description: 19 | - List credential types in EDA controller. 20 | version_added: 2.0.0 21 | options: 22 | name: 23 | description: 24 | - The name of the credential type. 25 | type: str 26 | required: false 27 | extends_documentation_fragment: 28 | - ansible.eda.eda_controller.auths 29 | notes: 30 | - M(ansible.eda.credential_type_info) supports AAP 2.5 and onwards. 31 | """ 32 | 33 | 34 | EXAMPLES = r""" 35 | - name: Get information about a credential type 36 | ansible.eda.credential_type_info: 37 | name: "Test" 38 | 39 | - name: List all credential types 40 | ansible.eda.credential_type_info: 41 | """ 42 | 43 | 44 | RETURN = r""" 45 | credential_types: 46 | description: Information about the credential types. 47 | returned: always 48 | type: list 49 | elements: dict 50 | sample: [{ 51 | "created_at": "2024-08-14T08:30:14.806638Z", 52 | "description": "A test credential type", 53 | "id": 37, 54 | "injectors": { 55 | "extra_vars": { 56 | "field1": "field1" 57 | } 58 | }, 59 | "inputs": { 60 | "fields": [ 61 | { 62 | "id": "field1", 63 | "label": "Field 5", 64 | "type": "string" 65 | } 66 | ] 67 | }, 68 | "kind": "cloud", 69 | "managed": false, 70 | "modified_at": "2024-08-14T08:30:14.807549Z", 71 | "name": "Example", 72 | "namespace": null 73 | }] 74 | """ 75 | 76 | 77 | from ansible.module_utils.basic import AnsibleModule 78 | 79 | from ..module_utils.arguments import AUTH_ARGSPEC 80 | from ..module_utils.client import Client 81 | from ..module_utils.controller import Controller 82 | from ..module_utils.errors import EDAError 83 | 84 | 85 | def main() -> None: 86 | argument_spec = dict( 87 | name=dict(type="str", required=False), 88 | ) 89 | 90 | argument_spec.update(AUTH_ARGSPEC) 91 | 92 | module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) 93 | 94 | client = Client( 95 | host=module.params.get("controller_host"), 96 | username=module.params.get("controller_username"), 97 | password=module.params.get("controller_password"), 98 | timeout=module.params.get("request_timeout"), 99 | validate_certs=module.params.get("validate_certs"), 100 | ) 101 | 102 | name = module.params.get("name") 103 | controller = Controller(client, module) 104 | credential_type_endpoint = "credential-types" 105 | credential_type_path = controller.get_endpoint(credential_type_endpoint) 106 | if credential_type_path.status in (404,): 107 | module.fail_json( 108 | msg="Module ansible.eda.credential_type_info supports AAP 2.5 and onwards" 109 | ) 110 | 111 | # Attempt to look up credential_type based on the provided name 112 | try: 113 | result = controller.get_one_or_many( 114 | credential_type_endpoint, name=name, want_one=False 115 | ) 116 | except EDAError as e: 117 | module.fail_json(msg=f"Failed to get credential type: {e}") 118 | 119 | module.exit_json(credential_types=result) 120 | 121 | 122 | if __name__ == "__main__": 123 | main() 124 | -------------------------------------------------------------------------------- /plugins/modules/decision_environment_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: Contributors to the Ansible project 5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | DOCUMENTATION = r""" 12 | --- 13 | module: decision_environment_info 14 | author: 15 | - Abhijeet Kasurde (@akasurde) 16 | short_description: List a decision environment in EDA Controller 17 | description: 18 | - This module allows user to list a decision environment in a EDA controller. 19 | version_added: '2.0.0' 20 | options: 21 | name: 22 | description: 23 | - The name of the decision environment. 24 | type: str 25 | extends_documentation_fragment: 26 | - ansible.eda.eda_controller.auths 27 | """ 28 | 29 | EXAMPLES = r""" 30 | - name: List all EDA Decision Environments 31 | ansible.eda.decision_environment_info: 32 | aap_hostname: https://my_eda_host/ 33 | aap_username: admin 34 | aap_password: MySuperSecretPassw0rd 35 | 36 | - name: List a particular EDA Decision Environments 37 | ansible.eda.decision_environment_info: 38 | aap_hostname: https://my_eda_host/ 39 | aap_username: admin 40 | aap_password: MySuperSecretPassw0rd 41 | name: Example 42 | """ 43 | 44 | RETURN = r""" 45 | decision_environments: 46 | description: List of dict containing information about decision environments 47 | returned: when exists 48 | type: list 49 | sample: [ 50 | { 51 | "created_at": "2024-08-15T21:12:52.218969Z", 52 | "description": "Example decision environment description", 53 | "eda_credential_id": null, 54 | "id": 35, 55 | "image_url": "https://quay.io/repository/ansible/eda-server", 56 | "modified_at": "2024-08-15T21:12:52.218994Z", 57 | "name": "Example Decision environment", 58 | "organization_id": 1 59 | } 60 | ] 61 | """ 62 | 63 | from typing import Any 64 | 65 | from ansible.module_utils.basic import AnsibleModule 66 | 67 | from ..module_utils.arguments import AUTH_ARGSPEC 68 | from ..module_utils.client import Client 69 | from ..module_utils.controller import Controller 70 | from ..module_utils.errors import EDAError 71 | 72 | 73 | def main() -> None: 74 | argument_spec: dict[str, Any] = dict( 75 | name=dict(), 76 | ) 77 | 78 | argument_spec.update(AUTH_ARGSPEC) 79 | 80 | module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) 81 | 82 | client = Client( 83 | host=module.params.get("controller_host"), 84 | username=module.params.get("controller_username"), 85 | password=module.params.get("controller_password"), 86 | timeout=module.params.get("request_timeout"), 87 | validate_certs=module.params.get("validate_certs"), 88 | ) 89 | 90 | decision_environment_endpoint = "decision-environments" 91 | controller = Controller(client, module) 92 | 93 | decision_environment_name = module.params.get("name") 94 | 95 | try: 96 | ret = controller.get_one_or_many( 97 | decision_environment_endpoint, 98 | name=decision_environment_name, 99 | want_one=False, 100 | ) 101 | except EDAError as eda_err: 102 | module.fail_json(msg=str(eda_err)) 103 | raise # https://github.com/ansible/ansible/pull/83814 104 | 105 | module.exit_json(decision_environments=ret) 106 | 107 | 108 | if __name__ == "__main__": 109 | main() 110 | -------------------------------------------------------------------------------- /plugins/modules/event_stream_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: Contributors to the Ansible project 5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | 12 | DOCUMENTATION = r""" 13 | --- 14 | module: event_stream_info 15 | author: 16 | - Alina Buzachis (@alinabuzachis) 17 | short_description: List event streams in the EDA Controller 18 | description: 19 | - List event streams in the EDA controller. 20 | version_added: 2.0.0 21 | options: 22 | name: 23 | description: 24 | - The name of the event stream. 25 | type: str 26 | required: false 27 | extends_documentation_fragment: 28 | - ansible.eda.eda_controller.auths 29 | notes: 30 | - M(ansible.eda.event_stream_info) supports AAP 2.5 and onwards. 31 | """ 32 | 33 | 34 | EXAMPLES = r""" 35 | - name: Get information about a event stream 36 | ansible.eda.event_stream_info: 37 | name: "Test" 38 | 39 | - name: List all event streams 40 | ansible.eda.event_stream_info: 41 | """ 42 | 43 | 44 | RETURN = r""" 45 | event_streams: 46 | description: Information about event streams. 47 | returned: always 48 | type: list 49 | elements: dict 50 | sample: [ 51 | { 52 | "additional_data_headers": "", 53 | "created_at": "2024-08-30T11:19:49.064112Z", 54 | "eda_credential": { 55 | "credential_type_id": 218, 56 | "description": "This is a test credential", 57 | "id": 200, 58 | "inputs": { 59 | "auth_type": "basic", 60 | "http_header_key": "Authorization", 61 | "password": "$encrypted$", 62 | "username": "test" 63 | }, 64 | "managed": false, 65 | "name": "Test_Credential_3db838f4-299c-51bf-8ff5-47f6d24fc5a7", 66 | "organization_id": 1 67 | }, 68 | "event_stream_type": "hmac", 69 | "events_received": 0, 70 | "id": 4, 71 | "last_event_received_at": null, 72 | "modified_at": "2024-08-30T11:19:51.825860Z", 73 | "name": "Test_EvenStream_3db838f4-299c-51bf-8ff5-47f6d24fc5a7", 74 | "organization": { 75 | "description": "The default organization", 76 | "id": 1, 77 | "name": "Default" 78 | }, 79 | "owner": "admin", 80 | "test_content": "", 81 | "test_content_type": "", 82 | "test_error_message": "", 83 | "test_headers": "", 84 | "test_mode": false, 85 | "url": "https://eda-server:8443/edgecafe/api/eda/v1/external_event_stream/18584cf5/post/" 86 | } 87 | ] 88 | """ 89 | 90 | 91 | from ansible.module_utils.basic import AnsibleModule 92 | 93 | from ..module_utils.arguments import AUTH_ARGSPEC 94 | from ..module_utils.client import Client 95 | from ..module_utils.controller import Controller 96 | from ..module_utils.errors import EDAError 97 | 98 | 99 | def main() -> None: 100 | argument_spec = dict( 101 | name=dict(type="str", required=False), 102 | ) 103 | 104 | argument_spec.update(AUTH_ARGSPEC) 105 | 106 | module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) 107 | 108 | client = Client( 109 | host=module.params.get("controller_host"), 110 | username=module.params.get("controller_username"), 111 | password=module.params.get("controller_password"), 112 | timeout=module.params.get("request_timeout"), 113 | validate_certs=module.params.get("validate_certs"), 114 | ) 115 | 116 | name = module.params.get("name") 117 | controller = Controller(client, module) 118 | event_stream_endpoint = "event-streams" 119 | event_stream_path = controller.get_endpoint(event_stream_endpoint) 120 | if event_stream_path.status in (404,): 121 | module.fail_json( 122 | msg="Module ansible.eda.event_stream_info supports AAP 2.5 and onwards" 123 | ) 124 | 125 | # Attempt to look up event_stream based on the provided name 126 | try: 127 | result = controller.get_one_or_many( 128 | event_stream_endpoint, name=name, want_one=False 129 | ) 130 | except EDAError as e: 131 | module.fail_json(msg=f"Failed to get event stream: {e}") 132 | 133 | module.exit_json(event_streams=result) 134 | 135 | 136 | if __name__ == "__main__": 137 | main() 138 | -------------------------------------------------------------------------------- /plugins/modules/project_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: Contributors to the Ansible project 5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | DOCUMENTATION = r""" 12 | --- 13 | module: project_info 14 | author: 15 | - Abhijeet Kasurde (@akasurde) 16 | short_description: List projects in EDA Controller 17 | description: 18 | - This module allows user to list project in a EDA controller. 19 | version_added: '2.0.0' 20 | options: 21 | name: 22 | description: 23 | - The name of the project. 24 | - Return information about particular project available on EDA Controller. 25 | type: str 26 | extends_documentation_fragment: 27 | - ansible.eda.eda_controller.auths 28 | """ 29 | 30 | EXAMPLES = r""" 31 | - name: List a particular project 32 | ansible.eda.project_info: 33 | aap_hostname: https://my_eda_host/ 34 | aap_username: admin 35 | aap_password: MySuperSecretPassw0rd 36 | name: "Example" 37 | register: r 38 | 39 | - name: List all projects 40 | ansible.eda.project_info: 41 | aap_hostname: https://my_eda_host/ 42 | aap_username: admin 43 | aap_password: MySuperSecretPassw0rd 44 | register: r 45 | """ 46 | 47 | RETURN = r""" 48 | projects: 49 | description: List of dicts containing information about projects. 50 | returned: success 51 | type: list 52 | sample: [ 53 | { 54 | "created_at": "2024-08-12T20:35:28.367702Z", 55 | "description": "", 56 | "eda_credential_id": null, 57 | "git_hash": "417b4dbe9b3472fd64212ef8233b865585e5ade3", 58 | "id": 17, 59 | "import_error": null, 60 | "import_state": "completed", 61 | "modified_at": "2024-08-12T20:35:28.367724Z", 62 | "name": "Sample Example Project", 63 | "organization_id": 1, 64 | "proxy": "", 65 | "scm_branch": "", 66 | "scm_refspec": "", 67 | "scm_type": "git", 68 | "signature_validation_credential_id": null, 69 | "url": "https://github.com/ansible/ansible-ui", 70 | "verify_ssl": true 71 | }, 72 | ] 73 | """ # NOQA 74 | 75 | # pylint: disable=wrong-import-position, 76 | from typing import Any 77 | 78 | from ansible.module_utils.basic import AnsibleModule 79 | 80 | # pylint: disable=relative-beyond-top-level 81 | from ..module_utils.arguments import AUTH_ARGSPEC 82 | from ..module_utils.client import Client 83 | from ..module_utils.controller import Controller 84 | from ..module_utils.errors import EDAError 85 | 86 | 87 | def main() -> None: 88 | argument_spec: dict[str, Any] = dict( 89 | name=dict(), 90 | ) 91 | 92 | argument_spec.update(AUTH_ARGSPEC) 93 | 94 | module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) 95 | 96 | client = Client( 97 | host=module.params.get("controller_host"), 98 | username=module.params.get("controller_username"), 99 | password=module.params.get("controller_password"), 100 | timeout=module.params.get("request_timeout"), 101 | validate_certs=module.params.get("validate_certs"), 102 | ) 103 | 104 | project_endpoint = "projects" 105 | controller = Controller(client, module) 106 | 107 | project_name = module.params.get("name") 108 | 109 | try: 110 | result = controller.get_one_or_many( 111 | project_endpoint, name=project_name, want_one=False 112 | ) 113 | except EDAError as eda_err: 114 | module.fail_json(msg=str(eda_err)) 115 | 116 | module.exit_json(projects=result) 117 | 118 | 119 | if __name__ == "__main__": 120 | main() 121 | -------------------------------------------------------------------------------- /plugins/modules/rulebook_activation_copy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding: utf-8 -*- 3 | 4 | # GNU General Public License v3.0+ 5 | # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | DOCUMENTATION = r""" 12 | --- 13 | module: rulebook_activation_copy 14 | author: 15 | - "Kaio Oliveira (@kaiokmo)" 16 | - "Brandon W (@b-whitt)" 17 | short_description: Copy rulebook activations in the EDA Controller 18 | description: 19 | - This module allows the user to copy rulebook activations in the EDA Controller. 20 | version_added: 2.7.0 21 | options: 22 | name: 23 | description: 24 | - The name of the new rulebook activation. 25 | type: str 26 | required: true 27 | copy_from: 28 | description: 29 | - Name of the existing rulebook activation to copy. 30 | type: str 31 | required: true 32 | extends_documentation_fragment: 33 | - ansible.eda.eda_controller.auths 34 | """ 35 | 36 | EXAMPLES = r""" 37 | - name: Copy an existing rulebook activation 38 | ansible.eda.rulebook_activation_copy: 39 | name: "Example Rulebook Activation - copy" 40 | copy_from: "Example Rulebook Activation" 41 | """ 42 | 43 | RETURN = r""" 44 | id: 45 | description: ID of the rulebook activation. 46 | returned: when exists 47 | type: int 48 | sample: 37 49 | """ 50 | 51 | 52 | from ansible.module_utils.basic import AnsibleModule 53 | 54 | from ..module_utils.arguments import AUTH_ARGSPEC 55 | from ..module_utils.client import Client 56 | from ..module_utils.controller import Controller 57 | from ..module_utils.errors import EDAError 58 | 59 | 60 | def main() -> None: 61 | argument_spec = dict( 62 | name=dict(type="str", required=True), 63 | copy_from=dict(type="str", required=True), 64 | ) 65 | 66 | argument_spec.update(AUTH_ARGSPEC) 67 | 68 | module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) 69 | copy_from = module.params.get("copy_from") 70 | 71 | module = AnsibleModule( 72 | argument_spec=argument_spec, 73 | supports_check_mode=True, 74 | ) 75 | 76 | client = Client( 77 | host=module.params.get("controller_host"), 78 | username=module.params.get("controller_username"), 79 | password=module.params.get("controller_password"), 80 | timeout=module.params.get("request_timeout"), 81 | validate_certs=module.params.get("validate_certs"), 82 | ) 83 | 84 | name = module.params.get("name") 85 | controller = Controller(client, module) 86 | 87 | # Attempt to find rulebook activation based on the provided name 88 | activation = {} 89 | try: 90 | activation = controller.get_exactly_one("activations", name=copy_from) 91 | except EDAError as e: 92 | module.fail_json(msg=f"Failed to get rulebook activation: {e}") 93 | 94 | try: 95 | result = controller.copy_if_needed( 96 | name, 97 | copy_from, 98 | endpoint=f"activations/{activation['id']}/copy", 99 | item_type="activation", 100 | ) 101 | module.exit_json(**result) 102 | except KeyError as e: 103 | module.fail_json(msg=f"Unable to access {e} of the activation to copy from.") 104 | except EDAError as e: 105 | module.fail_json(msg=f"Failed to copy rulebook activation: {e}") 106 | 107 | 108 | if __name__ == "__main__": 109 | main() 110 | -------------------------------------------------------------------------------- /plugins/modules/rulebook_activation_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: Contributors to the Ansible project 5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | 9 | __metaclass__ = type 10 | 11 | 12 | DOCUMENTATION = r""" 13 | --- 14 | module: rulebook_activation_info 15 | author: 16 | - Alina Buzachis (@alinabuzachis) 17 | short_description: List rulebook activations in the EDA Controller 18 | description: 19 | - List rulebook activations in the EDA controller. 20 | version_added: 2.0.0 21 | options: 22 | name: 23 | description: 24 | - The name of the rulebook activation. 25 | type: str 26 | required: false 27 | extends_documentation_fragment: 28 | - ansible.eda.eda_controller.auths 29 | """ 30 | 31 | 32 | EXAMPLES = r""" 33 | - name: Get information about a rulebook activation 34 | ansible.eda.rulebook_activation_info: 35 | name: "Example Rulebook Activation" 36 | 37 | - name: List all rulebook activations 38 | ansible.eda.rulebook_activation_info: 39 | """ 40 | 41 | 42 | RETURN = r""" 43 | activations: 44 | description: Information about rulebook activations. 45 | returned: always 46 | type: list 47 | elements: dict 48 | sample: [ 49 | { 50 | "id": 1, 51 | "name": "Test activation", 52 | "description": "A test activation", 53 | "is_enabled": true, 54 | "status": "running", 55 | "extra_var": "", 56 | "decision_environment_id": 1, 57 | "project_id": 2, 58 | "rulebook_id": 1, 59 | "organization_id": 1, 60 | "restart_policy": "on-failure", 61 | "restart_count": 2, 62 | "rulebook_name": "Test rulebook", 63 | "current_job_id": "2", 64 | "rules_count": 2, 65 | "rules_fired_count": 2, 66 | "created_at": "2024-08-10T14:22:30.123Z", 67 | "modified_at": "2024-08-15T11:45:00.987Z", 68 | "status_message": "Activation is running successfully.", 69 | "awx_token_id": 1, 70 | "log_level": "info", 71 | "eda_credentials": [], 72 | "k8s_service_name": "", 73 | "event_streams": [], 74 | "swap_single_source": false 75 | } 76 | ] 77 | """ 78 | 79 | 80 | from ansible.module_utils.basic import AnsibleModule 81 | 82 | from ..module_utils.arguments import AUTH_ARGSPEC 83 | from ..module_utils.client import Client 84 | from ..module_utils.controller import Controller 85 | from ..module_utils.errors import EDAError 86 | 87 | 88 | def main() -> None: 89 | argument_spec = dict( 90 | name=dict(type="str", required=False), 91 | ) 92 | 93 | argument_spec.update(AUTH_ARGSPEC) 94 | 95 | module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) 96 | 97 | client = Client( 98 | host=module.params.get("controller_host"), 99 | username=module.params.get("controller_username"), 100 | password=module.params.get("controller_password"), 101 | timeout=module.params.get("request_timeout"), 102 | validate_certs=module.params.get("validate_certs"), 103 | ) 104 | 105 | name = module.params.get("name") 106 | controller = Controller(client, module) 107 | 108 | # Attempt to look up rulebook activation based on the provided name 109 | try: 110 | result = controller.get_one_or_many("activations", name=name) 111 | except EDAError as e: 112 | module.fail_json(msg=f"Failed to get rulebook activations: {e}") 113 | 114 | module.exit_json(activations=result) 115 | 116 | 117 | if __name__ == "__main__": 118 | main() 119 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | target-version = ["py310"] 3 | 4 | # Keep this default because xml/report do not know to use load it from config file: 5 | # data_file = ".coverage" 6 | # [tool.coverage.paths] 7 | # source = ["plugins", "extensions"] 8 | 9 | # [tool.coverage.report] 10 | # exclude_also = ["pragma: no cover", "if TYPE_CHECKING:"] 11 | # omit = ["test/*"] 12 | # # Increase it just so it would pass on any single-python run 13 | # fail_under = 92 14 | # skip_covered = true 15 | # skip_empty = true 16 | # # During development we might remove code (files) with coverage data, and we dont want to fail: 17 | # ignore_errors = true 18 | # show_missing = true 19 | 20 | # [tool.coverage.run] 21 | # source = ["src"] 22 | # # Do not use branch until bug is fixes: 23 | # # https://github.com/nedbat/coveragepy/issues/605 24 | # # branch = true 25 | # parallel = true 26 | # concurrency = ["multiprocessing", "thread"] 27 | 28 | [tool.isort] 29 | profile = "black" 30 | multi_line_output = 3 31 | include_trailing_comma = true 32 | force_grid_wrap = 0 33 | use_parentheses = true 34 | ensure_newline_before_comments = true 35 | 36 | [tool.mypy] 37 | python_version = "3.9" 38 | color_output = true 39 | error_summary = true 40 | 41 | # TODO: Remove temporary skips and close https://github.com/ansible/event-driven-ansible/issues/258 42 | strict = true 43 | disallow_untyped_calls = true 44 | disallow_untyped_defs = true 45 | # disallow_any_generics = true 46 | # disallow_any_unimported = true 47 | # warn_redundant_casts = True 48 | # warn_return_any = True 49 | warn_unused_configs = true 50 | 51 | # site-packages is here to help vscode mypy integration getting confused 52 | exclude = "(build|dist|test/local-content|site-packages|~/.pyenv|examples/playbooks/collections|plugins/modules)" 53 | # https://github.com/python/mypy/issues/12664 54 | incremental = false 55 | namespace_packages = true 56 | explicit_package_bases = true 57 | 58 | [[tool.mypy.overrides]] 59 | module = [ 60 | # Dependencies not following pep-561 yet: 61 | "aiokafka.*", # https://github.com/aio-libs/aiokafka/issues/980 62 | "ansible.*", # https://github.com/ansible/ansible/issues/83801 63 | "asyncmock", # https://github.com/timsavage/asyncmock/issues/8 64 | "kafka.*", # https://github.com/dpkp/kafka-python/issues/2446 65 | ] 66 | ignore_missing_imports = true 67 | 68 | [tool.pylint.MASTER] 69 | # Temporary ignore until we are able to address issues on these: 70 | ignore-paths = "^(demos/dynatrace-demo/fake_app.py|tests/|plugins/modules).*$" 71 | 72 | [tool.pylint."MESSAGES CONTROL"] 73 | disable = [ 74 | "duplicate-code", 75 | "pointless-string-statement", 76 | "too-few-public-methods", 77 | "too-many-instance-attributes", 78 | "missing-module-docstring", 79 | "missing-class-docstring", 80 | "missing-function-docstring", 81 | "too-many-arguments", 82 | "too-many-branches", 83 | "inconsistent-return-statements", 84 | "invalid-name", 85 | "too-many-positional-arguments", 86 | ] 87 | max-line-length=120 88 | 89 | [tool.ruff.lint] 90 | ignore = [ "E402" ] 91 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ansible-core>=2.15 2 | pyyaml>=6.0.1 3 | aiobotocore 4 | aiohttp 5 | aiokafka[gssapi] 6 | azure-servicebus 7 | dpath 8 | # https://github.com/dpkp/kafka-python/issues/2412#issuecomment-2030459360 9 | kafka-python; python_version < "3.12" 10 | kafka-python-ng; python_version >= "3.12" 11 | psycopg[binary,pool] # extras needed to avoid install failure on macos-aarch64 12 | systemd-python; sys_platform != 'darwin' 13 | watchdog>=5.0.0 # types 14 | xxhash 15 | -------------------------------------------------------------------------------- /schemas/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Schemas Directory 3 | 4 | A schema is a description of the structure and types of a data structure. 5 | 6 | - event 7 | -------------------------------------------------------------------------------- /schemas/event/README.md: -------------------------------------------------------------------------------- 1 | 2 | # EDA Event Schemas 3 | -------------------------------------------------------------------------------- /schemas/event/range.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "id": "http://ansible.com/schema/event/range.json", 4 | "type": "object", 5 | "properties": { 6 | "i": { 7 | "type": "integer" 8 | }, 9 | "additionalProperties": false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/event-driven-ansible/fc4e83305b1c1ae32c9374f5bb7aec6876d9733f/tests/__init__.py -------------------------------------------------------------------------------- /tests/config.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/ansible/ansible/blob/devel/test/lib/ansible_test/config/config.yml 2 | --- 3 | modules: 4 | python_requires: controller 5 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Any 3 | 4 | import pytest 5 | 6 | 7 | class ListQueue(asyncio.Queue[Any]): 8 | def __init__(self) -> None: 9 | self.queue: list[Any] = [] 10 | 11 | async def put(self, item: Any) -> None: 12 | self.queue.append(item) 13 | 14 | def put_nowait(self, item: Any) -> None: 15 | self.queue.append(item) 16 | 17 | 18 | @pytest.fixture 19 | def eda_queue() -> ListQueue: 20 | return ListQueue() 21 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | TESTS_PATH = os.path.dirname(os.path.abspath(__file__)) 4 | -------------------------------------------------------------------------------- /tests/integration/conftest.py: -------------------------------------------------------------------------------- 1 | from subprocess import Popen 2 | from typing import Callable, Iterator 3 | 4 | import pytest 5 | 6 | 7 | @pytest.fixture(scope="function") 8 | def subprocess_teardown() -> Iterator[Callable[[Popen[bytes]], None]]: 9 | processes: list[Popen[bytes]] = [] 10 | 11 | def _teardown(process: Popen[bytes]) -> None: 12 | processes.append(process) 13 | 14 | yield _teardown 15 | for proc in processes: 16 | proc.terminate() 17 | -------------------------------------------------------------------------------- /tests/integration/default_inventory.yml: -------------------------------------------------------------------------------- 1 | all: 2 | hosts: 3 | localhost: 4 | ansible_connection: local 5 | -------------------------------------------------------------------------------- /tests/integration/event_source_aws_cloudtrail/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/event-driven-ansible/fc4e83305b1c1ae32c9374f5bb7aec6876d9733f/tests/integration/event_source_aws_cloudtrail/__init__.py -------------------------------------------------------------------------------- /tests/integration/event_source_aws_cloudtrail/test_awscloudtrail_rules.yml: -------------------------------------------------------------------------------- 1 | - name: test aws cloudtrail source plugin 2 | hosts: localhost 3 | 4 | sources: 5 | - ansible.eda.aws_cloudtrail: 6 | region: 'us-east-1' 7 | delay_seconds: 5 8 | rules: 9 | - name: match key pair creation event 10 | condition: event.CloudTrailEvent.eventName == 'CreateKeyPair' and event.CloudTrailEvent.requestParameters.keyName == '{{ test_ec2_key_pair_name }}' 11 | action: 12 | debug: 13 | msg: "SUCCESS" 14 | 15 | - name: match key pair deletion event 16 | condition: event.CloudTrailEvent.eventName == 'DeleteKeyPair' and event.CloudTrailEvent.requestParameters.keyName == '{{ test_ec2_key_pair_name }}' 17 | action: 18 | shutdown: 19 | -------------------------------------------------------------------------------- /tests/integration/event_source_kafka/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/event-driven-ansible/fc4e83305b1c1ae32c9374f5bb7aec6876d9733f/tests/integration/event_source_kafka/__init__.py -------------------------------------------------------------------------------- /tests/integration/event_source_kafka/ansible: -------------------------------------------------------------------------------- 1 | ansible 2 | -------------------------------------------------------------------------------- /tests/integration/event_source_kafka/broker_jaas.conf: -------------------------------------------------------------------------------- 1 | KafkaServer { 2 | org.apache.kafka.common.security.plain.PlainLoginModule required 3 | username="test" 4 | password="test" 5 | user_test="test"; 6 | }; 7 | Client{}; 8 | -------------------------------------------------------------------------------- /tests/integration/event_source_kafka/certs-clean.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | DIR=$(dirname "${BASH_SOURCE[0]}") 4 | rm -f "${DIR}/snakeoil-ca.key" \ 5 | "${DIR}/snakeoil-ca.crt" \ 6 | "${DIR}/broker.csr" \ 7 | "${DIR}/broker-ca-signed.crt" \ 8 | "${DIR}/kafka.broker.keystore.jks" \ 9 | "${DIR}/kafka.broker.truststore.jks" 10 | -------------------------------------------------------------------------------- /tests/integration/event_source_kafka/certs-create.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Generate self-signed certificate for Kafka broker 3 | # Greatly inspired by https://github.com/ansibleinc/cp-demo/blob/master/scripts/security/certs-create-per-user.sh 4 | # cspell: ignore keyout passin passout genkey keyalg trustore storetype pkcs certreq srand 5 | set -e 6 | 7 | CA_PATH=$(dirname "${BASH_SOURCE[0]}") 8 | 9 | # Generate CA 10 | openssl req -new -x509 -keyout snakeoil-ca.key -out snakeoil-ca.crt -days 365 -subj '/CN=snakeoil.ansible.com/OU=TEST/O=ANSIBLE/L=Boston/ST=MA/C=US' -passin pass:ansible -passout pass:ansible 11 | 12 | # Create broker keystore 13 | keytool -genkey -noprompt \ 14 | -alias broker \ 15 | -dname "CN=broker,OU=TEST,O=ANSIBLE,L=Boston,S=MA,C=US" \ 16 | -ext "SAN=dns:broker,dns:localhost" \ 17 | -keystore kafka.broker.keystore.jks \ 18 | -keyalg RSA \ 19 | -storepass ansible \ 20 | -keypass ansible \ 21 | -storetype pkcs12 22 | 23 | # Create broker CSR 24 | keytool -keystore kafka.broker.keystore.jks -alias broker -certreq -file broker.csr -storepass ansible -keypass ansible -ext "SAN=dns:broker,dns:localhost" 25 | 26 | # Sign the host certificate with the certificate authority (CA) 27 | # Set a random serial number (avoid problems from using '-CAcreateserial' when parallelizing certificate generation) 28 | CERT_SERIAL=$(awk -v seed="$RANDOM" 'BEGIN { srand(seed); printf("0x%.4x%.4x%.4x%.4x\n", rand()*65535 + 1, rand()*65535 + 1, rand()*65535 + 1, rand()*65535 + 1) }') 29 | openssl x509 -req -CA "${CA_PATH}/snakeoil-ca.crt" -CAkey "${CA_PATH}/snakeoil-ca.key" -in broker.csr -out broker-ca-signed.crt -sha256 -days 365 -set_serial "${CERT_SERIAL}" -passin pass:ansible -extensions v3_req -extfile <(cat < None: 17 | base_dir = os.path.join(TESTS_PATH, EVENT_SOURCE_DIR, "webserver_files") 18 | super().__init__(*args, **kwargs, directory=base_dir) 19 | 20 | def log_message(self, format: str, *args: Any) -> None: 21 | # do not log http.server messages 22 | pass 23 | 24 | 25 | @pytest.fixture(scope="function") 26 | def init_webserver() -> Generator[Any, Any, Any]: 27 | handler = HttpHandler 28 | port: int = 8000 29 | httpd = http.server.HTTPServer(("", port), handler) 30 | thread = threading.Thread(target=httpd.serve_forever) 31 | thread.start() 32 | yield 33 | httpd.shutdown() 34 | 35 | 36 | @pytest.mark.timeout(timeout=DEFAULT_TEST_TIMEOUT, method="signal") 37 | @pytest.mark.parametrize( 38 | "endpoint, expected_resp_data", 39 | [ 40 | pytest.param("", "Endpoint available", id="valid_endpoint"), 41 | pytest.param("nonexistent", "Endpoint unavailable", id="invalid_endpoint"), 42 | ], 43 | ) 44 | def test_url_check_source_sanity( 45 | init_webserver: None, 46 | subprocess_teardown: Callable[..., None], 47 | endpoint: str, 48 | expected_resp_data: str, 49 | ) -> None: 50 | """ 51 | Ensure the url check plugin queries the desired endpoint 52 | and receives the expected response. 53 | """ 54 | 55 | os.environ["URL_ENDPOINT"] = endpoint 56 | 57 | ruleset = os.path.join( 58 | TESTS_PATH, "event_source_url_check", "test_url_check_rules.yml" 59 | ) 60 | 61 | runner = CLIRunner(rules=ruleset, envvars="URL_ENDPOINT").run_in_background() 62 | subprocess_teardown(runner) 63 | 64 | assert runner.stdout is not None 65 | while line := runner.stdout.readline().decode(): 66 | if "msg" in line: 67 | assert f'"msg": "{expected_resp_data}"' in line 68 | break 69 | 70 | 71 | @pytest.mark.timeout(timeout=DEFAULT_TEST_TIMEOUT, method="signal") 72 | def test_url_check_source_error_handling( 73 | subprocess_teardown: Callable[..., None], 74 | ) -> None: 75 | """ 76 | Ensure the url check source plugin responds correctly 77 | when the desired HTTP server is unreachable 78 | """ 79 | 80 | ruleset = os.path.join( 81 | TESTS_PATH, "event_source_url_check", "test_url_check_rules.yml" 82 | ) 83 | 84 | runner = CLIRunner(rules=ruleset).run_in_background() 85 | subprocess_teardown(runner) 86 | 87 | assert runner.stdout is not None 88 | while line := runner.stdout.readline().decode(): 89 | if "msg" in line: 90 | assert "Endpoint down" in line 91 | break 92 | 93 | 94 | @pytest.mark.timeout(timeout=DEFAULT_TEST_TIMEOUT, method="signal") 95 | def test_url_check_source_urls( 96 | init_webserver: None, 97 | ) -> None: 98 | """ 99 | Ensure the url check source plugin reports correctly the status 100 | of all the urls, and not only until the first failure 101 | """ 102 | 103 | ruleset = os.path.join( 104 | TESTS_PATH, "event_source_url_check", "test_url_check_rules_urls.yml" 105 | ) 106 | 107 | runner = CLIRunner(rules=ruleset).run_in_background() 108 | # enough time to checks all the URLs 109 | time.sleep(10) 110 | runner.terminate() 111 | 112 | (stdout, stderr) = runner.communicate() 113 | assert stdout is not None 114 | msgs = [line for line in stdout.decode().splitlines() if "msg" in line] 115 | assert len(msgs) == 3 116 | assert len([line for line in msgs if "Endpoint available" in line]) == 1 117 | assert len([line for line in msgs if "Endpoint unavailable" in line]) == 1 118 | assert len([line for line in msgs if "Endpoint down" in line]) == 1 119 | -------------------------------------------------------------------------------- /tests/integration/event_source_url_check/webserver_files/index.html: -------------------------------------------------------------------------------- 1 | Hello there! 2 | -------------------------------------------------------------------------------- /tests/integration/event_source_webhook/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/event-driven-ansible/fc4e83305b1c1ae32c9374f5bb7aec6876d9733f/tests/integration/event_source_webhook/__init__.py -------------------------------------------------------------------------------- /tests/integration/event_source_webhook/test_webhook_hmac_rules.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: test webhook source plugin 3 | hosts: localhost 4 | sources: 5 | - ansible.eda.webhook: 6 | port: "{{ WH_PORT | default(5000) }}" 7 | hmac_secret: "{{ HMAC_SECRET }}" 8 | hmac_algo: "{{ HMAC_ALGO }}" 9 | rules: 10 | - name: match webhook event 11 | condition: event.payload.ping == "pong" 12 | action: 13 | debug: 14 | msg: "Rule fired successfully" 15 | 16 | - name: shutdown 17 | condition: event.payload.shutdown is defined 18 | action: 19 | shutdown: 20 | -------------------------------------------------------------------------------- /tests/integration/event_source_webhook/test_webhook_rules.yml: -------------------------------------------------------------------------------- 1 | - name: test webhook source plugin 2 | hosts: localhost 3 | sources: 4 | - ansible.eda.webhook: 5 | port: "{{ WH_PORT | default(5000) }}" 6 | token: "{{ SECRET }}" 7 | rules: 8 | - name: match webhook event 9 | condition: event.payload.ping == "pong" 10 | action: 11 | debug: 12 | msg: "Rule fired successfully" 13 | 14 | - name: shutdown 15 | condition: event.payload.shutdown is defined 16 | action: 17 | shutdown: 18 | -------------------------------------------------------------------------------- /tests/integration/integration_config.yaml.template: -------------------------------------------------------------------------------- 1 | aap_hostname: "https://localhost:8443" 2 | aap_username: "admin" 3 | aap_password: "testpass" 4 | aap_validate_certs: false 5 | -------------------------------------------------------------------------------- /tests/integration/targets/controller_token/tasks/create.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright: Contributors to the Ansible project 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | - name: Create controller token in check mode 6 | ansible.eda.controller_token: 7 | name: "{{ controller_token_name }}" 8 | description: "Example controller token description" 9 | token: "TOKEN_VALUE" 10 | state: present 11 | check_mode: true 12 | no_log: true 13 | register: r 14 | 15 | - name: Check controller token creation in check mode 16 | assert: 17 | that: 18 | - r.changed 19 | 20 | - name: Create controller token 21 | ansible.eda.controller_token: 22 | name: "{{ controller_token_name }}" 23 | description: "Example controller token description" 24 | token: "TOKEN_VALUE" 25 | state: present 26 | no_log: true 27 | register: r 28 | 29 | - name: Check controller token creation 30 | assert: 31 | that: 32 | - r.changed 33 | - "'id' in r" 34 | 35 | - name: Create controller token again 36 | ansible.eda.controller_token: 37 | name: "{{ controller_token_name }}" 38 | description: "Example controller token description" 39 | token: "TOKEN_VALUE" 40 | state: present 41 | no_log: true 42 | register: r 43 | 44 | - name: Check controller token creation again 45 | assert: 46 | that: 47 | - r.changed 48 | - "'id' in r" 49 | -------------------------------------------------------------------------------- /tests/integration/targets/controller_token/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright: Contributors to the Ansible project 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | - name: Controller token integration tests 6 | module_defaults: 7 | group/ansible.eda.eda: 8 | aap_hostname: "{{ aap_hostname }}" 9 | aap_username: "{{ aap_username }}" 10 | aap_password: "{{ aap_password }}" 11 | aap_validate_certs: "{{ aap_validate_certs }}" 12 | 13 | block: 14 | - name: Generate a random_string for the test 15 | set_fact: 16 | random_string: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" 17 | when: random_string is not defined 18 | 19 | - name: Generate a ID for the test 20 | set_fact: 21 | test_id: "{{ random_string | to_uuid }}" 22 | when: test_id is not defined 23 | 24 | - name: Define variables for controller token 25 | set_fact: 26 | controller_token_name: "controller_token_{{ test_id }}" 27 | 28 | - include_tasks: create.yml 29 | always: 30 | - name: Clean up - controller token 31 | ansible.eda.controller_token: 32 | name: "{{ item }}" 33 | state: absent 34 | loop: 35 | - "{{ controller_token_name }}" 36 | ignore_errors: true 37 | -------------------------------------------------------------------------------- /tests/integration/targets/decision_environment/tasks/create.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright: Contributors to the Ansible project 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | - name: Create a container registry credential 6 | ansible.eda.credential: 7 | state: present 8 | name: "{{ credential_name }}" 9 | description: "This is a container registry credential" 10 | credential_type_name: "{{ credential_type_name }}" 11 | inputs: 12 | host: "quay.io" 13 | username: "test" 14 | password: "test" 15 | organization_name: Default 16 | register: _result 17 | 18 | - name: Check credential creation 19 | assert: 20 | that: 21 | - _result.changed 22 | 23 | - name: Get information about the credential 24 | ansible.eda.credential_info: 25 | name: "{{ credential_name }}" 26 | register: _result_credential 27 | 28 | - name: Create decision environment in check mode 29 | ansible.eda.decision_environment: 30 | name: "{{ decision_env_name }}" 31 | description: "Example decision environment description" 32 | image_url: "{{ image_url }}" 33 | credential: "{{ credential_name }}" 34 | organization_name: Default 35 | state: present 36 | check_mode: true 37 | register: r 38 | 39 | - name: Check decision environment in check mode 40 | assert: 41 | that: 42 | - r.changed 43 | 44 | - name: Create decision environment 45 | ansible.eda.decision_environment: 46 | name: "{{ decision_env_name }}" 47 | description: "Example decision environment description" 48 | image_url: "{{ image_url }}" 49 | credential: "{{ credential_name }}" 50 | organization_name: Default 51 | state: present 52 | register: r 53 | 54 | - name: Check decision environment creation 55 | assert: 56 | that: 57 | - r.changed 58 | 59 | - name: Get information about the decision environment 60 | ansible.eda.decision_environment_info: 61 | name: "{{ decision_env_name }}" 62 | register: _result_de 63 | 64 | - name: Get credential and DE from above results 65 | set_fact: 66 | _created_de: "{{ _result_de.decision_environments | list | first }}" 67 | _created_credential: "{{ _result_credential.credentials | list | first }}" 68 | 69 | - name: Assert created decision environment has the correct credential 70 | ansible.builtin.assert: 71 | that: 72 | - _created_de["eda_credential_id"] == _created_credential["id"] 73 | 74 | - name: Create decision environment again 75 | ansible.eda.decision_environment: 76 | name: "{{ decision_env_name }}" 77 | description: "Example decision environment description" 78 | image_url: "{{ image_url }}" 79 | credential: "{{ credential_name }}" 80 | organization_name: Default 81 | state: present 82 | register: r 83 | 84 | - name: Check decision environment is not created again 85 | assert: 86 | that: 87 | - not r.changed 88 | 89 | - name: Delete decision environment in check mode 90 | ansible.eda.decision_environment: 91 | name: "{{ decision_env_name }}" 92 | state: absent 93 | check_mode: true 94 | register: r 95 | 96 | - name: Check if decision environment deleted in check mode 97 | assert: 98 | that: 99 | - r.changed 100 | 101 | - name: Delete decision environment 102 | ansible.eda.decision_environment: 103 | name: "{{ decision_env_name }}" 104 | state: absent 105 | register: r 106 | 107 | - name: Check if delete decision environment 108 | assert: 109 | that: 110 | - r.changed 111 | -------------------------------------------------------------------------------- /tests/integration/targets/decision_environment/tasks/delete.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright: Contributors to the Ansible project 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | - name: Delete operation without required name parameter 6 | ansible.eda.decision_environment: 7 | state: absent 8 | ignore_errors: true 9 | register: r 10 | 11 | - name: Check if decision environment name is required 12 | assert: 13 | that: 14 | - r.failed 15 | - "'missing required arguments: name' in r.msg" 16 | 17 | - name: Delete non-existing decision environment in check mode 18 | ansible.eda.decision_environment: 19 | name: Example 20 | state: absent 21 | check_mode: true 22 | register: r 23 | 24 | - name: Check if delete non-existing decision environment in check mode 25 | assert: 26 | that: 27 | - not r.changed 28 | 29 | - name: Delete non-existing decision environment 30 | ansible.eda.decision_environment: 31 | name: Example 32 | state: absent 33 | register: r 34 | 35 | - name: Check if delete non-existing project 36 | assert: 37 | that: 38 | - not r.changed 39 | -------------------------------------------------------------------------------- /tests/integration/targets/decision_environment/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright: Contributors to the Ansible project 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | - name: Decision environment integration tests 6 | module_defaults: 7 | group/ansible.eda.eda: 8 | aap_hostname: "{{ aap_hostname }}" 9 | aap_username: "{{ aap_username }}" 10 | aap_password: "{{ aap_password }}" 11 | aap_validate_certs: "{{ aap_validate_certs }}" 12 | block: 13 | - name: Generate a random_string for the test 14 | set_fact: 15 | random_string: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" 16 | when: random_string is not defined 17 | 18 | - name: Generate a ID for the test 19 | set_fact: 20 | test_id: "{{ random_string | to_uuid }}" 21 | when: test_id is not defined 22 | 23 | - name: Define variables for credential and decision environment 24 | set_fact: 25 | decision_env_name: "Test_Decision_Env_{{ test_id }}" 26 | image_url: "quay.io/ansible/ansible-rulebook:main" 27 | credential_type_name: "Container Registry" 28 | credential_name: "Test_Container_Registry_Credential_{{ test_id }}" 29 | 30 | - include_tasks: create.yml 31 | - include_tasks: delete.yml 32 | - include_tasks: update.yml 33 | always: 34 | - name: Clean up - decision environment 35 | ansible.eda.decision_environment: 36 | name: "{{ item }}" 37 | state: absent 38 | loop: 39 | - "{{ decision_env_name }}" 40 | - "{{ decision_env_name }}_new" 41 | ignore_errors: true 42 | 43 | - name: Clean up - credential 44 | ansible.eda.credential: 45 | name: "{{ credential_name }}" 46 | state: absent 47 | ignore_errors: true 48 | -------------------------------------------------------------------------------- /tests/integration/targets/decision_environment/tasks/update.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright: Contributors to the Ansible project 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | - name: Create decision environment 6 | ansible.eda.decision_environment: 7 | name: "{{ decision_env_name }}" 8 | description: "Example decision environment description" 9 | image_url: "{{ image_url }}" 10 | organization_name: Default 11 | state: present 12 | register: r 13 | 14 | - name: Check decision environment creation 15 | assert: 16 | that: 17 | - r.changed 18 | 19 | - name: Update decision environment name 20 | ansible.eda.decision_environment: 21 | name: "{{ decision_env_name }}" 22 | new_name: "{{ decision_env_name }}_new" 23 | description: "Example decision environment description" 24 | image_url: "{{ image_url }}" 25 | organization_name: Default 26 | state: present 27 | register: r 28 | 29 | - name: Check decision environment update 30 | assert: 31 | that: 32 | - r.changed 33 | 34 | - name: Update decision environment again 35 | ansible.eda.decision_environment: 36 | name: "{{ decision_env_name }}_new" 37 | new_name: "{{ decision_env_name }}_new" 38 | description: "Example decision environment description" 39 | image_url: "{{ image_url }}" 40 | organization_name: Default 41 | state: present 42 | register: r 43 | 44 | - name: Check decision environment is not updated again 45 | assert: 46 | that: 47 | - not r.changed 48 | 49 | - name: Delete updated decision environment 50 | ansible.eda.decision_environment: 51 | name: "{{ decision_env_name }}_new" 52 | state: absent 53 | register: r 54 | 55 | - name: Check if delete decision environment 56 | assert: 57 | that: 58 | - r.changed 59 | -------------------------------------------------------------------------------- /tests/integration/targets/decision_environment_info/tasks/list.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright: Contributors to the Ansible project 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | - name: Create decision environment 6 | ansible.eda.decision_environment: 7 | name: "{{ decision_env_name }}" 8 | description: "Example decision environment description" 9 | image_url: "{{ image_url }}" 10 | organization_name: Default 11 | state: present 12 | register: r 13 | 14 | - name: Check decision environment creation 15 | assert: 16 | that: 17 | - r.changed 18 | 19 | - name: List all decision environments in check mode 20 | ansible.eda.decision_environment_info: 21 | check_mode: true 22 | register: r 23 | 24 | - name: Check if list decision environments in check mode 25 | assert: 26 | that: 27 | - not r.changed 28 | - "'decision_environments' in r" 29 | 30 | - name: List all decision environments 31 | ansible.eda.decision_environment_info: 32 | register: r 33 | 34 | - name: Check if list decision environments 35 | assert: 36 | that: 37 | - not r.changed 38 | - "'decision_environments' in r" 39 | 40 | - name: List a particular decision environment 41 | ansible.eda.decision_environment_info: 42 | name: "{{ decision_env_name }}" 43 | register: r 44 | 45 | - name: Check if list decision environments 46 | assert: 47 | that: 48 | - not r.changed 49 | - "'decision_environments' in r" 50 | - "r['decision_environments'][0]['name'] == decision_env_name" 51 | 52 | - name: Delete decision environment 53 | ansible.eda.decision_environment: 54 | name: "{{ decision_env_name }}" 55 | state: absent 56 | register: r 57 | 58 | - name: Check if delete decision environment 59 | assert: 60 | that: 61 | - r.changed 62 | -------------------------------------------------------------------------------- /tests/integration/targets/decision_environment_info/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright: Contributors to the Ansible project 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | - name: Decision environment info module integration tests 6 | module_defaults: 7 | group/ansible.eda.eda: 8 | aap_hostname: "{{ aap_hostname }}" 9 | aap_username: "{{ aap_username }}" 10 | aap_password: "{{ aap_password }}" 11 | aap_validate_certs: "{{ aap_validate_certs }}" 12 | 13 | block: 14 | - name: Generate a random_string for the test 15 | set_fact: 16 | random_string: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" 17 | when: random_string is not defined 18 | 19 | - name: Generate a ID for the test 20 | set_fact: 21 | test_id: "{{ random_string | to_uuid }}" 22 | when: test_id is not defined 23 | 24 | - name: Define variables for credential and decision environment 25 | set_fact: 26 | decision_env_name: "Test_Decision_Env_{{ test_id }}" 27 | image_url: "quay.io/ansible/ansible-rulebook:main" 28 | 29 | - include_tasks: list.yml 30 | always: 31 | - name: Clean up - decision environment 32 | ansible.eda.decision_environment: 33 | name: "{{ item }}" 34 | state: absent 35 | loop: 36 | - "{{ decision_env_name }}" 37 | ignore_errors: true 38 | -------------------------------------------------------------------------------- /tests/integration/targets/project/tasks/create.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright: Contributors to the Ansible project 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | - name: Create project in check mode 6 | ansible.eda.project: 7 | name: "{{ project_name }}" 8 | url: "{{ url }}" 9 | description: "Example project description" 10 | organization_name: Default 11 | state: present 12 | check_mode: true 13 | register: r 14 | 15 | - name: Check project creation in check mode 16 | assert: 17 | that: 18 | - r.changed 19 | 20 | - name: Create project 21 | ansible.eda.project: 22 | name: "{{ project_name }}" 23 | description: "Example project description" 24 | url: "{{ url }}" 25 | proxy: "{{ proxy }}" 26 | organization_name: Default 27 | state: present 28 | register: r 29 | 30 | - name: Check project creation 31 | assert: 32 | that: 33 | - r.changed 34 | 35 | - name: Create project again 36 | ansible.eda.project: 37 | name: "{{ project_name }}" 38 | description: "Example project description" 39 | url: "{{ url }}" 40 | proxy: "{{ proxy }}" 41 | organization_name: Default 42 | state: present 43 | register: r 44 | 45 | - name: Check project is not created again 46 | assert: 47 | that: 48 | - not r.changed 49 | 50 | - name: Delete project in check mode 51 | ansible.eda.project: 52 | name: "{{ project_name }}" 53 | state: absent 54 | check_mode: true 55 | register: r 56 | 57 | - name: Check if delete project in check mode 58 | assert: 59 | that: 60 | - r.changed 61 | 62 | - name: Delete project 63 | ansible.eda.project: 64 | name: "{{ project_name }}" 65 | state: absent 66 | register: r 67 | 68 | - name: Check if delete project 69 | assert: 70 | that: 71 | - r.changed 72 | 73 | - name: Check create project with partial name match 74 | loop: 75 | - "{{ project_name }}_test_partial" 76 | - "{{ project_name }}" 77 | ansible.eda.project: 78 | name: "{{ item }}" 79 | url: "{{ url }}" 80 | description: "Example project description" 81 | organization_name: Default 82 | state: present 83 | register: r 84 | 85 | - name: Check if create projects with partial name match 86 | loop: "{{ r.results }}" 87 | assert: 88 | that: 89 | - item.changed 90 | 91 | - name: Delete project with partial name match 92 | loop: 93 | - "{{ project_name }}" 94 | - "{{ project_name }}_test_partial" 95 | ansible.eda.project: 96 | name: "{{ item }}" 97 | state: absent 98 | 99 | - name: Check create project with missing url 100 | ansible.eda.project: 101 | name: "{{ project_name }}" 102 | description: "Example project description" 103 | organization_name: Default 104 | state: present 105 | register: r 106 | ignore_errors: true 107 | 108 | - name: Check if missing url is required 109 | assert: 110 | that: 111 | - r.failed 112 | - "'Parameter url is required' in r.msg" 113 | -------------------------------------------------------------------------------- /tests/integration/targets/project/tasks/delete.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright: Contributors to the Ansible project 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | - name: Delete operation without required name parameter 6 | ansible.eda.project: 7 | state: absent 8 | ignore_errors: true 9 | register: r 10 | 11 | - name: Check if project name is required 12 | assert: 13 | that: 14 | - r.failed 15 | - "'missing required arguments: name' in r.msg" 16 | 17 | - name: Delete non-existing project in check mode 18 | ansible.eda.project: 19 | name: "{{ project_name }}" 20 | state: absent 21 | check_mode: true 22 | register: r 23 | 24 | - name: Check if delete non-existing project in check mode 25 | assert: 26 | that: 27 | - not r.changed 28 | 29 | - name: Delete non-existing project 30 | ansible.eda.project: 31 | name: "{{ project_name }}_ee" 32 | state: absent 33 | register: r 34 | 35 | - name: Check if delete non-existing project 36 | assert: 37 | that: 38 | - not r.changed 39 | -------------------------------------------------------------------------------- /tests/integration/targets/project/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright: Contributors to the Ansible project 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | - name: Project integration tests 5 | module_defaults: 6 | group/ansible.eda.eda: 7 | aap_hostname: "{{ aap_hostname }}" 8 | aap_username: "{{ aap_username }}" 9 | aap_password: "{{ aap_password }}" 10 | aap_validate_certs: "{{ aap_validate_certs }}" 11 | block: 12 | - name: Generate a random_string for the test 13 | set_fact: 14 | random_string: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" 15 | when: random_string is not defined 16 | 17 | - name: Generate a ID for the test 18 | set_fact: 19 | test_id: "{{ random_string | to_uuid }}" 20 | when: test_id is not defined 21 | 22 | - name: Define variables for credential and decision environment 23 | set_fact: 24 | project_name: "test_project_{{ test_id }}" 25 | url: "https://github.com/ansible/eda-sample-project" 26 | scm_branch: "stable-1.1" 27 | proxy: "http://eda:testpass@host.docker.internal:3128" 28 | 29 | 30 | - include_tasks: create.yml 31 | - include_tasks: delete.yml 32 | - include_tasks: update.yml 33 | - include_tasks: sync.yml 34 | - include_tasks: scm_branch.yml 35 | always: 36 | - name: Clean up - project 37 | ansible.eda.project: 38 | name: "{{ item }}" 39 | state: absent 40 | loop: 41 | - "{{ project_name }}" 42 | - "{{ project_name }}_new" 43 | ignore_errors: true 44 | -------------------------------------------------------------------------------- /tests/integration/targets/project/tasks/scm_branch.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright: Contributors to the Ansible project 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | - name: Create non existing project with branch 6 | block: 7 | - name: Create non existing project with branch 8 | ansible.eda.project: 9 | name: "{{ project_name }}_test_branch" 10 | url: "{{ url }}" 11 | description: "Example project description" 12 | organization_name: Default 13 | state: present 14 | sync: true 15 | scm_branch: "{{ scm_branch }}" 16 | register: r 17 | 18 | - name: Check project with branch creation 19 | assert: 20 | that: 21 | - r.changed 22 | 23 | - name: Wait for project sync state with branch 24 | ansible.eda.project_info: 25 | name: "{{ project_name }}_test_branch" 26 | register: r_info 27 | until: 28 | - r_info.projects[0].modified_at != r_info.projects[0].created_at 29 | - r_info.projects[0].scm_branch == scm_branch 30 | retries: 30 31 | delay: 1 32 | 33 | - name: Edit existing project with branch main 34 | ansible.eda.project: 35 | name: "{{ project_name }}_test_branch" 36 | url: "{{ url }}" 37 | description: "Example project description" 38 | organization_name: Default 39 | state: present 40 | sync: true 41 | scm_branch: "main" 42 | register: re 43 | 44 | - name: Wait for project sync state with edit branch 45 | ansible.eda.project_info: 46 | name: "{{ project_name }}_test_branch" 47 | register: r_info 48 | until: 49 | - r_info.projects[0].modified_at != r_info.projects[0].created_at 50 | - r_info.projects[0].scm_branch == "main" 51 | retries: 30 52 | delay: 1 53 | 54 | always: 55 | - name: Clean up - project 56 | ansible.eda.project: 57 | name: "{{ project_name }}_test_branch" 58 | state: absent 59 | ignore_errors: true 60 | -------------------------------------------------------------------------------- /tests/integration/targets/project/tasks/sync.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright: Contributors to the Ansible project 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | - name: Create non existing project with sync 6 | block: 7 | - name: Create non existing project with sync 8 | ansible.eda.project: 9 | name: "{{ project_name }}_test_sync" 10 | url: "{{ url }}" 11 | description: "Example project description" 12 | organization_name: Default 13 | state: present 14 | sync: true 15 | register: r 16 | 17 | - name: Check project creation 18 | assert: 19 | that: 20 | - r.changed 21 | always: 22 | - name: Clean up - project 23 | ansible.eda.project: 24 | name: "{{ project_name }}_test_sync" 25 | state: absent 26 | ignore_errors: true 27 | 28 | - name: Sync existing project 29 | block: 30 | - name: Create project before sync 31 | ansible.eda.project: 32 | name: "{{ project_name }}_test_sync" 33 | url: "{{ url }}" 34 | description: "Example project description" 35 | organization_name: Default 36 | state: present 37 | register: r 38 | 39 | # need to wait for project creation otherwise sync can fail 40 | - name: Wait for project creation 41 | pause: 42 | seconds: 5 43 | 44 | - name: Sync project 45 | ansible.eda.project: 46 | name: "{{ project_name }}_test_sync" 47 | sync: true 48 | register: r 49 | 50 | - name: Get info project after sync 51 | ansible.eda.project_info: 52 | name: "{{ project_name }}_test_sync" 53 | register: r_info 54 | 55 | - name: Check project sync 56 | assert: 57 | that: 58 | - r.changed 59 | - r_info.projects[0].modified_at != r_info.projects[0].created_at 60 | always: 61 | - name: Clean up - project 62 | ansible.eda.project: 63 | name: "{{ project_name }}_test_sync" 64 | state: absent 65 | ignore_errors: true 66 | 67 | - name: Check wrong parameters with sync enabled 68 | block: 69 | - name: Try to sync non existing project without url 70 | ansible.eda.project: 71 | name: "{{ project_name }}_test_sync" 72 | sync: true 73 | register: r 74 | ignore_errors: true 75 | 76 | - name: Check if sync non existing project without url 77 | assert: 78 | that: 79 | - r.failed 80 | - "'Parameter url is required' in r.msg" 81 | always: 82 | - name: Clean up - project 83 | ansible.eda.project: 84 | name: "{{ project_name }}_test_sync" 85 | state: absent 86 | ignore_errors: true 87 | -------------------------------------------------------------------------------- /tests/integration/targets/project/tasks/update.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright: Contributors to the Ansible project 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | - name: Create project 6 | ansible.eda.project: 7 | name: "{{ project_name }}" 8 | url: "{{ url }}" 9 | description: "Example project description" 10 | organization_name: Default 11 | state: present 12 | register: r 13 | 14 | - name: Check project creation 15 | assert: 16 | that: 17 | - r.changed 18 | 19 | - name: Update project name 20 | ansible.eda.project: 21 | name: "{{ project_name }}" 22 | url: "{{ url }}" 23 | new_name: "{{ project_name }}_new" 24 | description: "Example project description" 25 | organization_name: Default 26 | state: present 27 | register: r 28 | 29 | - name: Check project update 30 | assert: 31 | that: 32 | - r.changed 33 | 34 | - name: Update project url 35 | ansible.eda.project: 36 | name: "{{ project_name }}_new" 37 | new_name: "{{ project_name }}_new" 38 | description: "Example project description" 39 | url: "https://example.com/project1" 40 | organization_name: Default 41 | state: present 42 | register: r 43 | 44 | - name: Check project update 45 | assert: 46 | that: 47 | - r.changed 48 | 49 | - name: Update project again 50 | ansible.eda.project: 51 | name: "{{ project_name }}_new" 52 | new_name: "{{ project_name }}_new" 53 | description: "Example project description" 54 | url: "https://example.com/project1" 55 | organization_name: Default 56 | state: present 57 | register: r 58 | 59 | - name: Check project is not updated again 60 | assert: 61 | that: 62 | - not r.changed 63 | 64 | - name: Delete updated project 65 | ansible.eda.project: 66 | name: "{{ project_name }}_new" 67 | state: absent 68 | register: r 69 | 70 | - name: Check if delete project 71 | assert: 72 | that: 73 | - r.changed 74 | -------------------------------------------------------------------------------- /tests/integration/targets/project_info/tasks/create.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright: Contributors to the Ansible project 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | - name: Create a project 6 | ansible.eda.project: 7 | name: Example 8 | description: "Example project description" 9 | url: "https://example.com/project1" 10 | organization_name: Default 11 | state: present 12 | register: r 13 | 14 | - name: Check project creation 15 | assert: 16 | that: 17 | - r.changed 18 | -------------------------------------------------------------------------------- /tests/integration/targets/project_info/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright: Contributors to the Ansible project 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | - name: Project info module integration tests 6 | module_defaults: 7 | group/ansible.eda.eda: 8 | aap_hostname: "{{ aap_hostname }}" 9 | aap_username: "{{ aap_username }}" 10 | aap_password: "{{ aap_password }}" 11 | aap_validate_certs: "{{ aap_validate_certs }}" 12 | 13 | block: 14 | - include_tasks: create.yml 15 | - name: List all projects in the given EDA Controller in check mode 16 | ansible.eda.project_info: 17 | check_mode: true 18 | register: r 19 | 20 | - name: Check if all the projects are returned in check mode 21 | assert: 22 | that: 23 | - "'projects' in r" 24 | 25 | - name: List all projects in the given EDA Controller 26 | ansible.eda.project_info: 27 | register: r 28 | 29 | - name: Check if all the projects are returned 30 | assert: 31 | that: 32 | - "'projects' in r" 33 | 34 | - name: List a particular project in the given EDA Controller 35 | ansible.eda.project_info: 36 | name: "Example" 37 | register: r 38 | 39 | - name: Check if the project is returned 40 | assert: 41 | that: 42 | - "'projects' in r" 43 | - "'Example' in r['projects'][0]['name']" 44 | 45 | - name: List a non-existing particular project in the given EDA Controller 46 | ansible.eda.project_info: 47 | name: "Example2" 48 | register: r 49 | 50 | - name: Check if all the projects are returned 51 | assert: 52 | that: 53 | - "'projects' in r" 54 | - "r['projects'] == []" 55 | 56 | always: 57 | - name: Clean up - project 58 | ansible.eda.project: 59 | name: "{{ item }}" 60 | state: absent 61 | loop: 62 | - Example 63 | ignore_errors: true 64 | -------------------------------------------------------------------------------- /tests/integration/targets/user/tasks/create.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright: Contributors to the Ansible project 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | - name: Create super user in check mode 6 | ansible.eda.user: 7 | username: "{{ user_name }}" 8 | first_name: "{{ first_name }}" 9 | last_name: "{{ last_name }}" 10 | email: "{{ email }}" 11 | password: "{{ random_string }}" 12 | is_superuser: true 13 | state: present 14 | check_mode: true 15 | register: r 16 | 17 | - name: Check super user creation in check mode 18 | assert: 19 | that: 20 | - r.changed 21 | 22 | - name: Create super user 23 | ansible.eda.user: 24 | username: "{{ user_name }}" 25 | first_name: "{{ first_name }}" 26 | last_name: "{{ last_name }}" 27 | email: "{{ email }}" 28 | password: "{{ random_string }}" 29 | is_superuser: true 30 | state: present 31 | register: r 32 | 33 | - name: Check super user creation 34 | assert: 35 | that: 36 | - r.changed 37 | 38 | - name: Create super user again 39 | ansible.eda.user: 40 | username: "{{ user_name }}" 41 | first_name: "{{ first_name }}" 42 | last_name: "{{ last_name }}" 43 | email: "{{ email }}" 44 | password: "{{ random_string }}" 45 | is_superuser: true 46 | state: present 47 | register: r 48 | 49 | - name: Check super user creation again 50 | assert: 51 | that: 52 | - r.changed 53 | 54 | - name: Delete super user 55 | ansible.eda.user: 56 | username: "{{ user_name }}" 57 | state: absent 58 | register: r 59 | 60 | - name: Check super user deleted 61 | assert: 62 | that: 63 | - r.changed 64 | 65 | - name: Create normal user in check mode 66 | ansible.eda.user: 67 | username: "{{ user_name }}" 68 | first_name: "{{ first_name }}" 69 | last_name: "{{ last_name }}" 70 | email: "{{ email }}" 71 | password: "{{ random_string }}" 72 | is_superuser: false 73 | state: present 74 | check_mode: true 75 | register: r 76 | 77 | - name: Check normal user creation in check mode 78 | assert: 79 | that: 80 | - r.changed 81 | 82 | - name: Create normal user 83 | ansible.eda.user: 84 | username: "{{ user_name }}" 85 | first_name: "{{ first_name }}" 86 | last_name: "{{ last_name }}" 87 | email: "{{ email }}" 88 | password: "{{ random_string }}" 89 | is_superuser: false 90 | state: present 91 | register: r 92 | 93 | - name: Check normal user creation 94 | assert: 95 | that: 96 | - r.changed 97 | 98 | - name: Create normal user again 99 | ansible.eda.user: 100 | username: "{{ user_name }}" 101 | first_name: "{{ first_name }}" 102 | last_name: "{{ last_name }}" 103 | email: "{{ email }}" 104 | password: "{{ random_string }}" 105 | is_superuser: false 106 | state: present 107 | register: r 108 | 109 | - name: Check normal user creation again 110 | assert: 111 | that: 112 | - r.changed 113 | 114 | - name: Delete normal user 115 | ansible.eda.user: 116 | username: "{{ user_name }}" 117 | state: absent 118 | register: r 119 | 120 | - name: Check normal user deleted 121 | assert: 122 | that: 123 | - r.changed 124 | -------------------------------------------------------------------------------- /tests/integration/targets/user/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright: Contributors to the Ansible project 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | - name: User integration tests 6 | module_defaults: 7 | group/ansible.eda.eda: 8 | aap_hostname: "{{ aap_hostname }}" 9 | aap_username: "{{ aap_username }}" 10 | aap_password: "{{ aap_password }}" 11 | aap_validate_certs: "{{ aap_validate_certs }}" 12 | block: 13 | - name: Generate a random_string for the test 14 | set_fact: 15 | random_string: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" 16 | when: random_string is not defined 17 | 18 | - name: Define variables for user 19 | set_fact: 20 | user_name: "test_user_{{ random_string }}" 21 | first_name: "first_{{ random_string }}" 22 | last_name: "last_{{ random_string }}" 23 | email: "test_user_{{ random_string }}@example.com" 24 | 25 | - include_tasks: create.yml 26 | - include_tasks: update.yml 27 | always: 28 | - name: Clean up - user 29 | ansible.eda.project: 30 | name: "{{ item }}" 31 | state: absent 32 | loop: 33 | - "{{ user_name }}" 34 | ignore_errors: true 35 | -------------------------------------------------------------------------------- /tests/integration/targets/user/tasks/update.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright: Contributors to the Ansible project 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | - name: Create super user 6 | ansible.eda.user: 7 | username: "{{ user_name }}" 8 | first_name: "{{ first_name }}" 9 | last_name: "{{ last_name }}" 10 | email: "{{ email }}" 11 | password: "{{ random_string }}" 12 | is_superuser: true 13 | state: present 14 | register: r 15 | 16 | - name: Check super user creation 17 | assert: 18 | that: 19 | - r.changed 20 | 21 | - name: Update super user 22 | ansible.eda.user: 23 | username: "{{ user_name }}" 24 | new_username: "{{ user_name }}_new" 25 | first_name: "{{ first_name }}_new" 26 | last_name: "{{ last_name }}_new" 27 | email: "new_{{ email }}" 28 | password: "{{ random_string }}" 29 | is_superuser: true 30 | state: present 31 | register: r 32 | 33 | - name: Check super user update 34 | assert: 35 | that: 36 | - r.changed 37 | 38 | - name: Delete super user 39 | ansible.eda.user: 40 | username: "{{ user_name }}_new" 41 | state: absent 42 | register: r 43 | 44 | - name: Check super user deleted 45 | assert: 46 | that: 47 | - r.changed 48 | -------------------------------------------------------------------------------- /tests/integration/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from dataclasses import dataclass 4 | from typing import Any, List, Optional 5 | 6 | from . import TESTS_PATH 7 | 8 | DEFAULT_TEST_TIMEOUT: int = 25 9 | 10 | 11 | @dataclass 12 | class CLIRunner: 13 | """ 14 | Wrapper of subprocess.run to compose cmd's for ansible-rulebook CLI 15 | """ 16 | 17 | cwd: str = TESTS_PATH 18 | base_cmd: str = "ansible-rulebook" 19 | inventory: str = os.path.join(TESTS_PATH, "default_inventory.yml") 20 | rules: Optional[str] = None 21 | sources: Optional[str] = None 22 | extra_vars: Optional[str] = None 23 | envvars: Optional[str] = None 24 | proc_id: Optional[str] = None 25 | verbose: bool = False 26 | debug: bool = False 27 | timeout: float = 10.0 28 | env: Optional[dict[str, str]] = None 29 | 30 | def __post_init__(self) -> None: 31 | self.env = os.environ.copy() if self.env is None else self.env 32 | 33 | def _process_args(self) -> List[str]: 34 | args = [ 35 | self.base_cmd, 36 | ] 37 | 38 | args.extend(["-i", self.inventory]) 39 | 40 | if self.rules: 41 | args.extend(["--rulebook", self.rules]) 42 | if self.sources: 43 | args.extend(["-S", self.sources]) 44 | if self.extra_vars: 45 | args.extend(["--vars", self.extra_vars]) 46 | if self.envvars: 47 | args.extend(["--env-vars", self.envvars]) 48 | if self.proc_id: 49 | args.extend(["--id", self.proc_id]) 50 | if self.verbose: 51 | args.append("-v") 52 | if self.debug: 53 | args.append("-vv") 54 | 55 | return args 56 | 57 | def run(self) -> subprocess.CompletedProcess[Any]: 58 | args = self._process_args() 59 | print("Running command: ", " ".join(args)) 60 | return subprocess.run( 61 | args, 62 | cwd=self.cwd, 63 | capture_output=True, 64 | timeout=self.timeout, 65 | check=True, 66 | env=self.env, 67 | ) 68 | 69 | def run_in_background(self) -> subprocess.Popen[bytes]: 70 | args = self._process_args() 71 | print("Running command: ", " ".join(args)) 72 | return subprocess.Popen( 73 | args, 74 | cwd=self.cwd, 75 | stdout=subprocess.PIPE, 76 | stderr=subprocess.PIPE, 77 | env=self.env, 78 | ) 79 | -------------------------------------------------------------------------------- /tests/run-staging: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | # cspell: disable euxo 3 | set -euxo pipefail 4 | 5 | if [ -d "${TOX_WORK_DIR}/eda-server" ]; then 6 | git -C "${TOX_WORK_DIR}/eda-server" pull 7 | else 8 | git clone -q https://github.com/ansible/eda-server.git "${TOX_WORK_DIR}/eda-server" 9 | fi 10 | 11 | # Start EDA API Server 12 | bash -c 'cd ${TOX_WORK_DIR}/eda-server/tools/docker && \ 13 | docker compose -p eda -f docker-compose-stage.yaml pull -q && \ 14 | docker compose -p eda -f docker-compose-stage.yaml --profile proxy up -d && \ 15 | until curl -s http://localhost:8000/_healthz | grep -q "OK"; do \ 16 | echo "Waiting for API to be ready..."; sleep 1; done' 17 | 18 | # Create integration_config.yml 19 | python -c "import os; config = 'aap_hostname: \"{}\"\naap_username: \"{}\"\naap_password: \"{}\"\naap_validate_certs: {}'.format(os.getenv('EDA_CONTROLLER_HOST'), os.getenv('EDA_CONTROLLER_USERNAME'), os.getenv('EDA_CONTROLLER_PASSWORD'), os.getenv('EDA_CONTROLLER_VERIFY_SSL').lower()); open('tests/integration/integration_config.yml', 'w').write(config)" 20 | 21 | # Run ansible integration tests while passing tox {posargs} 22 | ansible-test integration -v --requirements --color --coverage --truncate 0 "$@" 23 | 24 | ansible-test coverage report --requirements --omit '.tox/*,tests/*' --color --all --show-missing -v --truncate 0 25 | ansible-test coverage combine "--export=${TOX_ENV_DIR:-.}" 26 | 27 | # Clean up containers 28 | bash -c 'cd ${TOX_WORK_DIR}/eda-server/tools/docker && docker compose -p eda -f docker-compose-stage.yaml --profile proxy down' 29 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/event-driven-ansible/fc4e83305b1c1ae32c9374f5bb7aec6876d9733f/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/event_filter/test_insert_hosts_to_meta.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | import pytest 4 | 5 | from extensions.eda.plugins.event_filter.insert_hosts_to_meta import PathNotExistError 6 | from extensions.eda.plugins.event_filter.insert_hosts_to_meta import main as hosts_main 7 | 8 | EVENT_DATA_1 = [ 9 | ( 10 | {"app": {"target": "webserver"}}, 11 | {"host_path": "app.target"}, 12 | ["webserver"], 13 | ), 14 | ( 15 | {"app": {"target": "webserver;postgres"}}, 16 | { 17 | "host_path": "app/target", 18 | "path_separator": "/", 19 | "host_separator": ";", 20 | }, 21 | ["webserver", "postgres"], 22 | ), 23 | ( 24 | { 25 | "app": {"target": ["webserver", "postgres"]}, 26 | "meta": {"source": "upstream"}, 27 | }, 28 | { 29 | "host_path": "app.target", 30 | }, 31 | ["webserver", "postgres"], 32 | ), 33 | ( 34 | {"app": "foo", "meta": {"source": "upstream"}}, 35 | { 36 | "host_path": "bar", 37 | }, 38 | [], 39 | ), 40 | ] 41 | 42 | 43 | @pytest.mark.parametrize("data, args, expected_hosts", EVENT_DATA_1) 44 | def test_find_hosts( 45 | data: dict[str, Any], args: dict[str, str], expected_hosts: list[str] 46 | ) -> None: 47 | data = hosts_main(data, **args) # type: ignore 48 | if expected_hosts: 49 | assert data["meta"]["hosts"] == expected_hosts 50 | else: 51 | assert "hosts" not in data["meta"] 52 | 53 | 54 | @pytest.mark.parametrize( 55 | "data, args", 56 | [ 57 | pytest.param( 58 | {"app": {"target": 5000}}, 59 | {"host_path": "app.target"}, 60 | ), 61 | pytest.param( 62 | {"app": {"target": ("host1", 5000)}}, 63 | {"host_path": "app.target"}, 64 | ), 65 | pytest.param( 66 | {"app": {"target": {"foo": "bar"}}}, 67 | {"host_path": "app.target"}, 68 | ), 69 | ], 70 | ) 71 | def test_fail_find_hosts(data: dict[str, Any], args: dict[str, str]) -> None: 72 | with pytest.raises(TypeError): 73 | hosts_main(data, **args) # type: ignore 74 | 75 | 76 | def test_host_path_not_exist() -> None: 77 | event = {"app": {"target": 5000}} 78 | host_path = "app.bad" 79 | assert hosts_main(event, host_path=host_path) == event 80 | 81 | 82 | def test_host_path_not_exist_exception() -> None: 83 | event = {"app": {"target": 5000}} 84 | host_path = "app.bad" 85 | with pytest.raises(PathNotExistError): 86 | hosts_main(event, host_path=host_path, raise_error=True) 87 | -------------------------------------------------------------------------------- /tests/unit/event_filter/test_normalize_keys.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | import pytest 4 | 5 | from extensions.eda.plugins.event_filter.normalize_keys import main as normalize_main 6 | 7 | TEST_DATA_1 = [ 8 | ( 9 | {"app": {"tar/get": "web/server"}}, 10 | True, 11 | {"app": {"tar_get": "web/server"}}, 12 | ), 13 | ( 14 | {"a.p.p": {"t?r/get": "?web/server"}}, 15 | True, 16 | {"a_p_p": {"t_r_get": "?web/server"}}, 17 | ), 18 | ( 19 | {"key1": {"key2": {"key?/+12-*": "?web/server"}}}, 20 | True, 21 | {"key1": {"key2": {"key_12_": "?web/server"}}}, 22 | ), 23 | ( 24 | {"key1": {"key?2": "abc", "key_2": "345"}}, 25 | False, 26 | {"key1": {"key_2": "345"}}, 27 | ), 28 | ( 29 | {"key1": {"key_2": "abc", "key?2": "345"}}, 30 | True, 31 | {"key1": {"key_2": "345"}}, 32 | ), 33 | ( 34 | {"key1": [{"key/2": "abc"}, {"key?2": "345"}]}, 35 | True, 36 | {"key1": [{"key_2": "abc"}, {"key_2": "345"}]}, 37 | ), 38 | ( 39 | {"key1": [{"key/2": "abc"}, 5]}, 40 | True, 41 | {"key1": [{"key_2": "abc"}, 5]}, 42 | ), 43 | ] 44 | 45 | 46 | @pytest.mark.parametrize("event, overwrite, updated_event", TEST_DATA_1) 47 | def test_normalize_keys( 48 | event: dict[str, Any], overwrite: bool, updated_event: dict[str, Any] 49 | ) -> None: 50 | data = normalize_main(event, overwrite) 51 | assert data == updated_event 52 | -------------------------------------------------------------------------------- /tests/unit/event_source/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/event-driven-ansible/fc4e83305b1c1ae32c9374f5bb7aec6876d9733f/tests/unit/event_source/__init__.py -------------------------------------------------------------------------------- /tests/unit/event_source/test_alertmanager.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Any 3 | 4 | import aiohttp 5 | import pytest 6 | 7 | from extensions.eda.plugins.event_source.alertmanager import main as alert_main 8 | 9 | 10 | async def start_server(queue: asyncio.Queue[Any], args: dict[str, Any]) -> None: 11 | await alert_main(queue, args) 12 | 13 | 14 | async def post_code(server_task: asyncio.Task[None], info: dict[str, Any]) -> None: 15 | url = f'http://{info["host"]}/{info["endpoint"]}' 16 | payload = info["payload"] 17 | 18 | async with aiohttp.ClientSession() as session: 19 | async with session.post(url, json=payload) as resp: 20 | print(resp.status) 21 | 22 | server_task.cancel() 23 | 24 | 25 | async def cancel_code(server_task: asyncio.Task[None]) -> None: 26 | server_task.cancel() 27 | 28 | 29 | @pytest.mark.asyncio 30 | async def test_cancel() -> None: 31 | queue: asyncio.Queue[Any] = asyncio.Queue() 32 | 33 | args = {"host": "localhost", "port": 8001} 34 | plugin_task = asyncio.create_task(start_server(queue, args)) 35 | cancel_task = asyncio.create_task(cancel_code(plugin_task)) 36 | 37 | with pytest.raises(asyncio.CancelledError): 38 | await asyncio.gather(plugin_task, cancel_task) 39 | 40 | 41 | @pytest.mark.asyncio 42 | async def test_post_endpoint_with_default() -> None: 43 | queue: asyncio.Queue[Any] = asyncio.Queue() 44 | 45 | args = {"host": "localhost", "port": 8002} 46 | plugin_task = asyncio.create_task(start_server(queue, args)) 47 | 48 | task_info = { 49 | "payload": { 50 | "alerts": [ 51 | { 52 | "message": "abc", 53 | "labels": {"instance": "host1"}, 54 | }, 55 | { 56 | "message": "xyz", 57 | "labels": {"instance": "host2"}, 58 | }, 59 | ] 60 | }, 61 | "endpoint": "test", 62 | "host": f'{args["host"]}:{args["port"]}', 63 | } 64 | 65 | post_task = asyncio.create_task(post_code(plugin_task, task_info)) 66 | 67 | await asyncio.gather(plugin_task, post_task) 68 | 69 | data = await queue.get() 70 | assert data["payload"] == task_info["payload"] 71 | assert data["meta"]["endpoint"] == task_info["endpoint"] 72 | assert data["meta"]["headers"]["Host"] == task_info["host"] 73 | 74 | data = await queue.get() 75 | assert isinstance(data, dict) 76 | assert isinstance(task_info["payload"], dict) 77 | assert isinstance(task_info["payload"]["alerts"], list) 78 | assert data["alert"] == task_info["payload"]["alerts"][0] 79 | assert data["meta"]["hosts"] == ["host1"] 80 | 81 | data = await queue.get() 82 | assert isinstance(data, dict) 83 | assert data["alert"] == task_info["payload"]["alerts"][1] 84 | assert data["meta"]["hosts"] == ["host2"] 85 | 86 | assert queue.empty() 87 | 88 | 89 | @pytest.mark.asyncio 90 | async def test_post_endpoint_with_options() -> None: 91 | queue: asyncio.Queue[Any] = asyncio.Queue() 92 | 93 | args = { 94 | "host": "localhost", 95 | "port": 8003, 96 | "data_alerts_path": "", 97 | "data_host_path": "node", 98 | "skip_original_data": True, 99 | } 100 | plugin_task = asyncio.create_task(start_server(queue, args)) 101 | 102 | task_info = { 103 | "payload": {"message": "abc", "node": "host1"}, 104 | "endpoint": "test", 105 | "host": f'{args["host"]}:{args["port"]}', 106 | } 107 | 108 | post_task = asyncio.create_task(post_code(plugin_task, task_info)) 109 | 110 | await asyncio.gather(plugin_task, post_task) 111 | 112 | data = await queue.get() 113 | assert data["alert"] == task_info["payload"] 114 | assert data["meta"]["hosts"] == ["host1"] 115 | 116 | assert queue.empty() 117 | -------------------------------------------------------------------------------- /tests/unit/event_source/test_aws_cloudtrail.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from unittest.mock import patch 3 | 4 | import pytest 5 | from asyncmock import AsyncMock 6 | from mock import MagicMock 7 | 8 | from extensions.eda.plugins.event_source.aws_cloudtrail import ( 9 | _cloudtrail_event_to_dict, 10 | _get_events, 11 | connection_args, 12 | ) 13 | from extensions.eda.plugins.event_source.aws_cloudtrail import main as cloudtrail_main 14 | from tests.conftest import ListQueue 15 | 16 | 17 | @pytest.mark.asyncio 18 | async def test_receive_from_cloudtrail(eda_queue: ListQueue) -> None: 19 | session = AsyncMock() 20 | t1 = datetime.datetime.now() 21 | t2 = datetime.datetime.now() 22 | raw_events = { 23 | "Events": [ 24 | { 25 | "EventTime": t1, 26 | "EventId": "1", 27 | "CloudTrailEvent": '{"hello": "world"}', 28 | }, 29 | { 30 | "EventTime": t2, 31 | "EventId": "2", 32 | "CloudTrailEvent": '{"foo": "bar"}', 33 | }, 34 | ] 35 | } 36 | with patch( 37 | "extensions.eda.plugins.event_source.aws_cloudtrail.get_session", # noqa: E501 38 | return_value=session, 39 | ): 40 | iterator = AsyncMock() 41 | iterator.build_full_result.return_value = raw_events 42 | paginator = MagicMock() 43 | paginator.paginate.return_value = iterator 44 | client = AsyncMock() 45 | client.get_paginator = MagicMock(side_effect=[paginator, ValueError()]) 46 | session.create_client.return_value = client 47 | session.create_client.not_async = True 48 | 49 | with pytest.raises(ValueError): 50 | await cloudtrail_main( 51 | eda_queue, 52 | { 53 | "region": "us-east-1", 54 | "delay_seconds": 1, 55 | }, 56 | ) 57 | assert eda_queue.queue[0] == { 58 | "EventTime": t1.isoformat(), 59 | "EventId": "1", 60 | "CloudTrailEvent": {"hello": "world"}, 61 | } 62 | assert eda_queue.queue[1] == { 63 | "EventTime": t2.isoformat(), 64 | "EventId": "2", 65 | "CloudTrailEvent": {"foo": "bar"}, 66 | } 67 | assert len(eda_queue.queue) == 2 68 | 69 | 70 | def test_get_events() -> None: 71 | events = [ 72 | { 73 | "EventId": "1", 74 | "EventTime": "2025-05-20T14:23:45", 75 | }, 76 | { 77 | "EventId": "2", 78 | "EventTime": "2025-05-20T14:23:55", 79 | }, 80 | { 81 | "EventId": "3", 82 | "EventTime": "2025-05-20T14:23:55", 83 | }, 84 | ] 85 | res = _get_events(events=events, last_event_ids=["1"]) 86 | assert res[0] == [events[1], events[2]] 87 | assert res[1] == events[1]["EventTime"] 88 | assert res[2] == [events[1]["EventId"], events[2]["EventId"]] 89 | 90 | 91 | def test_connection_args() -> None: 92 | conn_args = { 93 | "access_key": "secret", 94 | "secret_key": "secret", 95 | "session_token": "token", 96 | "endpoint_url": "http://example.com", 97 | "region": "US", 98 | } 99 | 100 | selected_args = connection_args(conn_args) 101 | 102 | assert selected_args["aws_access_key_id"] == conn_args["access_key"] 103 | assert selected_args["aws_secret_access_key"] == conn_args["secret_key"] 104 | assert selected_args["aws_session_token"] == conn_args["session_token"] 105 | assert selected_args["endpoint_url"] == conn_args["endpoint_url"] 106 | assert selected_args["region_name"] == conn_args["region"] 107 | 108 | 109 | def test_cloudtrail_event_to_dict() -> None: 110 | event = { 111 | "EventTime": datetime.datetime.now(), 112 | "CloudTrailEvent": '{"eventTime": "2024-05-20T18:25:43Z","eventSource": ' 113 | '"ec2.amazonaws.com","eventName": "StartInstances"}', 114 | } 115 | 116 | res_event = _cloudtrail_event_to_dict(event) 117 | 118 | assert res_event["EventTime"] == event["EventTime"] 119 | assert res_event["CloudTrailEvent"]["eventTime"] == "2024-05-20T18:25:43Z" 120 | assert res_event["CloudTrailEvent"]["eventSource"] == "ec2.amazonaws.com" 121 | assert res_event["CloudTrailEvent"]["eventName"] == "StartInstances" 122 | -------------------------------------------------------------------------------- /tests/unit/event_source/test_aws_sqs_queue.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | import pytest 4 | from asyncmock import AsyncMock 5 | 6 | from extensions.eda.plugins.event_source.aws_sqs_queue import main as sqs_main 7 | from tests.conftest import ListQueue 8 | 9 | 10 | @pytest.mark.asyncio 11 | async def test_receive_from_sqs(eda_queue: ListQueue) -> None: 12 | session = AsyncMock() 13 | with patch( 14 | "extensions.eda.plugins.event_source.aws_sqs_queue.get_session", # noqa: E501 15 | return_value=session, 16 | ): 17 | client = AsyncMock() 18 | client.get_queue_url.return_value = {"QueueUrl": "sqs_url"} 19 | 20 | message1 = { 21 | "MessageId": "1", 22 | "Body": "Hello World", 23 | "ReceiptHandle": "abcdef", 24 | } 25 | message2 = { 26 | "MessageId": "2", 27 | "Body": '{"Say":"Hello World"}', 28 | "ReceiptHandle": "xyz123", 29 | } 30 | delete_response1 = { 31 | "Id": "1", 32 | "ReceiptHandle": "abcdef", 33 | } 34 | delete_response2 = { 35 | "Id": "2", 36 | "ReceiptHandle": "xyz123", 37 | "Code": "405", 38 | "Message": "Failed to delete Message", 39 | "SenderFault": True, 40 | } 41 | 42 | response = {"Messages": [message1, message2]} 43 | delete_response = { 44 | "Successful": [delete_response1], 45 | "Failed": [delete_response2], 46 | } 47 | client.receive_message = AsyncMock(side_effect=[response, ValueError()]) 48 | client.delete_message_batch = AsyncMock( 49 | side_effect=[delete_response, ValueError()] 50 | ) 51 | 52 | session.create_client.return_value = client 53 | session.create_client.not_async = True 54 | 55 | with pytest.raises(ValueError): 56 | await sqs_main( 57 | eda_queue, 58 | { 59 | "region": "us-east-1", 60 | "name": "eda", 61 | "delay_seconds": 1, 62 | "max_number_of_messages": 3, 63 | "queue_owner_aws_account_id": "123456", 64 | }, 65 | ) 66 | assert eda_queue.queue[0] == {"body": "Hello World", "meta": {"MessageId": "1"}} 67 | assert eda_queue.queue[1] == { 68 | "body": {"Say": "Hello World"}, 69 | "meta": {"MessageId": "2"}, 70 | } 71 | assert len(eda_queue.queue) == 2 72 | -------------------------------------------------------------------------------- /tests/unit/event_source/test_azure_service_bus.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Any 3 | from unittest.mock import MagicMock, patch 4 | 5 | import pytest 6 | 7 | from extensions.eda.plugins.event_source.azure_service_bus import main as azure_main 8 | 9 | 10 | class MockQueue(asyncio.Queue[Any]): 11 | def __init__(self) -> None: 12 | self.queue: list[Any] = [] 13 | 14 | def put_nowait(self, item: Any) -> None: 15 | self.queue.append(item) 16 | 17 | 18 | @pytest.fixture 19 | def myqueue() -> MockQueue: 20 | return MockQueue() 21 | 22 | 23 | def test_receive_from_azure_service_bus(myqueue: MockQueue) -> None: 24 | client = MagicMock() 25 | with patch( 26 | "extensions.eda.plugins.event_source.azure_service_bus.ServiceBusClient." 27 | "from_connection_string", 28 | return_value=client, 29 | ): 30 | payload1 = MagicMock() 31 | payload1.message_id = 1 32 | payload1.__str__.return_value = "Hello World" # type: ignore[attr-defined] 33 | 34 | payload2 = MagicMock() 35 | payload2.message_id = 2 36 | payload2.__str__.return_value = '{"Say":"Hello World"}' # type: ignore[attr-defined] 37 | 38 | receiver = MagicMock() 39 | receiver.__iter__.return_value = [payload1, payload2] 40 | client.get_queue_receiver.return_value = receiver 41 | asyncio.run( 42 | azure_main( 43 | myqueue, 44 | { 45 | "conn_str": "Endpoint=sb://foo.servicebus.windows.net/", 46 | "queue_name": "eda-queue", 47 | }, 48 | ) 49 | ) 50 | assert myqueue.queue[0] == {"body": "Hello World", "meta": {"message_id": 1}} 51 | assert myqueue.queue[1] == { 52 | "body": {"Say": "Hello World"}, 53 | "meta": {"message_id": 2}, 54 | } 55 | assert len(myqueue.queue) == 2 56 | -------------------------------------------------------------------------------- /tests/unit/event_source/test_kafka.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import json 5 | from typing import Any 6 | from unittest.mock import MagicMock, patch 7 | 8 | import pytest 9 | from asyncmock import AsyncMock 10 | 11 | from extensions.eda.plugins.event_source.kafka import main as kafka_main 12 | 13 | 14 | class MockQueue(asyncio.Queue[Any]): 15 | def __init__(self) -> None: 16 | self.queue: list[Any] = [] 17 | 18 | async def put(self, item: Any) -> None: 19 | self.queue.append(item) 20 | 21 | 22 | @pytest.fixture 23 | def myqueue() -> MockQueue: 24 | return MockQueue() 25 | 26 | 27 | class AsyncIterator: 28 | def __init__(self) -> None: 29 | self.count = 0 30 | 31 | async def __anext__(self) -> MagicMock: 32 | if self.count < 2: 33 | mock = MagicMock() 34 | mock.value = f'{{"i": {self.count}}}'.encode("utf-8") 35 | mock.headers = [ 36 | (key, value.encode("utf-8")) 37 | for key, value in json.loads('{"foo": "bar"}').items() 38 | ] 39 | self.count += 1 40 | return mock 41 | else: 42 | raise StopAsyncIteration 43 | 44 | 45 | class MockConsumer(AsyncMock): # type: ignore[misc] 46 | def __aiter__(self) -> AsyncIterator: 47 | return AsyncIterator() 48 | 49 | 50 | def test_receive_from_kafka_place_in_queue(myqueue: MockQueue) -> None: 51 | with patch( 52 | "extensions.eda.plugins.event_source.kafka.AIOKafkaConsumer", new=MockConsumer 53 | ): 54 | asyncio.run( 55 | kafka_main( 56 | myqueue, 57 | { 58 | "topic": "eda", 59 | "host": "localhost", 60 | "port": "9092", 61 | "group_id": "test", 62 | }, 63 | ) 64 | ) 65 | assert myqueue.queue[0] == { 66 | "body": {"i": 0}, 67 | "meta": {"headers": {"foo": "bar"}}, 68 | } 69 | assert len(myqueue.queue) == 2 70 | -------------------------------------------------------------------------------- /tests/unit/plugins/module_utils/test_common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright: Contributors to the Ansible project 4 | # Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) 5 | 6 | from __future__ import absolute_import, division, print_function 7 | 8 | __metaclass__ = type 9 | 10 | from typing import Any, Optional 11 | from unittest.mock import MagicMock 12 | 13 | import pytest 14 | from ansible_collections.ansible.eda.plugins.module_utils.common import ( # type: ignore 15 | lookup_resource_id, 16 | ) 17 | from ansible_collections.ansible.eda.plugins.module_utils.errors import ( # type: ignore 18 | EDAError, 19 | ) 20 | 21 | 22 | # Define test cases as a list of tuples 23 | @pytest.mark.parametrize( 24 | "endpoint, name, params, resolve_name_to_id_return, resolve_name_to_id_side_effect," 25 | "expected_result, fail_json_called, fail_json_args", 26 | [ 27 | ( 28 | "test-endpoint", 29 | "test-name", 30 | {"param1": "value1"}, 31 | "resource-id-123", 32 | None, 33 | "resource-id-123", 34 | False, 35 | None, 36 | ), 37 | ( 38 | "test-endpoint", 39 | "test-name", 40 | None, 41 | None, 42 | EDAError("An error occurred"), 43 | None, 44 | True, 45 | {"msg": "Failed to lookup resource: An error occurred"}, 46 | ), 47 | ], 48 | ) 49 | def test_lookup_resource_id( 50 | endpoint: str, 51 | name: str, 52 | params: Optional[dict[str, Any]], 53 | resolve_name_to_id_return: Any, 54 | resolve_name_to_id_side_effect: Optional[Exception], 55 | expected_result: Optional[Any], 56 | fail_json_called: bool, 57 | fail_json_args: Optional[dict[str, Any]], 58 | ) -> None: 59 | mock_controller = MagicMock() 60 | mock_module = MagicMock() 61 | 62 | if resolve_name_to_id_side_effect: 63 | mock_controller.resolve_name_to_id.side_effect = resolve_name_to_id_side_effect 64 | else: 65 | mock_controller.resolve_name_to_id.return_value = resolve_name_to_id_return 66 | 67 | # Run the function and handle the exception case 68 | if fail_json_called: 69 | lookup_resource_id( 70 | module=mock_module, 71 | controller=mock_controller, 72 | endpoint=endpoint, 73 | name=name, 74 | params=params, 75 | ) 76 | # Verify that fail_json was called with the expected arguments 77 | mock_module.fail_json.assert_called_once_with(**fail_json_args) 78 | else: 79 | result = lookup_resource_id( 80 | module=mock_module, 81 | controller=mock_controller, 82 | endpoint=endpoint, 83 | name=name, 84 | params=params, 85 | ) 86 | assert result == expected_result 87 | mock_module.fail_json.assert_not_called() 88 | -------------------------------------------------------------------------------- /tests/unit/requirements.txt: -------------------------------------------------------------------------------- 1 | aiobotocore 2 | aiohttp 3 | aiokafka[gssapi] 4 | asyncio 5 | asyncmock 6 | azure-servicebus 7 | dpath 8 | pytest<8 # https://github.com/ansible/ansible/issues/82713 9 | pytest-asyncio 10 | psycopg[binary,pool] 11 | xxhash 12 | --------------------------------------------------------------------------------