summaryrefslogtreecommitdiffstats
path: root/test/integration/targets
diff options
context:
space:
mode:
Diffstat (limited to 'test/integration/targets')
-rw-r--r--test/integration/targets/add_host/aliases1
-rw-r--r--test/integration/targets/add_host/tasks/main.yml186
-rw-r--r--test/integration/targets/adhoc/aliases2
-rwxr-xr-xtest/integration/targets/adhoc/runme.sh9
-rw-r--r--test/integration/targets/ansiballz_python/aliases2
-rw-r--r--test/integration/targets/ansiballz_python/library/check_rlimit_and_maxfd.py31
-rw-r--r--test/integration/targets/ansiballz_python/library/custom_module.py19
-rw-r--r--test/integration/targets/ansiballz_python/library/sys_check.py23
-rw-r--r--test/integration/targets/ansiballz_python/module_utils/custom_util.py6
-rw-r--r--test/integration/targets/ansiballz_python/tasks/main.yml68
-rw-r--r--test/integration/targets/ansible-doc/aliases2
-rw-r--r--test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/MANIFEST.json30
-rw-r--r--test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/cache/notjsonfile.py70
-rw-r--r--test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/inventory/statichost.py36
-rw-r--r--test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/lookup/noop.py45
-rw-r--r--test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/modules/fakemodule.py28
-rw-r--r--test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/modules/notrealmodule.py13
-rw-r--r--test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py96
-rw-r--r--test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/vars/noop_vars_plugin.py30
-rw-r--r--test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/MANIFEST.json30
-rw-r--r--test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/cache/notjsonfile.py69
-rw-r--r--test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/filter/filter_subdir/in_subdir.py23
-rw-r--r--test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/filter/grouped.py28
-rw-r--r--test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/filter/ultimatequestion.yml21
-rw-r--r--test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/inventory/statichost.py35
-rw-r--r--test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/lookup/noop.py45
-rw-r--r--test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/database/database_type/subdir_module.py37
-rw-r--r--test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/fakemodule.py27
-rw-r--r--test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/notrealmodule.py13
-rw-r--r--test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py95
-rw-r--r--test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/test/test_test.py16
-rw-r--r--test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/test/yolo.yml18
-rw-r--r--test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/vars/noop_vars_plugin.py29
-rw-r--r--test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/roles/testrole/meta/main.yml26
-rw-r--r--test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/roles/testrole_with_no_argspecs/meta/empty0
-rw-r--r--test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/MANIFEST.json30
-rw-r--r--test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/plugins/doc_fragments/deprecation.py16
-rw-r--r--test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/plugins/doc_fragments/module.py21
-rw-r--r--test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/plugins/doc_fragments/plugin.py47
-rw-r--r--test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/plugins/doc_fragments/version_added.py13
-rw-r--r--test/integration/targets/ansible-doc/fakecollrole.output15
-rw-r--r--test/integration/targets/ansible-doc/fakemodule.output16
-rw-r--r--test/integration/targets/ansible-doc/fakerole.output32
-rw-r--r--test/integration/targets/ansible-doc/filter_plugins/donothing.yml21
-rw-r--r--test/integration/targets/ansible-doc/filter_plugins/other.py25
-rw-r--r--test/integration/targets/ansible-doc/filter_plugins/split.yml21
-rw-r--r--test/integration/targets/ansible-doc/inventory1
-rw-r--r--test/integration/targets/ansible-doc/library/double_doc.py19
-rw-r--r--test/integration/targets/ansible-doc/library/test_docs.py39
-rw-r--r--test/integration/targets/ansible-doc/library/test_docs_missing_description.py40
-rw-r--r--test/integration/targets/ansible-doc/library/test_docs_no_metadata.py35
-rw-r--r--test/integration/targets/ansible-doc/library/test_docs_no_status.py38
-rw-r--r--test/integration/targets/ansible-doc/library/test_docs_non_iterable_status.py39
-rw-r--r--test/integration/targets/ansible-doc/library/test_docs_removed_precedence.py40
-rw-r--r--test/integration/targets/ansible-doc/library/test_docs_removed_status.py39
-rw-r--r--test/integration/targets/ansible-doc/library/test_docs_returns.py56
-rw-r--r--test/integration/targets/ansible-doc/library/test_docs_returns_broken.py40
-rw-r--r--test/integration/targets/ansible-doc/library/test_docs_suboptions.py70
-rw-r--r--test/integration/targets/ansible-doc/library/test_docs_yaml_anchors.py71
-rw-r--r--test/integration/targets/ansible-doc/library/test_empty.py0
-rw-r--r--test/integration/targets/ansible-doc/library/test_no_docs.py23
-rw-r--r--test/integration/targets/ansible-doc/library/test_no_docs_no_metadata.py18
-rw-r--r--test/integration/targets/ansible-doc/library/test_no_docs_no_status.py22
-rw-r--r--test/integration/targets/ansible-doc/library/test_no_docs_non_iterable_status.py23
-rw-r--r--test/integration/targets/ansible-doc/library/test_win_module.ps121
-rw-r--r--test/integration/targets/ansible-doc/library/test_win_module.yml9
-rw-r--r--test/integration/targets/ansible-doc/lookup_plugins/_deprecated_with_adj_docs.py5
-rw-r--r--test/integration/targets/ansible-doc/lookup_plugins/_deprecated_with_docs.py26
-rw-r--r--test/integration/targets/ansible-doc/lookup_plugins/deprecated_with_adj_docs.yml16
-rw-r--r--test/integration/targets/ansible-doc/noop.output32
-rw-r--r--test/integration/targets/ansible-doc/noop_vars_plugin.output42
-rw-r--r--test/integration/targets/ansible-doc/notjsonfile.output157
-rw-r--r--test/integration/targets/ansible-doc/randommodule-text.output101
-rw-r--r--test/integration/targets/ansible-doc/randommodule.output115
-rw-r--r--test/integration/targets/ansible-doc/roles/test_role1/meta/argument_specs.yml34
-rw-r--r--test/integration/targets/ansible-doc/roles/test_role1/meta/main.yml13
-rw-r--r--test/integration/targets/ansible-doc/roles/test_role2/meta/empty0
-rw-r--r--test/integration/targets/ansible-doc/roles/test_role3/meta/main.yml0
-rwxr-xr-xtest/integration/targets/ansible-doc/runme.sh214
-rw-r--r--test/integration/targets/ansible-doc/test.yml172
-rw-r--r--test/integration/targets/ansible-doc/test_docs_returns.output33
-rw-r--r--test/integration/targets/ansible-doc/test_docs_suboptions.output42
-rw-r--r--test/integration/targets/ansible-doc/test_docs_yaml_anchors.output46
-rw-r--r--test/integration/targets/ansible-doc/test_role1/README.txt3
-rw-r--r--test/integration/targets/ansible-doc/test_role1/meta/main.yml8
-rw-r--r--test/integration/targets/ansible-galaxy-collection-cli/aliases2
-rw-r--r--test/integration/targets/ansible-galaxy-collection-cli/files/expected.txt107
-rw-r--r--test/integration/targets/ansible-galaxy-collection-cli/files/expected_full_manifest.txt108
-rw-r--r--test/integration/targets/ansible-galaxy-collection-cli/files/full_manifest_galaxy.yml39
-rw-r--r--test/integration/targets/ansible-galaxy-collection-cli/files/galaxy.yml10
-rw-r--r--test/integration/targets/ansible-galaxy-collection-cli/files/make_collection_dir.py114
-rw-r--r--test/integration/targets/ansible-galaxy-collection-cli/tasks/main.yml19
-rw-r--r--test/integration/targets/ansible-galaxy-collection-cli/tasks/manifest.yml57
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/aliases2
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/meta/main.yml4
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/tasks/download.yml48
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/tasks/empty_installed_collections.yml7
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/tasks/individual_collection_repo.yml18
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/tasks/main.yml61
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/tasks/multi_collection_repo_all.yml45
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/tasks/multi_collection_repo_individual.yml15
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/tasks/reinstalling.yml31
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/tasks/requirements.yml103
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/tasks/scm_dependency.yml29
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/tasks/scm_dependency_deduplication.yml92
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/tasks/setup.yml19
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/tasks/setup_collection_bad_version.yml47
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/tasks/setup_multi_collection_repo.yml70
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/tasks/setup_recursive_scm_dependency.yml33
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/tasks/test_invalid_version.yml58
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/tasks/test_manifest_metadata.yml55
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/tasks/test_supported_resolvelib_versions.yml25
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/templates/git_prefix_name.yml2
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/templates/name_and_type.yml3
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/templates/name_without_type.yml3
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/templates/source_and_name.yml4
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/templates/source_and_name_and_type.yml5
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/templates/source_only.yml3
-rw-r--r--test/integration/targets/ansible-galaxy-collection-scm/vars/main.yml11
-rw-r--r--test/integration/targets/ansible-galaxy-collection/aliases4
-rw-r--r--test/integration/targets/ansible-galaxy-collection/files/build_bad_tar.py84
-rw-r--r--test/integration/targets/ansible-galaxy-collection/files/test_module.py80
-rw-r--r--test/integration/targets/ansible-galaxy-collection/library/reset_pulp.py211
-rw-r--r--test/integration/targets/ansible-galaxy-collection/library/setup_collections.py269
-rw-r--r--test/integration/targets/ansible-galaxy-collection/meta/main.yml4
-rw-r--r--test/integration/targets/ansible-galaxy-collection/tasks/build.yml74
-rw-r--r--test/integration/targets/ansible-galaxy-collection/tasks/download.yml176
-rw-r--r--test/integration/targets/ansible-galaxy-collection/tasks/fail_fast_resolvelib.yml45
-rw-r--r--test/integration/targets/ansible-galaxy-collection/tasks/init.yml124
-rw-r--r--test/integration/targets/ansible-galaxy-collection/tasks/install.yml1035
-rw-r--r--test/integration/targets/ansible-galaxy-collection/tasks/install_offline.yml137
-rw-r--r--test/integration/targets/ansible-galaxy-collection/tasks/list.yml167
-rw-r--r--test/integration/targets/ansible-galaxy-collection/tasks/main.yml220
-rw-r--r--test/integration/targets/ansible-galaxy-collection/tasks/publish.yml33
-rw-r--r--test/integration/targets/ansible-galaxy-collection/tasks/pulp.yml11
-rw-r--r--test/integration/targets/ansible-galaxy-collection/tasks/revoke_gpg_key.yml14
-rw-r--r--test/integration/targets/ansible-galaxy-collection/tasks/setup_gpg.yml24
-rw-r--r--test/integration/targets/ansible-galaxy-collection/tasks/supported_resolvelib.yml44
-rw-r--r--test/integration/targets/ansible-galaxy-collection/tasks/unsupported_resolvelib.yml44
-rw-r--r--test/integration/targets/ansible-galaxy-collection/tasks/upgrade.yml282
-rw-r--r--test/integration/targets/ansible-galaxy-collection/tasks/verify.yml475
-rw-r--r--test/integration/targets/ansible-galaxy-collection/templates/ansible.cfg.j228
-rw-r--r--test/integration/targets/ansible-galaxy-collection/vars/main.yml164
-rw-r--r--test/integration/targets/ansible-galaxy-role/aliases2
-rw-r--r--test/integration/targets/ansible-galaxy-role/meta/main.yml1
-rw-r--r--test/integration/targets/ansible-galaxy-role/tasks/main.yml61
-rw-r--r--test/integration/targets/ansible-galaxy/aliases3
-rw-r--r--test/integration/targets/ansible-galaxy/cleanup-default.yml13
-rw-r--r--test/integration/targets/ansible-galaxy/cleanup-freebsd.yml12
-rw-r--r--test/integration/targets/ansible-galaxy/cleanup.yml26
-rw-r--r--test/integration/targets/ansible-galaxy/files/testserver.py20
-rwxr-xr-xtest/integration/targets/ansible-galaxy/runme.sh571
-rw-r--r--test/integration/targets/ansible-galaxy/setup.yml57
-rw-r--r--test/integration/targets/ansible-inventory/aliases2
-rw-r--r--test/integration/targets/ansible-inventory/files/invalid_sample.yml7
-rw-r--r--test/integration/targets/ansible-inventory/files/unicode.yml3
-rw-r--r--test/integration/targets/ansible-inventory/files/valid_sample.toml2
-rw-r--r--test/integration/targets/ansible-inventory/files/valid_sample.yml7
-rwxr-xr-xtest/integration/targets/ansible-inventory/runme.sh7
-rw-r--r--test/integration/targets/ansible-inventory/tasks/main.yml147
-rw-r--r--test/integration/targets/ansible-inventory/tasks/toml.yml66
-rw-r--r--test/integration/targets/ansible-inventory/test.yml3
-rw-r--r--test/integration/targets/ansible-pull/aliases2
-rw-r--r--test/integration/targets/ansible-pull/cleanup.yml16
-rw-r--r--test/integration/targets/ansible-pull/pull-integration-test/ansible.cfg2
-rw-r--r--test/integration/targets/ansible-pull/pull-integration-test/inventory2
-rw-r--r--test/integration/targets/ansible-pull/pull-integration-test/local.yml20
-rw-r--r--test/integration/targets/ansible-pull/pull-integration-test/multi_play_1.yml6
-rw-r--r--test/integration/targets/ansible-pull/pull-integration-test/multi_play_2.yml6
-rwxr-xr-xtest/integration/targets/ansible-pull/runme.sh87
-rw-r--r--test/integration/targets/ansible-pull/setup.yml11
-rw-r--r--test/integration/targets/ansible-runner/aliases5
-rw-r--r--test/integration/targets/ansible-runner/files/adhoc_example1.py29
-rw-r--r--test/integration/targets/ansible-runner/files/playbook_example1.py41
-rw-r--r--test/integration/targets/ansible-runner/filter_plugins/parse.py17
-rw-r--r--test/integration/targets/ansible-runner/inventory1
-rwxr-xr-xtest/integration/targets/ansible-runner/runme.sh7
-rw-r--r--test/integration/targets/ansible-runner/tasks/adhoc_example1.yml14
-rw-r--r--test/integration/targets/ansible-runner/tasks/main.yml4
-rw-r--r--test/integration/targets/ansible-runner/tasks/playbook_example1.yml21
-rw-r--r--test/integration/targets/ansible-runner/tasks/setup.yml4
-rw-r--r--test/integration/targets/ansible-runner/test.yml3
-rw-r--r--test/integration/targets/ansible-test-cloud-acme/aliases3
-rw-r--r--test/integration/targets/ansible-test-cloud-acme/tasks/main.yml7
-rw-r--r--test/integration/targets/ansible-test-cloud-aws/aliases3
-rw-r--r--test/integration/targets/ansible-test-cloud-aws/tasks/main.yml17
-rw-r--r--test/integration/targets/ansible-test-cloud-azure/aliases3
-rw-r--r--test/integration/targets/ansible-test-cloud-azure/tasks/main.yml18
-rw-r--r--test/integration/targets/ansible-test-cloud-cs/aliases3
-rw-r--r--test/integration/targets/ansible-test-cloud-cs/tasks/main.yml8
-rw-r--r--test/integration/targets/ansible-test-cloud-foreman/aliases3
-rw-r--r--test/integration/targets/ansible-test-cloud-foreman/tasks/main.yml6
-rw-r--r--test/integration/targets/ansible-test-cloud-galaxy/aliases4
-rw-r--r--test/integration/targets/ansible-test-cloud-galaxy/tasks/main.yml25
-rw-r--r--test/integration/targets/ansible-test-cloud-httptester-windows/aliases4
-rw-r--r--test/integration/targets/ansible-test-cloud-httptester-windows/tasks/main.yml15
-rw-r--r--test/integration/targets/ansible-test-cloud-httptester/aliases3
-rw-r--r--test/integration/targets/ansible-test-cloud-httptester/tasks/main.yml15
-rw-r--r--test/integration/targets/ansible-test-cloud-nios/aliases3
-rw-r--r--test/integration/targets/ansible-test-cloud-nios/tasks/main.yml10
-rw-r--r--test/integration/targets/ansible-test-cloud-openshift/aliases4
-rw-r--r--test/integration/targets/ansible-test-cloud-openshift/tasks/main.yml6
-rw-r--r--test/integration/targets/ansible-test-cloud-vcenter/aliases3
-rw-r--r--test/integration/targets/ansible-test-cloud-vcenter/tasks/main.yml6
-rw-r--r--test/integration/targets/ansible-test-config-invalid/aliases4
-rw-r--r--test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/config.yml1
-rw-r--r--test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/integration/targets/test/aliases1
-rwxr-xr-xtest/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/integration/targets/test/runme.sh1
-rw-r--r--test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_test.py2
-rwxr-xr-xtest/integration/targets/ansible-test-config-invalid/runme.sh12
-rw-r--r--test/integration/targets/ansible-test-config/aliases4
-rw-r--r--test/integration/targets/ansible-test-config/ansible_collections/ns/col/plugins/module_utils/test.py14
-rw-r--r--test/integration/targets/ansible-test-config/ansible_collections/ns/col/tests/config.yml2
-rw-r--r--test/integration/targets/ansible-test-config/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_test.py5
-rwxr-xr-xtest/integration/targets/ansible-test-config/runme.sh15
-rw-r--r--test/integration/targets/ansible-test-container/aliases5
-rwxr-xr-xtest/integration/targets/ansible-test-container/runme.py1090
-rwxr-xr-xtest/integration/targets/ansible-test-container/runme.sh5
-rw-r--r--test/integration/targets/ansible-test-coverage/aliases4
-rw-r--r--test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/plugins/module_utils/test_util.py6
-rwxr-xr-xtest/integration/targets/ansible-test-coverage/runme.sh16
-rw-r--r--test/integration/targets/ansible-test-docker/aliases3
-rw-r--r--test/integration/targets/ansible-test-docker/ansible_collections/ns/col/galaxy.yml6
-rw-r--r--test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/doc_fragments/ps_util.py21
-rw-r--r--test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/module_utils/PSUtil.psm116
-rw-r--r--test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/module_utils/my_util.py6
-rw-r--r--test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/modules/hello.py46
-rw-r--r--test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/modules/win_util_args.ps116
-rw-r--r--test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/modules/win_util_args.py39
-rw-r--r--test/integration/targets/ansible-test-docker/ansible_collections/ns/col/tests/integration/targets/minimal/aliases1
-rw-r--r--test/integration/targets/ansible-test-docker/ansible_collections/ns/col/tests/integration/targets/minimal/tasks/main.yml7
-rw-r--r--test/integration/targets/ansible-test-docker/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_my_util.py8
-rw-r--r--test/integration/targets/ansible-test-docker/ansible_collections/ns/col/tests/unit/plugins/modules/test_hello.py8
-rwxr-xr-xtest/integration/targets/ansible-test-docker/runme.sh14
-rw-r--r--test/integration/targets/ansible-test-git/aliases4
-rw-r--r--test/integration/targets/ansible-test-git/ansible_collections/ns/col/tests/.keep0
-rwxr-xr-xtest/integration/targets/ansible-test-git/collection-tests/git-at-collection-base.sh10
-rwxr-xr-xtest/integration/targets/ansible-test-git/collection-tests/git-at-collection-root.sh10
-rwxr-xr-xtest/integration/targets/ansible-test-git/collection-tests/git-common.bash65
-rw-r--r--test/integration/targets/ansible-test-git/collection-tests/install-git.yml5
-rw-r--r--test/integration/targets/ansible-test-git/collection-tests/uninstall-git.yml18
-rwxr-xr-xtest/integration/targets/ansible-test-git/runme.sh24
-rw-r--r--test/integration/targets/ansible-test-integration-constraints/aliases4
-rw-r--r--test/integration/targets/ansible-test-integration-constraints/ansible_collections/ns/col/tests/integration/constraints.txt1
-rw-r--r--test/integration/targets/ansible-test-integration-constraints/ansible_collections/ns/col/tests/integration/requirements.txt1
-rw-r--r--test/integration/targets/ansible-test-integration-constraints/ansible_collections/ns/col/tests/integration/targets/constraints/aliases1
-rw-r--r--test/integration/targets/ansible-test-integration-constraints/ansible_collections/ns/col/tests/integration/targets/constraints/tasks/main.yml7
-rwxr-xr-xtest/integration/targets/ansible-test-integration-constraints/runme.sh7
-rw-r--r--test/integration/targets/ansible-test-integration-targets/aliases4
-rw-r--r--test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/destructive_a/aliases2
-rw-r--r--test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/destructive_b/aliases2
-rw-r--r--test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/disabled_a/aliases2
-rw-r--r--test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/disabled_b/aliases2
-rw-r--r--test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/unstable_a/aliases2
-rw-r--r--test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/unstable_b/aliases2
-rw-r--r--test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/unsupported_a/aliases2
-rw-r--r--test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/unsupported_b/aliases2
-rwxr-xr-xtest/integration/targets/ansible-test-integration-targets/runme.sh9
-rwxr-xr-xtest/integration/targets/ansible-test-integration-targets/test.py35
-rw-r--r--test/integration/targets/ansible-test-integration/aliases4
-rw-r--r--test/integration/targets/ansible-test-integration/ansible_collections/ns/col/plugins/module_utils/my_util.py6
-rw-r--r--test/integration/targets/ansible-test-integration/ansible_collections/ns/col/plugins/modules/hello.py46
-rw-r--r--test/integration/targets/ansible-test-integration/ansible_collections/ns/col/tests/integration/targets/hello/aliases1
-rw-r--r--test/integration/targets/ansible-test-integration/ansible_collections/ns/col/tests/integration/targets/hello/tasks/main.yml7
-rwxr-xr-xtest/integration/targets/ansible-test-integration/runme.sh7
-rw-r--r--test/integration/targets/ansible-test-no-tty/aliases4
-rwxr-xr-xtest/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/run-with-pty.py11
-rw-r--r--test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/tests/integration/targets/no-tty/aliases1
-rwxr-xr-xtest/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/tests/integration/targets/no-tty/assert-no-tty.py13
-rwxr-xr-xtest/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/tests/integration/targets/no-tty/runme.sh5
-rw-r--r--test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/vendored_pty.py189
-rwxr-xr-xtest/integration/targets/ansible-test-no-tty/runme.sh13
-rw-r--r--test/integration/targets/ansible-test-sanity-ansible-doc/aliases4
-rw-r--r--test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/lookup/a/b/lookup2.py28
-rw-r--r--test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/lookup/lookup1.py28
-rw-r--r--test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/modules/a/b/module2.py34
-rw-r--r--test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/modules/module1.py34
-rwxr-xr-xtest/integration/targets/ansible-test-sanity-ansible-doc/runme.sh9
-rw-r--r--test/integration/targets/ansible-test-sanity-import/aliases5
-rw-r--r--test/integration/targets/ansible-test-sanity-import/ansible_collections/ns/col/plugins/lookup/vendor1.py33
-rw-r--r--test/integration/targets/ansible-test-sanity-import/ansible_collections/ns/col/plugins/lookup/vendor2.py33
-rwxr-xr-xtest/integration/targets/ansible-test-sanity-import/runme.sh20
-rw-r--r--test/integration/targets/ansible-test-sanity-lint/aliases4
-rw-r--r--test/integration/targets/ansible-test-sanity-lint/expected.txt1
-rwxr-xr-xtest/integration/targets/ansible-test-sanity-lint/runme.sh47
-rw-r--r--test/integration/targets/ansible-test-sanity-shebang/aliases4
-rw-r--r--test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/plugins/modules/powershell.ps11
-rw-r--r--test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/plugins/modules/python-no-shebang.py0
-rw-r--r--test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/plugins/modules/python.py1
-rwxr-xr-xtest/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/scripts/env_bash.sh1
-rwxr-xr-xtest/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/scripts/env_python.py1
-rwxr-xr-xtest/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/scripts/sh.sh1
-rwxr-xr-xtest/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/tests/integration/targets/valid/env_bash.sh1
-rwxr-xr-xtest/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/tests/integration/targets/valid/env_python.py1
-rwxr-xr-xtest/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/tests/integration/targets/valid/sh.sh1
-rw-r--r--test/integration/targets/ansible-test-sanity-shebang/expected.txt9
-rwxr-xr-xtest/integration/targets/ansible-test-sanity-shebang/runme.sh47
-rw-r--r--test/integration/targets/ansible-test-sanity-validate-modules/aliases4
-rw-r--r--test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/invalid_yaml_syntax.py27
-rw-r--r--test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/no_callable.py23
-rw-r--r--test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/sidecar.py11
-rw-r--r--test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/sidecar.yaml31
-rw-r--r--test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/README.rst3
-rw-r--r--test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/galaxy.yml6
-rw-r--r--test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/meta/main.yml1
-rw-r--r--test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/plugins/modules/failure_ps.ps116
-rw-r--r--test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/plugins/modules/failure_ps.yml31
-rw-r--r--test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/README.rst3
-rw-r--r--test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/galaxy.yml6
-rw-r--r--test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/meta/runtime.yml1
-rw-r--r--test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/module_utils/share_module.psm119
-rw-r--r--test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/module_utils/validate.psm18
-rw-r--r--test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/in_function.ps17
-rw-r--r--test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/in_function.yml25
-rw-r--r--test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/sidecar.ps114
-rw-r--r--test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/sidecar.yml31
-rw-r--r--test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/validate.ps18
-rw-r--r--test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/validate.py14
-rw-r--r--test/integration/targets/ansible-test-sanity-validate-modules/expected.txt5
-rwxr-xr-xtest/integration/targets/ansible-test-sanity-validate-modules/runme.sh34
-rw-r--r--test/integration/targets/ansible-test-sanity/aliases4
-rw-r--r--test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/README.rst3
-rw-r--r--test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/galaxy.yml6
-rw-r--r--test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/meta/runtime.yml5
-rw-r--r--test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/filter/check_pylint.py23
-rw-r--r--test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/lookup/bad.py31
-rw-r--r--test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/lookup/world.py29
-rw-r--r--test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/module_utils/__init__.py0
-rw-r--r--test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/modules/bad.py34
-rw-r--r--test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/random_directory/bad.py8
-rw-r--r--test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/integration/targets/hello/files/bad.py16
-rw-r--r--test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/sanity/ignore.txt6
-rwxr-xr-xtest/integration/targets/ansible-test-sanity/runme.sh7
-rw-r--r--test/integration/targets/ansible-test-shell/aliases4
-rw-r--r--test/integration/targets/ansible-test-shell/ansible_collections/ns/col/.keep0
-rw-r--r--test/integration/targets/ansible-test-shell/expected-stderr.txt1
-rw-r--r--test/integration/targets/ansible-test-shell/expected-stdout.txt1
-rwxr-xr-xtest/integration/targets/ansible-test-shell/runme.sh30
-rw-r--r--test/integration/targets/ansible-test-units-constraints/aliases5
-rw-r--r--test/integration/targets/ansible-test-units-constraints/ansible_collections/ns/col/tests/unit/constraints.txt1
-rw-r--r--test/integration/targets/ansible-test-units-constraints/ansible_collections/ns/col/tests/unit/plugins/modules/test_constraints.py8
-rw-r--r--test/integration/targets/ansible-test-units-constraints/ansible_collections/ns/col/tests/unit/requirements.txt1
-rwxr-xr-xtest/integration/targets/ansible-test-units-constraints/runme.sh10
-rw-r--r--test/integration/targets/ansible-test-units/aliases5
-rw-r--r--test/integration/targets/ansible-test-units/ansible_collections/ns/col/plugins/module_utils/my_util.py6
-rw-r--r--test/integration/targets/ansible-test-units/ansible_collections/ns/col/plugins/modules/hello.py46
-rw-r--r--test/integration/targets/ansible-test-units/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_my_util.py8
-rw-r--r--test/integration/targets/ansible-test-units/ansible_collections/ns/col/tests/unit/plugins/modules/test_hello.py8
-rwxr-xr-xtest/integration/targets/ansible-test-units/runme.sh10
-rw-r--r--test/integration/targets/ansible-test-unsupported-directory/aliases4
-rw-r--r--test/integration/targets/ansible-test-unsupported-directory/ansible_collections/ns/col/.keep0
-rwxr-xr-xtest/integration/targets/ansible-test-unsupported-directory/runme.sh29
-rw-r--r--test/integration/targets/ansible-test/aliases1
-rwxr-xr-xtest/integration/targets/ansible-test/venv-pythons.py42
-rw-r--r--test/integration/targets/ansible-vault/aliases2
-rw-r--r--test/integration/targets/ansible-vault/empty-password0
-rw-r--r--test/integration/targets/ansible-vault/encrypted-vault-password6
-rw-r--r--test/integration/targets/ansible-vault/encrypted_file_encrypted_var_password1
-rw-r--r--test/integration/targets/ansible-vault/example1_password1
-rw-r--r--test/integration/targets/ansible-vault/example2_password1
-rw-r--r--test/integration/targets/ansible-vault/example3_password1
-rwxr-xr-xtest/integration/targets/ansible-vault/faux-editor.py44
-rw-r--r--test/integration/targets/ansible-vault/files/test_assemble/nonsecret.txt1
-rw-r--r--test/integration/targets/ansible-vault/files/test_assemble/secret.vault7
-rw-r--r--test/integration/targets/ansible-vault/format_1_1_AES256.yml6
-rw-r--r--test/integration/targets/ansible-vault/format_1_2_AES256.yml6
-rw-r--r--test/integration/targets/ansible-vault/host_vars/myhost.yml7
-rw-r--r--test/integration/targets/ansible-vault/host_vars/testhost.yml7
-rw-r--r--test/integration/targets/ansible-vault/invalid_format/README.md1
-rw-r--r--test/integration/targets/ansible-vault/invalid_format/broken-group-vars-tasks.yml23
-rw-r--r--test/integration/targets/ansible-vault/invalid_format/broken-host-vars-tasks.yml7
-rw-r--r--test/integration/targets/ansible-vault/invalid_format/group_vars/broken-group-vars.yml8
-rw-r--r--test/integration/targets/ansible-vault/invalid_format/host_vars/broken-host-vars.example.com/vars11
-rw-r--r--test/integration/targets/ansible-vault/invalid_format/inventory5
-rw-r--r--test/integration/targets/ansible-vault/invalid_format/original-broken-host-vars6
-rw-r--r--test/integration/targets/ansible-vault/invalid_format/original-group-vars.yml2
-rw-r--r--test/integration/targets/ansible-vault/invalid_format/some-vars6
-rw-r--r--test/integration/targets/ansible-vault/invalid_format/vault-secret1
-rw-r--r--test/integration/targets/ansible-vault/inventory.toml5
-rwxr-xr-xtest/integration/targets/ansible-vault/password-script.py33
-rw-r--r--test/integration/targets/ansible-vault/realpath.yml10
-rw-r--r--test/integration/targets/ansible-vault/roles/test_vault/tasks/main.yml9
-rw-r--r--test/integration/targets/ansible-vault/roles/test_vault/vars/main.yml9
-rw-r--r--test/integration/targets/ansible-vault/roles/test_vault_embedded/tasks/main.yml13
-rw-r--r--test/integration/targets/ansible-vault/roles/test_vault_embedded/vars/main.yml17
-rw-r--r--test/integration/targets/ansible-vault/roles/test_vault_embedded_ids/tasks/main.yml29
-rw-r--r--test/integration/targets/ansible-vault/roles/test_vault_embedded_ids/vars/main.yml194
-rw-r--r--test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/README.md1
-rw-r--r--test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml13
-rw-r--r--test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/vars/main.yml76
-rw-r--r--test/integration/targets/ansible-vault/roles/test_vaulted_template/tasks/main.yml19
-rw-r--r--test/integration/targets/ansible-vault/roles/test_vaulted_template/templates/vaulted_template.j26
-rwxr-xr-xtest/integration/targets/ansible-vault/runme.sh576
-rwxr-xr-xtest/integration/targets/ansible-vault/script/vault-secret.sh24
-rw-r--r--test/integration/targets/ansible-vault/single_vault_as_string.yml117
-rw-r--r--test/integration/targets/ansible-vault/symlink.yml10
-rwxr-xr-xtest/integration/targets/ansible-vault/symlink/get-password-symlink24
-rwxr-xr-xtest/integration/targets/ansible-vault/test-vault-client.py66
-rw-r--r--test/integration/targets/ansible-vault/test_dangling_temp.yml34
-rw-r--r--test/integration/targets/ansible-vault/test_utf8_value_in_filename.yml16
-rw-r--r--test/integration/targets/ansible-vault/test_vault.yml6
-rw-r--r--test/integration/targets/ansible-vault/test_vault_embedded.yml4
-rw-r--r--test/integration/targets/ansible-vault/test_vault_embedded_ids.yml4
-rw-r--r--test/integration/targets/ansible-vault/test_vault_file_encrypted_embedded.yml4
-rw-r--r--test/integration/targets/ansible-vault/test_vaulted_inventory.yml5
-rw-r--r--test/integration/targets/ansible-vault/test_vaulted_inventory_toml.yml9
-rw-r--r--test/integration/targets/ansible-vault/test_vaulted_template.yml6
-rw-r--r--test/integration/targets/ansible-vault/test_vaulted_utf8_value.yml15
-rw-r--r--test/integration/targets/ansible-vault/vars/vaulted.yml15
-rw-r--r--test/integration/targets/ansible-vault/vault-café.yml6
-rw-r--r--test/integration/targets/ansible-vault/vault-password1
-rw-r--r--test/integration/targets/ansible-vault/vault-password-ansible1
-rw-r--r--test/integration/targets/ansible-vault/vault-password-wrong1
-rw-r--r--test/integration/targets/ansible-vault/vault-secret.txt6
-rw-r--r--test/integration/targets/ansible-vault/vaulted.inventory8
-rw-r--r--test/integration/targets/ansible/adhoc-callback.stdout12
-rw-r--r--test/integration/targets/ansible/aliases2
-rw-r--r--test/integration/targets/ansible/ansible-testé.cfg3
-rw-r--r--test/integration/targets/ansible/callback_plugins/callback_debug.py24
-rw-r--r--test/integration/targets/ansible/callback_plugins/callback_meta.py23
-rwxr-xr-xtest/integration/targets/ansible/module_common_regex_regression.sh15
-rw-r--r--test/integration/targets/ansible/no-extension2
-rw-r--r--test/integration/targets/ansible/playbook.yml8
-rw-r--r--test/integration/targets/ansible/playbookdir_cfg.ini2
-rwxr-xr-xtest/integration/targets/ansible/runme.sh86
-rw-r--r--test/integration/targets/ansible/vars.yml1
-rw-r--r--test/integration/targets/any_errors_fatal/50897.yml19
-rw-r--r--test/integration/targets/any_errors_fatal/aliases2
-rw-r--r--test/integration/targets/any_errors_fatal/always_block.yml27
-rw-r--r--test/integration/targets/any_errors_fatal/inventory6
-rw-r--r--test/integration/targets/any_errors_fatal/on_includes.yml7
-rw-r--r--test/integration/targets/any_errors_fatal/play_level.yml15
-rwxr-xr-xtest/integration/targets/any_errors_fatal/runme.sh37
-rw-r--r--test/integration/targets/any_errors_fatal/test_fatal.yml12
-rw-r--r--test/integration/targets/apt/aliases6
-rw-r--r--test/integration/targets/apt/defaults/main.yml2
-rw-r--r--test/integration/targets/apt/handlers/main.yml4
-rw-r--r--test/integration/targets/apt/meta/main.yml3
-rw-r--r--test/integration/targets/apt/tasks/apt-builddep.yml55
-rw-r--r--test/integration/targets/apt/tasks/apt-multiarch.yml44
-rw-r--r--test/integration/targets/apt/tasks/apt.yml547
-rw-r--r--test/integration/targets/apt/tasks/downgrade.yml77
-rw-r--r--test/integration/targets/apt/tasks/main.yml40
-rw-r--r--test/integration/targets/apt/tasks/repo.yml452
-rw-r--r--test/integration/targets/apt/tasks/upgrade.yml64
-rw-r--r--test/integration/targets/apt/tasks/url-with-deps.yml56
-rw-r--r--test/integration/targets/apt/vars/Ubuntu-20.yml1
-rw-r--r--test/integration/targets/apt/vars/Ubuntu-22.yml1
-rw-r--r--test/integration/targets/apt/vars/default.yml1
-rw-r--r--test/integration/targets/apt_key/aliases5
-rw-r--r--test/integration/targets/apt_key/meta/main.yml2
-rw-r--r--test/integration/targets/apt_key/tasks/apt_key.yml25
-rw-r--r--test/integration/targets/apt_key/tasks/apt_key_binary.yml12
-rw-r--r--test/integration/targets/apt_key/tasks/apt_key_inline_data.yml5
-rw-r--r--test/integration/targets/apt_key/tasks/file.yml52
-rw-r--r--test/integration/targets/apt_key/tasks/main.yml29
-rw-r--r--test/integration/targets/apt_repository/aliases6
-rw-r--r--test/integration/targets/apt_repository/meta/main.yml2
-rw-r--r--test/integration/targets/apt_repository/tasks/apt.yml243
-rw-r--r--test/integration/targets/apt_repository/tasks/cleanup.yml17
-rw-r--r--test/integration/targets/apt_repository/tasks/main.yml25
-rw-r--r--test/integration/targets/apt_repository/tasks/mode.yaml135
-rw-r--r--test/integration/targets/apt_repository/tasks/mode_cleanup.yaml7
-rw-r--r--test/integration/targets/args/aliases2
-rwxr-xr-xtest/integration/targets/args/runme.sh12
-rw-r--r--test/integration/targets/argspec/aliases2
-rw-r--r--test/integration/targets/argspec/library/argspec.py268
-rw-r--r--test/integration/targets/argspec/tasks/main.yml659
-rw-r--r--test/integration/targets/argspec/tasks/password_no_log.yml14
-rw-r--r--test/integration/targets/assemble/aliases1
-rw-r--r--test/integration/targets/assemble/files/fragment11
-rw-r--r--test/integration/targets/assemble/files/fragment21
-rw-r--r--test/integration/targets/assemble/files/fragment31
-rw-r--r--test/integration/targets/assemble/files/fragment41
-rw-r--r--test/integration/targets/assemble/files/fragment51
-rw-r--r--test/integration/targets/assemble/meta/main.yml21
-rw-r--r--test/integration/targets/assemble/tasks/main.yml154
-rw-r--r--test/integration/targets/assert/aliases2
-rw-r--r--test/integration/targets/assert/assert_quiet.out.quiet.stderr2
-rw-r--r--test/integration/targets/assert/assert_quiet.out.quiet.stdout17
-rw-r--r--test/integration/targets/assert/inventory3
-rw-r--r--test/integration/targets/assert/quiet.yml16
-rwxr-xr-xtest/integration/targets/assert/runme.sh71
-rw-r--r--test/integration/targets/async/aliases3
-rw-r--r--test/integration/targets/async/callback_test.yml7
-rw-r--r--test/integration/targets/async/library/async_test.py49
-rw-r--r--test/integration/targets/async/meta/main.yml2
-rw-r--r--test/integration/targets/async/tasks/main.yml300
-rw-r--r--test/integration/targets/async_extra_data/aliases2
-rw-r--r--test/integration/targets/async_extra_data/library/junkping.py15
-rwxr-xr-xtest/integration/targets/async_extra_data/runme.sh7
-rw-r--r--test/integration/targets/async_extra_data/test_async.yml10
-rw-r--r--test/integration/targets/async_fail/action_plugins/normal.py62
-rw-r--r--test/integration/targets/async_fail/aliases3
-rw-r--r--test/integration/targets/async_fail/library/async_test.py53
-rw-r--r--test/integration/targets/async_fail/meta/main.yml2
-rw-r--r--test/integration/targets/async_fail/tasks/main.yml36
-rw-r--r--test/integration/targets/become/aliases4
-rw-r--r--test/integration/targets/become/files/copy.txt1
-rw-r--r--test/integration/targets/become/meta/main.yml2
-rw-r--r--test/integration/targets/become/tasks/become.yml49
-rw-r--r--test/integration/targets/become/tasks/main.yml20
-rw-r--r--test/integration/targets/become/vars/main.yml14
-rw-r--r--test/integration/targets/become_su/aliases3
-rwxr-xr-xtest/integration/targets/become_su/runme.sh6
-rw-r--r--test/integration/targets/become_unprivileged/action_plugins/tmpdir.py14
-rw-r--r--test/integration/targets/become_unprivileged/aliases5
-rw-r--r--test/integration/targets/become_unprivileged/chmod_acl_macos/test.yml26
-rw-r--r--test/integration/targets/become_unprivileged/cleanup_unpriv_users.yml53
-rw-r--r--test/integration/targets/become_unprivileged/common_remote_group/cleanup.yml35
-rw-r--r--test/integration/targets/become_unprivileged/common_remote_group/setup.yml43
-rw-r--r--test/integration/targets/become_unprivileged/common_remote_group/test.yml36
-rw-r--r--test/integration/targets/become_unprivileged/inventory10
-rwxr-xr-xtest/integration/targets/become_unprivileged/runme.sh52
-rw-r--r--test/integration/targets/become_unprivileged/setup_unpriv_users.yml109
-rw-r--r--test/integration/targets/binary/aliases2
-rw-r--r--test/integration/targets/binary/files/b64_latin11
-rw-r--r--test/integration/targets/binary/files/b64_utf81
-rw-r--r--test/integration/targets/binary/files/from_playbook1
-rw-r--r--test/integration/targets/binary/meta/main.yml3
-rw-r--r--test/integration/targets/binary/tasks/main.yml131
-rw-r--r--test/integration/targets/binary/templates/b64_latin1_template.j21
-rw-r--r--test/integration/targets/binary/templates/b64_utf8_template.j21
-rw-r--r--test/integration/targets/binary/templates/from_playbook_template.j21
-rw-r--r--test/integration/targets/binary/vars/main.yml3
-rw-r--r--test/integration/targets/binary_modules/Makefile15
-rw-r--r--test/integration/targets/binary_modules/aliases1
-rw-r--r--test/integration/targets/binary_modules/download_binary_modules.yml9
-rw-r--r--test/integration/targets/binary_modules/group_vars/all3
-rw-r--r--test/integration/targets/binary_modules/library/.gitignore1
-rw-r--r--test/integration/targets/binary_modules/library/helloworld.go89
-rw-r--r--test/integration/targets/binary_modules/roles/test_binary_modules/tasks/main.yml53
-rwxr-xr-xtest/integration/targets/binary_modules/test.sh8
-rw-r--r--test/integration/targets/binary_modules/test_binary_modules.yml5
-rw-r--r--test/integration/targets/binary_modules_posix/aliases3
-rwxr-xr-xtest/integration/targets/binary_modules_posix/runme.sh6
-rw-r--r--test/integration/targets/binary_modules_winrm/aliases4
-rwxr-xr-xtest/integration/targets/binary_modules_winrm/runme.sh6
-rw-r--r--test/integration/targets/blockinfile/aliases1
-rw-r--r--test/integration/targets/blockinfile/files/sshd_config135
-rw-r--r--test/integration/targets/blockinfile/meta/main.yml3
-rw-r--r--test/integration/targets/blockinfile/tasks/add_block_to_existing_file.yml52
-rw-r--r--test/integration/targets/blockinfile/tasks/block_without_trailing_newline.yml30
-rw-r--r--test/integration/targets/blockinfile/tasks/create_file.yml32
-rw-r--r--test/integration/targets/blockinfile/tasks/diff.yml18
-rw-r--r--test/integration/targets/blockinfile/tasks/file_without_trailing_newline.yml36
-rw-r--r--test/integration/targets/blockinfile/tasks/insertafter.yml37
-rw-r--r--test/integration/targets/blockinfile/tasks/insertbefore.yml39
-rw-r--r--test/integration/targets/blockinfile/tasks/main.yml41
-rw-r--r--test/integration/targets/blockinfile/tasks/multiline_search.yml70
-rw-r--r--test/integration/targets/blockinfile/tasks/preserve_line_endings.yml24
-rw-r--r--test/integration/targets/blockinfile/tasks/validate.yml28
-rw-r--r--test/integration/targets/blocks/43191-2.yml17
-rw-r--r--test/integration/targets/blocks/43191.yml18
-rw-r--r--test/integration/targets/blocks/69848.yml5
-rw-r--r--test/integration/targets/blocks/72725.yml24
-rw-r--r--test/integration/targets/blocks/72781.yml13
-rw-r--r--test/integration/targets/blocks/78612.yml16
-rw-r--r--test/integration/targets/blocks/79711.yml17
-rw-r--r--test/integration/targets/blocks/aliases2
-rw-r--r--test/integration/targets/blocks/always_failure_no_rescue_rc.yml13
-rw-r--r--test/integration/targets/blocks/always_failure_with_rescue_rc.yml16
-rw-r--r--test/integration/targets/blocks/always_no_rescue_rc.yml12
-rw-r--r--test/integration/targets/blocks/block_fail.yml5
-rw-r--r--test/integration/targets/blocks/block_fail_tasks.yml9
-rw-r--r--test/integration/targets/blocks/block_in_rescue.yml33
-rw-r--r--test/integration/targets/blocks/block_rescue_vars.yml16
-rw-r--r--test/integration/targets/blocks/fail.yml2
-rw-r--r--test/integration/targets/blocks/finalized_task.yml17
-rw-r--r--test/integration/targets/blocks/inherit_notify.yml19
-rw-r--r--test/integration/targets/blocks/issue29047.yml4
-rw-r--r--test/integration/targets/blocks/issue29047_tasks.yml13
-rw-r--r--test/integration/targets/blocks/issue71306.yml16
-rw-r--r--test/integration/targets/blocks/main.yml128
-rw-r--r--test/integration/targets/blocks/nested_fail.yml3
-rw-r--r--test/integration/targets/blocks/nested_nested_fail.yml3
-rw-r--r--test/integration/targets/blocks/roles/fail/tasks/main.yml3
-rw-r--r--test/integration/targets/blocks/roles/role-69848-1/meta/main.yml2
-rw-r--r--test/integration/targets/blocks/roles/role-69848-2/meta/main.yml2
-rw-r--r--test/integration/targets/blocks/roles/role-69848-3/tasks/main.yml8
-rwxr-xr-xtest/integration/targets/blocks/runme.sh138
-rw-r--r--test/integration/targets/blocks/unsafe_failed_task.yml17
-rw-r--r--test/integration/targets/builtin_vars_prompt/aliases4
-rwxr-xr-xtest/integration/targets/builtin_vars_prompt/runme.sh6
-rw-r--r--test/integration/targets/builtin_vars_prompt/test-vars_prompt.py130
-rw-r--r--test/integration/targets/builtin_vars_prompt/unsafe.yml20
-rw-r--r--test/integration/targets/builtin_vars_prompt/unsupported.yml18
-rw-r--r--test/integration/targets/builtin_vars_prompt/vars_prompt-1.yml15
-rw-r--r--test/integration/targets/builtin_vars_prompt/vars_prompt-2.yml16
-rw-r--r--test/integration/targets/builtin_vars_prompt/vars_prompt-3.yml17
-rw-r--r--test/integration/targets/builtin_vars_prompt/vars_prompt-4.yml16
-rw-r--r--test/integration/targets/builtin_vars_prompt/vars_prompt-5.yml14
-rw-r--r--test/integration/targets/builtin_vars_prompt/vars_prompt-6.yml20
-rw-r--r--test/integration/targets/builtin_vars_prompt/vars_prompt-7.yml12
-rw-r--r--test/integration/targets/callback_default/aliases1
-rw-r--r--test/integration/targets/callback_default/callback_default.out.check_markers_dry.stderr2
-rw-r--r--test/integration/targets/callback_default/callback_default.out.check_markers_dry.stdout78
-rw-r--r--test/integration/targets/callback_default/callback_default.out.check_markers_wet.stderr2
-rw-r--r--test/integration/targets/callback_default/callback_default.out.check_markers_wet.stdout74
-rw-r--r--test/integration/targets/callback_default/callback_default.out.check_nomarkers_dry.stderr2
-rw-r--r--test/integration/targets/callback_default/callback_default.out.check_nomarkers_dry.stdout74
-rw-r--r--test/integration/targets/callback_default/callback_default.out.check_nomarkers_wet.stderr2
-rw-r--r--test/integration/targets/callback_default/callback_default.out.check_nomarkers_wet.stdout74
-rw-r--r--test/integration/targets/callback_default/callback_default.out.default.stderr2
-rw-r--r--test/integration/targets/callback_default/callback_default.out.default.stdout108
-rw-r--r--test/integration/targets/callback_default/callback_default.out.display_path_on_failure.stderr2
-rw-r--r--test/integration/targets/callback_default/callback_default.out.display_path_on_failure.stdout111
-rw-r--r--test/integration/targets/callback_default/callback_default.out.failed_to_stderr.stderr8
-rw-r--r--test/integration/targets/callback_default/callback_default.out.failed_to_stderr.stdout102
-rw-r--r--test/integration/targets/callback_default/callback_default.out.fqcn_free.stdout35
-rw-r--r--test/integration/targets/callback_default/callback_default.out.free.stdout35
-rw-r--r--test/integration/targets/callback_default/callback_default.out.hide_ok.stderr2
-rw-r--r--test/integration/targets/callback_default/callback_default.out.hide_ok.stdout86
-rw-r--r--test/integration/targets/callback_default/callback_default.out.hide_skipped.stderr2
-rw-r--r--test/integration/targets/callback_default/callback_default.out.hide_skipped.stdout93
-rw-r--r--test/integration/targets/callback_default/callback_default.out.hide_skipped_ok.stderr2
-rw-r--r--test/integration/targets/callback_default/callback_default.out.hide_skipped_ok.stdout71
-rw-r--r--test/integration/targets/callback_default/callback_default.out.host_pinned.stdout35
-rw-r--r--test/integration/targets/callback_default/callback_default.out.result_format_yaml.stderr2
-rw-r--r--test/integration/targets/callback_default/callback_default.out.result_format_yaml.stdout108
-rw-r--r--test/integration/targets/callback_default/callback_default.out.result_format_yaml_lossy_verbose.stderr2
-rw-r--r--test/integration/targets/callback_default/callback_default.out.result_format_yaml_lossy_verbose.stdout300
-rw-r--r--test/integration/targets/callback_default/callback_default.out.result_format_yaml_verbose.stderr2
-rw-r--r--test/integration/targets/callback_default/callback_default.out.result_format_yaml_verbose.stdout312
-rw-r--r--test/integration/targets/callback_default/callback_default.out.yaml_result_format_yaml_verbose.stderr2
-rw-r--r--test/integration/targets/callback_default/callback_default.out.yaml_result_format_yaml_verbose.stdout29
-rw-r--r--test/integration/targets/callback_default/include_me.yml2
-rw-r--r--test/integration/targets/callback_default/inventory10
-rw-r--r--test/integration/targets/callback_default/no_implicit_meta_banners.yml11
-rwxr-xr-xtest/integration/targets/callback_default/runme.sh241
-rw-r--r--test/integration/targets/callback_default/test.yml128
-rw-r--r--test/integration/targets/callback_default/test_2.yml6
-rw-r--r--test/integration/targets/callback_default/test_async.yml14
-rw-r--r--test/integration/targets/callback_default/test_dryrun.yml93
-rw-r--r--test/integration/targets/callback_default/test_non_lockstep.yml7
-rw-r--r--test/integration/targets/callback_default/test_yaml.yml19
-rw-r--r--test/integration/targets/changed_when/aliases2
-rw-r--r--test/integration/targets/changed_when/meta/main.yml2
-rw-r--r--test/integration/targets/changed_when/tasks/main.yml111
-rw-r--r--test/integration/targets/check_mode/aliases2
-rw-r--r--test/integration/targets/check_mode/check_mode-not-on-cli.yml37
-rw-r--r--test/integration/targets/check_mode/check_mode-on-cli.yml36
-rw-r--r--test/integration/targets/check_mode/check_mode.yml7
-rw-r--r--test/integration/targets/check_mode/roles/test_always_run/meta/main.yml17
-rw-r--r--test/integration/targets/check_mode/roles/test_always_run/tasks/main.yml29
-rw-r--r--test/integration/targets/check_mode/roles/test_check_mode/files/foo.txt1
-rw-r--r--test/integration/targets/check_mode/roles/test_check_mode/tasks/main.yml50
-rw-r--r--test/integration/targets/check_mode/roles/test_check_mode/templates/foo.j21
-rw-r--r--test/integration/targets/check_mode/roles/test_check_mode/vars/main.yml1
-rwxr-xr-xtest/integration/targets/check_mode/runme.sh7
-rw-r--r--test/integration/targets/cli/aliases6
-rwxr-xr-xtest/integration/targets/cli/runme.sh9
-rw-r--r--test/integration/targets/cli/setup.yml42
-rw-r--r--test/integration/targets/cli/test-cli.py21
-rw-r--r--test/integration/targets/cli/test_k_and_K.py27
-rw-r--r--test/integration/targets/cli/test_syntax/files/vaultsecret1
-rw-r--r--test/integration/targets/cli/test_syntax/group_vars/all/testvault.yml6
-rw-r--r--test/integration/targets/cli/test_syntax/roles/some_role/tasks/main.yml1
-rw-r--r--test/integration/targets/cli/test_syntax/syntax_check.yml7
-rw-r--r--test/integration/targets/collection/aliases1
-rwxr-xr-xtest/integration/targets/collection/setup.sh29
-rwxr-xr-xtest/integration/targets/collection/update-ignore.py56
-rw-r--r--test/integration/targets/collections/a.statichost.yml3
-rw-r--r--test/integration/targets/collections/aliases4
-rw-r--r--test/integration/targets/collections/ansiballz_dupe/collections/ansible_collections/duplicate/name/plugins/modules/ping.py3
-rw-r--r--test/integration/targets/collections/ansiballz_dupe/test_ansiballz_cache_dupe_shortname.yml15
-rw-r--r--test/integration/targets/collections/cache.statichost.yml7
-rw-r--r--test/integration/targets/collections/check_populated_inventory.yml11
-rw-r--r--test/integration/targets/collections/collection_root_sys/ansible_collections/testns/coll_in_sys/plugins/modules/systestmodule.py13
-rw-r--r--test/integration/targets/collections/collection_root_sys/ansible_collections/testns/testcoll/plugins/modules/maskedmodule.py13
-rw-r--r--test/integration/targets/collections/collection_root_sys/ansible_collections/testns/testcoll/plugins/modules/testmodule.py13
-rw-r--r--test/integration/targets/collections/collection_root_sys/ansible_collections/testns/testcoll/roles/maskedrole/tasks/main.yml2
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/ansible/builtin/plugins/modules/ping.py13
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/ansible/bullcoll/plugins/modules/bullmodule.py13
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/othercoll/plugins/module_utils/formerly_testcoll_pkg/__init__.py1
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/othercoll/plugins/module_utils/formerly_testcoll_pkg/submod.py1
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testbroken/plugins/filter/broken_filter.py13
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/meta/runtime.yml52
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/default_collection_playbook.yml49
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/play.yml4
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role/library/embedded_module.py13
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role/tasks/main.yml29
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role_to_call/tasks/main.yml7
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/type/play.yml4
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/type/subtype/play.yml4
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/action_subdir/subdir_ping_action.py19
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/bypass_host_loop.py17
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/plugin_lookup.py40
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/subclassed_normal.py11
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/uses_redirected_import.py20
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/callback/usercallback.py26
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/connection/localconn.py41
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/doc_fragments/frag.py18
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/filter/filter_subdir/my_subdir_filters.py14
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/filter/myfilters.py14
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/filter/myfilters2.py14
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/lookup/lookup_subdir/my_subdir_lookup.py11
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/lookup/mylookup.py11
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/lookup/mylookup2.py12
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/AnotherCSMU.cs12
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyCSMU.cs19
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyCSMUOptional.cs19
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyPSMU.psm19
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyPSMUOptional.psm116
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/base.py12
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/leaf.py6
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/nested_same/__init__.py0
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/nested_same/nested_same/__init__.py0
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/nested_same/nested_same/nested_same.py6
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/secondary.py6
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg/__init__.py0
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg/subcs.cs13
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg/submod.py6
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg/subps.psm19
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg_with_init.py11
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg_with_init/__init__.py10
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg_with_init/mod_in_subpkg_with_init.py6
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/deprecated_ping.py13
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/module_subdir/subdir_ping_module.py14
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/ping.py13
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/testmodule.py21
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/testmodule_bad_docfrags.py25
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_base_mu_granular_nested_import.py19
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_collection_redirected_mu.py21
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_core_redirected_mu.py19
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_flat_import.bak3
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_flat_import.py19
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_flat_import.yml3
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_granular_import.py19
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_module_import_from.py31
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing.py16
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing_redirect_collection.py16
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing_redirect_module.py16
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_nested_same_as_func.py19
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_nested_same_as_module.py19
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_csbasic_only.ps122
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_selfcontained.ps19
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_selfcontained.py1
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_uses_coll_csmu.ps126
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_uses_coll_psmu.ps125
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_uses_optional.ps133
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/test/mytests.py13
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/test/mytests2.py13
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/test/test_subdir/my_subdir_tests.py13
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/vars/custom_vars.py48
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/call_standalone/tasks/main.yml6
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/calls_intra_collection_dep_role_unqualified/meta/main.yml2
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/calls_intra_collection_dep_role_unqualified/tasks/main.yml7
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/common_handlers/handlers/main.yml27
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/role_subdir/subdir_testrole/tasks/main.yml10
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/test_fqcn_handlers/meta/main.yml2
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/test_fqcn_handlers/tasks/main.yml16
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/testrole/meta/main.yml4
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/testrole/tasks/main.yml39
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/testrole_main_yaml/meta/main.yml4
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/testrole_main_yaml/tasks/main.yml33
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testredirect/meta/runtime.yml23
-rw-r--r--test/integration/targets/collections/collections/ansible_collections/me/mycoll1/plugins/action/action1.py29
-rw-r--r--test/integration/targets/collections/collections/ansible_collections/me/mycoll1/plugins/modules/action1.py24
-rw-r--r--test/integration/targets/collections/collections/ansible_collections/me/mycoll2/plugins/modules/module1.py43
-rw-r--r--test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/cache/custom_jsonfile.py63
-rw-r--r--test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/inventory/statichost.py68
-rw-r--r--test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/module_utils/__init__.py0
-rw-r--r--test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/module_utils/sub1/__init__.py0
-rw-r--r--test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/module_utils/sub1/foomodule.py6
-rw-r--r--test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/modules/contentadjmodule.py13
-rw-r--r--test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/vars/custom_adj_vars.py45
-rw-r--r--test/integration/targets/collections/custom_vars_plugins/v1_vars_plugin.py37
-rw-r--r--test/integration/targets/collections/custom_vars_plugins/v2_vars_plugin.py45
-rw-r--r--test/integration/targets/collections/filter_plugins/override_formerly_core_masked_filter.py13
-rw-r--r--test/integration/targets/collections/import_collection_pb.yml17
-rw-r--r--test/integration/targets/collections/includeme.yml6
-rw-r--r--test/integration/targets/collections/inventory_test.yml26
-rw-r--r--test/integration/targets/collections/invocation_tests.yml5
-rw-r--r--test/integration/targets/collections/library/ping.py13
-rw-r--r--test/integration/targets/collections/noop.yml4
-rw-r--r--test/integration/targets/collections/posix.yml443
-rw-r--r--test/integration/targets/collections/redirected.statichost.yml3
-rw-r--r--test/integration/targets/collections/roles/standalone/tasks/main.yml2
-rw-r--r--test/integration/targets/collections/roles/testrole/tasks/main.yml28
-rwxr-xr-xtest/integration/targets/collections/runme.sh150
-rw-r--r--test/integration/targets/collections/test_bypass_host_loop.yml19
-rw-r--r--test/integration/targets/collections/test_collection_meta.yml75
-rw-r--r--test/integration/targets/collections/test_plugins/override_formerly_core_masked_test.py16
-rw-r--r--test/integration/targets/collections/test_redirect_list.yml86
-rwxr-xr-xtest/integration/targets/collections/test_task_resolved_plugin.sh48
-rw-r--r--test/integration/targets/collections/test_task_resolved_plugin/action_plugins/legacy_action.py14
-rw-r--r--test/integration/targets/collections/test_task_resolved_plugin/callback_plugins/display_resolved_action.py37
-rw-r--r--test/integration/targets/collections/test_task_resolved_plugin/collections/ansible_collections/test_ns/test_coll/meta/runtime.yml7
-rw-r--r--test/integration/targets/collections/test_task_resolved_plugin/collections/ansible_collections/test_ns/test_coll/plugins/action/collection_action.py14
-rw-r--r--test/integration/targets/collections/test_task_resolved_plugin/collections/ansible_collections/test_ns/test_coll/plugins/modules/collection_module.py29
-rw-r--r--test/integration/targets/collections/test_task_resolved_plugin/fqcn.yml14
-rw-r--r--test/integration/targets/collections/test_task_resolved_plugin/library/legacy_module.py29
-rw-r--r--test/integration/targets/collections/test_task_resolved_plugin/unqualified.yml8
-rw-r--r--test/integration/targets/collections/test_task_resolved_plugin/unqualified_and_collections_kw.yml14
-rw-r--r--test/integration/targets/collections/testcoll2/MANIFEST.json0
-rw-r--r--test/integration/targets/collections/testcoll2/plugins/modules/testmodule2.py33
-rwxr-xr-xtest/integration/targets/collections/vars_plugin_tests.sh87
-rw-r--r--test/integration/targets/collections/windows.yml34
-rw-r--r--test/integration/targets/collections_plugin_namespace/aliases2
-rw-r--r--test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/plugins/filter/test_filter.py15
-rw-r--r--test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/plugins/lookup/lookup_name.py9
-rw-r--r--test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/plugins/lookup/lookup_no_future_boilerplate.py10
-rw-r--r--test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/plugins/test/test_test.py13
-rw-r--r--test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/roles/test/tasks/main.yml12
-rwxr-xr-xtest/integration/targets/collections_plugin_namespace/runme.sh5
-rw-r--r--test/integration/targets/collections_plugin_namespace/test.yml3
-rw-r--r--test/integration/targets/collections_relative_imports/aliases4
-rw-r--r--test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/PSRel1.psm111
-rw-r--r--test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/PSRel4.psm112
-rw-r--r--test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/my_util1.py6
-rw-r--r--test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/my_util2.py8
-rw-r--r--test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/my_util3.py8
-rw-r--r--test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/sub_pkg/PSRel2.psm111
-rw-r--r--test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/modules/my_module.py24
-rw-r--r--test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/modules/win_relative.ps110
-rw-r--r--test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/modules/win_relative_optional.ps117
-rw-r--r--test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/roles/test/tasks/main.yml4
-rw-r--r--test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col2/plugins/module_utils/PSRel3.psm111
-rw-r--r--test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col2/plugins/module_utils/sub_pkg/CSRel4.cs14
-rwxr-xr-xtest/integration/targets/collections_relative_imports/runme.sh13
-rw-r--r--test/integration/targets/collections_relative_imports/test.yml3
-rw-r--r--test/integration/targets/collections_relative_imports/windows.yml20
-rw-r--r--test/integration/targets/collections_runtime_pythonpath/aliases2
-rw-r--r--test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-boo/ansible_collections/python/dist/plugins/modules/boo.py28
-rw-r--r--test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-boo/pyproject.toml6
-rw-r--r--test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-boo/setup.cfg15
-rw-r--r--test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-foo/ansible_collections/python/dist/plugins/modules/boo.py28
-rwxr-xr-xtest/integration/targets/collections_runtime_pythonpath/runme.sh60
-rw-r--r--test/integration/targets/command_nonexisting/aliases2
-rw-r--r--test/integration/targets/command_nonexisting/tasks/main.yml4
-rw-r--r--test/integration/targets/command_shell/aliases3
-rwxr-xr-xtest/integration/targets/command_shell/files/create_afile.sh3
-rwxr-xr-xtest/integration/targets/command_shell/files/remove_afile.sh3
-rwxr-xr-xtest/integration/targets/command_shell/files/test.sh3
-rw-r--r--test/integration/targets/command_shell/meta/main.yml3
-rw-r--r--test/integration/targets/command_shell/tasks/main.yml548
-rw-r--r--test/integration/targets/common_network/aliases2
-rw-r--r--test/integration/targets/common_network/tasks/main.yml4
-rw-r--r--test/integration/targets/common_network/test_plugins/is_mac.py14
-rw-r--r--test/integration/targets/conditionals/aliases2
-rw-r--r--test/integration/targets/conditionals/play.yml667
-rwxr-xr-xtest/integration/targets/conditionals/runme.sh5
-rw-r--r--test/integration/targets/conditionals/vars/main.yml29
-rw-r--r--test/integration/targets/config/aliases2
-rw-r--r--test/integration/targets/config/files/types.env11
-rw-r--r--test/integration/targets/config/files/types.ini13
-rw-r--r--test/integration/targets/config/files/types.vars15
-rw-r--r--test/integration/targets/config/files/types_dump.txt8
-rw-r--r--test/integration/targets/config/inline_comment_ansible.cfg2
-rw-r--r--test/integration/targets/config/lookup_plugins/bogus.py51
-rw-r--r--test/integration/targets/config/lookup_plugins/types.py82
-rwxr-xr-xtest/integration/targets/config/runme.sh43
-rw-r--r--test/integration/targets/config/type_munging.cfg8
-rw-r--r--test/integration/targets/config/types.yml25
-rw-r--r--test/integration/targets/config/validation.yml17
-rw-r--r--test/integration/targets/connection/aliases1
-rwxr-xr-xtest/integration/targets/connection/test.sh22
-rw-r--r--test/integration/targets/connection/test_connection.yml43
-rw-r--r--test/integration/targets/connection/test_reset_connection.yml5
-rw-r--r--test/integration/targets/connection_delegation/action_plugins/delegation_action.py12
-rw-r--r--test/integration/targets/connection_delegation/aliases6
-rw-r--r--test/integration/targets/connection_delegation/connection_plugins/delegation_connection.py45
-rw-r--r--test/integration/targets/connection_delegation/inventory.ini1
-rwxr-xr-xtest/integration/targets/connection_delegation/runme.sh9
-rw-r--r--test/integration/targets/connection_delegation/test.yml23
-rw-r--r--test/integration/targets/connection_local/aliases2
-rwxr-xr-xtest/integration/targets/connection_local/runme.sh14
-rw-r--r--test/integration/targets/connection_local/test_connection.inventory7
-rw-r--r--test/integration/targets/connection_paramiko_ssh/aliases5
-rwxr-xr-xtest/integration/targets/connection_paramiko_ssh/runme.sh7
-rwxr-xr-xtest/integration/targets/connection_paramiko_ssh/test.sh14
-rw-r--r--test/integration/targets/connection_paramiko_ssh/test_connection.inventory7
-rw-r--r--test/integration/targets/connection_psrp/aliases4
-rw-r--r--test/integration/targets/connection_psrp/files/empty.txt0
-rwxr-xr-xtest/integration/targets/connection_psrp/runme.sh24
-rw-r--r--test/integration/targets/connection_psrp/test_connection.inventory.j29
-rw-r--r--test/integration/targets/connection_psrp/tests.yml133
-rw-r--r--test/integration/targets/connection_remote_is_local/aliases2
-rw-r--r--test/integration/targets/connection_remote_is_local/connection_plugins/remote_is_local.py25
-rw-r--r--test/integration/targets/connection_remote_is_local/tasks/main.yml15
-rw-r--r--test/integration/targets/connection_remote_is_local/test.yml8
-rw-r--r--test/integration/targets/connection_ssh/aliases3
-rw-r--r--test/integration/targets/connection_ssh/check_ssh_defaults.yml29
-rw-r--r--test/integration/targets/connection_ssh/files/port_overrride_ssh.cfg2
-rwxr-xr-xtest/integration/targets/connection_ssh/posix.sh14
-rwxr-xr-xtest/integration/targets/connection_ssh/runme.sh81
-rw-r--r--test/integration/targets/connection_ssh/test_connection.inventory7
-rw-r--r--test/integration/targets/connection_ssh/test_ssh_defaults.cfg5
-rw-r--r--test/integration/targets/connection_ssh/verify_config.yml21
-rw-r--r--test/integration/targets/connection_windows_ssh/aliases5
-rwxr-xr-xtest/integration/targets/connection_windows_ssh/runme.sh54
-rw-r--r--test/integration/targets/connection_windows_ssh/test_connection.inventory.j212
-rw-r--r--test/integration/targets/connection_windows_ssh/tests.yml32
-rw-r--r--test/integration/targets/connection_windows_ssh/tests_fetch.yml41
-rwxr-xr-xtest/integration/targets/connection_windows_ssh/windows.sh25
-rw-r--r--test/integration/targets/connection_winrm/aliases5
-rwxr-xr-xtest/integration/targets/connection_winrm/runme.sh23
-rw-r--r--test/integration/targets/connection_winrm/test_connection.inventory.j210
-rw-r--r--test/integration/targets/connection_winrm/tests.yml28
-rw-r--r--test/integration/targets/controller/aliases2
-rw-r--r--test/integration/targets/controller/tasks/main.yml9
-rw-r--r--test/integration/targets/copy/aliases3
-rw-r--r--test/integration/targets/copy/defaults/main.yml2
-rw-r--r--test/integration/targets/copy/files-different/vault/folder/nested-vault-file6
-rw-r--r--test/integration/targets/copy/files-different/vault/readme.txt5
-rw-r--r--test/integration/targets/copy/files-different/vault/vault-file6
-rw-r--r--test/integration/targets/copy/files/foo.txt1
-rw-r--r--test/integration/targets/copy/files/subdir/bar.txt1
-rw-r--r--test/integration/targets/copy/files/subdir/subdir1/empty.txt0
-rw-r--r--test/integration/targets/copy/files/subdir/subdir2/baz.txt1
-rw-r--r--test/integration/targets/copy/files/subdir/subdir2/subdir3/subdir4/qux.txt1
-rw-r--r--test/integration/targets/copy/meta/main.yml4
-rw-r--r--test/integration/targets/copy/tasks/acls.yml38
-rw-r--r--test/integration/targets/copy/tasks/check_mode.yml126
-rw-r--r--test/integration/targets/copy/tasks/dest_in_non_existent_directories.yml29
-rw-r--r--test/integration/targets/copy/tasks/dest_in_non_existent_directories_remote_src.yml43
-rw-r--r--test/integration/targets/copy/tasks/main.yml126
-rw-r--r--test/integration/targets/copy/tasks/no_log.yml82
-rw-r--r--test/integration/targets/copy/tasks/selinux.yml36
-rw-r--r--test/integration/targets/copy/tasks/src_file_dest_file_in_non_existent_dir.yml26
-rw-r--r--test/integration/targets/copy/tasks/src_file_dest_file_in_non_existent_dir_remote_src.yml32
-rw-r--r--test/integration/targets/copy/tasks/src_remote_file_is_not_file.yml39
-rw-r--r--test/integration/targets/copy/tasks/tests.yml2286
-rw-r--r--test/integration/targets/cron/aliases4
-rw-r--r--test/integration/targets/cron/defaults/main.yml1
-rw-r--r--test/integration/targets/cron/meta/main.yml2
-rw-r--r--test/integration/targets/cron/tasks/main.yml328
-rw-r--r--test/integration/targets/cron/vars/alpine.yml1
-rw-r--r--test/integration/targets/cron/vars/default.yml1
-rw-r--r--test/integration/targets/dataloader/aliases2
-rw-r--r--test/integration/targets/dataloader/attempt_to_load_invalid_json.yml4
-rwxr-xr-xtest/integration/targets/dataloader/runme.sh6
-rw-r--r--test/integration/targets/dataloader/vars/invalid.json1
-rw-r--r--test/integration/targets/debconf/aliases1
-rw-r--r--test/integration/targets/debconf/meta/main.yml2
-rw-r--r--test/integration/targets/debconf/tasks/main.yml36
-rw-r--r--test/integration/targets/debug/aliases2
-rw-r--r--test/integration/targets/debug/main.yml6
-rw-r--r--test/integration/targets/debug/main_fqcn.yml6
-rw-r--r--test/integration/targets/debug/nosetfacts.yml21
-rwxr-xr-xtest/integration/targets/debug/runme.sh20
-rw-r--r--test/integration/targets/debugger/aliases3
-rw-r--r--test/integration/targets/debugger/inventory2
-rwxr-xr-xtest/integration/targets/debugger/runme.sh5
-rwxr-xr-xtest/integration/targets/debugger/test_run_once.py35
-rw-r--r--test/integration/targets/debugger/test_run_once_playbook.yml12
-rw-r--r--test/integration/targets/delegate_to/aliases4
-rw-r--r--test/integration/targets/delegate_to/connection_plugins/fakelocal.py76
-rw-r--r--test/integration/targets/delegate_to/delegate_and_nolog.yml8
-rw-r--r--test/integration/targets/delegate_to/delegate_facts_block.yml25
-rw-r--r--test/integration/targets/delegate_to/delegate_facts_loop.yml40
-rw-r--r--test/integration/targets/delegate_to/delegate_local_from_root.yml10
-rw-r--r--test/integration/targets/delegate_to/delegate_to_lookup_context.yml4
-rw-r--r--test/integration/targets/delegate_to/delegate_vars_hanldling.yml58
-rw-r--r--test/integration/targets/delegate_to/delegate_with_fact_from_delegate_host.yml18
-rw-r--r--test/integration/targets/delegate_to/discovery_applied.yml8
-rw-r--r--test/integration/targets/delegate_to/files/testfile1
-rw-r--r--test/integration/targets/delegate_to/has_hostvars.yml64
-rw-r--r--test/integration/targets/delegate_to/inventory17
-rw-r--r--test/integration/targets/delegate_to/inventory_interpreters5
-rw-r--r--test/integration/targets/delegate_to/library/detect_interpreter.py18
-rw-r--r--test/integration/targets/delegate_to/resolve_vars.yml16
-rw-r--r--test/integration/targets/delegate_to/roles/delegate_to_lookup_context/tasks/main.yml5
-rw-r--r--test/integration/targets/delegate_to/roles/delegate_to_lookup_context/templates/one.j21
-rw-r--r--test/integration/targets/delegate_to/roles/delegate_to_lookup_context/templates/two.j21
-rw-r--r--test/integration/targets/delegate_to/roles/test_template/templates/foo.j23
-rwxr-xr-xtest/integration/targets/delegate_to/runme.sh78
-rw-r--r--test/integration/targets/delegate_to/test_delegate_to.yml82
-rw-r--r--test/integration/targets/delegate_to/test_delegate_to_lookup_context.yml12
-rw-r--r--test/integration/targets/delegate_to/test_delegate_to_loop_caching.yml45
-rw-r--r--test/integration/targets/delegate_to/test_delegate_to_loop_randomness.yml73
-rw-r--r--test/integration/targets/delegate_to/test_loop_control.yml16
-rw-r--r--test/integration/targets/delegate_to/verify_interpreter.yml47
-rw-r--r--test/integration/targets/dict_transformations/aliases2
-rw-r--r--test/integration/targets/dict_transformations/library/convert_camelCase.py48
-rw-r--r--test/integration/targets/dict_transformations/library/convert_snake_case.py55
-rw-r--r--test/integration/targets/dict_transformations/tasks/main.yml3
-rw-r--r--test/integration/targets/dict_transformations/tasks/test_convert_camelCase.yml33
-rw-r--r--test/integration/targets/dict_transformations/tasks/test_convert_snake_case.yml35
-rw-r--r--test/integration/targets/dnf/aliases6
-rw-r--r--test/integration/targets/dnf/meta/main.yml4
-rw-r--r--test/integration/targets/dnf/tasks/cacheonly.yml16
-rw-r--r--test/integration/targets/dnf/tasks/dnf.yml834
-rw-r--r--test/integration/targets/dnf/tasks/dnfinstallroot.yml35
-rw-r--r--test/integration/targets/dnf/tasks/dnfreleasever.yml47
-rw-r--r--test/integration/targets/dnf/tasks/filters.yml162
-rw-r--r--test/integration/targets/dnf/tasks/filters_check_mode.yml118
-rw-r--r--test/integration/targets/dnf/tasks/gpg.yml88
-rw-r--r--test/integration/targets/dnf/tasks/logging.yml48
-rw-r--r--test/integration/targets/dnf/tasks/main.yml74
-rw-r--r--test/integration/targets/dnf/tasks/modularity.yml104
-rw-r--r--test/integration/targets/dnf/tasks/repo.yml309
-rw-r--r--test/integration/targets/dnf/tasks/skip_broken_and_nobest.yml318
-rw-r--r--test/integration/targets/dnf/tasks/test_sos_removal.yml19
-rw-r--r--test/integration/targets/dnf/vars/CentOS.yml2
-rw-r--r--test/integration/targets/dnf/vars/Fedora.yml6
-rw-r--r--test/integration/targets/dnf/vars/RedHat-9.yml3
-rw-r--r--test/integration/targets/dnf/vars/RedHat.yml2
-rw-r--r--test/integration/targets/dnf/vars/main.yml6
-rw-r--r--test/integration/targets/dpkg_selections/aliases6
-rw-r--r--test/integration/targets/dpkg_selections/defaults/main.yaml1
-rw-r--r--test/integration/targets/dpkg_selections/tasks/dpkg_selections.yaml89
-rw-r--r--test/integration/targets/dpkg_selections/tasks/main.yaml3
-rw-r--r--test/integration/targets/egg-info/aliases2
-rw-r--r--test/integration/targets/egg-info/lookup_plugins/import_pkg_resources.py11
-rw-r--r--test/integration/targets/egg-info/tasks/main.yml3
-rw-r--r--test/integration/targets/embedded_module/aliases2
-rw-r--r--test/integration/targets/embedded_module/library/test_integration_module3
-rw-r--r--test/integration/targets/embedded_module/tasks/main.yml9
-rw-r--r--test/integration/targets/entry_points/aliases2
-rwxr-xr-xtest/integration/targets/entry_points/runme.sh31
-rw-r--r--test/integration/targets/environment/aliases2
-rwxr-xr-xtest/integration/targets/environment/runme.sh5
-rw-r--r--test/integration/targets/environment/test_environment.yml173
-rw-r--r--test/integration/targets/error_from_connection/aliases2
-rw-r--r--test/integration/targets/error_from_connection/connection_plugins/dummy.py42
-rw-r--r--test/integration/targets/error_from_connection/inventory2
-rw-r--r--test/integration/targets/error_from_connection/play.yml20
-rwxr-xr-xtest/integration/targets/error_from_connection/runme.sh5
-rw-r--r--test/integration/targets/expect/aliases3
-rw-r--r--test/integration/targets/expect/files/foo.txt1
-rw-r--r--test/integration/targets/expect/files/test_command.py25
-rw-r--r--test/integration/targets/expect/meta/main.yml2
-rw-r--r--test/integration/targets/expect/tasks/main.yml209
-rw-r--r--test/integration/targets/facts_d/aliases2
-rw-r--r--test/integration/targets/facts_d/files/basdscript.fact3
-rw-r--r--test/integration/targets/facts_d/files/goodscript.fact3
-rw-r--r--test/integration/targets/facts_d/files/preferences.fact2
-rw-r--r--test/integration/targets/facts_d/files/unreadable.fact1
-rw-r--r--test/integration/targets/facts_d/meta/main.yml3
-rw-r--r--test/integration/targets/facts_d/tasks/main.yml53
-rw-r--r--test/integration/targets/facts_linux_network/aliases7
-rw-r--r--test/integration/targets/facts_linux_network/meta/main.yml2
-rw-r--r--test/integration/targets/facts_linux_network/tasks/main.yml51
-rw-r--r--test/integration/targets/failed_when/aliases2
-rw-r--r--test/integration/targets/failed_when/tasks/main.yml80
-rw-r--r--test/integration/targets/fetch/aliases3
-rw-r--r--test/integration/targets/fetch/cleanup.yml16
-rw-r--r--test/integration/targets/fetch/injection/avoid_slurp_return.yml26
-rw-r--r--test/integration/targets/fetch/injection/here.txt1
-rw-r--r--test/integration/targets/fetch/injection/library/slurp.py29
-rw-r--r--test/integration/targets/fetch/roles/fetch_tests/defaults/main.yml1
-rw-r--r--test/integration/targets/fetch/roles/fetch_tests/handlers/main.yml8
-rw-r--r--test/integration/targets/fetch/roles/fetch_tests/meta/main.yml2
-rw-r--r--test/integration/targets/fetch/roles/fetch_tests/tasks/fail_on_missing.yml53
-rw-r--r--test/integration/targets/fetch/roles/fetch_tests/tasks/failures.yml41
-rw-r--r--test/integration/targets/fetch/roles/fetch_tests/tasks/main.yml5
-rw-r--r--test/integration/targets/fetch/roles/fetch_tests/tasks/normal.yml38
-rw-r--r--test/integration/targets/fetch/roles/fetch_tests/tasks/setup.yml46
-rw-r--r--test/integration/targets/fetch/roles/fetch_tests/tasks/symlink.yml13
-rw-r--r--test/integration/targets/fetch/roles/fetch_tests/vars/Darwin.yml4
-rw-r--r--test/integration/targets/fetch/roles/fetch_tests/vars/default.yml1
-rw-r--r--test/integration/targets/fetch/run_fetch_tests.yml5
-rwxr-xr-xtest/integration/targets/fetch/runme.sh34
-rw-r--r--test/integration/targets/fetch/setup_unreadable_test.yml40
-rw-r--r--test/integration/targets/fetch/test_unreadable_with_stat.yml36
-rw-r--r--test/integration/targets/file/aliases2
-rw-r--r--test/integration/targets/file/defaults/main.yml2
-rw-r--r--test/integration/targets/file/files/foo.txt1
-rw-r--r--test/integration/targets/file/files/foobar/directory/fileC0
-rw-r--r--test/integration/targets/file/files/foobar/directory/fileD0
-rw-r--r--test/integration/targets/file/files/foobar/fileA0
-rw-r--r--test/integration/targets/file/files/foobar/fileB0
-rw-r--r--test/integration/targets/file/handlers/main.yml20
-rw-r--r--test/integration/targets/file/meta/main.yml4
-rw-r--r--test/integration/targets/file/tasks/diff_peek.yml10
-rw-r--r--test/integration/targets/file/tasks/directory_as_dest.yml345
-rw-r--r--test/integration/targets/file/tasks/initialize.yml15
-rw-r--r--test/integration/targets/file/tasks/link_rewrite.yml47
-rw-r--r--test/integration/targets/file/tasks/main.yml960
-rw-r--r--test/integration/targets/file/tasks/modification_time.yml70
-rw-r--r--test/integration/targets/file/tasks/selinux_tests.yml33
-rw-r--r--test/integration/targets/file/tasks/state_link.yml501
-rw-r--r--test/integration/targets/file/tasks/unicode_path.yml10
-rw-r--r--test/integration/targets/filter_core/aliases1
-rw-r--r--test/integration/targets/filter_core/files/9851.txt3
-rw-r--r--test/integration/targets/filter_core/files/fileglob/one.txt0
-rw-r--r--test/integration/targets/filter_core/files/fileglob/two.txt0
-rw-r--r--test/integration/targets/filter_core/files/foo.txt69
-rw-r--r--test/integration/targets/filter_core/handle_undefined_type_errors.yml29
-rw-r--r--test/integration/targets/filter_core/host_vars/localhost1
-rw-r--r--test/integration/targets/filter_core/meta/main.yml3
-rwxr-xr-xtest/integration/targets/filter_core/runme.sh6
-rw-r--r--test/integration/targets/filter_core/runme.yml3
-rw-r--r--test/integration/targets/filter_core/tasks/main.yml708
-rw-r--r--test/integration/targets/filter_core/templates/foo.j262
-rw-r--r--test/integration/targets/filter_core/templates/py26json.j22
-rw-r--r--test/integration/targets/filter_core/vars/main.yml106
-rw-r--r--test/integration/targets/filter_encryption/aliases1
-rw-r--r--test/integration/targets/filter_encryption/base.yml37
-rwxr-xr-xtest/integration/targets/filter_encryption/runme.sh5
-rw-r--r--test/integration/targets/filter_mathstuff/aliases1
-rw-r--r--test/integration/targets/filter_mathstuff/host_vars/localhost.yml1
-rwxr-xr-xtest/integration/targets/filter_mathstuff/runme.sh7
-rw-r--r--test/integration/targets/filter_mathstuff/runme.yml4
-rw-r--r--test/integration/targets/filter_mathstuff/tasks/main.yml320
-rw-r--r--test/integration/targets/filter_mathstuff/vars/defined_later.yml3
-rw-r--r--test/integration/targets/filter_mathstuff/vars/main.yml1
-rw-r--r--test/integration/targets/filter_urls/aliases1
-rw-r--r--test/integration/targets/filter_urls/tasks/main.yml24
-rw-r--r--test/integration/targets/filter_urlsplit/aliases1
-rw-r--r--test/integration/targets/filter_urlsplit/tasks/main.yml30
-rw-r--r--test/integration/targets/find/aliases1
-rw-r--r--test/integration/targets/find/files/a.txt2
-rw-r--r--test/integration/targets/find/files/log.txt4
-rw-r--r--test/integration/targets/find/meta/main.yml3
-rw-r--r--test/integration/targets/find/tasks/main.yml376
-rw-r--r--test/integration/targets/fork_safe_stdio/aliases3
-rw-r--r--test/integration/targets/fork_safe_stdio/callback_plugins/spewstdio.py58
-rw-r--r--test/integration/targets/fork_safe_stdio/hosts5
-rwxr-xr-xtest/integration/targets/fork_safe_stdio/run-with-pty.py11
-rwxr-xr-xtest/integration/targets/fork_safe_stdio/runme.sh20
-rw-r--r--test/integration/targets/fork_safe_stdio/test.yml5
-rw-r--r--test/integration/targets/fork_safe_stdio/vendored_pty.py189
-rw-r--r--test/integration/targets/gathering/aliases2
-rw-r--r--test/integration/targets/gathering/explicit.yml14
-rw-r--r--test/integration/targets/gathering/implicit.yml23
-rwxr-xr-xtest/integration/targets/gathering/runme.sh7
-rw-r--r--test/integration/targets/gathering/smart.yml23
-rw-r--r--test/integration/targets/gathering/uuid.fact10
-rw-r--r--test/integration/targets/gathering_facts/aliases3
-rw-r--r--test/integration/targets/gathering_facts/cache_plugins/none.py50
-rw-r--r--test/integration/targets/gathering_facts/collections/ansible_collections/cisco/ios/plugins/modules/ios_facts.py38
-rw-r--r--test/integration/targets/gathering_facts/inventory2
-rw-r--r--test/integration/targets/gathering_facts/library/bogus_facts12
-rw-r--r--test/integration/targets/gathering_facts/library/facts_one25
-rw-r--r--test/integration/targets/gathering_facts/library/facts_two24
-rw-r--r--test/integration/targets/gathering_facts/library/file_utils.py54
-rw-r--r--test/integration/targets/gathering_facts/one_two.json27
-rw-r--r--test/integration/targets/gathering_facts/prevent_clobbering.yml8
-rwxr-xr-xtest/integration/targets/gathering_facts/runme.sh27
-rw-r--r--test/integration/targets/gathering_facts/test_gathering_facts.yml536
-rw-r--r--test/integration/targets/gathering_facts/test_module_defaults.yml130
-rw-r--r--test/integration/targets/gathering_facts/test_prevent_injection.yml14
-rw-r--r--test/integration/targets/gathering_facts/test_run_once.yml32
-rw-r--r--test/integration/targets/gathering_facts/two_one.json27
-rw-r--r--test/integration/targets/gathering_facts/uuid.fact10
-rw-r--r--test/integration/targets/gathering_facts/verify_merge_facts.yml41
-rw-r--r--test/integration/targets/gathering_facts/verify_subset.yml13
-rw-r--r--test/integration/targets/get_url/aliases3
-rw-r--r--test/integration/targets/get_url/files/testserver.py23
-rw-r--r--test/integration/targets/get_url/meta/main.yml4
-rw-r--r--test/integration/targets/get_url/tasks/ciphers.yml19
-rw-r--r--test/integration/targets/get_url/tasks/main.yml674
-rw-r--r--test/integration/targets/get_url/tasks/use_gssapi.yml45
-rw-r--r--test/integration/targets/get_url/tasks/use_netrc.yml67
-rw-r--r--test/integration/targets/getent/aliases1
-rw-r--r--test/integration/targets/getent/meta/main.yml2
-rw-r--r--test/integration/targets/getent/tasks/main.yml46
-rw-r--r--test/integration/targets/git/aliases1
-rw-r--r--test/integration/targets/git/handlers/cleanup-default.yml6
-rw-r--r--test/integration/targets/git/handlers/cleanup-freebsd.yml5
-rw-r--r--test/integration/targets/git/handlers/main.yml7
-rw-r--r--test/integration/targets/git/meta/main.yml4
-rw-r--r--test/integration/targets/git/tasks/ambiguous-ref.yml37
-rw-r--r--test/integration/targets/git/tasks/archive.yml122
-rw-r--r--test/integration/targets/git/tasks/change-repo-url.yml132
-rw-r--r--test/integration/targets/git/tasks/checkout-new-tag.yml54
-rw-r--r--test/integration/targets/git/tasks/depth.yml229
-rw-r--r--test/integration/targets/git/tasks/forcefully-fetch-tag.yml38
-rw-r--r--test/integration/targets/git/tasks/formats.yml40
-rw-r--r--test/integration/targets/git/tasks/gpg-verification.yml212
-rw-r--r--test/integration/targets/git/tasks/localmods.yml112
-rw-r--r--test/integration/targets/git/tasks/main.yml42
-rw-r--r--test/integration/targets/git/tasks/missing_hostkey.yml61
-rw-r--r--test/integration/targets/git/tasks/missing_hostkey_acceptnew.yml78
-rw-r--r--test/integration/targets/git/tasks/no-destination.yml13
-rw-r--r--test/integration/targets/git/tasks/reset-origin.yml25
-rw-r--r--test/integration/targets/git/tasks/separate-git-dir.yml132
-rw-r--r--test/integration/targets/git/tasks/setup-local-repos.yml45
-rw-r--r--test/integration/targets/git/tasks/setup.yml43
-rw-r--r--test/integration/targets/git/tasks/single-branch.yml87
-rw-r--r--test/integration/targets/git/tasks/specific-revision.yml238
-rw-r--r--test/integration/targets/git/tasks/submodules.yml150
-rw-r--r--test/integration/targets/git/vars/main.yml98
-rw-r--r--test/integration/targets/group/aliases1
-rw-r--r--test/integration/targets/group/files/gidget.py15
-rw-r--r--test/integration/targets/group/files/grouplist.sh20
-rw-r--r--test/integration/targets/group/meta/main.yml2
-rw-r--r--test/integration/targets/group/tasks/main.yml40
-rw-r--r--test/integration/targets/group/tasks/tests.yml343
-rw-r--r--test/integration/targets/group_by/aliases1
-rw-r--r--test/integration/targets/group_by/create_groups.yml39
-rw-r--r--test/integration/targets/group_by/group_vars/all3
-rw-r--r--test/integration/targets/group_by/group_vars/camelus1
-rw-r--r--test/integration/targets/group_by/group_vars/vicugna1
-rw-r--r--test/integration/targets/group_by/inventory.group_by9
-rwxr-xr-xtest/integration/targets/group_by/runme.sh6
-rw-r--r--test/integration/targets/group_by/test_group_by.yml187
-rw-r--r--test/integration/targets/group_by/test_group_by_skipped.yml30
-rw-r--r--test/integration/targets/groupby_filter/aliases2
-rw-r--r--test/integration/targets/groupby_filter/tasks/main.yml16
-rw-r--r--test/integration/targets/handler_race/aliases2
-rw-r--r--test/integration/targets/handler_race/inventory30
-rw-r--r--test/integration/targets/handler_race/roles/do_handlers/handlers/main.yml4
-rw-r--r--test/integration/targets/handler_race/roles/do_handlers/tasks/main.yml9
-rw-r--r--test/integration/targets/handler_race/roles/more_sleep/tasks/main.yml8
-rw-r--r--test/integration/targets/handler_race/roles/random_sleep/tasks/main.yml8
-rwxr-xr-xtest/integration/targets/handler_race/runme.sh6
-rw-r--r--test/integration/targets/handler_race/test_handler_race.yml10
-rw-r--r--test/integration/targets/handlers/46447.yml16
-rw-r--r--test/integration/targets/handlers/52561.yml20
-rw-r--r--test/integration/targets/handlers/54991.yml11
-rw-r--r--test/integration/targets/handlers/58841.yml9
-rw-r--r--test/integration/targets/handlers/79776-handlers.yml2
-rw-r--r--test/integration/targets/handlers/79776.yml10
-rw-r--r--test/integration/targets/handlers/aliases2
-rw-r--r--test/integration/targets/handlers/from_handlers.yml39
-rw-r--r--test/integration/targets/handlers/handlers.yml2
-rw-r--r--test/integration/targets/handlers/include_handlers_fail_force-handlers.yml2
-rw-r--r--test/integration/targets/handlers/include_handlers_fail_force.yml11
-rw-r--r--test/integration/targets/handlers/inventory.handlers10
-rw-r--r--test/integration/targets/handlers/order.yml34
-rw-r--r--test/integration/targets/handlers/roles/import_template_handler_names/tasks/main.yml11
-rw-r--r--test/integration/targets/handlers/roles/template_handler_names/handlers/main.yml5
-rw-r--r--test/integration/targets/handlers/roles/template_handler_names/tasks/evaluation_time.yml5
-rw-r--r--test/integration/targets/handlers/roles/template_handler_names/tasks/lazy_evaluation.yml5
-rw-r--r--test/integration/targets/handlers/roles/test_force_handlers/handlers/main.yml2
-rw-r--r--test/integration/targets/handlers/roles/test_force_handlers/tasks/main.yml26
-rw-r--r--test/integration/targets/handlers/roles/test_handlers/handlers/main.yml5
-rw-r--r--test/integration/targets/handlers/roles/test_handlers/meta/main.yml1
-rw-r--r--test/integration/targets/handlers/roles/test_handlers/tasks/main.yml52
-rw-r--r--test/integration/targets/handlers/roles/test_handlers_include/handlers/main.yml1
-rw-r--r--test/integration/targets/handlers/roles/test_handlers_include/tasks/main.yml4
-rw-r--r--test/integration/targets/handlers/roles/test_handlers_include_role/handlers/main.yml5
-rw-r--r--test/integration/targets/handlers/roles/test_handlers_include_role/meta/main.yml1
-rw-r--r--test/integration/targets/handlers/roles/test_handlers_include_role/tasks/main.yml47
-rw-r--r--test/integration/targets/handlers/roles/test_handlers_listen/handlers/main.yml10
-rw-r--r--test/integration/targets/handlers/roles/test_handlers_listen/tasks/main.yml6
-rw-r--r--test/integration/targets/handlers/roles/test_handlers_meta/handlers/alternate.yml12
-rw-r--r--test/integration/targets/handlers/roles/test_handlers_meta/handlers/main.yml10
-rw-r--r--test/integration/targets/handlers/roles/test_handlers_meta/tasks/main.yml75
-rw-r--r--test/integration/targets/handlers/roles/test_role_handlers_include_tasks/handlers/A.yml1
-rw-r--r--test/integration/targets/handlers/roles/test_role_handlers_include_tasks/handlers/main.yml5
-rw-r--r--test/integration/targets/handlers/roles/test_role_handlers_include_tasks/tasks/B.yml1
-rw-r--r--test/integration/targets/handlers/roles/test_templating_in_handlers/handlers/main.yml21
-rw-r--r--test/integration/targets/handlers/roles/test_templating_in_handlers/tasks/main.yml26
-rwxr-xr-xtest/integration/targets/handlers/runme.sh183
-rw-r--r--test/integration/targets/handlers/test_block_as_handler-import.yml7
-rw-r--r--test/integration/targets/handlers/test_block_as_handler-include.yml8
-rw-r--r--test/integration/targets/handlers/test_block_as_handler-include_import-handlers.yml8
-rw-r--r--test/integration/targets/handlers/test_block_as_handler.yml13
-rw-r--r--test/integration/targets/handlers/test_flush_handlers_as_handler.yml9
-rw-r--r--test/integration/targets/handlers/test_flush_handlers_rescue_always.yml22
-rw-r--r--test/integration/targets/handlers/test_flush_in_rescue_always.yml35
-rw-r--r--test/integration/targets/handlers/test_force_handlers.yml27
-rw-r--r--test/integration/targets/handlers/test_fqcn_meta_flush_handlers.yml14
-rw-r--r--test/integration/targets/handlers/test_handlers.yml47
-rw-r--r--test/integration/targets/handlers/test_handlers_any_errors_fatal.yml24
-rw-r--r--test/integration/targets/handlers/test_handlers_include.yml14
-rw-r--r--test/integration/targets/handlers/test_handlers_include_role.yml8
-rw-r--r--test/integration/targets/handlers/test_handlers_including_task.yml16
-rw-r--r--test/integration/targets/handlers/test_handlers_inexistent_notify.yml10
-rw-r--r--test/integration/targets/handlers/test_handlers_infinite_loop.yml25
-rw-r--r--test/integration/targets/handlers/test_handlers_listen.yml128
-rw-r--r--test/integration/targets/handlers/test_handlers_meta.yml9
-rw-r--r--test/integration/targets/handlers/test_handlers_template_run_once.yml12
-rw-r--r--test/integration/targets/handlers/test_listening_handlers.yml39
-rw-r--r--test/integration/targets/handlers/test_notify_included-handlers.yml3
-rw-r--r--test/integration/targets/handlers/test_notify_included.yml16
-rw-r--r--test/integration/targets/handlers/test_role_as_handler.yml11
-rw-r--r--test/integration/targets/handlers/test_role_handlers_including_tasks.yml18
-rw-r--r--test/integration/targets/handlers/test_skip_flush.yml13
-rw-r--r--test/integration/targets/handlers/test_templating_in_handlers.yml62
-rw-r--r--test/integration/targets/hardware_facts/aliases4
-rw-r--r--test/integration/targets/hardware_facts/meta/main.yml2
-rw-r--r--test/integration/targets/hardware_facts/tasks/Linux.yml92
-rw-r--r--test/integration/targets/hardware_facts/tasks/main.yml2
-rw-r--r--test/integration/targets/hash/aliases2
-rw-r--r--test/integration/targets/hash/group_vars/all3
-rw-r--r--test/integration/targets/hash/host_vars/testhost2
-rw-r--r--test/integration/targets/hash/roles/test_hash_behaviour/defaults/main.yml21
-rw-r--r--test/integration/targets/hash/roles/test_hash_behaviour/meta/main.yml17
-rw-r--r--test/integration/targets/hash/roles/test_hash_behaviour/tasks/main.yml37
-rw-r--r--test/integration/targets/hash/roles/test_hash_behaviour/vars/main.yml21
-rwxr-xr-xtest/integration/targets/hash/runme.sh11
-rw-r--r--test/integration/targets/hash/test_hash.yml21
-rw-r--r--test/integration/targets/hash/test_inv1.yml10
-rw-r--r--test/integration/targets/hash/test_inv2.yml8
-rw-r--r--test/integration/targets/hash/test_inventory_hash.yml41
-rw-r--r--test/integration/targets/hash/vars/test_hash_vars.yml3
-rw-r--r--test/integration/targets/hostname/aliases2
-rw-r--r--test/integration/targets/hostname/tasks/Debian.yml20
-rw-r--r--test/integration/targets/hostname/tasks/MacOSX.yml52
-rw-r--r--test/integration/targets/hostname/tasks/RedHat.yml15
-rw-r--r--test/integration/targets/hostname/tasks/check_mode.yml20
-rw-r--r--test/integration/targets/hostname/tasks/default.yml2
-rw-r--r--test/integration/targets/hostname/tasks/main.yml52
-rw-r--r--test/integration/targets/hostname/tasks/test_check_mode.yml50
-rw-r--r--test/integration/targets/hostname/tasks/test_normal.yml54
-rw-r--r--test/integration/targets/hostname/vars/FreeBSD.yml1
-rw-r--r--test/integration/targets/hostname/vars/RedHat.yml1
-rw-r--r--test/integration/targets/hostname/vars/default.yml1
-rw-r--r--test/integration/targets/hosts_field/aliases2
-rw-r--r--test/integration/targets/hosts_field/inventory.hosts_field1
-rwxr-xr-xtest/integration/targets/hosts_field/runme.sh49
-rw-r--r--test/integration/targets/hosts_field/test_hosts_field.json1
-rw-r--r--test/integration/targets/hosts_field/test_hosts_field.yml62
-rw-r--r--test/integration/targets/ignore_errors/aliases2
-rw-r--r--test/integration/targets/ignore_errors/meta/main.yml2
-rw-r--r--test/integration/targets/ignore_errors/tasks/main.yml22
-rw-r--r--test/integration/targets/ignore_unreachable/aliases2
-rw-r--r--test/integration/targets/ignore_unreachable/fake_connectors/bad_exec.py14
-rw-r--r--test/integration/targets/ignore_unreachable/fake_connectors/bad_put_file.py14
-rw-r--r--test/integration/targets/ignore_unreachable/inventory3
-rw-r--r--test/integration/targets/ignore_unreachable/meta/main.yml2
-rwxr-xr-xtest/integration/targets/ignore_unreachable/runme.sh16
-rw-r--r--test/integration/targets/ignore_unreachable/test_base_cannot_connect.yml5
-rw-r--r--test/integration/targets/ignore_unreachable/test_cannot_connect.yml29
-rw-r--r--test/integration/targets/ignore_unreachable/test_with_bad_plugins.yml24
-rw-r--r--test/integration/targets/import_tasks/aliases2
-rw-r--r--test/integration/targets/import_tasks/inherit_notify.yml15
-rwxr-xr-xtest/integration/targets/import_tasks/runme.sh5
-rw-r--r--test/integration/targets/import_tasks/tasks/trigger_change.yml2
-rw-r--r--test/integration/targets/incidental_ios_file/aliases2
-rw-r--r--test/integration/targets/incidental_ios_file/defaults/main.yaml2
-rw-r--r--test/integration/targets/incidental_ios_file/ios1.cfg3
-rw-r--r--test/integration/targets/incidental_ios_file/nonascii.binbin0 -> 32768 bytes
-rw-r--r--test/integration/targets/incidental_ios_file/tasks/cli.yaml17
-rw-r--r--test/integration/targets/incidental_ios_file/tasks/main.yaml2
-rw-r--r--test/integration/targets/incidental_ios_file/tests/cli/net_get.yaml52
-rw-r--r--test/integration/targets/incidental_ios_file/tests/cli/net_put.yaml73
-rw-r--r--test/integration/targets/incidental_vyos_config/aliases2
-rw-r--r--test/integration/targets/incidental_vyos_config/defaults/main.yaml3
-rw-r--r--test/integration/targets/incidental_vyos_config/tasks/cli.yaml26
-rw-r--r--test/integration/targets/incidental_vyos_config/tasks/cli_config.yaml18
-rw-r--r--test/integration/targets/incidental_vyos_config/tasks/main.yaml3
-rw-r--r--test/integration/targets/incidental_vyos_config/tests/cli/backup.yaml113
-rw-r--r--test/integration/targets/incidental_vyos_config/tests/cli/check_config.yaml63
-rw-r--r--test/integration/targets/incidental_vyos_config/tests/cli/comment.yaml34
-rw-r--r--test/integration/targets/incidental_vyos_config/tests/cli/config.cfg3
-rw-r--r--test/integration/targets/incidental_vyos_config/tests/cli/save.yaml54
-rw-r--r--test/integration/targets/incidental_vyos_config/tests/cli/simple.yaml53
-rw-r--r--test/integration/targets/incidental_vyos_config/tests/cli_config/cli_backup.yaml114
-rw-r--r--test/integration/targets/incidental_vyos_config/tests/cli_config/cli_basic.yaml28
-rw-r--r--test/integration/targets/incidental_vyos_config/tests/cli_config/cli_comment.yaml30
-rw-r--r--test/integration/targets/incidental_vyos_lldp_interfaces/aliases2
-rw-r--r--test/integration/targets/incidental_vyos_lldp_interfaces/defaults/main.yaml3
-rw-r--r--test/integration/targets/incidental_vyos_lldp_interfaces/meta/main.yaml3
-rw-r--r--test/integration/targets/incidental_vyos_lldp_interfaces/tasks/cli.yaml19
-rw-r--r--test/integration/targets/incidental_vyos_lldp_interfaces/tasks/main.yaml2
-rw-r--r--test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/_populate.yaml14
-rw-r--r--test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/_populate_intf.yaml10
-rw-r--r--test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/_remove_config.yaml8
-rw-r--r--test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/deleted.yaml46
-rw-r--r--test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/empty_config.yaml36
-rw-r--r--test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/merged.yaml58
-rw-r--r--test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/overridden.yaml49
-rw-r--r--test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/replaced.yaml63
-rw-r--r--test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/rtt.yaml57
-rw-r--r--test/integration/targets/incidental_vyos_lldp_interfaces/vars/main.yaml130
-rw-r--r--test/integration/targets/incidental_vyos_prepare_tests/aliases1
-rw-r--r--test/integration/targets/incidental_vyos_prepare_tests/tasks/main.yaml13
-rw-r--r--test/integration/targets/incidental_win_reboot/aliases2
-rw-r--r--test/integration/targets/incidental_win_reboot/tasks/main.yml70
-rw-r--r--test/integration/targets/incidental_win_reboot/templates/post_reboot.ps18
-rw-r--r--test/integration/targets/include_import/aliases2
-rw-r--r--test/integration/targets/include_import/apply/import_apply.yml31
-rw-r--r--test/integration/targets/include_import/apply/include_apply.yml50
-rw-r--r--test/integration/targets/include_import/apply/include_apply_65710.yml11
-rw-r--r--test/integration/targets/include_import/apply/include_tasks.yml2
-rw-r--r--test/integration/targets/include_import/apply/roles/include_role/tasks/main.yml2
-rw-r--r--test/integration/targets/include_import/apply/roles/include_role2/tasks/main.yml2
-rw-r--r--test/integration/targets/include_import/empty_group_warning/playbook.yml13
-rw-r--r--test/integration/targets/include_import/empty_group_warning/tasks.yml3
-rw-r--r--test/integration/targets/include_import/grandchild/block_include_tasks.yml2
-rw-r--r--test/integration/targets/include_import/grandchild/import.yml1
-rw-r--r--test/integration/targets/include_import/grandchild/import_include_include_tasks.yml2
-rw-r--r--test/integration/targets/include_import/grandchild/include_level_1.yml1
-rw-r--r--test/integration/targets/include_import/handler_addressing/playbook.yml11
-rw-r--r--test/integration/targets/include_import/handler_addressing/roles/import_handler_test/handlers/main.yml2
-rw-r--r--test/integration/targets/include_import/handler_addressing/roles/import_handler_test/tasks/handlers.yml2
-rw-r--r--test/integration/targets/include_import/handler_addressing/roles/import_handler_test/tasks/main.yml3
-rw-r--r--test/integration/targets/include_import/handler_addressing/roles/include_handler_test/handlers/main.yml2
-rw-r--r--test/integration/targets/include_import/handler_addressing/roles/include_handler_test/tasks/handlers.yml2
-rw-r--r--test/integration/targets/include_import/handler_addressing/roles/include_handler_test/tasks/main.yml3
-rw-r--r--test/integration/targets/include_import/include_role_omit/playbook.yml12
-rw-r--r--test/integration/targets/include_import/include_role_omit/roles/foo/tasks/main.yml2
-rw-r--r--test/integration/targets/include_import/inventory6
-rw-r--r--test/integration/targets/include_import/issue73657.yml8
-rw-r--r--test/integration/targets/include_import/issue73657_tasks.yml2
-rw-r--r--test/integration/targets/include_import/nestedtasks/nested/nested.yml2
-rw-r--r--test/integration/targets/include_import/parent_templating/playbook.yml11
-rw-r--r--test/integration/targets/include_import/parent_templating/roles/test/tasks/localhost.yml1
-rw-r--r--test/integration/targets/include_import/parent_templating/roles/test/tasks/main.yml1
-rw-r--r--test/integration/targets/include_import/parent_templating/roles/test/tasks/other.yml2
-rw-r--r--test/integration/targets/include_import/playbook/group_vars/all.yml1
-rw-r--r--test/integration/targets/include_import/playbook/playbook1.yml9
-rw-r--r--test/integration/targets/include_import/playbook/playbook2.yml9
-rw-r--r--test/integration/targets/include_import/playbook/playbook3.yml10
-rw-r--r--test/integration/targets/include_import/playbook/playbook4.yml9
-rw-r--r--test/integration/targets/include_import/playbook/playbook_needing_vars.yml6
-rw-r--r--test/integration/targets/include_import/playbook/roles/import_playbook_role/tasks/main.yml2
-rw-r--r--test/integration/targets/include_import/playbook/sub_playbook/library/helloworld.py30
-rw-r--r--test/integration/targets/include_import/playbook/sub_playbook/sub_playbook.yml4
-rw-r--r--test/integration/targets/include_import/playbook/test_import_playbook.yml22
-rw-r--r--test/integration/targets/include_import/playbook/test_import_playbook_tags.yml10
-rw-r--r--test/integration/targets/include_import/playbook/test_templated_filenames.yml47
-rw-r--r--test/integration/targets/include_import/playbook/validate1.yml10
-rw-r--r--test/integration/targets/include_import/playbook/validate2.yml10
-rw-r--r--test/integration/targets/include_import/playbook/validate34.yml11
-rw-r--r--test/integration/targets/include_import/playbook/validate_tags.yml11
-rw-r--r--test/integration/targets/include_import/playbook/validate_templated_playbook.yml5
-rw-r--r--test/integration/targets/include_import/playbook/validate_templated_tasks.yml1
-rw-r--r--test/integration/targets/include_import/public_exposure/no_bleeding.yml25
-rw-r--r--test/integration/targets/include_import/public_exposure/no_overwrite_roles.yml4
-rw-r--r--test/integration/targets/include_import/public_exposure/playbook.yml56
-rw-r--r--test/integration/targets/include_import/public_exposure/roles/call_import/tasks/main.yml6
-rw-r--r--test/integration/targets/include_import/public_exposure/roles/dynamic/defaults/main.yml1
-rw-r--r--test/integration/targets/include_import/public_exposure/roles/dynamic/tasks/main.yml5
-rw-r--r--test/integration/targets/include_import/public_exposure/roles/dynamic/vars/main.yml1
-rw-r--r--test/integration/targets/include_import/public_exposure/roles/dynamic_private/defaults/main.yml1
-rw-r--r--test/integration/targets/include_import/public_exposure/roles/dynamic_private/tasks/main.yml5
-rw-r--r--test/integration/targets/include_import/public_exposure/roles/dynamic_private/vars/main.yml1
-rw-r--r--test/integration/targets/include_import/public_exposure/roles/from/defaults/from.yml1
-rw-r--r--test/integration/targets/include_import/public_exposure/roles/from/tasks/from.yml5
-rw-r--r--test/integration/targets/include_import/public_exposure/roles/from/vars/from.yml1
-rw-r--r--test/integration/targets/include_import/public_exposure/roles/regular/defaults/main.yml1
-rw-r--r--test/integration/targets/include_import/public_exposure/roles/regular/tasks/main.yml5
-rw-r--r--test/integration/targets/include_import/public_exposure/roles/regular/vars/main.yml1
-rw-r--r--test/integration/targets/include_import/public_exposure/roles/static/defaults/main.yml1
-rw-r--r--test/integration/targets/include_import/public_exposure/roles/static/tasks/main.yml5
-rw-r--r--test/integration/targets/include_import/public_exposure/roles/static/vars/main.yml1
-rw-r--r--test/integration/targets/include_import/role/test_import_role.yml139
-rw-r--r--test/integration/targets/include_import/role/test_include_role.yml166
-rw-r--r--test/integration/targets/include_import/role/test_include_role_vars_from.yml10
-rw-r--r--test/integration/targets/include_import/roles/delegated_handler/handlers/main.yml4
-rw-r--r--test/integration/targets/include_import/roles/delegated_handler/tasks/main.yml3
-rw-r--r--test/integration/targets/include_import/roles/dup_allowed_role/meta/main.yml2
-rw-r--r--test/integration/targets/include_import/roles/dup_allowed_role/tasks/main.yml3
-rw-r--r--test/integration/targets/include_import/roles/loop_name_assert/tasks/main.yml4
-rw-r--r--test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/defaults/main.yml3
-rw-r--r--test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/meta/main.yml2
-rw-r--r--test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/tasks/main.yml2
-rw-r--r--test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/tasks/rund.yml2
-rw-r--r--test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/vars/main.yml2
-rw-r--r--test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/defaults/main.yml3
-rw-r--r--test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/meta/main.yml2
-rw-r--r--test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/tasks/main.yml2
-rw-r--r--test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/tasks/rune.yml2
-rw-r--r--test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/vars/main.yml2
-rw-r--r--test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/defaults/main.yml3
-rw-r--r--test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/meta/main.yml1
-rw-r--r--test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/tasks/main.yml2
-rw-r--r--test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/tasks/runf.yml2
-rw-r--r--test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/vars/main.yml2
-rw-r--r--test/integration/targets/include_import/roles/nested/nested_dep_role/defaults/main.yml3
-rw-r--r--test/integration/targets/include_import/roles/nested/nested_dep_role/meta/main.yml2
-rw-r--r--test/integration/targets/include_import/roles/nested/nested_dep_role/tasks/main.yml2
-rw-r--r--test/integration/targets/include_import/roles/nested/nested_dep_role/tasks/runc.yml4
-rw-r--r--test/integration/targets/include_import/roles/nested/nested_dep_role/vars/main.yml2
-rw-r--r--test/integration/targets/include_import/roles/nested_include_task/meta/main.yml2
-rw-r--r--test/integration/targets/include_import/roles/nested_include_task/tasks/main.yml2
-rw-r--r--test/integration/targets/include_import/roles/nested_include_task/tasks/runa.yml3
-rw-r--r--test/integration/targets/include_import/roles/role1/tasks/canary1.yml2
-rw-r--r--test/integration/targets/include_import/roles/role1/tasks/canary2.yml2
-rw-r--r--test/integration/targets/include_import/roles/role1/tasks/canary3.yml2
-rw-r--r--test/integration/targets/include_import/roles/role1/tasks/fail.yml3
-rw-r--r--test/integration/targets/include_import/roles/role1/tasks/main.yml3
-rw-r--r--test/integration/targets/include_import/roles/role1/tasks/r1t01.yml1
-rw-r--r--test/integration/targets/include_import/roles/role1/tasks/r1t02.yml1
-rw-r--r--test/integration/targets/include_import/roles/role1/tasks/r1t03.yml1
-rw-r--r--test/integration/targets/include_import/roles/role1/tasks/r1t04.yml1
-rw-r--r--test/integration/targets/include_import/roles/role1/tasks/r1t05.yml1
-rw-r--r--test/integration/targets/include_import/roles/role1/tasks/r1t06.yml1
-rw-r--r--test/integration/targets/include_import/roles/role1/tasks/r1t07.yml1
-rw-r--r--test/integration/targets/include_import/roles/role1/tasks/r1t08.yml1
-rw-r--r--test/integration/targets/include_import/roles/role1/tasks/r1t09.yml1
-rw-r--r--test/integration/targets/include_import/roles/role1/tasks/r1t10.yml1
-rw-r--r--test/integration/targets/include_import/roles/role1/tasks/r1t11.yml1
-rw-r--r--test/integration/targets/include_import/roles/role1/tasks/r1t12.yml2
-rw-r--r--test/integration/targets/include_import/roles/role1/tasks/tasks.yml2
-rw-r--r--test/integration/targets/include_import/roles/role1/tasks/templated.yml1
-rw-r--r--test/integration/targets/include_import/roles/role1/tasks/vartest.yml2
-rw-r--r--test/integration/targets/include_import/roles/role1/vars/main.yml1
-rw-r--r--test/integration/targets/include_import/roles/role1/vars/role1vars.yml1
-rw-r--r--test/integration/targets/include_import/roles/role2/tasks/main.yml3
-rw-r--r--test/integration/targets/include_import/roles/role3/defaults/main.yml2
-rw-r--r--test/integration/targets/include_import/roles/role3/handlers/main.yml3
-rw-r--r--test/integration/targets/include_import/roles/role3/tasks/main.yml3
-rw-r--r--test/integration/targets/include_import/roles/role3/tasks/tasks.yml2
-rw-r--r--test/integration/targets/include_import/roles/role3/tasks/vartest.yml2
-rw-r--r--test/integration/targets/include_import/roles/role3/vars/main.yml1
-rw-r--r--test/integration/targets/include_import/roles/role3/vars/role3vars.yml2
-rw-r--r--test/integration/targets/include_import/roles/role_with_deps/meta/main.yml3
-rw-r--r--test/integration/targets/include_import/roles/role_with_deps/tasks/main.yml2
-rw-r--r--test/integration/targets/include_import/run_once/include_me.yml2
-rw-r--r--test/integration/targets/include_import/run_once/playbook.yml61
-rwxr-xr-xtest/integration/targets/include_import/runme.sh141
-rw-r--r--test/integration/targets/include_import/tasks/debug_item.yml2
-rw-r--r--test/integration/targets/include_import/tasks/hello/.gitignore1
-rw-r--r--test/integration/targets/include_import/tasks/hello/keep0
-rw-r--r--test/integration/targets/include_import/tasks/nested/nested.yml2
-rw-r--r--test/integration/targets/include_import/tasks/tasks1.yml5
-rw-r--r--test/integration/targets/include_import/tasks/tasks2.yml5
-rw-r--r--test/integration/targets/include_import/tasks/tasks3.yml5
-rw-r--r--test/integration/targets/include_import/tasks/tasks4.yml5
-rw-r--r--test/integration/targets/include_import/tasks/tasks5.yml6
-rw-r--r--test/integration/targets/include_import/tasks/tasks6.yml5
-rw-r--r--test/integration/targets/include_import/tasks/test_allow_single_role_dup.yml8
-rw-r--r--test/integration/targets/include_import/tasks/test_import_tasks.yml41
-rw-r--r--test/integration/targets/include_import/tasks/test_import_tasks_tags.yml23
-rw-r--r--test/integration/targets/include_import/tasks/test_include_dupe_loop.yml8
-rw-r--r--test/integration/targets/include_import/tasks/test_include_tasks.yml44
-rw-r--r--test/integration/targets/include_import/tasks/test_include_tasks_tags.yml25
-rw-r--r--test/integration/targets/include_import/tasks/test_recursion.yml6
-rw-r--r--test/integration/targets/include_import/tasks/validate3.yml4
-rw-r--r--test/integration/targets/include_import/tasks/validate_tags.yml8
-rw-r--r--test/integration/targets/include_import/test_copious_include_tasks.yml44
-rw-r--r--test/integration/targets/include_import/test_copious_include_tasks_fqcn.yml44
-rw-r--r--test/integration/targets/include_import/test_grandparent_inheritance.yml29
-rw-r--r--test/integration/targets/include_import/test_grandparent_inheritance_fqcn.yml29
-rw-r--r--test/integration/targets/include_import/test_include_loop.yml17
-rw-r--r--test/integration/targets/include_import/test_include_loop_fqcn.yml17
-rw-r--r--test/integration/targets/include_import/test_loop_var_bleed.yaml9
-rw-r--r--test/integration/targets/include_import/test_nested_tasks.yml6
-rw-r--r--test/integration/targets/include_import/test_nested_tasks_fqcn.yml6
-rw-r--r--test/integration/targets/include_import/test_role_recursion.yml7
-rw-r--r--test/integration/targets/include_import/test_role_recursion_fqcn.yml7
-rw-r--r--test/integration/targets/include_import/undefined_var/include_tasks.yml5
-rw-r--r--test/integration/targets/include_import/undefined_var/include_that_defines_var.yml5
-rw-r--r--test/integration/targets/include_import/undefined_var/playbook.yml35
-rw-r--r--test/integration/targets/include_import/valid_include_keywords/include_me.yml6
-rw-r--r--test/integration/targets/include_import/valid_include_keywords/include_me_listen.yml2
-rw-r--r--test/integration/targets/include_import/valid_include_keywords/include_me_notify.yml2
-rw-r--r--test/integration/targets/include_import/valid_include_keywords/playbook.yml40
-rw-r--r--test/integration/targets/include_import_tasks_nested/aliases2
-rw-r--r--test/integration/targets/include_import_tasks_nested/tasks/main.yml11
-rw-r--r--test/integration/targets/include_import_tasks_nested/tasks/nested/nested_adjacent.yml2
-rw-r--r--test/integration/targets/include_import_tasks_nested/tasks/nested/nested_import.yml1
-rw-r--r--test/integration/targets/include_import_tasks_nested/tasks/nested/nested_include.yml1
-rw-r--r--test/integration/targets/include_parent_role_vars/aliases2
-rw-r--r--test/integration/targets/include_parent_role_vars/tasks/included_by_other_role.yml37
-rw-r--r--test/integration/targets/include_parent_role_vars/tasks/included_by_ourselves.yml14
-rw-r--r--test/integration/targets/include_parent_role_vars/tasks/main.yml21
-rw-r--r--test/integration/targets/include_vars-ad-hoc/aliases2
-rw-r--r--test/integration/targets/include_vars-ad-hoc/dir/inc.yml1
-rwxr-xr-xtest/integration/targets/include_vars-ad-hoc/runme.sh6
-rw-r--r--test/integration/targets/include_vars/aliases1
-rw-r--r--test/integration/targets/include_vars/defaults/main.yml3
-rw-r--r--test/integration/targets/include_vars/tasks/main.yml217
-rw-r--r--test/integration/targets/include_vars/vars/all/all.yml3
-rw-r--r--test/integration/targets/include_vars/vars/environments/development/all.yml3
-rw-r--r--test/integration/targets/include_vars/vars/environments/development/services/webapp.yml4
-rw-r--r--test/integration/targets/include_vars/vars/no_auto_unsafe.yml1
-rw-r--r--test/integration/targets/include_vars/vars/services/service_vars.yml2
-rw-r--r--test/integration/targets/include_vars/vars/services/service_vars_fqcn.yml3
-rw-r--r--test/integration/targets/include_vars/vars/services/webapp.yml4
-rw-r--r--test/integration/targets/include_vars/vars/webapp/file_without_extension2
-rw-r--r--test/integration/targets/include_vars/vars2/hashes/hash1.yml5
-rw-r--r--test/integration/targets/include_vars/vars2/hashes/hash2.yml5
-rw-r--r--test/integration/targets/include_when_parent_is_dynamic/aliases2
-rw-r--r--test/integration/targets/include_when_parent_is_dynamic/playbook.yml4
-rwxr-xr-xtest/integration/targets/include_when_parent_is_dynamic/runme.sh13
-rw-r--r--test/integration/targets/include_when_parent_is_dynamic/syntax_error.yml1
-rw-r--r--test/integration/targets/include_when_parent_is_dynamic/tasks.yml12
-rw-r--r--test/integration/targets/include_when_parent_is_static/aliases2
-rw-r--r--test/integration/targets/include_when_parent_is_static/playbook.yml4
-rwxr-xr-xtest/integration/targets/include_when_parent_is_static/runme.sh13
-rw-r--r--test/integration/targets/include_when_parent_is_static/syntax_error.yml1
-rw-r--r--test/integration/targets/include_when_parent_is_static/tasks.yml12
-rw-r--r--test/integration/targets/includes/aliases2
-rw-r--r--test/integration/targets/includes/include_on_playbook_should_fail.yml1
-rw-r--r--test/integration/targets/includes/includes_loop_rescue.yml29
-rw-r--r--test/integration/targets/includes/inherit_notify.yml18
-rw-r--r--test/integration/targets/includes/roles/test_includes/handlers/main.yml1
-rw-r--r--test/integration/targets/includes/roles/test_includes/handlers/more_handlers.yml12
-rw-r--r--test/integration/targets/includes/roles/test_includes/tasks/branch_toplevel.yml11
-rw-r--r--test/integration/targets/includes/roles/test_includes/tasks/empty.yml0
-rw-r--r--test/integration/targets/includes/roles/test_includes/tasks/included_task1.yml9
-rw-r--r--test/integration/targets/includes/roles/test_includes/tasks/leaf_sublevel.yml2
-rw-r--r--test/integration/targets/includes/roles/test_includes/tasks/main.yml114
-rw-r--r--test/integration/targets/includes/roles/test_includes/tasks/not_a_role_task.yml4
-rw-r--r--test/integration/targets/includes/roles/test_includes_free/tasks/inner.yml2
-rw-r--r--test/integration/targets/includes/roles/test_includes_free/tasks/inner_fqcn.yml2
-rw-r--r--test/integration/targets/includes/roles/test_includes_free/tasks/main.yml9
-rw-r--r--test/integration/targets/includes/roles/test_includes_host_pinned/tasks/inner.yml2
-rw-r--r--test/integration/targets/includes/roles/test_includes_host_pinned/tasks/main.yml6
-rwxr-xr-xtest/integration/targets/includes/runme.sh16
-rw-r--r--test/integration/targets/includes/tasks/trigger_change.yml2
-rw-r--r--test/integration/targets/includes/test_include_free.yml10
-rw-r--r--test/integration/targets/includes/test_include_host_pinned.yml9
-rw-r--r--test/integration/targets/includes/test_includes.yml10
-rw-r--r--test/integration/targets/includes/test_includes2.yml22
-rw-r--r--test/integration/targets/includes/test_includes3.yml6
-rw-r--r--test/integration/targets/includes/test_includes4.yml2
-rw-r--r--test/integration/targets/includes_race/aliases2
-rw-r--r--test/integration/targets/includes_race/inventory30
-rw-r--r--test/integration/targets/includes_race/roles/random_sleep/tasks/main.yml8
-rw-r--r--test/integration/targets/includes_race/roles/set_a_fact/tasks/fact1.yml4
-rw-r--r--test/integration/targets/includes_race/roles/set_a_fact/tasks/fact2.yml4
-rwxr-xr-xtest/integration/targets/includes_race/runme.sh5
-rw-r--r--test/integration/targets/includes_race/test_includes_race.yml19
-rw-r--r--test/integration/targets/infra/aliases4
-rw-r--r--test/integration/targets/infra/inventory.local2
-rw-r--r--test/integration/targets/infra/library/test.py24
-rwxr-xr-xtest/integration/targets/infra/runme.sh39
-rw-r--r--test/integration/targets/infra/test_test_infra.yml25
-rw-r--r--test/integration/targets/interpreter_discovery_python/aliases3
-rw-r--r--test/integration/targets/interpreter_discovery_python/library/test_echo_module.py29
-rw-r--r--test/integration/targets/interpreter_discovery_python/tasks/main.yml183
-rw-r--r--test/integration/targets/interpreter_discovery_python_delegate_facts/aliases3
-rw-r--r--test/integration/targets/interpreter_discovery_python_delegate_facts/delegate_facts.yml10
-rw-r--r--test/integration/targets/interpreter_discovery_python_delegate_facts/inventory2
-rwxr-xr-xtest/integration/targets/interpreter_discovery_python_delegate_facts/runme.sh5
-rw-r--r--test/integration/targets/inventory-invalid-group/aliases2
-rw-r--r--test/integration/targets/inventory-invalid-group/inventory.ini5
-rwxr-xr-xtest/integration/targets/inventory-invalid-group/runme.sh13
-rw-r--r--test/integration/targets/inventory-invalid-group/test.yml3
-rw-r--r--test/integration/targets/inventory/1/2/3/extra_vars_relative.yml19
-rw-r--r--test/integration/targets/inventory/1/2/inventory.yml3
-rw-r--r--test/integration/targets/inventory/1/vars.yml1
-rw-r--r--test/integration/targets/inventory/aliases2
-rw-r--r--test/integration/targets/inventory/extra_vars_constructed.yml5
-rw-r--r--test/integration/targets/inventory/host_vars_constructed.yml6
-rw-r--r--test/integration/targets/inventory/inv_with_host_vars.yml5
-rw-r--r--test/integration/targets/inventory/inv_with_int.yml6
-rw-r--r--test/integration/targets/inventory/inventory_plugins/contructed_with_hostvars.py44
-rw-r--r--test/integration/targets/inventory/playbook.yml4
-rwxr-xr-xtest/integration/targets/inventory/runme.sh114
-rw-r--r--test/integration/targets/inventory/strategy.yml12
-rw-r--r--test/integration/targets/inventory/test_empty.yml0
-rw-r--r--test/integration/targets/inventory_advanced_host_list/aliases1
-rwxr-xr-xtest/integration/targets/inventory_advanced_host_list/runme.sh36
-rw-r--r--test/integration/targets/inventory_advanced_host_list/test_advanced_host_list.yml9
-rw-r--r--test/integration/targets/inventory_cache/aliases2
-rw-r--r--test/integration/targets/inventory_cache/cache/.keep0
-rw-r--r--test/integration/targets/inventory_cache/cache_host.yml4
-rw-r--r--test/integration/targets/inventory_cache/exercise_cache.yml4
-rw-r--r--test/integration/targets/inventory_cache/plugins/inventory/cache_host.py56
-rw-r--r--test/integration/targets/inventory_cache/plugins/inventory/exercise_cache.py344
-rwxr-xr-xtest/integration/targets/inventory_cache/runme.sh25
-rw-r--r--test/integration/targets/inventory_constructed/aliases1
-rw-r--r--test/integration/targets/inventory_constructed/constructed.yml19
-rw-r--r--test/integration/targets/inventory_constructed/invs/1/group_vars/stuff.yml1
-rw-r--r--test/integration/targets/inventory_constructed/invs/1/host_vars/testing.yml1
-rw-r--r--test/integration/targets/inventory_constructed/invs/1/one.yml5
-rw-r--r--test/integration/targets/inventory_constructed/invs/2/constructed.yml7
-rw-r--r--test/integration/targets/inventory_constructed/keyed_group_default_value.yml5
-rw-r--r--test/integration/targets/inventory_constructed/keyed_group_list_default_value.yml5
-rw-r--r--test/integration/targets/inventory_constructed/keyed_group_str_default_value.yml5
-rw-r--r--test/integration/targets/inventory_constructed/keyed_group_trailing_separator.yml5
-rw-r--r--test/integration/targets/inventory_constructed/no_leading_separator_constructed.yml20
-rwxr-xr-xtest/integration/targets/inventory_constructed/runme.sh59
-rw-r--r--test/integration/targets/inventory_constructed/static_inventory.yml8
-rw-r--r--test/integration/targets/inventory_constructed/tag_inventory.yml12
-rw-r--r--test/integration/targets/inventory_ini/aliases1
-rw-r--r--test/integration/targets/inventory_ini/inventory.ini5
-rwxr-xr-xtest/integration/targets/inventory_ini/runme.sh5
-rw-r--r--test/integration/targets/inventory_ini/test_ansible_become.yml11
-rw-r--r--test/integration/targets/inventory_script/aliases1
-rw-r--r--test/integration/targets/inventory_script/inventory.json1045
-rwxr-xr-xtest/integration/targets/inventory_script/inventory.sh7
-rwxr-xr-xtest/integration/targets/inventory_script/runme.sh5
-rw-r--r--test/integration/targets/inventory_yaml/aliases1
-rw-r--r--test/integration/targets/inventory_yaml/empty.json10
-rwxr-xr-xtest/integration/targets/inventory_yaml/runme.sh6
-rw-r--r--test/integration/targets/inventory_yaml/success.json61
-rw-r--r--test/integration/targets/inventory_yaml/test.yml27
-rw-r--r--test/integration/targets/inventory_yaml/test_int_hostname.yml5
-rw-r--r--test/integration/targets/iptables/aliases5
-rw-r--r--test/integration/targets/iptables/tasks/chain_management.yml71
-rw-r--r--test/integration/targets/iptables/tasks/main.yml36
-rw-r--r--test/integration/targets/iptables/vars/alpine.yml2
-rw-r--r--test/integration/targets/iptables/vars/centos.yml2
-rw-r--r--test/integration/targets/iptables/vars/default.yml2
-rw-r--r--test/integration/targets/iptables/vars/fedora.yml2
-rw-r--r--test/integration/targets/iptables/vars/redhat.yml2
-rw-r--r--test/integration/targets/iptables/vars/suse.yml2
-rw-r--r--test/integration/targets/jinja2_native_types/aliases2
-rw-r--r--test/integration/targets/jinja2_native_types/nested_undefined.yml23
-rwxr-xr-xtest/integration/targets/jinja2_native_types/runme.sh11
-rw-r--r--test/integration/targets/jinja2_native_types/runtests.yml40
-rw-r--r--test/integration/targets/jinja2_native_types/test_bool.yml53
-rw-r--r--test/integration/targets/jinja2_native_types/test_casting.yml31
-rw-r--r--test/integration/targets/jinja2_native_types/test_concatentation.yml88
-rw-r--r--test/integration/targets/jinja2_native_types/test_dunder.yml23
-rw-r--r--test/integration/targets/jinja2_native_types/test_hostvars.yml10
-rw-r--r--test/integration/targets/jinja2_native_types/test_none.yml11
-rw-r--r--test/integration/targets/jinja2_native_types/test_preserving_quotes.yml14
-rw-r--r--test/integration/targets/jinja2_native_types/test_template.yml27
-rw-r--r--test/integration/targets/jinja2_native_types/test_template_newlines.j24
-rw-r--r--test/integration/targets/jinja2_native_types/test_types.yml20
-rw-r--r--test/integration/targets/jinja2_native_types/test_vault.yml16
-rw-r--r--test/integration/targets/jinja2_native_types/test_vault_pass1
-rw-r--r--test/integration/targets/jinja_plugins/aliases2
-rw-r--r--test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/filter/bad_collection_filter.py11
-rw-r--r--test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/filter/bad_collection_filter2.py10
-rw-r--r--test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/filter/good_collection_filter.py13
-rw-r--r--test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/test/bad_collection_test.py11
-rw-r--r--test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/test/good_collection_test.py13
-rw-r--r--test/integration/targets/jinja_plugins/filter_plugins/bad_filter.py11
-rw-r--r--test/integration/targets/jinja_plugins/filter_plugins/good_filter.py13
-rw-r--r--test/integration/targets/jinja_plugins/playbook.yml10
-rw-r--r--test/integration/targets/jinja_plugins/tasks/main.yml23
-rw-r--r--test/integration/targets/jinja_plugins/test_plugins/bad_test.py11
-rw-r--r--test/integration/targets/jinja_plugins/test_plugins/good_test.py13
-rw-r--r--test/integration/targets/json_cleanup/aliases2
-rw-r--r--test/integration/targets/json_cleanup/library/bad_json11
-rw-r--r--test/integration/targets/json_cleanup/module_output_cleaning.yml26
-rwxr-xr-xtest/integration/targets/json_cleanup/runme.sh5
-rw-r--r--test/integration/targets/keyword_inheritance/aliases3
-rw-r--r--test/integration/targets/keyword_inheritance/roles/whoami/tasks/main.yml3
-rwxr-xr-xtest/integration/targets/keyword_inheritance/runme.sh5
-rw-r--r--test/integration/targets/keyword_inheritance/test.yml8
-rw-r--r--test/integration/targets/known_hosts/aliases1
-rw-r--r--test/integration/targets/known_hosts/defaults/main.yml6
-rw-r--r--test/integration/targets/known_hosts/files/existing_known_hosts5
-rw-r--r--test/integration/targets/known_hosts/meta/main.yml3
-rw-r--r--test/integration/targets/known_hosts/tasks/main.yml409
-rw-r--r--test/integration/targets/limit_inventory/aliases2
-rw-r--r--test/integration/targets/limit_inventory/hosts.yml5
-rwxr-xr-xtest/integration/targets/limit_inventory/runme.sh31
-rw-r--r--test/integration/targets/lineinfile/aliases1
-rw-r--r--test/integration/targets/lineinfile/files/firstmatch.txt5
-rw-r--r--test/integration/targets/lineinfile/files/test.conf5
-rw-r--r--test/integration/targets/lineinfile/files/test.txt5
-rw-r--r--test/integration/targets/lineinfile/files/test_58923.txt4
-rw-r--r--test/integration/targets/lineinfile/files/testempty.txt0
-rw-r--r--test/integration/targets/lineinfile/files/testmultiple.txt7
-rw-r--r--test/integration/targets/lineinfile/files/testnoeof.txt2
-rw-r--r--test/integration/targets/lineinfile/files/teststring.conf5
-rw-r--r--test/integration/targets/lineinfile/files/teststring.txt5
-rw-r--r--test/integration/targets/lineinfile/files/teststring_58923.txt4
-rw-r--r--test/integration/targets/lineinfile/meta/main.yml21
-rw-r--r--test/integration/targets/lineinfile/tasks/main.yml1399
-rw-r--r--test/integration/targets/lineinfile/tasks/test_string01.yml142
-rw-r--r--test/integration/targets/lineinfile/tasks/test_string02.yml166
-rw-r--r--test/integration/targets/lineinfile/vars/main.yml29
-rw-r--r--test/integration/targets/lookup_config/aliases1
-rw-r--r--test/integration/targets/lookup_config/tasks/main.yml74
-rw-r--r--test/integration/targets/lookup_csvfile/aliases1
-rw-r--r--test/integration/targets/lookup_csvfile/files/cool list of things.csv3
-rw-r--r--test/integration/targets/lookup_csvfile/files/crlf.csv2
-rw-r--r--test/integration/targets/lookup_csvfile/files/people.csv6
-rw-r--r--test/integration/targets/lookup_csvfile/files/tabs.csv4
-rw-r--r--test/integration/targets/lookup_csvfile/files/x1a.csv3
-rw-r--r--test/integration/targets/lookup_csvfile/tasks/main.yml83
-rw-r--r--test/integration/targets/lookup_dict/aliases1
-rw-r--r--test/integration/targets/lookup_dict/tasks/main.yml56
-rw-r--r--test/integration/targets/lookup_env/aliases1
-rwxr-xr-xtest/integration/targets/lookup_env/runme.sh12
-rw-r--r--test/integration/targets/lookup_env/tasks/main.yml15
-rw-r--r--test/integration/targets/lookup_file/aliases1
-rw-r--r--test/integration/targets/lookup_file/tasks/main.yml13
-rw-r--r--test/integration/targets/lookup_fileglob/aliases1
-rw-r--r--test/integration/targets/lookup_fileglob/find_levels/files/play_adj_subdir.txt1
-rw-r--r--test/integration/targets/lookup_fileglob/find_levels/files/somepath/play_adj_subsubdir.txt1
-rw-r--r--test/integration/targets/lookup_fileglob/find_levels/play.yml13
-rw-r--r--test/integration/targets/lookup_fileglob/find_levels/play_adj.txt1
-rw-r--r--test/integration/targets/lookup_fileglob/find_levels/roles/get_file/files/in_role.txt1
-rw-r--r--test/integration/targets/lookup_fileglob/find_levels/roles/get_file/files/otherpath/in_role_subdir.txt1
-rw-r--r--test/integration/targets/lookup_fileglob/find_levels/roles/get_file/tasks/main.yml10
-rw-r--r--test/integration/targets/lookup_fileglob/issue72873/test.yml31
-rw-r--r--test/integration/targets/lookup_fileglob/non_existent/play.yml6
-rwxr-xr-xtest/integration/targets/lookup_fileglob/runme.sh18
-rw-r--r--test/integration/targets/lookup_first_found/aliases1
-rw-r--r--test/integration/targets/lookup_first_found/files/bar11
-rw-r--r--test/integration/targets/lookup_first_found/files/foo11
-rw-r--r--test/integration/targets/lookup_first_found/files/vars file spaces.yml1
-rw-r--r--test/integration/targets/lookup_first_found/tasks/main.yml96
-rw-r--r--test/integration/targets/lookup_indexed_items/aliases1
-rw-r--r--test/integration/targets/lookup_indexed_items/tasks/main.yml32
-rw-r--r--test/integration/targets/lookup_ini/aliases1
-rw-r--r--test/integration/targets/lookup_ini/duplicate.ini3
-rw-r--r--test/integration/targets/lookup_ini/duplicate_case_check.ini3
-rw-r--r--test/integration/targets/lookup_ini/inventory2
-rw-r--r--test/integration/targets/lookup_ini/lookup-8859-15.ini7
-rw-r--r--test/integration/targets/lookup_ini/lookup.ini25
-rw-r--r--test/integration/targets/lookup_ini/lookup.properties6
-rw-r--r--test/integration/targets/lookup_ini/lookup_case_check.properties2
-rw-r--r--test/integration/targets/lookup_ini/mysql.ini8
-rwxr-xr-xtest/integration/targets/lookup_ini/runme.sh5
-rw-r--r--test/integration/targets/lookup_ini/test_allow_no_value.yml23
-rw-r--r--test/integration/targets/lookup_ini/test_case_sensitive.yml31
-rw-r--r--test/integration/targets/lookup_ini/test_errors.yml62
-rw-r--r--test/integration/targets/lookup_ini/test_ini.yml4
-rw-r--r--test/integration/targets/lookup_ini/test_lookup_properties.yml88
-rw-r--r--test/integration/targets/lookup_inventory_hostnames/aliases1
-rw-r--r--test/integration/targets/lookup_inventory_hostnames/inventory18
-rw-r--r--test/integration/targets/lookup_inventory_hostnames/main.yml23
-rwxr-xr-xtest/integration/targets/lookup_inventory_hostnames/runme.sh5
-rw-r--r--test/integration/targets/lookup_items/aliases1
-rw-r--r--test/integration/targets/lookup_items/tasks/main.yml20
-rw-r--r--test/integration/targets/lookup_lines/aliases1
-rw-r--r--test/integration/targets/lookup_lines/tasks/main.yml13
-rw-r--r--test/integration/targets/lookup_list/aliases1
-rw-r--r--test/integration/targets/lookup_list/tasks/main.yml19
-rw-r--r--test/integration/targets/lookup_nested/aliases1
-rw-r--r--test/integration/targets/lookup_nested/tasks/main.yml18
-rw-r--r--test/integration/targets/lookup_password/aliases1
-rwxr-xr-xtest/integration/targets/lookup_password/runme.sh11
-rw-r--r--test/integration/targets/lookup_password/runme.yml4
-rw-r--r--test/integration/targets/lookup_password/tasks/main.yml149
-rw-r--r--test/integration/targets/lookup_pipe/aliases1
-rw-r--r--test/integration/targets/lookup_pipe/tasks/main.yml9
-rw-r--r--test/integration/targets/lookup_random_choice/aliases1
-rw-r--r--test/integration/targets/lookup_random_choice/tasks/main.yml10
-rw-r--r--test/integration/targets/lookup_sequence/aliases1
-rw-r--r--test/integration/targets/lookup_sequence/tasks/main.yml198
-rw-r--r--test/integration/targets/lookup_subelements/aliases1
-rw-r--r--test/integration/targets/lookup_subelements/tasks/main.yml224
-rw-r--r--test/integration/targets/lookup_subelements/vars/main.yml43
-rw-r--r--test/integration/targets/lookup_template/aliases1
-rw-r--r--test/integration/targets/lookup_template/tasks/main.yml34
-rw-r--r--test/integration/targets/lookup_template/templates/dict.j21
-rw-r--r--test/integration/targets/lookup_template/templates/hello.txt1
-rw-r--r--test/integration/targets/lookup_template/templates/hello_comment.txt2
-rw-r--r--test/integration/targets/lookup_template/templates/hello_string.txt1
-rw-r--r--test/integration/targets/lookup_template/templates/world.txt1
-rw-r--r--test/integration/targets/lookup_together/aliases1
-rw-r--r--test/integration/targets/lookup_together/tasks/main.yml29
-rw-r--r--test/integration/targets/lookup_unvault/aliases2
-rw-r--r--test/integration/targets/lookup_unvault/files/foot.txt1
-rw-r--r--test/integration/targets/lookup_unvault/files/foot.txt.vault6
-rwxr-xr-xtest/integration/targets/lookup_unvault/runme.sh6
-rw-r--r--test/integration/targets/lookup_unvault/secret1
-rw-r--r--test/integration/targets/lookup_unvault/unvault.yml9
-rw-r--r--test/integration/targets/lookup_url/aliases4
-rw-r--r--test/integration/targets/lookup_url/meta/main.yml2
-rw-r--r--test/integration/targets/lookup_url/tasks/main.yml54
-rw-r--r--test/integration/targets/lookup_url/tasks/use_netrc.yml37
-rw-r--r--test/integration/targets/lookup_varnames/aliases1
-rw-r--r--test/integration/targets/lookup_varnames/tasks/main.yml38
-rw-r--r--test/integration/targets/lookup_vars/aliases1
-rw-r--r--test/integration/targets/lookup_vars/tasks/main.yml56
-rw-r--r--test/integration/targets/loop-connection/aliases2
-rw-r--r--test/integration/targets/loop-connection/collections/ansible_collections/ns/name/meta/runtime.yml4
-rw-r--r--test/integration/targets/loop-connection/collections/ansible_collections/ns/name/plugins/connection/dummy.py50
-rw-r--r--test/integration/targets/loop-connection/main.yml33
-rwxr-xr-xtest/integration/targets/loop-connection/runme.sh5
-rw-r--r--test/integration/targets/loop-until/aliases2
-rw-r--r--test/integration/targets/loop-until/tasks/main.yml160
-rw-r--r--test/integration/targets/loop_control/aliases2
-rw-r--r--test/integration/targets/loop_control/extended.yml23
-rw-r--r--test/integration/targets/loop_control/inner.yml9
-rw-r--r--test/integration/targets/loop_control/label.yml23
-rwxr-xr-xtest/integration/targets/loop_control/runme.sh12
-rw-r--r--test/integration/targets/loops/aliases2
-rw-r--r--test/integration/targets/loops/files/data1.txt1
-rw-r--r--test/integration/targets/loops/files/data2.txt1
-rw-r--r--test/integration/targets/loops/tasks/index_var_tasks.yml3
-rw-r--r--test/integration/targets/loops/tasks/main.yml407
-rw-r--r--test/integration/targets/loops/tasks/templated_loop_var_tasks.yml4
-rw-r--r--test/integration/targets/loops/vars/64169.yml2
-rw-r--r--test/integration/targets/loops/vars/main.yml8
-rw-r--r--test/integration/targets/meta_tasks/aliases2
-rw-r--r--test/integration/targets/meta_tasks/inventory.yml9
-rw-r--r--test/integration/targets/meta_tasks/inventory_new.yml8
-rw-r--r--test/integration/targets/meta_tasks/inventory_old.yml8
-rw-r--r--test/integration/targets/meta_tasks/inventory_refresh.yml8
-rw-r--r--test/integration/targets/meta_tasks/refresh.yml38
-rw-r--r--test/integration/targets/meta_tasks/refresh_preserve_dynamic.yml69
-rwxr-xr-xtest/integration/targets/meta_tasks/runme.sh78
-rw-r--r--test/integration/targets/meta_tasks/test_end_batch.yml13
-rw-r--r--test/integration/targets/meta_tasks/test_end_host.yml14
-rw-r--r--test/integration/targets/meta_tasks/test_end_host_all.yml13
-rw-r--r--test/integration/targets/meta_tasks/test_end_host_all_fqcn.yml13
-rw-r--r--test/integration/targets/meta_tasks/test_end_host_fqcn.yml14
-rw-r--r--test/integration/targets/meta_tasks/test_end_play.yml12
-rw-r--r--test/integration/targets/meta_tasks/test_end_play_fqcn.yml12
-rw-r--r--test/integration/targets/meta_tasks/test_end_play_multiple_plays.yml18
-rw-r--r--test/integration/targets/meta_tasks/test_end_play_serial_one.yml13
-rw-r--r--test/integration/targets/missing_required_lib/aliases2
-rw-r--r--test/integration/targets/missing_required_lib/library/missing_required_lib.py37
-rwxr-xr-xtest/integration/targets/missing_required_lib/runme.sh5
-rw-r--r--test/integration/targets/missing_required_lib/runme.yml57
-rw-r--r--test/integration/targets/missing_required_lib/tasks/main.yml3
-rw-r--r--test/integration/targets/module_defaults/action_plugins/debug.py80
-rw-r--r--test/integration/targets/module_defaults/aliases2
-rw-r--r--test/integration/targets/module_defaults/collections/ansible_collections/testns/othercoll/plugins/action/other_echoaction.py8
-rw-r--r--test/integration/targets/module_defaults/collections/ansible_collections/testns/othercoll/plugins/modules/other_echo1.py13
-rw-r--r--test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/meta/runtime.yml85
-rw-r--r--test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/echoaction.py19
-rw-r--r--test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/eos.py18
-rw-r--r--test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/ios.py18
-rw-r--r--test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/vyos.py18
-rw-r--r--test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/module_utils/echo_impl.py15
-rw-r--r--test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/echo1.py13
-rw-r--r--test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/echo2.py13
-rw-r--r--test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/eosfacts.py35
-rw-r--r--test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/ios_facts.py35
-rw-r--r--test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/metadata.py45
-rw-r--r--test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/module.py35
-rw-r--r--test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/ping.py83
-rw-r--r--test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/vyosfacts.py35
-rw-r--r--test/integration/targets/module_defaults/library/legacy_ping.py83
-rw-r--r--test/integration/targets/module_defaults/library/test_module_defaults.py30
-rwxr-xr-xtest/integration/targets/module_defaults/runme.sh14
-rw-r--r--test/integration/targets/module_defaults/tasks/main.yml89
-rw-r--r--test/integration/targets/module_defaults/templates/test_metadata_warning.yml.j28
-rw-r--r--test/integration/targets/module_defaults/test_action_group_metadata.yml123
-rw-r--r--test/integration/targets/module_defaults/test_action_groups.yml132
-rw-r--r--test/integration/targets/module_defaults/test_defaults.yml249
-rw-r--r--test/integration/targets/module_no_log/aliases5
-rw-r--r--test/integration/targets/module_no_log/library/module_that_logs.py18
-rw-r--r--test/integration/targets/module_no_log/tasks/main.yml61
-rw-r--r--test/integration/targets/module_precedence/aliases2
-rw-r--r--test/integration/targets/module_precedence/lib_no_extension/ping71
-rw-r--r--test/integration/targets/module_precedence/lib_with_extension/a.ini13
-rw-r--r--test/integration/targets/module_precedence/lib_with_extension/a.py13
-rw-r--r--test/integration/targets/module_precedence/lib_with_extension/ping.ini13
-rw-r--r--test/integration/targets/module_precedence/lib_with_extension/ping.py71
-rw-r--r--test/integration/targets/module_precedence/modules_test.yml10
-rw-r--r--test/integration/targets/module_precedence/modules_test_envvar.yml11
-rw-r--r--test/integration/targets/module_precedence/modules_test_envvar_ext.yml16
-rw-r--r--test/integration/targets/module_precedence/modules_test_multiple_roles.yml17
-rw-r--r--test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml16
-rw-r--r--test/integration/targets/module_precedence/modules_test_role.yml13
-rw-r--r--test/integration/targets/module_precedence/modules_test_role_ext.yml18
-rw-r--r--test/integration/targets/module_precedence/multiple_roles/bar/library/ping.py71
-rw-r--r--test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml10
-rw-r--r--test/integration/targets/module_precedence/multiple_roles/foo/library/ping.py71
-rw-r--r--test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml10
-rw-r--r--test/integration/targets/module_precedence/roles_no_extension/foo/library/ping71
-rw-r--r--test/integration/targets/module_precedence/roles_no_extension/foo/tasks/main.yml10
-rw-r--r--test/integration/targets/module_precedence/roles_with_extension/foo/library/a.ini13
-rw-r--r--test/integration/targets/module_precedence/roles_with_extension/foo/library/a.py13
-rw-r--r--test/integration/targets/module_precedence/roles_with_extension/foo/library/ping.ini13
-rw-r--r--test/integration/targets/module_precedence/roles_with_extension/foo/library/ping.py71
-rw-r--r--test/integration/targets/module_precedence/roles_with_extension/foo/tasks/main.yml10
-rwxr-xr-xtest/integration/targets/module_precedence/runme.sh49
-rw-r--r--test/integration/targets/module_tracebacks/aliases3
-rw-r--r--test/integration/targets/module_tracebacks/inventory5
-rwxr-xr-xtest/integration/targets/module_tracebacks/runme.sh5
-rw-r--r--test/integration/targets/module_tracebacks/traceback.yml21
-rw-r--r--test/integration/targets/module_utils/aliases6
-rw-r--r--test/integration/targets/module_utils/callback/pure_json.py31
-rw-r--r--test/integration/targets/module_utils/collections/ansible_collections/testns/testcoll/plugins/module_utils/legit.py6
-rw-r--r--test/integration/targets/module_utils/library/test.py87
-rw-r--r--test/integration/targets/module_utils/library/test_alias_deprecation.py16
-rw-r--r--test/integration/targets/module_utils/library/test_cwd_missing.py33
-rw-r--r--test/integration/targets/module_utils/library/test_cwd_unreadable.py28
-rw-r--r--test/integration/targets/module_utils/library/test_datetime.py19
-rw-r--r--test/integration/targets/module_utils/library/test_env_override.py14
-rw-r--r--test/integration/targets/module_utils/library/test_failure.py14
-rw-r--r--test/integration/targets/module_utils/library/test_network.py28
-rw-r--r--test/integration/targets/module_utils/library/test_no_log.py35
-rw-r--r--test/integration/targets/module_utils/library/test_optional.py84
-rw-r--r--test/integration/targets/module_utils/library/test_override.py11
-rw-r--r--test/integration/targets/module_utils/library/test_recursive_diff.py29
-rw-r--r--test/integration/targets/module_utils/module_utils/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/a/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/a/b/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/a/b/c/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/a/b/c/d/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/a/b/c/d/e/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/g/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/g/h/__init__.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/ansible_release.py4
-rw-r--r--test/integration/targets/module_utils/module_utils/bar0/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/bar0/foo.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/bar1/__init__.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/bar2/__init__.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/baz1/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/baz1/one.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/baz2/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/baz2/one.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/foo.py3
-rw-r--r--test/integration/targets/module_utils/module_utils/foo0.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/foo1.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/foo2.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/qux1/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/qux1/quux.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/qux2/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/qux2/quux.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/qux2/quuz.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/service.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/spam1/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam1/ham/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam1/ham/eggs/__init__.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/spam2/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam2/ham/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam2/ham/eggs/__init__.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/spam3/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam3/ham/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam3/ham/bacon.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/spam4/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam4/ham/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam4/ham/bacon.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/spam5/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam5/ham/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam5/ham/bacon.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/spam5/ham/eggs.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/spam6/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam6/ham/__init__.py2
-rw-r--r--test/integration/targets/module_utils/module_utils/spam7/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam7/ham/__init__.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/spam7/ham/bacon.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/spam8/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam8/ham/__init__.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/spam8/ham/bacon.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/sub/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/sub/bam.py3
-rw-r--r--test/integration/targets/module_utils/module_utils/sub/bam/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/sub/bam/bam.py3
-rw-r--r--test/integration/targets/module_utils/module_utils/sub/bar/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/sub/bar/bam.py3
-rw-r--r--test/integration/targets/module_utils/module_utils/sub/bar/bar.py3
-rw-r--r--test/integration/targets/module_utils/module_utils/yak/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/yak/zebra/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/yak/zebra/foo.py1
-rw-r--r--test/integration/targets/module_utils/module_utils_basic_setcwd.yml53
-rw-r--r--test/integration/targets/module_utils/module_utils_common_dict_transformation.yml34
-rw-r--r--test/integration/targets/module_utils/module_utils_common_network.yml10
-rw-r--r--test/integration/targets/module_utils/module_utils_envvar.yml51
-rw-r--r--test/integration/targets/module_utils/module_utils_test.yml121
-rw-r--r--test/integration/targets/module_utils/module_utils_test_no_log.yml12
-rw-r--r--test/integration/targets/module_utils/module_utils_vvvvv.yml29
-rw-r--r--test/integration/targets/module_utils/other_mu_dir/__init__.py0
-rw-r--r--test/integration/targets/module_utils/other_mu_dir/a/__init__.py0
-rw-r--r--test/integration/targets/module_utils/other_mu_dir/a/b/__init__.py0
-rw-r--r--test/integration/targets/module_utils/other_mu_dir/a/b/c/__init__.py0
-rw-r--r--test/integration/targets/module_utils/other_mu_dir/a/b/c/d/__init__.py0
-rw-r--r--test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/__init__.py0
-rw-r--r--test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/__init__.py0
-rw-r--r--test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/g/__init__.py0
-rw-r--r--test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/g/h/__init__.py1
-rw-r--r--test/integration/targets/module_utils/other_mu_dir/facts.py1
-rw-r--r--test/integration/targets/module_utils/other_mu_dir/json_utils.py1
-rw-r--r--test/integration/targets/module_utils/other_mu_dir/mork.py1
-rwxr-xr-xtest/integration/targets/module_utils/runme.sh19
-rw-r--r--test/integration/targets/module_utils_Ansible.AccessToken/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.AccessToken/library/ansible_access_token_tests.ps1407
-rw-r--r--test/integration/targets/module_utils_Ansible.AccessToken/tasks/main.yml29
-rw-r--r--test/integration/targets/module_utils_Ansible.Basic/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.Basic/library/ansible_basic_tests.ps13206
-rw-r--r--test/integration/targets/module_utils_Ansible.Basic/tasks/main.yml9
-rw-r--r--test/integration/targets/module_utils_Ansible.Become/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.Become/library/ansible_become_tests.ps11022
-rw-r--r--test/integration/targets/module_utils_Ansible.Become/tasks/main.yml28
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/library/add_type_test.ps1332
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/tasks/main.yml10
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/library/argv_parser_test.ps193
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/meta/main.yml3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/tasks/main.yml9
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/library/backup_file_test.ps192
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/tasks/main.yml10
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/library/camel_conversion_test.ps181
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/tasks/main.yml8
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/library/command_util_test.ps1139
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/meta/main.yml3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/tasks/main.yml9
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/library/file_util_test.ps1114
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/tasks/main.yml8
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/library/testlist.ps112
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/library/testpath.ps19
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/tasks/main.yml41
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/library/symbolic_link_test.ps1174
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/tasks/main.yml8
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/library/privilege_util_test.ps1113
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/tasks/main.yml8
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.SID/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.SID/library/sid_utils_test.ps1101
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.SID/tasks/main.yml22
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/aliases4
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/library/web_request_test.ps1473
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/meta/main.yml3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/tasks/main.yml10
-rw-r--r--test/integration/targets/module_utils_Ansible.Privilege/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.Privilege/library/ansible_privilege_tests.ps1278
-rw-r--r--test/integration/targets/module_utils_Ansible.Privilege/tasks/main.yml9
-rw-r--r--test/integration/targets/module_utils_Ansible.Process/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.Process/library/ansible_process_tests.ps1242
-rw-r--r--test/integration/targets/module_utils_Ansible.Process/tasks/main.yml9
-rw-r--r--test/integration/targets/module_utils_Ansible.Service/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.Service/library/ansible_service_tests.ps1953
-rw-r--r--test/integration/targets/module_utils_Ansible.Service/tasks/main.yml9
-rw-r--r--test/integration/targets/module_utils_ansible_release/aliases2
-rw-r--r--test/integration/targets/module_utils_ansible_release/library/ansible_release.py40
-rw-r--r--test/integration/targets/module_utils_ansible_release/tasks/main.yml9
-rw-r--r--test/integration/targets/module_utils_common.respawn/aliases1
-rw-r--r--test/integration/targets/module_utils_common.respawn/library/respawnme.py44
-rw-r--r--test/integration/targets/module_utils_common.respawn/tasks/main.yml24
-rw-r--r--test/integration/targets/module_utils_distro/aliases2
-rw-r--r--test/integration/targets/module_utils_distro/meta/main.yml2
-rwxr-xr-xtest/integration/targets/module_utils_distro/runme.sh24
-rw-r--r--test/integration/targets/module_utils_facts.system.selinux/aliases1
-rw-r--r--test/integration/targets/module_utils_facts.system.selinux/tasks/main.yml38
-rw-r--r--test/integration/targets/module_utils_facts.system.selinux/tasks/selinux.yml93
-rw-r--r--test/integration/targets/module_utils_urls/aliases2
-rw-r--r--test/integration/targets/module_utils_urls/library/test_peercert.py98
-rw-r--r--test/integration/targets/module_utils_urls/meta/main.yml3
-rw-r--r--test/integration/targets/module_utils_urls/tasks/main.yml32
-rw-r--r--test/integration/targets/network_cli/aliases3
-rw-r--r--test/integration/targets/network_cli/passworded_user.yml14
-rwxr-xr-xtest/integration/targets/network_cli/runme.sh27
-rw-r--r--test/integration/targets/network_cli/setup.yml14
-rw-r--r--test/integration/targets/network_cli/teardown.yml14
-rw-r--r--test/integration/targets/no_log/aliases2
-rw-r--r--test/integration/targets/no_log/dynamic.yml27
-rw-r--r--test/integration/targets/no_log/library/module.py45
-rw-r--r--test/integration/targets/no_log/no_log_local.yml92
-rw-r--r--test/integration/targets/no_log/no_log_suboptions.yml24
-rw-r--r--test/integration/targets/no_log/no_log_suboptions_invalid.yml45
-rwxr-xr-xtest/integration/targets/no_log/runme.sh21
-rw-r--r--test/integration/targets/noexec/aliases4
-rw-r--r--test/integration/targets/noexec/inventory1
-rwxr-xr-xtest/integration/targets/noexec/runme.sh9
-rw-r--r--test/integration/targets/noexec/test-noexec.yml8
-rw-r--r--test/integration/targets/old_style_cache_plugins/aliases6
-rw-r--r--test/integration/targets/old_style_cache_plugins/cleanup.yml41
-rw-r--r--test/integration/targets/old_style_cache_plugins/inspect_cache.yml36
-rw-r--r--test/integration/targets/old_style_cache_plugins/inventory_config1
-rw-r--r--test/integration/targets/old_style_cache_plugins/plugins/cache/configurable_redis.py147
-rw-r--r--test/integration/targets/old_style_cache_plugins/plugins/cache/legacy_redis.py141
-rw-r--r--test/integration/targets/old_style_cache_plugins/plugins/inventory/test.py59
-rwxr-xr-xtest/integration/targets/old_style_cache_plugins/runme.sh47
-rw-r--r--test/integration/targets/old_style_cache_plugins/setup_redis_cache.yml51
-rw-r--r--test/integration/targets/old_style_cache_plugins/test_fact_gathering.yml22
-rw-r--r--test/integration/targets/old_style_cache_plugins/test_inventory_cache.yml45
-rw-r--r--test/integration/targets/old_style_modules_posix/aliases2
-rw-r--r--test/integration/targets/old_style_modules_posix/library/helloworld.sh29
-rw-r--r--test/integration/targets/old_style_modules_posix/meta/main.yml2
-rw-r--r--test/integration/targets/old_style_modules_posix/tasks/main.yml44
-rw-r--r--test/integration/targets/old_style_vars_plugins/aliases2
-rw-r--r--test/integration/targets/old_style_vars_plugins/deprecation_warning/vars.py8
-rwxr-xr-xtest/integration/targets/old_style_vars_plugins/runme.sh20
-rw-r--r--test/integration/targets/old_style_vars_plugins/vars_plugins/auto_enabled.py8
-rw-r--r--test/integration/targets/old_style_vars_plugins/vars_plugins/implicitly_auto_enabled.py7
-rw-r--r--test/integration/targets/old_style_vars_plugins/vars_plugins/require_enabled.py8
-rw-r--r--test/integration/targets/omit/48673.yml4
-rw-r--r--test/integration/targets/omit/75692.yml31
-rw-r--r--test/integration/targets/omit/C75692.yml44
-rw-r--r--test/integration/targets/omit/aliases3
-rwxr-xr-xtest/integration/targets/omit/runme.sh11
-rw-r--r--test/integration/targets/order/aliases2
-rw-r--r--test/integration/targets/order/inventory9
-rw-r--r--test/integration/targets/order/order.yml39
-rwxr-xr-xtest/integration/targets/order/runme.sh24
-rw-r--r--test/integration/targets/package/aliases2
-rw-r--r--test/integration/targets/package/meta/main.yml2
-rw-r--r--test/integration/targets/package/tasks/main.yml242
-rw-r--r--test/integration/targets/package_facts/aliases3
-rw-r--r--test/integration/targets/package_facts/tasks/main.yml115
-rw-r--r--test/integration/targets/parsing/aliases2
-rw-r--r--test/integration/targets/parsing/bad_parsing.yml12
-rw-r--r--test/integration/targets/parsing/good_parsing.yml9
-rw-r--r--test/integration/targets/parsing/roles/test_bad_parsing/tasks/main.yml60
-rw-r--r--test/integration/targets/parsing/roles/test_bad_parsing/tasks/scenario1.yml4
-rw-r--r--test/integration/targets/parsing/roles/test_bad_parsing/tasks/scenario2.yml4
-rw-r--r--test/integration/targets/parsing/roles/test_bad_parsing/tasks/scenario3.yml4
-rw-r--r--test/integration/targets/parsing/roles/test_bad_parsing/tasks/scenario4.yml4
-rw-r--r--test/integration/targets/parsing/roles/test_bad_parsing/vars/main.yml2
-rw-r--r--test/integration/targets/parsing/roles/test_good_parsing/tasks/main.yml217
-rw-r--r--test/integration/targets/parsing/roles/test_good_parsing/tasks/test_include.yml1
-rw-r--r--test/integration/targets/parsing/roles/test_good_parsing/tasks/test_include_conditional.yml1
-rw-r--r--test/integration/targets/parsing/roles/test_good_parsing/tasks/test_include_nested.yml2
-rw-r--r--test/integration/targets/parsing/roles/test_good_parsing/vars/main.yml2
-rwxr-xr-xtest/integration/targets/parsing/runme.sh6
-rw-r--r--test/integration/targets/path_lookups/aliases2
-rw-r--r--test/integration/targets/path_lookups/play.yml49
-rw-r--r--test/integration/targets/path_lookups/roles/showfile/tasks/main.yml2
-rwxr-xr-xtest/integration/targets/path_lookups/runme.sh5
-rw-r--r--test/integration/targets/path_lookups/testplay.yml20
-rw-r--r--test/integration/targets/path_with_comma_in_inventory/aliases2
-rw-r--r--test/integration/targets/path_with_comma_in_inventory/playbook.yml9
-rwxr-xr-xtest/integration/targets/path_with_comma_in_inventory/runme.sh5
-rw-r--r--test/integration/targets/path_with_comma_in_inventory/this,path,has,commas/group_vars/all.yml1
-rw-r--r--test/integration/targets/path_with_comma_in_inventory/this,path,has,commas/hosts1
-rw-r--r--test/integration/targets/pause/aliases3
-rw-r--r--test/integration/targets/pause/pause-1.yml11
-rw-r--r--test/integration/targets/pause/pause-2.yml12
-rw-r--r--test/integration/targets/pause/pause-3.yml12
-rw-r--r--test/integration/targets/pause/pause-4.yml13
-rw-r--r--test/integration/targets/pause/pause-5.yml35
-rwxr-xr-xtest/integration/targets/pause/runme.sh43
-rw-r--r--test/integration/targets/pause/setup.yml4
-rw-r--r--test/integration/targets/pause/test-pause-background.yml10
-rw-r--r--test/integration/targets/pause/test-pause-no-tty.yml7
-rwxr-xr-xtest/integration/targets/pause/test-pause.py292
-rw-r--r--test/integration/targets/pause/test-pause.yml72
-rw-r--r--test/integration/targets/ping/aliases1
-rw-r--r--test/integration/targets/ping/tasks/main.yml53
-rw-r--r--test/integration/targets/pip/aliases2
-rw-r--r--test/integration/targets/pip/files/ansible_test_pip_chdir/__init__.py6
-rwxr-xr-xtest/integration/targets/pip/files/setup.py17
-rw-r--r--test/integration/targets/pip/meta/main.yml4
-rw-r--r--test/integration/targets/pip/tasks/default_cleanup.yml5
-rw-r--r--test/integration/targets/pip/tasks/freebsd_cleanup.yml6
-rw-r--r--test/integration/targets/pip/tasks/main.yml54
-rw-r--r--test/integration/targets/pip/tasks/pip.yml601
-rw-r--r--test/integration/targets/pip/vars/main.yml13
-rw-r--r--test/integration/targets/pkg_resources/aliases2
-rw-r--r--test/integration/targets/pkg_resources/lookup_plugins/check_pkg_resources.py23
-rw-r--r--test/integration/targets/pkg_resources/tasks/main.yml3
-rw-r--r--test/integration/targets/play_iterator/aliases2
-rw-r--r--test/integration/targets/play_iterator/playbook.yml10
-rwxr-xr-xtest/integration/targets/play_iterator/runme.sh5
-rw-r--r--test/integration/targets/playbook/aliases2
-rw-r--r--test/integration/targets/playbook/empty.yml1
-rw-r--r--test/integration/targets/playbook/empty_hosts.yml4
-rw-r--r--test/integration/targets/playbook/malformed_post_tasks.yml2
-rw-r--r--test/integration/targets/playbook/malformed_pre_tasks.yml2
-rw-r--r--test/integration/targets/playbook/malformed_roles.yml2
-rw-r--r--test/integration/targets/playbook/malformed_tasks.yml2
-rw-r--r--test/integration/targets/playbook/malformed_vars_prompt.yml3
-rw-r--r--test/integration/targets/playbook/old_style_role.yml3
-rw-r--r--test/integration/targets/playbook/remote_user_and_user.yml6
-rw-r--r--test/integration/targets/playbook/roles_null.yml3
-rwxr-xr-xtest/integration/targets/playbook/runme.sh92
-rw-r--r--test/integration/targets/playbook/some_vars.yml2
-rw-r--r--test/integration/targets/playbook/timeout.yml12
-rw-r--r--test/integration/targets/playbook/types.yml21
-rw-r--r--test/integration/targets/playbook/user.yml23
-rw-r--r--test/integration/targets/playbook/vars_files_null.yml3
-rw-r--r--test/integration/targets/playbook/vars_files_string.yml6
-rw-r--r--test/integration/targets/playbook/vars_prompt_null.yml3
-rw-r--r--test/integration/targets/plugin_config_for_inventory/aliases2
-rw-r--r--test/integration/targets/plugin_config_for_inventory/cache_plugins/none.py62
-rw-r--r--test/integration/targets/plugin_config_for_inventory/config_with_parameter.yml5
-rw-r--r--test/integration/targets/plugin_config_for_inventory/config_without_parameter.yml1
-rwxr-xr-xtest/integration/targets/plugin_config_for_inventory/runme.sh22
-rw-r--r--test/integration/targets/plugin_config_for_inventory/test_inventory.py84
-rw-r--r--test/integration/targets/plugin_filtering/aliases2
-rw-r--r--test/integration/targets/plugin_filtering/copy.yml10
-rw-r--r--test/integration/targets/plugin_filtering/filter_lookup.ini4
-rw-r--r--test/integration/targets/plugin_filtering/filter_lookup.yml6
-rw-r--r--test/integration/targets/plugin_filtering/filter_modules.ini4
-rw-r--r--test/integration/targets/plugin_filtering/filter_modules.yml9
-rw-r--r--test/integration/targets/plugin_filtering/filter_ping.ini4
-rw-r--r--test/integration/targets/plugin_filtering/filter_ping.yml5
-rw-r--r--test/integration/targets/plugin_filtering/filter_stat.ini4
-rw-r--r--test/integration/targets/plugin_filtering/filter_stat.yml5
-rw-r--r--test/integration/targets/plugin_filtering/lookup.yml14
-rw-r--r--test/integration/targets/plugin_filtering/no_blacklist_module.ini3
-rw-r--r--test/integration/targets/plugin_filtering/no_blacklist_module.yml3
-rw-r--r--test/integration/targets/plugin_filtering/no_filters.ini4
-rw-r--r--test/integration/targets/plugin_filtering/pause.yml6
-rw-r--r--test/integration/targets/plugin_filtering/ping.yml6
-rwxr-xr-xtest/integration/targets/plugin_filtering/runme.sh137
-rw-r--r--test/integration/targets/plugin_filtering/stat.yml6
-rw-r--r--test/integration/targets/plugin_filtering/tempfile.yml9
-rw-r--r--test/integration/targets/plugin_loader/aliases2
-rw-r--r--test/integration/targets/plugin_loader/normal/action_plugins/self_referential.py29
-rw-r--r--test/integration/targets/plugin_loader/normal/filters.yml13
-rw-r--r--test/integration/targets/plugin_loader/normal/library/_underscore.py13
-rw-r--r--test/integration/targets/plugin_loader/normal/self_referential.yml5
-rw-r--r--test/integration/targets/plugin_loader/normal/underscore.yml15
-rw-r--r--test/integration/targets/plugin_loader/override/filter_plugins/core.py18
-rw-r--r--test/integration/targets/plugin_loader/override/filters.yml15
-rwxr-xr-xtest/integration/targets/plugin_loader/runme.sh36
-rw-r--r--test/integration/targets/plugin_loader/use_coll_name.yml7
-rw-r--r--test/integration/targets/plugin_namespace/aliases2
-rw-r--r--test/integration/targets/plugin_namespace/filter_plugins/test_filter.py15
-rw-r--r--test/integration/targets/plugin_namespace/lookup_plugins/lookup_name.py9
-rw-r--r--test/integration/targets/plugin_namespace/tasks/main.yml11
-rw-r--r--test/integration/targets/plugin_namespace/test_plugins/test_test.py16
-rw-r--r--test/integration/targets/preflight_encoding/aliases2
-rw-r--r--test/integration/targets/preflight_encoding/tasks/main.yml62
-rw-r--r--test/integration/targets/preflight_encoding/vars/main.yml2
-rw-r--r--test/integration/targets/prepare_http_tests/defaults/main.yml5
-rw-r--r--test/integration/targets/prepare_http_tests/handlers/main.yml4
-rw-r--r--test/integration/targets/prepare_http_tests/library/httptester_kinit.py138
-rw-r--r--test/integration/targets/prepare_http_tests/meta/main.yml3
-rw-r--r--test/integration/targets/prepare_http_tests/tasks/default.yml55
-rw-r--r--test/integration/targets/prepare_http_tests/tasks/kerberos.yml65
-rw-r--r--test/integration/targets/prepare_http_tests/tasks/main.yml35
-rw-r--r--test/integration/targets/prepare_http_tests/tasks/windows.yml33
-rw-r--r--test/integration/targets/prepare_http_tests/templates/krb5.conf.j225
-rw-r--r--test/integration/targets/prepare_http_tests/vars/Alpine.yml3
-rw-r--r--test/integration/targets/prepare_http_tests/vars/Debian.yml3
-rw-r--r--test/integration/targets/prepare_http_tests/vars/FreeBSD.yml2
-rw-r--r--test/integration/targets/prepare_http_tests/vars/RedHat-9.yml4
-rw-r--r--test/integration/targets/prepare_http_tests/vars/Suse.yml3
-rw-r--r--test/integration/targets/prepare_http_tests/vars/default.yml3
-rw-r--r--test/integration/targets/prepare_http_tests/vars/httptester.yml6
-rw-r--r--test/integration/targets/prepare_tests/tasks/main.yml0
-rw-r--r--test/integration/targets/pyyaml/aliases2
-rwxr-xr-xtest/integration/targets/pyyaml/runme.sh11
-rw-r--r--test/integration/targets/raw/aliases2
-rw-r--r--test/integration/targets/raw/meta/main.yml3
-rwxr-xr-xtest/integration/targets/raw/runme.sh6
-rw-r--r--test/integration/targets/raw/runme.yml4
-rw-r--r--test/integration/targets/raw/tasks/main.yml107
-rw-r--r--test/integration/targets/reboot/aliases5
-rw-r--r--test/integration/targets/reboot/handlers/main.yml4
-rw-r--r--test/integration/targets/reboot/tasks/check_reboot.yml10
-rw-r--r--test/integration/targets/reboot/tasks/get_boot_time.yml3
-rw-r--r--test/integration/targets/reboot/tasks/main.yml43
-rw-r--r--test/integration/targets/reboot/tasks/test_invalid_parameter.yml11
-rw-r--r--test/integration/targets/reboot/tasks/test_invalid_test_command.yml8
-rw-r--r--test/integration/targets/reboot/tasks/test_molly_guard.yml20
-rw-r--r--test/integration/targets/reboot/tasks/test_reboot_command.yml22
-rw-r--r--test/integration/targets/reboot/tasks/test_standard_scenarios.yml32
-rw-r--r--test/integration/targets/reboot/vars/main.yml9
-rw-r--r--test/integration/targets/register/aliases2
-rw-r--r--test/integration/targets/register/can_register.yml21
-rw-r--r--test/integration/targets/register/invalid.yml11
-rw-r--r--test/integration/targets/register/invalid_skipped.yml12
-rwxr-xr-xtest/integration/targets/register/runme.sh12
-rw-r--r--test/integration/targets/rel_plugin_loading/aliases2
-rw-r--r--test/integration/targets/rel_plugin_loading/notyaml.yml5
-rwxr-xr-xtest/integration/targets/rel_plugin_loading/runme.sh5
-rw-r--r--test/integration/targets/rel_plugin_loading/subdir/inventory_plugins/notyaml.py169
-rw-r--r--test/integration/targets/rel_plugin_loading/subdir/play.yml6
-rw-r--r--test/integration/targets/remote_tmp/aliases3
-rw-r--r--test/integration/targets/remote_tmp/playbook.yml59
-rwxr-xr-xtest/integration/targets/remote_tmp/runme.sh5
-rw-r--r--test/integration/targets/replace/aliases1
-rw-r--r--test/integration/targets/replace/meta/main.yml3
-rw-r--r--test/integration/targets/replace/tasks/main.yml265
-rw-r--r--test/integration/targets/retry_task_name_in_callback/aliases2
-rwxr-xr-xtest/integration/targets/retry_task_name_in_callback/runme.sh13
-rw-r--r--test/integration/targets/retry_task_name_in_callback/test.yml28
-rw-r--r--test/integration/targets/roles/aliases2
-rw-r--r--test/integration/targets/roles/allowed_dupes.yml18
-rw-r--r--test/integration/targets/roles/data_integrity.yml4
-rw-r--r--test/integration/targets/roles/no_dupes.yml29
-rw-r--r--test/integration/targets/roles/no_outside.yml7
-rw-r--r--test/integration/targets/roles/roles/a/tasks/main.yml1
-rw-r--r--test/integration/targets/roles/roles/b/meta/main.yml4
-rw-r--r--test/integration/targets/roles/roles/b/tasks/main.yml1
-rw-r--r--test/integration/targets/roles/roles/c/meta/main.yml2
-rw-r--r--test/integration/targets/roles/roles/c/tasks/main.yml1
-rw-r--r--test/integration/targets/roles/roles/data/defaults/main/00.yml1
-rw-r--r--test/integration/targets/roles/roles/data/defaults/main/01.yml0
-rw-r--r--test/integration/targets/roles/roles/data/tasks/main.yml5
-rwxr-xr-xtest/integration/targets/roles/runme.sh28
-rw-r--r--test/integration/targets/roles/tasks/dummy.yml1
-rw-r--r--test/integration/targets/roles_arg_spec/aliases2
-rw-r--r--test/integration/targets/roles_arg_spec/collections/ansible_collections/foo/bar/MANIFEST.json0
-rw-r--r--test/integration/targets/roles_arg_spec/collections/ansible_collections/foo/bar/roles/blah/meta/argument_specs.yml8
-rw-r--r--test/integration/targets/roles_arg_spec/collections/ansible_collections/foo/bar/roles/blah/tasks/main.yml3
-rw-r--r--test/integration/targets/roles_arg_spec/roles/a/meta/argument_specs.yml17
-rw-r--r--test/integration/targets/roles_arg_spec/roles/a/meta/main.yml13
-rw-r--r--test/integration/targets/roles_arg_spec/roles/a/tasks/alternate.yml3
-rw-r--r--test/integration/targets/roles_arg_spec/roles/a/tasks/main.yml3
-rw-r--r--test/integration/targets/roles_arg_spec/roles/a/tasks/no_spec_entrypoint.yml3
-rw-r--r--test/integration/targets/roles_arg_spec/roles/b/meta/argument_specs.yaml13
-rw-r--r--test/integration/targets/roles_arg_spec/roles/b/tasks/main.yml9
-rw-r--r--test/integration/targets/roles_arg_spec/roles/c/meta/main.yml7
-rw-r--r--test/integration/targets/roles_arg_spec/roles/c/tasks/main.yml12
-rw-r--r--test/integration/targets/roles_arg_spec/roles/empty_argspec/meta/argument_specs.yml2
-rw-r--r--test/integration/targets/roles_arg_spec/roles/empty_argspec/tasks/main.yml3
-rw-r--r--test/integration/targets/roles_arg_spec/roles/empty_file/meta/argument_specs.yml1
-rw-r--r--test/integration/targets/roles_arg_spec/roles/empty_file/tasks/main.yml3
-rw-r--r--test/integration/targets/roles_arg_spec/roles/role_with_no_tasks/meta/argument_specs.yml7
-rw-r--r--test/integration/targets/roles_arg_spec/roles/test1/defaults/main.yml3
-rw-r--r--test/integration/targets/roles_arg_spec/roles/test1/meta/argument_specs.yml112
-rw-r--r--test/integration/targets/roles_arg_spec/roles/test1/tasks/main.yml11
-rw-r--r--test/integration/targets/roles_arg_spec/roles/test1/tasks/other.yml11
-rw-r--r--test/integration/targets/roles_arg_spec/roles/test1/tasks/test1_other.yml11
-rw-r--r--test/integration/targets/roles_arg_spec/roles/test1/vars/main.yml4
-rw-r--r--test/integration/targets/roles_arg_spec/roles/test1/vars/other.yml4
-rwxr-xr-xtest/integration/targets/roles_arg_spec/runme.sh32
-rw-r--r--test/integration/targets/roles_arg_spec/test.yml356
-rw-r--r--test/integration/targets/roles_arg_spec/test_complex_role_fails.yml197
-rw-r--r--test/integration/targets/roles_arg_spec/test_play_level_role_fails.yml5
-rw-r--r--test/integration/targets/roles_arg_spec/test_tags.yml11
-rw-r--r--test/integration/targets/roles_var_inheritance/aliases2
-rw-r--r--test/integration/targets/roles_var_inheritance/play.yml4
-rw-r--r--test/integration/targets/roles_var_inheritance/roles/A/meta/main.yml4
-rw-r--r--test/integration/targets/roles_var_inheritance/roles/B/meta/main.yml4
-rw-r--r--test/integration/targets/roles_var_inheritance/roles/child_nested_dep/vars/main.yml1
-rw-r--r--test/integration/targets/roles_var_inheritance/roles/common_dep/meta/main.yml4
-rw-r--r--test/integration/targets/roles_var_inheritance/roles/common_dep/vars/main.yml1
-rw-r--r--test/integration/targets/roles_var_inheritance/roles/nested_dep/meta/main.yml3
-rw-r--r--test/integration/targets/roles_var_inheritance/roles/nested_dep/tasks/main.yml5
-rwxr-xr-xtest/integration/targets/roles_var_inheritance/runme.sh9
-rw-r--r--test/integration/targets/rpm_key/aliases2
-rw-r--r--test/integration/targets/rpm_key/defaults/main.yaml0
-rw-r--r--test/integration/targets/rpm_key/meta/main.yml2
-rw-r--r--test/integration/targets/rpm_key/tasks/main.yaml2
-rw-r--r--test/integration/targets/rpm_key/tasks/rpm_key.yaml180
-rw-r--r--test/integration/targets/run_modules/aliases2
-rw-r--r--test/integration/targets/run_modules/args.json1
-rw-r--r--test/integration/targets/run_modules/library/test.py10
-rwxr-xr-xtest/integration/targets/run_modules/runme.sh6
-rw-r--r--test/integration/targets/script/aliases1
-rwxr-xr-xtest/integration/targets/script/files/create_afile.sh3
-rw-r--r--test/integration/targets/script/files/no_shebang.py6
-rwxr-xr-xtest/integration/targets/script/files/remove_afile.sh3
-rwxr-xr-xtest/integration/targets/script/files/space path/test.sh3
-rwxr-xr-xtest/integration/targets/script/files/test.sh3
-rwxr-xr-xtest/integration/targets/script/files/test_with_args.sh5
-rw-r--r--test/integration/targets/script/meta/main.yml3
-rw-r--r--test/integration/targets/script/tasks/main.yml241
-rw-r--r--test/integration/targets/service/aliases4
-rw-r--r--test/integration/targets/service/files/ansible-broken.upstart10
-rw-r--r--test/integration/targets/service/files/ansible.rc16
-rw-r--r--test/integration/targets/service/files/ansible.systemd11
-rwxr-xr-xtest/integration/targets/service/files/ansible.sysv134
-rw-r--r--test/integration/targets/service/files/ansible.upstart9
-rw-r--r--test/integration/targets/service/files/ansible_test_service.py74
-rw-r--r--test/integration/targets/service/meta/main.yml20
-rw-r--r--test/integration/targets/service/tasks/main.yml62
-rw-r--r--test/integration/targets/service/tasks/rc_cleanup.yml9
-rw-r--r--test/integration/targets/service/tasks/rc_setup.yml21
-rw-r--r--test/integration/targets/service/tasks/systemd_cleanup.yml25
-rw-r--r--test/integration/targets/service/tasks/systemd_setup.yml17
-rw-r--r--test/integration/targets/service/tasks/sysv_cleanup.yml9
-rw-r--r--test/integration/targets/service/tasks/sysv_setup.yml11
-rw-r--r--test/integration/targets/service/tasks/tests.yml258
-rw-r--r--test/integration/targets/service/tasks/upstart_cleanup.yml17
-rw-r--r--test/integration/targets/service/tasks/upstart_setup.yml19
-rw-r--r--test/integration/targets/service/templates/main.yml0
-rw-r--r--test/integration/targets/service_facts/aliases4
-rw-r--r--test/integration/targets/service_facts/files/ansible.systemd11
-rw-r--r--test/integration/targets/service_facts/files/ansible_test_service.py73
-rw-r--r--test/integration/targets/service_facts/tasks/main.yml29
-rw-r--r--test/integration/targets/service_facts/tasks/systemd_cleanup.yml32
-rw-r--r--test/integration/targets/service_facts/tasks/systemd_setup.yml26
-rw-r--r--test/integration/targets/service_facts/tasks/tests.yml36
-rw-r--r--test/integration/targets/set_fact/aliases2
-rw-r--r--test/integration/targets/set_fact/incremental.yml35
-rw-r--r--test/integration/targets/set_fact/inventory3
-rw-r--r--test/integration/targets/set_fact/nowarn_clean_facts.yml10
-rwxr-xr-xtest/integration/targets/set_fact/runme.sh36
-rw-r--r--test/integration/targets/set_fact/set_fact_auto_unsafe.yml10
-rw-r--r--test/integration/targets/set_fact/set_fact_bool_conv.yml35
-rw-r--r--test/integration/targets/set_fact/set_fact_bool_conv_jinja2_native.yml35
-rw-r--r--test/integration/targets/set_fact/set_fact_cached_1.yml324
-rw-r--r--test/integration/targets/set_fact/set_fact_cached_2.yml57
-rw-r--r--test/integration/targets/set_fact/set_fact_empty_str_key.yml15
-rw-r--r--test/integration/targets/set_fact/set_fact_no_cache.yml39
-rw-r--r--test/integration/targets/set_stats/aliases2
-rwxr-xr-xtest/integration/targets/set_stats/runme.sh13
-rw-r--r--test/integration/targets/set_stats/test_aggregate.yml13
-rw-r--r--test/integration/targets/set_stats/test_simple.yml79
-rw-r--r--test/integration/targets/setup_cron/defaults/main.yml1
-rw-r--r--test/integration/targets/setup_cron/meta/main.yml2
-rw-r--r--test/integration/targets/setup_cron/tasks/main.yml99
-rw-r--r--test/integration/targets/setup_cron/vars/alpine.yml1
-rw-r--r--test/integration/targets/setup_cron/vars/debian.yml3
-rw-r--r--test/integration/targets/setup_cron/vars/default.yml0
-rw-r--r--test/integration/targets/setup_cron/vars/fedora.yml3
-rw-r--r--test/integration/targets/setup_cron/vars/freebsd.yml3
-rw-r--r--test/integration/targets/setup_cron/vars/redhat.yml4
-rw-r--r--test/integration/targets/setup_cron/vars/suse.yml3
-rw-r--r--test/integration/targets/setup_deb_repo/files/package_specs/stable/foo-1.0.010
-rw-r--r--test/integration/targets/setup_deb_repo/files/package_specs/stable/foo-1.0.110
-rw-r--r--test/integration/targets/setup_deb_repo/files/package_specs/stable/foobar-1.0.011
-rw-r--r--test/integration/targets/setup_deb_repo/files/package_specs/stable/foobar-1.0.110
-rw-r--r--test/integration/targets/setup_deb_repo/files/package_specs/testing/foo-2.0.010
-rw-r--r--test/integration/targets/setup_deb_repo/files/package_specs/testing/foo-2.0.110
-rw-r--r--test/integration/targets/setup_deb_repo/meta/main.yml2
-rw-r--r--test/integration/targets/setup_deb_repo/tasks/main.yml75
-rw-r--r--test/integration/targets/setup_epel/tasks/main.yml10
-rw-r--r--test/integration/targets/setup_gnutar/handlers/main.yml6
-rw-r--r--test/integration/targets/setup_gnutar/tasks/main.yml18
-rw-r--r--test/integration/targets/setup_nobody/handlers/main.yml5
-rw-r--r--test/integration/targets/setup_nobody/tasks/main.yml7
-rw-r--r--test/integration/targets/setup_paramiko/aliases1
-rw-r--r--test/integration/targets/setup_paramiko/constraints.txt1
-rw-r--r--test/integration/targets/setup_paramiko/install-Alpine-3-python-3.yml9
-rw-r--r--test/integration/targets/setup_paramiko/install-CentOS-6-python-2.yml3
-rw-r--r--test/integration/targets/setup_paramiko/install-Darwin-python-3.yml9
-rw-r--r--test/integration/targets/setup_paramiko/install-Fedora-35-python-3.yml9
-rw-r--r--test/integration/targets/setup_paramiko/install-FreeBSD-python-3.yml8
-rw-r--r--test/integration/targets/setup_paramiko/install-RedHat-8-python-3.yml8
-rw-r--r--test/integration/targets/setup_paramiko/install-RedHat-9-python-3.yml9
-rw-r--r--test/integration/targets/setup_paramiko/install-Ubuntu-16-python-2.yml3
-rw-r--r--test/integration/targets/setup_paramiko/install-fail.yml7
-rw-r--r--test/integration/targets/setup_paramiko/install-python-2.yml3
-rw-r--r--test/integration/targets/setup_paramiko/install-python-3.yml3
-rw-r--r--test/integration/targets/setup_paramiko/install.yml19
-rw-r--r--test/integration/targets/setup_paramiko/inventory1
-rw-r--r--test/integration/targets/setup_paramiko/library/detect_paramiko.py31
-rw-r--r--test/integration/targets/setup_paramiko/setup-remote-constraints.yml12
-rw-r--r--test/integration/targets/setup_paramiko/setup.sh8
-rw-r--r--test/integration/targets/setup_paramiko/uninstall-Alpine-3-python-3.yml4
-rw-r--r--test/integration/targets/setup_paramiko/uninstall-Darwin-python-3.yml4
-rw-r--r--test/integration/targets/setup_paramiko/uninstall-Fedora-35-python-3.yml5
-rw-r--r--test/integration/targets/setup_paramiko/uninstall-FreeBSD-python-3.yml4
-rw-r--r--test/integration/targets/setup_paramiko/uninstall-RedHat-8-python-3.yml4
-rw-r--r--test/integration/targets/setup_paramiko/uninstall-RedHat-9-python-3.yml7
-rw-r--r--test/integration/targets/setup_paramiko/uninstall-apt-python-2.yml5
-rw-r--r--test/integration/targets/setup_paramiko/uninstall-apt-python-3.yml5
-rw-r--r--test/integration/targets/setup_paramiko/uninstall-dnf.yml2
-rw-r--r--test/integration/targets/setup_paramiko/uninstall-fail.yml7
-rw-r--r--test/integration/targets/setup_paramiko/uninstall-yum.yml2
-rw-r--r--test/integration/targets/setup_paramiko/uninstall-zypper-python-2.yml2
-rw-r--r--test/integration/targets/setup_paramiko/uninstall-zypper-python-3.yml2
-rw-r--r--test/integration/targets/setup_paramiko/uninstall.yml21
-rw-r--r--test/integration/targets/setup_passlib/tasks/main.yml4
-rw-r--r--test/integration/targets/setup_pexpect/files/constraints.txt2
-rw-r--r--test/integration/targets/setup_pexpect/meta/main.yml2
-rw-r--r--test/integration/targets/setup_pexpect/tasks/main.yml19
-rw-r--r--test/integration/targets/setup_remote_constraints/aliases1
-rw-r--r--test/integration/targets/setup_remote_constraints/meta/main.yml2
-rw-r--r--test/integration/targets/setup_remote_constraints/tasks/main.yml8
-rw-r--r--test/integration/targets/setup_remote_tmp_dir/defaults/main.yml2
-rw-r--r--test/integration/targets/setup_remote_tmp_dir/handlers/main.yml7
-rw-r--r--test/integration/targets/setup_remote_tmp_dir/tasks/default-cleanup.yml5
-rw-r--r--test/integration/targets/setup_remote_tmp_dir/tasks/default.yml12
-rw-r--r--test/integration/targets/setup_remote_tmp_dir/tasks/main.yml10
-rw-r--r--test/integration/targets/setup_remote_tmp_dir/tasks/windows-cleanup.yml4
-rw-r--r--test/integration/targets/setup_remote_tmp_dir/tasks/windows.yml11
-rw-r--r--test/integration/targets/setup_rpm_repo/aliases1
-rw-r--r--test/integration/targets/setup_rpm_repo/defaults/main.yml1
-rw-r--r--test/integration/targets/setup_rpm_repo/files/comps.xml36
-rw-r--r--test/integration/targets/setup_rpm_repo/handlers/main.yml5
-rw-r--r--test/integration/targets/setup_rpm_repo/library/create_repo.py125
-rw-r--r--test/integration/targets/setup_rpm_repo/meta/main.yml2
-rw-r--r--test/integration/targets/setup_rpm_repo/tasks/main.yml100
-rw-r--r--test/integration/targets/setup_rpm_repo/vars/Fedora.yml4
-rw-r--r--test/integration/targets/setup_rpm_repo/vars/RedHat-6.yml5
-rw-r--r--test/integration/targets/setup_rpm_repo/vars/RedHat-7.yml5
-rw-r--r--test/integration/targets/setup_rpm_repo/vars/RedHat-8.yml5
-rw-r--r--test/integration/targets/setup_rpm_repo/vars/RedHat-9.yml4
-rw-r--r--test/integration/targets/setup_rpm_repo/vars/main.yml1
-rw-r--r--test/integration/targets/setup_test_user/handlers/main.yml6
-rw-r--r--test/integration/targets/setup_test_user/tasks/default.yml14
-rw-r--r--test/integration/targets/setup_test_user/tasks/macosx.yml14
-rw-r--r--test/integration/targets/setup_test_user/tasks/main.yml37
-rw-r--r--test/integration/targets/setup_win_printargv/files/PrintArgv.cs13
-rw-r--r--test/integration/targets/setup_win_printargv/meta/main.yml3
-rw-r--r--test/integration/targets/setup_win_printargv/tasks/main.yml9
-rw-r--r--test/integration/targets/shell/action_plugins/test_shell.py19
-rw-r--r--test/integration/targets/shell/aliases1
-rw-r--r--test/integration/targets/shell/connection_plugins/test_connection_default.py41
-rw-r--r--test/integration/targets/shell/connection_plugins/test_connection_override.py42
-rw-r--r--test/integration/targets/shell/tasks/main.yml36
-rw-r--r--test/integration/targets/slurp/aliases2
-rw-r--r--test/integration/targets/slurp/files/bar.binbin0 -> 256 bytes
-rw-r--r--test/integration/targets/slurp/meta/main.yml3
-rw-r--r--test/integration/targets/slurp/tasks/main.yml69
-rw-r--r--test/integration/targets/slurp/tasks/test_unreadable.yml74
-rw-r--r--test/integration/targets/special_vars/aliases3
-rw-r--r--test/integration/targets/special_vars/meta/main.yml2
-rw-r--r--test/integration/targets/special_vars/tasks/main.yml100
-rw-r--r--test/integration/targets/special_vars/templates/foo.j27
-rw-r--r--test/integration/targets/special_vars/vars/main.yml0
-rw-r--r--test/integration/targets/special_vars_hosts/aliases2
-rw-r--r--test/integration/targets/special_vars_hosts/inventory3
-rw-r--r--test/integration/targets/special_vars_hosts/playbook.yml53
-rwxr-xr-xtest/integration/targets/special_vars_hosts/runme.sh7
-rw-r--r--test/integration/targets/split/aliases3
-rw-r--r--test/integration/targets/split/tasks/main.yml36
-rw-r--r--test/integration/targets/stat/aliases1
-rw-r--r--test/integration/targets/stat/files/foo.txt1
-rw-r--r--test/integration/targets/stat/meta/main.yml3
-rw-r--r--test/integration/targets/stat/tasks/main.yml179
-rw-r--r--test/integration/targets/strategy_free/aliases1
-rw-r--r--test/integration/targets/strategy_free/inventory2
-rw-r--r--test/integration/targets/strategy_free/last_include_tasks.yml2
-rwxr-xr-xtest/integration/targets/strategy_free/runme.sh10
-rw-r--r--test/integration/targets/strategy_free/test_last_include_in_always.yml9
-rw-r--r--test/integration/targets/strategy_linear/aliases1
-rw-r--r--test/integration/targets/strategy_linear/inventory3
-rw-r--r--test/integration/targets/strategy_linear/roles/role1/tasks/main.yml6
-rw-r--r--test/integration/targets/strategy_linear/roles/role1/tasks/tasks.yml7
-rw-r--r--test/integration/targets/strategy_linear/roles/role2/tasks/main.yml7
-rwxr-xr-xtest/integration/targets/strategy_linear/runme.sh7
-rw-r--r--test/integration/targets/strategy_linear/task_action_templating.yml26
-rw-r--r--test/integration/targets/strategy_linear/test_include_file_noop.yml16
-rw-r--r--test/integration/targets/subversion/aliases7
-rw-r--r--test/integration/targets/subversion/roles/subversion/defaults/main.yml10
-rw-r--r--test/integration/targets/subversion/roles/subversion/files/create_repo.sh6
-rw-r--r--test/integration/targets/subversion/roles/subversion/tasks/cleanup.yml8
-rw-r--r--test/integration/targets/subversion/roles/subversion/tasks/main.yml20
-rw-r--r--test/integration/targets/subversion/roles/subversion/tasks/setup.yml72
-rw-r--r--test/integration/targets/subversion/roles/subversion/tasks/setup_selinux.yml11
-rw-r--r--test/integration/targets/subversion/roles/subversion/tasks/tests.yml145
-rw-r--r--test/integration/targets/subversion/roles/subversion/tasks/warnings.yml7
-rw-r--r--test/integration/targets/subversion/roles/subversion/templates/subversion.conf.j271
-rwxr-xr-xtest/integration/targets/subversion/runme.sh35
-rw-r--r--test/integration/targets/subversion/runme.yml15
-rw-r--r--test/integration/targets/subversion/vars/Alpine.yml7
-rw-r--r--test/integration/targets/subversion/vars/Debian.yml6
-rw-r--r--test/integration/targets/subversion/vars/FreeBSD.yml7
-rw-r--r--test/integration/targets/subversion/vars/RedHat.yml10
-rw-r--r--test/integration/targets/subversion/vars/Suse.yml6
-rw-r--r--test/integration/targets/subversion/vars/Ubuntu-18.yml6
-rw-r--r--test/integration/targets/subversion/vars/Ubuntu-20.yml6
-rw-r--r--test/integration/targets/systemd/aliases1
-rw-r--r--test/integration/targets/systemd/defaults/main.yml1
-rw-r--r--test/integration/targets/systemd/handlers/main.yml12
-rw-r--r--test/integration/targets/systemd/meta/main.yml2
-rw-r--r--test/integration/targets/systemd/tasks/main.yml122
-rw-r--r--test/integration/targets/systemd/tasks/test_indirect_service.yml37
-rw-r--r--test/integration/targets/systemd/tasks/test_unit_template.yml50
-rw-r--r--test/integration/targets/systemd/templates/dummy.service11
-rw-r--r--test/integration/targets/systemd/templates/dummy.socket8
-rw-r--r--test/integration/targets/systemd/templates/sleeper@.service8
-rw-r--r--test/integration/targets/systemd/vars/Debian.yml3
-rw-r--r--test/integration/targets/systemd/vars/default.yml3
-rw-r--r--test/integration/targets/tags/aliases2
-rw-r--r--test/integration/targets/tags/ansible_run_tags.yml49
-rwxr-xr-xtest/integration/targets/tags/runme.sh75
-rw-r--r--test/integration/targets/tags/test_tags.yml36
-rw-r--r--test/integration/targets/task_ordering/aliases2
-rw-r--r--test/integration/targets/task_ordering/meta/main.yml2
-rw-r--r--test/integration/targets/task_ordering/tasks/main.yml15
-rw-r--r--test/integration/targets/task_ordering/tasks/taskorder-include.yml10
-rw-r--r--test/integration/targets/tasks/aliases2
-rw-r--r--test/integration/targets/tasks/playbook.yml19
-rwxr-xr-xtest/integration/targets/tasks/runme.sh3
-rw-r--r--test/integration/targets/tempfile/aliases1
-rw-r--r--test/integration/targets/tempfile/meta/main.yml2
-rw-r--r--test/integration/targets/tempfile/tasks/main.yml63
-rw-r--r--test/integration/targets/template/6653.yml10
-rw-r--r--test/integration/targets/template/72262.yml6
-rw-r--r--test/integration/targets/template/72615.yml18
-rw-r--r--test/integration/targets/template/aliases3
-rw-r--r--test/integration/targets/template/ansible_managed.cfg2
-rw-r--r--test/integration/targets/template/ansible_managed.yml14
-rw-r--r--test/integration/targets/template/badnull1.cfg2
-rw-r--r--test/integration/targets/template/badnull2.cfg2
-rw-r--r--test/integration/targets/template/badnull3.cfg2
-rw-r--r--test/integration/targets/template/corner_cases.yml55
-rw-r--r--test/integration/targets/template/custom_tasks/tasks/main.yml15
-rw-r--r--test/integration/targets/template/custom_tasks/templates/test1
-rw-r--r--test/integration/targets/template/custom_template.yml4
-rw-r--r--test/integration/targets/template/files/custom_comment_string.expected2
-rw-r--r--test/integration/targets/template/files/encoding_1252_utf-8.expected1
-rw-r--r--test/integration/targets/template/files/encoding_1252_windows-1252.expected1
-rw-r--r--test/integration/targets/template/files/foo-py26.txt9
-rw-r--r--test/integration/targets/template/files/foo.dos.txt3
-rw-r--r--test/integration/targets/template/files/foo.txt9
-rw-r--r--test/integration/targets/template/files/foo.unix.txt3
-rw-r--r--test/integration/targets/template/files/import_as.expected3
-rw-r--r--test/integration/targets/template/files/import_as_with_context.expected2
-rw-r--r--test/integration/targets/template/files/import_with_context.expected3
-rw-r--r--test/integration/targets/template/files/lstrip_blocks_false.expected4
-rw-r--r--test/integration/targets/template/files/lstrip_blocks_true.expected3
-rw-r--r--test/integration/targets/template/files/override_colon_value.expected1
-rw-r--r--test/integration/targets/template/files/string_type_filters.expected4
-rw-r--r--test/integration/targets/template/files/trim_blocks_false.expected4
-rw-r--r--test/integration/targets/template/files/trim_blocks_true.expected2
-rw-r--r--test/integration/targets/template/filter_plugins.yml9
-rw-r--r--test/integration/targets/template/in_template_overrides.j25
-rw-r--r--test/integration/targets/template/in_template_overrides.yml28
-rw-r--r--test/integration/targets/template/lazy_eval.yml24
-rw-r--r--test/integration/targets/template/meta/main.yml3
-rw-r--r--test/integration/targets/template/role_filter/filter_plugins/myplugin.py12
-rw-r--r--test/integration/targets/template/role_filter/tasks/main.yml3
-rwxr-xr-xtest/integration/targets/template/runme.sh54
-rw-r--r--test/integration/targets/template/tasks/backup_test.yml60
-rw-r--r--test/integration/targets/template/tasks/main.yml811
-rw-r--r--test/integration/targets/template/template.yml4
-rw-r--r--test/integration/targets/template/templates/6653-include.j21
-rw-r--r--test/integration/targets/template/templates/6653.j24
-rw-r--r--test/integration/targets/template/templates/72262-included.j21
-rw-r--r--test/integration/targets/template/templates/72262-vars.j21
-rw-r--r--test/integration/targets/template/templates/72262.j23
-rw-r--r--test/integration/targets/template/templates/72615-macro-nested.j24
-rw-r--r--test/integration/targets/template/templates/72615-macro.j28
-rw-r--r--test/integration/targets/template/templates/72615.j24
-rw-r--r--test/integration/targets/template/templates/bar1
-rw-r--r--test/integration/targets/template/templates/café.j21
-rw-r--r--test/integration/targets/template/templates/custom_comment_string.j23
-rw-r--r--test/integration/targets/template/templates/empty_template.j20
-rw-r--r--test/integration/targets/template/templates/encoding_1252.j21
-rw-r--r--test/integration/targets/template/templates/foo.j23
-rw-r--r--test/integration/targets/template/templates/foo2.j23
-rw-r--r--test/integration/targets/template/templates/foo3.j23
-rw-r--r--test/integration/targets/template/templates/for_loop.j24
-rw-r--r--test/integration/targets/template/templates/for_loop_include.j23
-rw-r--r--test/integration/targets/template/templates/for_loop_include_nested.j21
-rw-r--r--test/integration/targets/template/templates/import_as.j24
-rw-r--r--test/integration/targets/template/templates/import_as_with_context.j23
-rw-r--r--test/integration/targets/template/templates/import_with_context.j24
-rw-r--r--test/integration/targets/template/templates/indirect_dict.j21
-rw-r--r--test/integration/targets/template/templates/json_macro.j22
-rw-r--r--test/integration/targets/template/templates/lstrip_blocks.j28
-rw-r--r--test/integration/targets/template/templates/macro_using_globals.j23
-rw-r--r--test/integration/targets/template/templates/override_colon_value.j24
-rw-r--r--test/integration/targets/template/templates/override_separator.j21
-rw-r--r--test/integration/targets/template/templates/parent.j23
-rw-r--r--test/integration/targets/template/templates/qux1
-rw-r--r--test/integration/targets/template/templates/short.j21
-rw-r--r--test/integration/targets/template/templates/subtemplate.j22
-rw-r--r--test/integration/targets/template/templates/template_destpath_test.j21
-rw-r--r--test/integration/targets/template/templates/template_import_macro_globals.j22
-rw-r--r--test/integration/targets/template/templates/trim_blocks.j24
-rw-r--r--test/integration/targets/template/templates/unused_vars_include.j21
-rw-r--r--test/integration/targets/template/templates/unused_vars_template.j22
-rw-r--r--test/integration/targets/template/undefined_in_import-import.j21
-rw-r--r--test/integration/targets/template/undefined_in_import.j21
-rw-r--r--test/integration/targets/template/undefined_in_import.yml11
-rw-r--r--test/integration/targets/template/undefined_var_info.yml15
-rw-r--r--test/integration/targets/template/unsafe.yml64
-rw-r--r--test/integration/targets/template/unused_vars_include.yml8
-rw-r--r--test/integration/targets/template/vars/main.yml20
-rw-r--r--test/integration/targets/template_jinja2_non_native/46169.yml31
-rw-r--r--test/integration/targets/template_jinja2_non_native/aliases2
-rwxr-xr-xtest/integration/targets/template_jinja2_non_native/runme.sh7
-rw-r--r--test/integration/targets/template_jinja2_non_native/templates/46169.json.j23
-rw-r--r--test/integration/targets/templating/aliases2
-rw-r--r--test/integration/targets/templating/tasks/main.yml35
-rw-r--r--test/integration/targets/templating/templates/invalid_test_name.j21
-rw-r--r--test/integration/targets/templating_lookups/aliases2
-rwxr-xr-xtest/integration/targets/templating_lookups/runme.sh9
-rw-r--r--test/integration/targets/templating_lookups/runme.yml4
-rw-r--r--test/integration/targets/templating_lookups/template_deepcopy/hosts1
-rw-r--r--test/integration/targets/templating_lookups/template_deepcopy/playbook.yml10
-rw-r--r--test/integration/targets/templating_lookups/template_deepcopy/template.in1
-rw-r--r--test/integration/targets/templating_lookups/template_lookup_vaulted/playbook.yml13
-rw-r--r--test/integration/targets/templating_lookups/template_lookup_vaulted/templates/vaulted_hello.j26
-rw-r--r--test/integration/targets/templating_lookups/template_lookup_vaulted/test_vault_pass1
-rw-r--r--test/integration/targets/templating_lookups/template_lookups/mock_lookup_plugins/77788.py6
-rw-r--r--test/integration/targets/templating_lookups/template_lookups/tasks/errors.yml31
-rw-r--r--test/integration/targets/templating_lookups/template_lookups/tasks/main.yml101
-rw-r--r--test/integration/targets/templating_lookups/template_lookups/vars/main.yml9
-rw-r--r--test/integration/targets/templating_settings/aliases2
-rw-r--r--test/integration/targets/templating_settings/dont_warn_register.yml6
-rwxr-xr-xtest/integration/targets/templating_settings/runme.sh6
-rw-r--r--test/integration/targets/templating_settings/test_templating_settings.yml14
-rw-r--r--test/integration/targets/test_core/aliases1
-rw-r--r--test/integration/targets/test_core/inventory1
-rwxr-xr-xtest/integration/targets/test_core/runme.sh5
-rw-r--r--test/integration/targets/test_core/runme.yml4
-rw-r--r--test/integration/targets/test_core/tasks/main.yml350
-rw-r--r--test/integration/targets/test_core/vault-password1
-rw-r--r--test/integration/targets/test_files/aliases1
-rw-r--r--test/integration/targets/test_files/tasks/main.yml60
-rw-r--r--test/integration/targets/test_mathstuff/aliases1
-rw-r--r--test/integration/targets/test_mathstuff/tasks/main.yml27
-rw-r--r--test/integration/targets/test_uri/aliases1
-rw-r--r--test/integration/targets/test_uri/tasks/main.yml43
-rw-r--r--test/integration/targets/throttle/aliases2
-rw-r--r--test/integration/targets/throttle/group_vars/all.yml4
-rw-r--r--test/integration/targets/throttle/inventory6
-rwxr-xr-xtest/integration/targets/throttle/runme.sh7
-rwxr-xr-xtest/integration/targets/throttle/test_throttle.py34
-rw-r--r--test/integration/targets/throttle/test_throttle.yml84
-rw-r--r--test/integration/targets/unarchive/aliases3
-rw-r--r--test/integration/targets/unarchive/files/foo.txt1
-rw-r--r--test/integration/targets/unarchive/files/test-unarchive-nonascii-ãらã¨ã¿.tar.gzbin0 -> 4947 bytes
-rw-r--r--test/integration/targets/unarchive/handlers/main.yml3
-rw-r--r--test/integration/targets/unarchive/meta/main.yml4
-rw-r--r--test/integration/targets/unarchive/tasks/main.yml22
-rw-r--r--test/integration/targets/unarchive/tasks/prepare_tests.yml115
-rw-r--r--test/integration/targets/unarchive/tasks/test_different_language_var.yml41
-rw-r--r--test/integration/targets/unarchive/tasks/test_download.yml44
-rw-r--r--test/integration/targets/unarchive/tasks/test_exclude.yml54
-rw-r--r--test/integration/targets/unarchive/tasks/test_include.yml81
-rw-r--r--test/integration/targets/unarchive/tasks/test_invalid_options.yml27
-rw-r--r--test/integration/targets/unarchive/tasks/test_missing_binaries.yml87
-rw-r--r--test/integration/targets/unarchive/tasks/test_missing_files.yml47
-rw-r--r--test/integration/targets/unarchive/tasks/test_mode.yml151
-rw-r--r--test/integration/targets/unarchive/tasks/test_non_ascii_filename.yml66
-rw-r--r--test/integration/targets/unarchive/tasks/test_owner_group.yml164
-rw-r--r--test/integration/targets/unarchive/tasks/test_ownership_top_folder.yml50
-rw-r--r--test/integration/targets/unarchive/tasks/test_parent_not_writeable.yml32
-rw-r--r--test/integration/targets/unarchive/tasks/test_quotable_characters.yml38
-rw-r--r--test/integration/targets/unarchive/tasks/test_symlink.yml64
-rw-r--r--test/integration/targets/unarchive/tasks/test_tar.yml33
-rw-r--r--test/integration/targets/unarchive/tasks/test_tar_gz.yml35
-rw-r--r--test/integration/targets/unarchive/tasks/test_tar_gz_creates.yml53
-rw-r--r--test/integration/targets/unarchive/tasks/test_tar_gz_keep_newer.yml57
-rw-r--r--test/integration/targets/unarchive/tasks/test_tar_gz_owner_group.yml50
-rw-r--r--test/integration/targets/unarchive/tasks/test_tar_zst.yml40
-rw-r--r--test/integration/targets/unarchive/tasks/test_unprivileged_user.yml63
-rw-r--r--test/integration/targets/unarchive/tasks/test_zip.yml57
-rw-r--r--test/integration/targets/unarchive/vars/Darwin.yml1
-rw-r--r--test/integration/targets/unarchive/vars/FreeBSD.yml4
-rw-r--r--test/integration/targets/unarchive/vars/Linux.yml4
-rw-r--r--test/integration/targets/undefined/aliases2
-rw-r--r--test/integration/targets/undefined/tasks/main.yml16
-rw-r--r--test/integration/targets/unexpected_executor_exception/action_plugins/unexpected.py8
-rw-r--r--test/integration/targets/unexpected_executor_exception/aliases2
-rw-r--r--test/integration/targets/unexpected_executor_exception/tasks/main.yml7
-rw-r--r--test/integration/targets/unicode/aliases2
-rw-r--r--test/integration/targets/unicode/inventory5
-rw-r--r--test/integration/targets/unicode/křížek-ansible-project/ansible.cfg2
-rwxr-xr-xtest/integration/targets/unicode/runme.sh13
-rwxr-xr-xtest/integration/targets/unicode/unicode-test-script7
-rw-r--r--test/integration/targets/unicode/unicode.yml149
-rw-r--r--test/integration/targets/unsafe_writes/aliases7
-rw-r--r--test/integration/targets/unsafe_writes/basic.yml83
-rwxr-xr-xtest/integration/targets/unsafe_writes/runme.sh12
-rw-r--r--test/integration/targets/until/action_plugins/shell_no_failed.py28
-rw-r--r--test/integration/targets/until/aliases2
-rw-r--r--test/integration/targets/until/tasks/main.yml84
-rw-r--r--test/integration/targets/unvault/aliases2
-rw-r--r--test/integration/targets/unvault/main.yml9
-rw-r--r--test/integration/targets/unvault/password1
-rwxr-xr-xtest/integration/targets/unvault/runme.sh6
-rw-r--r--test/integration/targets/unvault/vault6
-rw-r--r--test/integration/targets/uri/aliases3
-rw-r--r--test/integration/targets/uri/files/README9
-rw-r--r--test/integration/targets/uri/files/fail0.json1
-rw-r--r--test/integration/targets/uri/files/fail1.json1
-rw-r--r--test/integration/targets/uri/files/fail10.json1
-rw-r--r--test/integration/targets/uri/files/fail11.json1
-rw-r--r--test/integration/targets/uri/files/fail12.json1
-rw-r--r--test/integration/targets/uri/files/fail13.json1
-rw-r--r--test/integration/targets/uri/files/fail14.json1
-rw-r--r--test/integration/targets/uri/files/fail15.json1
-rw-r--r--test/integration/targets/uri/files/fail16.json1
-rw-r--r--test/integration/targets/uri/files/fail17.json1
-rw-r--r--test/integration/targets/uri/files/fail18.json1
-rw-r--r--test/integration/targets/uri/files/fail19.json1
-rw-r--r--test/integration/targets/uri/files/fail2.json1
-rw-r--r--test/integration/targets/uri/files/fail20.json1
-rw-r--r--test/integration/targets/uri/files/fail21.json1
-rw-r--r--test/integration/targets/uri/files/fail22.json1
-rw-r--r--test/integration/targets/uri/files/fail23.json1
-rw-r--r--test/integration/targets/uri/files/fail24.json1
-rw-r--r--test/integration/targets/uri/files/fail25.json1
-rw-r--r--test/integration/targets/uri/files/fail26.json2
-rw-r--r--test/integration/targets/uri/files/fail27.json2
-rw-r--r--test/integration/targets/uri/files/fail28.json1
-rw-r--r--test/integration/targets/uri/files/fail29.json1
-rw-r--r--test/integration/targets/uri/files/fail3.json1
-rw-r--r--test/integration/targets/uri/files/fail30.json1
-rw-r--r--test/integration/targets/uri/files/fail4.json1
-rw-r--r--test/integration/targets/uri/files/fail5.json1
-rw-r--r--test/integration/targets/uri/files/fail6.json1
-rw-r--r--test/integration/targets/uri/files/fail7.json1
-rw-r--r--test/integration/targets/uri/files/fail8.json1
-rw-r--r--test/integration/targets/uri/files/fail9.json1
-rw-r--r--test/integration/targets/uri/files/formdata.txt1
-rw-r--r--test/integration/targets/uri/files/pass0.json58
-rw-r--r--test/integration/targets/uri/files/pass1.json1
-rw-r--r--test/integration/targets/uri/files/pass2.json6
-rw-r--r--test/integration/targets/uri/files/pass3.json1
-rw-r--r--test/integration/targets/uri/files/pass4.json1
-rw-r--r--test/integration/targets/uri/files/testserver.py23
-rw-r--r--test/integration/targets/uri/meta/main.yml4
-rw-r--r--test/integration/targets/uri/tasks/ciphers.yml32
-rw-r--r--test/integration/targets/uri/tasks/main.yml779
-rw-r--r--test/integration/targets/uri/tasks/redirect-all.yml272
-rw-r--r--test/integration/targets/uri/tasks/redirect-none.yml296
-rw-r--r--test/integration/targets/uri/tasks/redirect-safe.yml274
-rw-r--r--test/integration/targets/uri/tasks/redirect-urllib2.yml294
-rw-r--r--test/integration/targets/uri/tasks/return-content.yml49
-rw-r--r--test/integration/targets/uri/tasks/unexpected-failures.yml26
-rw-r--r--test/integration/targets/uri/tasks/use_gssapi.yml62
-rw-r--r--test/integration/targets/uri/tasks/use_netrc.yml51
-rw-r--r--test/integration/targets/uri/templates/netrc.j23
-rw-r--r--test/integration/targets/uri/vars/main.yml20
-rw-r--r--test/integration/targets/user/aliases2
-rw-r--r--test/integration/targets/user/files/userlist.sh20
-rw-r--r--test/integration/targets/user/meta/main.yml2
-rw-r--r--test/integration/targets/user/tasks/main.yml42
-rw-r--r--test/integration/targets/user/tasks/test_create_system_user.yml12
-rw-r--r--test/integration/targets/user/tasks/test_create_user.yml67
-rw-r--r--test/integration/targets/user/tasks/test_create_user_home.yml136
-rw-r--r--test/integration/targets/user/tasks/test_create_user_password.yml90
-rw-r--r--test/integration/targets/user/tasks/test_create_user_uid.yml26
-rw-r--r--test/integration/targets/user/tasks/test_expires.yml147
-rw-r--r--test/integration/targets/user/tasks/test_expires_min_max.yml73
-rw-r--r--test/integration/targets/user/tasks/test_expires_new_account.yml55
-rw-r--r--test/integration/targets/user/tasks/test_expires_new_account_epoch_negative.yml112
-rw-r--r--test/integration/targets/user/tasks/test_local.yml196
-rw-r--r--test/integration/targets/user/tasks/test_local_expires.yml333
-rw-r--r--test/integration/targets/user/tasks/test_no_home_fallback.yml106
-rw-r--r--test/integration/targets/user/tasks/test_password_lock.yml140
-rw-r--r--test/integration/targets/user/tasks/test_password_lock_new_user.yml63
-rw-r--r--test/integration/targets/user/tasks/test_remove_user.yml19
-rw-r--r--test/integration/targets/user/tasks/test_shadow_backup.yml21
-rw-r--r--test/integration/targets/user/tasks/test_ssh_key_passphrase.yml30
-rw-r--r--test/integration/targets/user/tasks/test_umask.yml57
-rw-r--r--test/integration/targets/user/vars/main.yml13
-rw-r--r--test/integration/targets/var_blending/aliases2
-rw-r--r--test/integration/targets/var_blending/group_vars/all9
-rw-r--r--test/integration/targets/var_blending/group_vars/local1
-rw-r--r--test/integration/targets/var_blending/host_vars/testhost4
-rw-r--r--test/integration/targets/var_blending/inventory26
-rw-r--r--test/integration/targets/var_blending/roles/test_var_blending/defaults/main.yml4
-rw-r--r--test/integration/targets/var_blending/roles/test_var_blending/files/foo.txt77
-rw-r--r--test/integration/targets/var_blending/roles/test_var_blending/tasks/main.yml57
-rw-r--r--test/integration/targets/var_blending/roles/test_var_blending/templates/foo.j277
-rw-r--r--test/integration/targets/var_blending/roles/test_var_blending/vars/main.yml4
-rw-r--r--test/integration/targets/var_blending/roles/test_var_blending/vars/more_vars.yml3
-rwxr-xr-xtest/integration/targets/var_blending/runme.sh5
-rw-r--r--test/integration/targets/var_blending/test_var_blending.yml8
-rw-r--r--test/integration/targets/var_blending/test_vars.yml1
-rw-r--r--test/integration/targets/var_blending/vars_file.yml12
-rw-r--r--test/integration/targets/var_inheritance/aliases2
-rw-r--r--test/integration/targets/var_inheritance/tasks/main.yml16
-rw-r--r--test/integration/targets/var_precedence/aliases2
-rwxr-xr-xtest/integration/targets/var_precedence/ansible-var-precedence-check.py544
-rw-r--r--test/integration/targets/var_precedence/host_vars/testhost2
-rw-r--r--test/integration/targets/var_precedence/inventory13
-rw-r--r--test/integration/targets/var_precedence/roles/test_var_precedence/meta/main.yml4
-rw-r--r--test/integration/targets/var_precedence/roles/test_var_precedence/tasks/main.yml10
-rw-r--r--test/integration/targets/var_precedence/roles/test_var_precedence_dep/defaults/main.yml5
-rw-r--r--test/integration/targets/var_precedence/roles/test_var_precedence_dep/tasks/main.yml14
-rw-r--r--test/integration/targets/var_precedence/roles/test_var_precedence_dep/vars/main.yml4
-rw-r--r--test/integration/targets/var_precedence/roles/test_var_precedence_inven_override/tasks/main.yml5
-rw-r--r--test/integration/targets/var_precedence/roles/test_var_precedence_role1/defaults/main.yml5
-rw-r--r--test/integration/targets/var_precedence/roles/test_var_precedence_role1/meta/main.yml2
-rw-r--r--test/integration/targets/var_precedence/roles/test_var_precedence_role1/tasks/main.yml14
-rw-r--r--test/integration/targets/var_precedence/roles/test_var_precedence_role1/vars/main.yml4
-rw-r--r--test/integration/targets/var_precedence/roles/test_var_precedence_role2/defaults/main.yml5
-rw-r--r--test/integration/targets/var_precedence/roles/test_var_precedence_role2/tasks/main.yml14
-rw-r--r--test/integration/targets/var_precedence/roles/test_var_precedence_role2/vars/main.yml5
-rw-r--r--test/integration/targets/var_precedence/roles/test_var_precedence_role3/defaults/main.yml7
-rw-r--r--test/integration/targets/var_precedence/roles/test_var_precedence_role3/tasks/main.yml14
-rw-r--r--test/integration/targets/var_precedence/roles/test_var_precedence_role3/vars/main.yml3
-rwxr-xr-xtest/integration/targets/var_precedence/runme.sh9
-rw-r--r--test/integration/targets/var_precedence/test_var_precedence.yml44
-rw-r--r--test/integration/targets/var_precedence/vars/test_var_precedence.yml5
-rw-r--r--test/integration/targets/var_reserved/aliases2
-rw-r--r--test/integration/targets/var_reserved/reserved_varname_warning.yml6
-rwxr-xr-xtest/integration/targets/var_reserved/runme.sh5
-rw-r--r--test/integration/targets/var_templating/aliases2
-rw-r--r--test/integration/targets/var_templating/ansible_debug_template.j21
-rw-r--r--test/integration/targets/var_templating/group_vars/all.yml7
-rwxr-xr-xtest/integration/targets/var_templating/runme.sh21
-rw-r--r--test/integration/targets/var_templating/task_vars_templating.yml58
-rw-r--r--test/integration/targets/var_templating/test_connection_vars.yml26
-rw-r--r--test/integration/targets/var_templating/test_vars_with_sources.yml9
-rw-r--r--test/integration/targets/var_templating/undall.yml6
-rw-r--r--test/integration/targets/var_templating/undefined.yml13
-rw-r--r--test/integration/targets/var_templating/vars/connection.yml3
-rw-r--r--test/integration/targets/wait_for/aliases2
-rw-r--r--test/integration/targets/wait_for/files/testserver.py19
-rw-r--r--test/integration/targets/wait_for/files/write_utf16.py20
-rw-r--r--test/integration/targets/wait_for/files/zombie.py13
-rw-r--r--test/integration/targets/wait_for/meta/main.yml3
-rw-r--r--test/integration/targets/wait_for/tasks/main.yml199
-rw-r--r--test/integration/targets/wait_for/vars/main.yml4
-rw-r--r--test/integration/targets/wait_for_connection/aliases2
-rw-r--r--test/integration/targets/wait_for_connection/tasks/main.yml30
-rw-r--r--test/integration/targets/want_json_modules_posix/aliases2
-rw-r--r--test/integration/targets/want_json_modules_posix/library/helloworld.py34
-rw-r--r--test/integration/targets/want_json_modules_posix/meta/main.yml2
-rw-r--r--test/integration/targets/want_json_modules_posix/tasks/main.yml43
-rw-r--r--test/integration/targets/win_async_wrapper/aliases3
-rw-r--r--test/integration/targets/win_async_wrapper/library/async_test.ps147
-rw-r--r--test/integration/targets/win_async_wrapper/tasks/main.yml257
-rw-r--r--test/integration/targets/win_become/aliases2
-rw-r--r--test/integration/targets/win_become/tasks/main.yml251
-rw-r--r--test/integration/targets/win_exec_wrapper/aliases2
-rw-r--r--test/integration/targets/win_exec_wrapper/library/test_all_options.ps112
-rw-r--r--test/integration/targets/win_exec_wrapper/library/test_common_functions.ps143
-rw-r--r--test/integration/targets/win_exec_wrapper/library/test_fail.ps166
-rw-r--r--test/integration/targets/win_exec_wrapper/library/test_invalid_requires.ps19
-rw-r--r--test/integration/targets/win_exec_wrapper/library/test_min_os_version.ps18
-rw-r--r--test/integration/targets/win_exec_wrapper/library/test_min_ps_version.ps18
-rw-r--r--test/integration/targets/win_exec_wrapper/tasks/main.yml274
-rw-r--r--test/integration/targets/win_fetch/aliases1
-rw-r--r--test/integration/targets/win_fetch/meta/main.yml2
-rw-r--r--test/integration/targets/win_fetch/tasks/main.yml212
-rw-r--r--test/integration/targets/win_module_utils/aliases2
-rw-r--r--test/integration/targets/win_module_utils/library/csharp_util.ps112
-rw-r--r--test/integration/targets/win_module_utils/library/legacy_only_new_way.ps15
-rw-r--r--test/integration/targets/win_module_utils/library/legacy_only_new_way_win_line_ending.ps16
-rw-r--r--test/integration/targets/win_module_utils/library/legacy_only_old_way.ps15
-rw-r--r--test/integration/targets/win_module_utils/library/legacy_only_old_way_win_line_ending.ps14
-rw-r--r--test/integration/targets/win_module_utils/library/recursive_requires.ps113
-rw-r--r--test/integration/targets/win_module_utils/library/uses_bogus_utils.ps16
-rw-r--r--test/integration/targets/win_module_utils/library/uses_local_utils.ps19
-rw-r--r--test/integration/targets/win_module_utils/module_utils/Ansible.ModuleUtils.Recursive1.psm19
-rw-r--r--test/integration/targets/win_module_utils/module_utils/Ansible.ModuleUtils.Recursive2.psm112
-rw-r--r--test/integration/targets/win_module_utils/module_utils/Ansible.ModuleUtils.Recursive3.psm120
-rw-r--r--test/integration/targets/win_module_utils/module_utils/Ansible.ModuleUtils.ValidTestModule.psm13
-rw-r--r--test/integration/targets/win_module_utils/module_utils/Ansible.Test.cs26
-rw-r--r--test/integration/targets/win_module_utils/tasks/main.yml71
-rw-r--r--test/integration/targets/win_raw/aliases2
-rw-r--r--test/integration/targets/win_raw/tasks/main.yml143
-rw-r--r--test/integration/targets/win_script/aliases2
-rw-r--r--test/integration/targets/win_script/defaults/main.yml5
-rw-r--r--test/integration/targets/win_script/files/fail.bat1
-rw-r--r--test/integration/targets/win_script/files/space path/test_script.ps11
-rw-r--r--test/integration/targets/win_script/files/test_script.bat2
-rw-r--r--test/integration/targets/win_script/files/test_script.cmd2
-rw-r--r--test/integration/targets/win_script/files/test_script.ps12
-rw-r--r--test/integration/targets/win_script/files/test_script_bool.ps16
-rw-r--r--test/integration/targets/win_script/files/test_script_creates_file.ps13
-rw-r--r--test/integration/targets/win_script/files/test_script_removes_file.ps13
-rw-r--r--test/integration/targets/win_script/files/test_script_whoami.ps12
-rw-r--r--test/integration/targets/win_script/files/test_script_with_args.ps16
-rw-r--r--test/integration/targets/win_script/files/test_script_with_env.ps11
-rw-r--r--test/integration/targets/win_script/files/test_script_with_errors.ps18
-rw-r--r--test/integration/targets/win_script/files/test_script_with_splatting.ps16
-rw-r--r--test/integration/targets/win_script/tasks/main.yml316
-rw-r--r--test/integration/targets/windows-minimal/aliases4
-rw-r--r--test/integration/targets/windows-minimal/library/win_ping.ps121
-rw-r--r--test/integration/targets/windows-minimal/library/win_ping.py55
-rw-r--r--test/integration/targets/windows-minimal/library/win_ping_set_attr.ps131
-rw-r--r--test/integration/targets/windows-minimal/library/win_ping_strict_mode_error.ps130
-rw-r--r--test/integration/targets/windows-minimal/library/win_ping_syntax_error.ps130
-rw-r--r--test/integration/targets/windows-minimal/library/win_ping_throw.ps130
-rw-r--r--test/integration/targets/windows-minimal/library/win_ping_throw_string.ps130
-rw-r--r--test/integration/targets/windows-minimal/tasks/main.yml67
-rw-r--r--test/integration/targets/windows-paths/aliases3
-rw-r--r--test/integration/targets/windows-paths/tasks/main.yml191
-rw-r--r--test/integration/targets/yaml_parsing/aliases2
-rw-r--r--test/integration/targets/yaml_parsing/playbook.yml5
-rw-r--r--test/integration/targets/yaml_parsing/tasks/main.yml37
-rw-r--r--test/integration/targets/yaml_parsing/tasks/unsafe.yml36
-rw-r--r--test/integration/targets/yaml_parsing/vars/main.yml1
-rw-r--r--test/integration/targets/yum/aliases5
-rw-r--r--test/integration/targets/yum/files/yum.conf5
-rw-r--r--test/integration/targets/yum/filter_plugins/filter_list_of_tuples_by_first_param.py25
-rw-r--r--test/integration/targets/yum/meta/main.yml4
-rw-r--r--test/integration/targets/yum/tasks/cacheonly.yml16
-rw-r--r--test/integration/targets/yum/tasks/check_mode_consistency.yml61
-rw-r--r--test/integration/targets/yum/tasks/lock.yml28
-rw-r--r--test/integration/targets/yum/tasks/main.yml82
-rw-r--r--test/integration/targets/yum/tasks/multiarch.yml154
-rw-r--r--test/integration/targets/yum/tasks/proxy.yml186
-rw-r--r--test/integration/targets/yum/tasks/repo.yml729
-rw-r--r--test/integration/targets/yum/tasks/yum.yml884
-rw-r--r--test/integration/targets/yum/tasks/yum_group_remove.yml152
-rw-r--r--test/integration/targets/yum/tasks/yuminstallroot.yml132
-rw-r--r--test/integration/targets/yum/vars/main.yml1
-rw-r--r--test/integration/targets/yum_repository/aliases2
-rw-r--r--test/integration/targets/yum_repository/defaults/main.yml5
-rw-r--r--test/integration/targets/yum_repository/handlers/main.yml4
-rw-r--r--test/integration/targets/yum_repository/meta/main.yml4
-rw-r--r--test/integration/targets/yum_repository/tasks/main.yml196
3010 files changed, 98127 insertions, 0 deletions
diff --git a/test/integration/targets/add_host/aliases b/test/integration/targets/add_host/aliases
new file mode 100644
index 0000000..765b70d
--- /dev/null
+++ b/test/integration/targets/add_host/aliases
@@ -0,0 +1 @@
+shippable/posix/group2
diff --git a/test/integration/targets/add_host/tasks/main.yml b/test/integration/targets/add_host/tasks/main.yml
new file mode 100644
index 0000000..d81e6dd
--- /dev/null
+++ b/test/integration/targets/add_host/tasks/main.yml
@@ -0,0 +1,186 @@
+# test code for the add_host action
+# (c) 2015, Matt Davis <mdavis@ansible.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# See https://github.com/ansible/ansible/issues/36045
+- set_fact:
+ inventory_data:
+ ansible_ssh_common_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
+ # ansible_ssh_host: "127.0.0.3"
+ ansible_host: "127.0.0.3"
+ ansible_ssh_pass: "foobar"
+ # ansible_ssh_port: "2222"
+ ansible_port: "2222"
+ ansible_ssh_private_key_file: "/tmp/inventory-cloudj9cGz5/identity"
+ ansible_ssh_user: "root"
+ hostname: "newdynamichost2"
+
+- name: Show inventory_data for 36045
+ debug:
+ msg: "{{ inventory_data }}"
+
+- name: Add host from dict 36045
+ add_host: "{{ inventory_data }}"
+
+- name: show newly added host
+ debug:
+ msg: "{{hostvars['newdynamichost2'].group_names}}"
+
+- name: ensure that dynamically-added newdynamichost2 is visible via hostvars, groups 36045
+ assert:
+ that:
+ - hostvars['newdynamichost2'] is defined
+ - hostvars['newdynamichost2'].group_names is defined
+
+# end of https://github.com/ansible/ansible/issues/36045 related tests
+
+- name: add a host to the runtime inventory
+ add_host:
+ name: newdynamichost
+ groups: newdynamicgroup
+ a_var: from add_host
+
+- debug: msg={{hostvars['newdynamichost'].group_names}}
+
+- name: ensure that dynamically-added host is visible via hostvars, groups, etc (there are several caches that could break this)
+ assert:
+ that:
+ - hostvars['bogushost'] is not defined # there was a bug where an undefined host was a "type" instead of an instance- ensure this works before we rely on it
+ - hostvars['newdynamichost'] is defined
+ - hostvars['newdynamichost'].group_names is defined
+ - "'newdynamicgroup' in hostvars['newdynamichost'].group_names"
+ - hostvars['newdynamichost']['bogusvar'] is not defined
+ - hostvars['newdynamichost']['a_var'] is defined
+ - hostvars['newdynamichost']['a_var'] == 'from add_host'
+ - groups['bogusgroup'] is not defined # same check as above to ensure that bogus groups are undefined...
+ - groups['newdynamicgroup'] is defined
+ - "'newdynamichost' in groups['newdynamicgroup']"
+
+# Tests for idempotency
+- name: Add testhost01 dynamic host
+ add_host:
+ name: testhost01
+ register: add_testhost01
+
+- name: Try adding testhost01 again, with no changes
+ add_host:
+ name: testhost01
+ register: add_testhost01_idem
+
+- name: Add a host variable to testhost01
+ add_host:
+ name: testhost01
+ foo: bar
+ register: hostvar_testhost01
+
+- name: Add the same host variable to testhost01, with no changes
+ add_host:
+ name: testhost01
+ foo: bar
+ register: hostvar_testhost01_idem
+
+- name: Add another host, testhost02
+ add_host:
+ name: testhost02
+ register: add_testhost02
+
+- name: Add it again for good measure
+ add_host:
+ name: testhost02
+ register: add_testhost02_idem
+
+- name: Add testhost02 to a group
+ add_host:
+ name: testhost02
+ groups:
+ - testhostgroup
+ register: add_group_testhost02
+
+- name: Add testhost01 to the same group
+ add_host:
+ name: testhost01
+ groups:
+ - testhostgroup
+ register: add_group_testhost01
+
+- name: Add testhost02 to the group again
+ add_host:
+ name: testhost02
+ groups:
+ - testhostgroup
+ register: add_group_testhost02_idem
+
+- name: Add testhost01 to the group again
+ add_host:
+ name: testhost01
+ groups:
+ - testhostgroup
+ register: add_group_testhost01_idem
+
+- assert:
+ that:
+ - add_testhost01 is changed
+ - add_testhost01_idem is not changed
+ - hostvar_testhost01 is changed
+ - hostvar_testhost01_idem is not changed
+ - add_testhost02 is changed
+ - add_testhost02_idem is not changed
+ - add_group_testhost02 is changed
+ - add_group_testhost01 is changed
+ - add_group_testhost02_idem is not changed
+ - add_group_testhost01_idem is not changed
+ - groups['testhostgroup']|length == 2
+ - "'testhost01' in groups['testhostgroup']"
+ - "'testhost02' in groups['testhostgroup']"
+ - hostvars['testhost01']['foo'] == 'bar'
+
+- name: Give invalid input
+ add_host: namenewdynamichost groupsnewdynamicgroup a_varfromadd_host
+ ignore_errors: true
+ register: badinput
+
+- name: verify we detected bad input
+ assert:
+ that:
+ - badinput is failed
+
+- name: Add hosts in a loop
+ add_host:
+ name: 'host_{{item}}'
+ loop:
+ - 1
+ - 2
+ - 2
+ register: add_host_loop_res
+
+- name: verify correct changed results
+ assert:
+ that:
+ - add_host_loop_res.results[0] is changed
+ - add_host_loop_res.results[1] is changed
+ - add_host_loop_res.results[2] is not changed
+ - add_host_loop_res is changed
+
+- name: Add host with host:port in name
+ add_host:
+ name: '127.0.1.1:2222'
+ register: hostport
+
+- assert:
+ that:
+ - hostport.add_host.host_name == '127.0.1.1'
+ - hostport.add_host.host_vars.ansible_ssh_port == 2222
diff --git a/test/integration/targets/adhoc/aliases b/test/integration/targets/adhoc/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/adhoc/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/adhoc/runme.sh b/test/integration/targets/adhoc/runme.sh
new file mode 100755
index 0000000..eda6d66
--- /dev/null
+++ b/test/integration/targets/adhoc/runme.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# run type tests
+ansible -a 'sleep 20' --task-timeout 5 localhost |grep 'The command action failed to execute in the expected time frame (5) and was terminated'
+
+# -a parsing with json
+ansible --task-timeout 5 localhost -m command -a '{"cmd": "whoami"}' | grep 'rc=0'
diff --git a/test/integration/targets/ansiballz_python/aliases b/test/integration/targets/ansiballz_python/aliases
new file mode 100644
index 0000000..7ae73ab
--- /dev/null
+++ b/test/integration/targets/ansiballz_python/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group1
+context/target
diff --git a/test/integration/targets/ansiballz_python/library/check_rlimit_and_maxfd.py b/test/integration/targets/ansiballz_python/library/check_rlimit_and_maxfd.py
new file mode 100644
index 0000000..a01ee99
--- /dev/null
+++ b/test/integration/targets/ansiballz_python/library/check_rlimit_and_maxfd.py
@@ -0,0 +1,31 @@
+#!/usr/bin/python
+#
+# Copyright 2018 Red Hat | Ansible
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import resource
+import subprocess
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict()
+ )
+
+ rlimit_nofile = resource.getrlimit(resource.RLIMIT_NOFILE)
+
+ try:
+ maxfd = subprocess.MAXFD
+ except AttributeError:
+ maxfd = -1
+
+ module.exit_json(rlimit_nofile=rlimit_nofile, maxfd=maxfd, infinity=resource.RLIM_INFINITY)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansiballz_python/library/custom_module.py b/test/integration/targets/ansiballz_python/library/custom_module.py
new file mode 100644
index 0000000..625823e
--- /dev/null
+++ b/test/integration/targets/ansiballz_python/library/custom_module.py
@@ -0,0 +1,19 @@
+#!/usr/bin/python
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ..module_utils.basic import AnsibleModule # pylint: disable=relative-beyond-top-level
+from ..module_utils.custom_util import forty_two # pylint: disable=relative-beyond-top-level
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict()
+ )
+
+ module.exit_json(answer=forty_two())
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansiballz_python/library/sys_check.py b/test/integration/targets/ansiballz_python/library/sys_check.py
new file mode 100644
index 0000000..aa22fe6
--- /dev/null
+++ b/test/integration/targets/ansiballz_python/library/sys_check.py
@@ -0,0 +1,23 @@
+#!/usr/bin/python
+# https://github.com/ansible/ansible/issues/64664
+# https://github.com/ansible/ansible/issues/64479
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import sys
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule({})
+
+ this_module = sys.modules[__name__]
+ module.exit_json(
+ failed=not getattr(this_module, 'AnsibleModule', False)
+ )
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansiballz_python/module_utils/custom_util.py b/test/integration/targets/ansiballz_python/module_utils/custom_util.py
new file mode 100644
index 0000000..0393db4
--- /dev/null
+++ b/test/integration/targets/ansiballz_python/module_utils/custom_util.py
@@ -0,0 +1,6 @@
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+def forty_two():
+ return 42
diff --git a/test/integration/targets/ansiballz_python/tasks/main.yml b/test/integration/targets/ansiballz_python/tasks/main.yml
new file mode 100644
index 0000000..0aaa645
--- /dev/null
+++ b/test/integration/targets/ansiballz_python/tasks/main.yml
@@ -0,0 +1,68 @@
+- name: get the ansible-test imposed file descriptor limit
+ check_rlimit_and_maxfd:
+ register: rlimit_limited_return
+
+- name: get existing file descriptor limit
+ check_rlimit_and_maxfd:
+ register: rlimit_original_return
+ vars:
+ ansible_python_module_rlimit_nofile: 0 # ignore limit set by ansible-test
+
+- name: attempt to set a value lower than existing soft limit
+ check_rlimit_and_maxfd:
+ vars:
+ ansible_python_module_rlimit_nofile: '{{ rlimit_original_return.rlimit_nofile[0] - 1 }}'
+ register: rlimit_below_soft_return
+
+- name: attempt to set a value higher than existing soft limit
+ check_rlimit_and_maxfd:
+ vars:
+ ansible_python_module_rlimit_nofile: '{{ rlimit_original_return.rlimit_nofile[0] + 1 }}'
+ register: rlimit_above_soft_return
+
+- name: attempt to set a value lower than existing hard limit
+ check_rlimit_and_maxfd:
+ vars:
+ ansible_python_module_rlimit_nofile: '{{ rlimit_original_return.rlimit_nofile[1] - 1 }}'
+ register: rlimit_below_hard_return
+
+- name: attempt to set a value higher than existing hard limit
+ check_rlimit_and_maxfd:
+ vars:
+ ansible_python_module_rlimit_nofile: '{{ rlimit_original_return.rlimit_nofile[1] + 1 }}'
+ register: rlimit_above_hard_return
+
+- name: run a role module which uses a role module_util using relative imports
+ custom_module:
+ register: custom_module_return
+
+- assert:
+ that:
+ # make sure ansible-test was able to set the limit unless it exceeds the hard limit or the value is lower on macOS
+ - rlimit_limited_return.rlimit_nofile[0] == 1024 or rlimit_original_return.rlimit_nofile[1] < 1024 or (rlimit_limited_return.rlimit_nofile[0] < 1024 and ansible_distribution == 'MacOSX')
+ # make sure that maxfd matches the soft limit on Python 2.x (-1 on Python 3.x)
+ - rlimit_limited_return.maxfd == rlimit_limited_return.rlimit_nofile[0] or rlimit_limited_return.maxfd == -1
+
+ # we should always be able to set the limit lower than the existing soft limit
+ - rlimit_below_soft_return.rlimit_nofile[0] == rlimit_original_return.rlimit_nofile[0] - 1
+ # the hard limit should not have changed
+ - rlimit_below_soft_return.rlimit_nofile[1] == rlimit_original_return.rlimit_nofile[1]
+ # lowering the limit should also lower the max file descriptors reported by Python 2.x (-1 on Python 3.x)
+ - rlimit_below_soft_return.maxfd == rlimit_original_return.rlimit_nofile[0] - 1 or rlimit_below_soft_return.maxfd == -1
+
+ # we should be able to set the limit higher than the existing soft limit if it does not exceed the hard limit (except on macOS)
+ - rlimit_above_soft_return.rlimit_nofile[0] == rlimit_original_return.rlimit_nofile[0] + 1 or rlimit_original_return.rlimit_nofile[0] == rlimit_original_return.rlimit_nofile[1] or ansible_distribution == 'MacOSX'
+
+ # we should be able to set the limit lower than the existing hard limit (except on macOS)
+ - rlimit_below_hard_return.rlimit_nofile[0] == rlimit_original_return.rlimit_nofile[1] - 1 or ansible_distribution == 'MacOSX'
+
+ # setting the limit higher than the existing hard limit should use the hard limit (except on macOS)
+ - rlimit_above_hard_return.rlimit_nofile[0] == rlimit_original_return.rlimit_nofile[1] or ansible_distribution == 'MacOSX'
+
+ # custom module returned the correct answer
+ - custom_module_return.answer == 42
+
+# https://github.com/ansible/ansible/issues/64664
+# https://github.com/ansible/ansible/issues/64479
+- name: Run module that tries to access itself via sys.modules
+ sys_check:
diff --git a/test/integration/targets/ansible-doc/aliases b/test/integration/targets/ansible-doc/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/ansible-doc/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/MANIFEST.json b/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/MANIFEST.json
new file mode 100644
index 0000000..243a5e4
--- /dev/null
+++ b/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/MANIFEST.json
@@ -0,0 +1,30 @@
+{
+ "collection_info": {
+ "description": null,
+ "repository": "",
+ "tags": [],
+ "dependencies": {},
+ "authors": [
+ "Ansible (https://ansible.com)"
+ ],
+ "issues": "",
+ "name": "testcol",
+ "license": [
+ "GPL-3.0-or-later"
+ ],
+ "documentation": "",
+ "namespace": "testns",
+ "version": "0.1.1231",
+ "readme": "README.md",
+ "license_file": "COPYING",
+ "homepage": "",
+ },
+ "file_manifest_file": {
+ "format": 1,
+ "ftype": "file",
+ "chksum_sha256": "4c15a867ceba8ba1eaf2f4a58844bb5dbb82fec00645fc7eb74a3d31964900f6",
+ "name": "FILES.json",
+ "chksum_type": "sha256"
+ },
+ "format": 1
+}
diff --git a/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/cache/notjsonfile.py b/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/cache/notjsonfile.py
new file mode 100644
index 0000000..9fa25b4
--- /dev/null
+++ b/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/cache/notjsonfile.py
@@ -0,0 +1,70 @@
+# (c) 2020 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ cache: notjsonfile
+ broken:
+ short_description: JSON formatted files.
+ description:
+ - This cache uses JSON formatted, per host, files saved to the filesystem.
+ author: Ansible Core (@ansible-core)
+ version_added: 0.7.0
+ options:
+ _uri:
+ required: True
+ description:
+ - Path in which the cache plugin will save the JSON files
+ env:
+ - name: ANSIBLE_CACHE_PLUGIN_CONNECTION
+ version_added: 1.2.0
+ ini:
+ - key: fact_caching_connection
+ section: defaults
+ deprecated:
+ alternative: none
+ why: Test deprecation
+ version: '2.0.0'
+ _prefix:
+ description: User defined prefix to use when creating the JSON files
+ env:
+ - name: ANSIBLE_CACHE_PLUGIN_PREFIX
+ version_added: 1.1.0
+ ini:
+ - key: fact_caching_prefix
+ section: defaults
+ deprecated:
+ alternative: none
+ why: Another test deprecation
+ removed_at_date: '2050-01-01'
+ _timeout:
+ default: 86400
+ description: Expiration timeout for the cache plugin data
+ env:
+ - name: ANSIBLE_CACHE_PLUGIN_TIMEOUT
+ ini:
+ - key: fact_caching_timeout
+ section: defaults
+ vars:
+ - name: notsjonfile_fact_caching_timeout
+ version_added: 1.5.0
+ deprecated:
+ alternative: do not use a variable
+ why: Test deprecation
+ version: '3.0.0'
+ type: integer
+ extends_documentation_fragment:
+ - testns.testcol2.plugin
+'''
+
+from ansible.plugins.cache import BaseFileCacheModule
+
+
+class CacheModule(BaseFileCacheModule):
+ """
+ A caching module backed by json files.
+ """
+ pass
diff --git a/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/inventory/statichost.py b/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/inventory/statichost.py
new file mode 100644
index 0000000..caec2ed
--- /dev/null
+++ b/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/inventory/statichost.py
@@ -0,0 +1,36 @@
+# Copyright (c) 2018 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ inventory: statichost
+ broken:
+ short_description: Add a single host
+ description: Add a single host
+ extends_documentation_fragment:
+ - inventory_cache
+ options:
+ plugin:
+ description: plugin name (must be statichost)
+ required: true
+ hostname:
+ description: Toggle display of stderr even when script was successful
+ required: True
+'''
+
+from ansible.errors import AnsibleParserError
+from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable
+
+
+class InventoryModule(BaseInventoryPlugin, Cacheable):
+
+ NAME = 'testns.content_adj.statichost'
+
+ def verify_file(self, path):
+ pass
+
+ def parse(self, inventory, loader, path, cache=None):
+
+ pass
diff --git a/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/lookup/noop.py b/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/lookup/noop.py
new file mode 100644
index 0000000..d456986
--- /dev/null
+++ b/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/lookup/noop.py
@@ -0,0 +1,45 @@
+# (c) 2020 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+
+__metaclass__ = type
+
+DOCUMENTATION = """
+ lookup: noop
+ broken:
+ author: Ansible core team
+ short_description: returns input
+ description:
+ - this is a noop
+ deprecated:
+ alternative: Use some other lookup
+ why: Test deprecation
+ removed_in: '3.0.0'
+ extends_documentation_fragment:
+ - testns.testcol2.version_added
+"""
+
+EXAMPLES = """
+- name: do nothing
+ debug: msg="{{ lookup('testns.testcol.noop', [1,2,3,4] }}"
+"""
+
+RETURN = """
+ _list:
+ description: input given
+ version_added: 1.0.0
+"""
+
+from ansible.module_utils.common._collections_compat import Sequence
+from ansible.plugins.lookup import LookupBase
+from ansible.errors import AnsibleError
+
+
+class LookupModule(LookupBase):
+
+ def run(self, terms, **kwargs):
+ if not isinstance(terms, Sequence):
+ raise AnsibleError("testns.testcol.noop expects a list")
+ return terms
diff --git a/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/modules/fakemodule.py b/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/modules/fakemodule.py
new file mode 100644
index 0000000..a1caeb1
--- /dev/null
+++ b/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/modules/fakemodule.py
@@ -0,0 +1,28 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+ module: fakemodule
+ broken:
+ short_desciption: fake module
+ description:
+ - this is a fake module
+ version_added: 1.0.0
+ options:
+ _notreal:
+ description: really not a real option
+ author:
+ - me
+"""
+
+import json
+
+
+def main():
+ print(json.dumps(dict(changed=False, source='testns.testcol.fakemodule')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/modules/notrealmodule.py b/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/modules/notrealmodule.py
new file mode 100644
index 0000000..4479f23
--- /dev/null
+++ b/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/modules/notrealmodule.py
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+
+def main():
+ print(json.dumps(dict(changed=False, source='testns.testcol.notrealmodule')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py b/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py
new file mode 100644
index 0000000..fb0e319
--- /dev/null
+++ b/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py
@@ -0,0 +1,96 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: randommodule
+short_description: A random module
+description:
+ - A random module.
+author:
+ - Ansible Core Team
+version_added: 1.0.0
+deprecated:
+ alternative: Use some other module
+ why: Test deprecation
+ removed_in: '3.0.0'
+options:
+ test:
+ description: Some text.
+ type: str
+ version_added: 1.2.0
+ sub:
+ description: Suboptions.
+ type: dict
+ suboptions:
+ subtest:
+ description: A suboption.
+ type: int
+ version_added: 1.1.0
+ # The following is the wrong syntax, and should not get processed
+ # by add_collection_to_versions_and_dates()
+ options:
+ subtest2:
+ description: Another suboption.
+ type: float
+ version_added: 1.1.0
+ # The following is not supported in modules, and should not get processed
+ # by add_collection_to_versions_and_dates()
+ env:
+ - name: TEST_ENV
+ version_added: 1.0.0
+ deprecated:
+ alternative: none
+ why: Test deprecation
+ removed_in: '2.0.0'
+ version: '2.0.0'
+extends_documentation_fragment:
+ - testns.testcol2.module
+'''
+
+EXAMPLES = '''
+'''
+
+RETURN = '''
+z_last:
+ description: A last result.
+ broken:
+ type: str
+ returned: success
+ version_added: 1.3.0
+
+m_middle:
+ description:
+ - This should be in the middle.
+ - Has some more data
+ type: dict
+ returned: success and 1st of month
+ contains:
+ suboption:
+ description: A suboption.
+ type: str
+ choices: [ARF, BARN, c_without_capital_first_letter]
+ version_added: 1.4.0
+
+a_first:
+ description: A first result.
+ type: str
+ returned: success
+'''
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(),
+ )
+
+ module.exit_json()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/vars/noop_vars_plugin.py b/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/vars/noop_vars_plugin.py
new file mode 100644
index 0000000..ae0f75e
--- /dev/null
+++ b/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/vars/noop_vars_plugin.py
@@ -0,0 +1,30 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ vars: noop_vars_plugin
+ broken:
+ short_description: Do NOT load host and group vars
+ description: don't test loading host and group vars from a collection
+ options:
+ stage:
+ default: all
+ choices: ['all', 'inventory', 'task']
+ type: str
+ ini:
+ - key: stage
+ section: testns.testcol.noop_vars_plugin
+ env:
+ - name: ANSIBLE_VARS_PLUGIN_STAGE
+ extends_documentation_fragment:
+ - testns.testcol2.deprecation
+'''
+
+from ansible.plugins.vars import BaseVarsPlugin
+
+
+class VarsModule(BaseVarsPlugin):
+
+ def get_vars(self, loader, path, entities, cache=True):
+ super(VarsModule, self).get_vars(loader, path, entities)
+ return {'collection': 'yes', 'notreal': 'value'}
diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/MANIFEST.json b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/MANIFEST.json
new file mode 100644
index 0000000..243a5e4
--- /dev/null
+++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/MANIFEST.json
@@ -0,0 +1,30 @@
+{
+ "collection_info": {
+ "description": null,
+ "repository": "",
+ "tags": [],
+ "dependencies": {},
+ "authors": [
+ "Ansible (https://ansible.com)"
+ ],
+ "issues": "",
+ "name": "testcol",
+ "license": [
+ "GPL-3.0-or-later"
+ ],
+ "documentation": "",
+ "namespace": "testns",
+ "version": "0.1.1231",
+ "readme": "README.md",
+ "license_file": "COPYING",
+ "homepage": "",
+ },
+ "file_manifest_file": {
+ "format": 1,
+ "ftype": "file",
+ "chksum_sha256": "4c15a867ceba8ba1eaf2f4a58844bb5dbb82fec00645fc7eb74a3d31964900f6",
+ "name": "FILES.json",
+ "chksum_type": "sha256"
+ },
+ "format": 1
+}
diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/cache/notjsonfile.py b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/cache/notjsonfile.py
new file mode 100644
index 0000000..ea4a722
--- /dev/null
+++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/cache/notjsonfile.py
@@ -0,0 +1,69 @@
+# (c) 2020 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ cache: notjsonfile
+ short_description: JSON formatted files.
+ description:
+ - This cache uses JSON formatted, per host, files saved to the filesystem.
+ author: Ansible Core (@ansible-core)
+ version_added: 0.7.0
+ options:
+ _uri:
+ required: True
+ description:
+ - Path in which the cache plugin will save the JSON files
+ env:
+ - name: ANSIBLE_CACHE_PLUGIN_CONNECTION
+ version_added: 1.2.0
+ ini:
+ - key: fact_caching_connection
+ section: defaults
+ deprecated:
+ alternative: none
+ why: Test deprecation
+ version: '2.0.0'
+ _prefix:
+ description: User defined prefix to use when creating the JSON files
+ env:
+ - name: ANSIBLE_CACHE_PLUGIN_PREFIX
+ version_added: 1.1.0
+ ini:
+ - key: fact_caching_prefix
+ section: defaults
+ deprecated:
+ alternative: none
+ why: Another test deprecation
+ removed_at_date: '2050-01-01'
+ _timeout:
+ default: 86400
+ description: Expiration timeout for the cache plugin data
+ env:
+ - name: ANSIBLE_CACHE_PLUGIN_TIMEOUT
+ ini:
+ - key: fact_caching_timeout
+ section: defaults
+ vars:
+ - name: notsjonfile_fact_caching_timeout
+ version_added: 1.5.0
+ deprecated:
+ alternative: do not use a variable
+ why: Test deprecation
+ version: '3.0.0'
+ type: integer
+ extends_documentation_fragment:
+ - testns.testcol2.plugin
+'''
+
+from ansible.plugins.cache import BaseFileCacheModule
+
+
+class CacheModule(BaseFileCacheModule):
+ """
+ A caching module backed by json files.
+ """
+ pass
diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/filter/filter_subdir/in_subdir.py b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/filter/filter_subdir/in_subdir.py
new file mode 100644
index 0000000..a8924e1
--- /dev/null
+++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/filter/filter_subdir/in_subdir.py
@@ -0,0 +1,23 @@
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.utils.display import Display
+
+display = Display()
+
+
+def nochange(a):
+ return a
+
+
+class FilterModule(object):
+ ''' Ansible core jinja2 filters '''
+
+ def filters(self):
+ return {
+ 'noop': nochange,
+ 'nested': nochange,
+ }
diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/filter/grouped.py b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/filter/grouped.py
new file mode 100644
index 0000000..a10c7aa
--- /dev/null
+++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/filter/grouped.py
@@ -0,0 +1,28 @@
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.utils.display import Display
+
+display = Display()
+
+
+def nochange(a):
+ return a
+
+
+def meaningoflife(a):
+ return 42
+
+
+class FilterModule(object):
+ ''' Ansible core jinja2 filters '''
+
+ def filters(self):
+ return {
+ 'noop': nochange,
+ 'ultimatequestion': meaningoflife,
+ 'b64decode': nochange, # here to colide with basename of builtin
+ }
diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/filter/ultimatequestion.yml b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/filter/ultimatequestion.yml
new file mode 100644
index 0000000..a67654d
--- /dev/null
+++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/filter/ultimatequestion.yml
@@ -0,0 +1,21 @@
+DOCUMENTATION:
+ name: ultimatequestion
+ author: Terry Prachet
+ version_added: 'histerical'
+ short_description: Ask any question but it will only respond with the answer to the ulitmate one
+ description:
+ - read the book
+ options:
+ _input:
+ description: Anything you want, goign to ignore it anywayss ...
+ type: raw
+ required: true
+
+EXAMPLES: |
+ # set first 10 volumes rw, rest as dp
+ meaning: "{{ (stuff|ulmtimatequestion }}"
+
+RETURN:
+ _value:
+ description: guess
+ type: int
diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/inventory/statichost.py b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/inventory/statichost.py
new file mode 100644
index 0000000..cbb8f0f
--- /dev/null
+++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/inventory/statichost.py
@@ -0,0 +1,35 @@
+# Copyright (c) 2018 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ inventory: statichost
+ short_description: Add a single host
+ description: Add a single host
+ extends_documentation_fragment:
+ - inventory_cache
+ options:
+ plugin:
+ description: plugin name (must be statichost)
+ required: true
+ hostname:
+ description: Toggle display of stderr even when script was successful
+ required: True
+'''
+
+from ansible.errors import AnsibleParserError
+from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable
+
+
+class InventoryModule(BaseInventoryPlugin, Cacheable):
+
+ NAME = 'testns.content_adj.statichost'
+
+ def verify_file(self, path):
+ pass
+
+ def parse(self, inventory, loader, path, cache=None):
+
+ pass
diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/lookup/noop.py b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/lookup/noop.py
new file mode 100644
index 0000000..7a64a5d
--- /dev/null
+++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/lookup/noop.py
@@ -0,0 +1,45 @@
+# (c) 2020 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+
+__metaclass__ = type
+
+DOCUMENTATION = """
+ lookup: noop
+ author: Ansible core team
+ short_description: returns input
+ description:
+ - this is a noop
+ deprecated:
+ alternative: Use some other lookup
+ why: Test deprecation
+ removed_in: '3.0.0'
+ extends_documentation_fragment:
+ - testns.testcol2.version_added
+"""
+
+EXAMPLES = """
+- name: do nothing
+ debug: msg="{{ lookup('testns.testcol.noop', [1,2,3,4] }}"
+"""
+
+RETURN = """
+ _list:
+ description: input given
+ version_added: 1.0.0
+"""
+
+from collections.abc import Sequence
+
+from ansible.plugins.lookup import LookupBase
+from ansible.errors import AnsibleError
+
+
+class LookupModule(LookupBase):
+
+ def run(self, terms, **kwargs):
+ if not isinstance(terms, Sequence):
+ raise AnsibleError("testns.testcol.noop expects a list")
+ return terms
diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/database/database_type/subdir_module.py b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/database/database_type/subdir_module.py
new file mode 100644
index 0000000..dd41305
--- /dev/null
+++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/database/database_type/subdir_module.py
@@ -0,0 +1,37 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: subdir_module
+short_description: A module in multiple subdirectories
+description:
+ - A module in multiple subdirectories
+author:
+ - Ansible Core Team
+version_added: 1.0.0
+options: {}
+'''
+
+EXAMPLES = '''
+'''
+
+RETURN = '''
+'''
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(),
+ )
+
+ module.exit_json()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/fakemodule.py b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/fakemodule.py
new file mode 100644
index 0000000..6d18c08
--- /dev/null
+++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/fakemodule.py
@@ -0,0 +1,27 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+ module: fakemodule
+ short_desciption: fake module
+ description:
+ - this is a fake module
+ version_added: 1.0.0
+ options:
+ _notreal:
+ description: really not a real option
+ author:
+ - me
+"""
+
+import json
+
+
+def main():
+ print(json.dumps(dict(changed=False, source='testns.testcol.fakemodule')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/notrealmodule.py b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/notrealmodule.py
new file mode 100644
index 0000000..4479f23
--- /dev/null
+++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/notrealmodule.py
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+
+def main():
+ print(json.dumps(dict(changed=False, source='testns.testcol.notrealmodule')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py
new file mode 100644
index 0000000..f251a69
--- /dev/null
+++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py
@@ -0,0 +1,95 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: randommodule
+short_description: A random module
+description:
+ - A random module.
+author:
+ - Ansible Core Team
+version_added: 1.0.0
+deprecated:
+ alternative: Use some other module
+ why: Test deprecation
+ removed_in: '3.0.0'
+options:
+ test:
+ description: Some text.
+ type: str
+ version_added: 1.2.0
+ sub:
+ description: Suboptions.
+ type: dict
+ suboptions:
+ subtest:
+ description: A suboption.
+ type: int
+ version_added: 1.1.0
+ # The following is the wrong syntax, and should not get processed
+ # by add_collection_to_versions_and_dates()
+ options:
+ subtest2:
+ description: Another suboption.
+ type: float
+ version_added: 1.1.0
+ # The following is not supported in modules, and should not get processed
+ # by add_collection_to_versions_and_dates()
+ env:
+ - name: TEST_ENV
+ version_added: 1.0.0
+ deprecated:
+ alternative: none
+ why: Test deprecation
+ removed_in: '2.0.0'
+ version: '2.0.0'
+extends_documentation_fragment:
+ - testns.testcol2.module
+'''
+
+EXAMPLES = '''
+'''
+
+RETURN = '''
+z_last:
+ description: A last result.
+ type: str
+ returned: success
+ version_added: 1.3.0
+
+m_middle:
+ description:
+ - This should be in the middle.
+ - Has some more data
+ type: dict
+ returned: success and 1st of month
+ contains:
+ suboption:
+ description: A suboption.
+ type: str
+ choices: [ARF, BARN, c_without_capital_first_letter]
+ version_added: 1.4.0
+
+a_first:
+ description: A first result.
+ type: str
+ returned: success
+'''
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(),
+ )
+
+ module.exit_json()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/test/test_test.py b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/test/test_test.py
new file mode 100644
index 0000000..f1c2b3a
--- /dev/null
+++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/test/test_test.py
@@ -0,0 +1,16 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+def yolo(value):
+ return True
+
+
+class TestModule(object):
+ ''' Ansible core jinja2 tests '''
+
+ def tests(self):
+ return {
+ # failure testing
+ 'yolo': yolo,
+ }
diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/test/yolo.yml b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/test/yolo.yml
new file mode 100644
index 0000000..cc60945
--- /dev/null
+++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/test/yolo.yml
@@ -0,0 +1,18 @@
+DOCUMENTATION:
+ name: yolo
+ short_description: you only live once
+ description:
+ - This is always true
+ options:
+ _input:
+ description: does not matter
+ type: raw
+ required: true
+
+EXAMPLES: |
+ {{ 'anything' is yolo }}
+
+RETURN:
+ output:
+ type: boolean
+ description: always true
diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/vars/noop_vars_plugin.py b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/vars/noop_vars_plugin.py
new file mode 100644
index 0000000..94e7feb
--- /dev/null
+++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/vars/noop_vars_plugin.py
@@ -0,0 +1,29 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ vars: noop_vars_plugin
+ short_description: Do NOT load host and group vars
+ description: don't test loading host and group vars from a collection
+ options:
+ stage:
+ default: all
+ choices: ['all', 'inventory', 'task']
+ type: str
+ ini:
+ - key: stage
+ section: testns.testcol.noop_vars_plugin
+ env:
+ - name: ANSIBLE_VARS_PLUGIN_STAGE
+ extends_documentation_fragment:
+ - testns.testcol2.deprecation
+'''
+
+from ansible.plugins.vars import BaseVarsPlugin
+
+
+class VarsModule(BaseVarsPlugin):
+
+ def get_vars(self, loader, path, entities, cache=True):
+ super(VarsModule, self).get_vars(loader, path, entities)
+ return {'collection': 'yes', 'notreal': 'value'}
diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/roles/testrole/meta/main.yml b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/roles/testrole/meta/main.yml
new file mode 100644
index 0000000..bc6af69
--- /dev/null
+++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/roles/testrole/meta/main.yml
@@ -0,0 +1,26 @@
+---
+dependencies:
+galaxy_info:
+
+argument_specs:
+ main:
+ short_description: testns.testcol.testrole short description for main entry point
+ description:
+ - Longer description for testns.testcol.testrole main entry point.
+ author: Ansible Core (@ansible)
+ options:
+ opt1:
+ description: opt1 description
+ type: "str"
+ required: true
+
+ alternate:
+ short_description: testns.testcol.testrole short description for alternate entry point
+ description:
+ - Longer description for testns.testcol.testrole alternate entry point.
+ author: Ansible Core (@ansible)
+ options:
+ altopt1:
+ description: altopt1 description
+ type: "int"
+ required: true
diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/roles/testrole_with_no_argspecs/meta/empty b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/roles/testrole_with_no_argspecs/meta/empty
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/roles/testrole_with_no_argspecs/meta/empty
diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/MANIFEST.json b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/MANIFEST.json
new file mode 100644
index 0000000..02ec289
--- /dev/null
+++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/MANIFEST.json
@@ -0,0 +1,30 @@
+{
+ "collection_info": {
+ "description": null,
+ "repository": "",
+ "tags": [],
+ "dependencies": {},
+ "authors": [
+ "Ansible (https://ansible.com)"
+ ],
+ "issues": "",
+ "name": "testcol2",
+ "license": [
+ "GPL-3.0-or-later"
+ ],
+ "documentation": "",
+ "namespace": "testns",
+ "version": "1.2.0",
+ "readme": "README.md",
+ "license_file": "COPYING",
+ "homepage": "",
+ },
+ "file_manifest_file": {
+ "format": 1,
+ "ftype": "file",
+ "chksum_sha256": "4c15a867ceba8ba1eaf2f4a58844bb5dbb82fec00645fc7eb74a3d31964900f6",
+ "name": "FILES.json",
+ "chksum_type": "sha256"
+ },
+ "format": 1
+}
diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/plugins/doc_fragments/deprecation.py b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/plugins/doc_fragments/deprecation.py
new file mode 100644
index 0000000..3942d72
--- /dev/null
+++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/plugins/doc_fragments/deprecation.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+class ModuleDocFragment(object):
+ DOCUMENTATION = r'''
+options: {}
+deprecated:
+ alternative: Use some other module
+ why: Test deprecation
+ removed_in: '3.0.0'
+'''
diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/plugins/doc_fragments/module.py b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/plugins/doc_fragments/module.py
new file mode 100644
index 0000000..a572363
--- /dev/null
+++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/plugins/doc_fragments/module.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+class ModuleDocFragment(object):
+ DOCUMENTATION = r'''
+options:
+ testcol2option:
+ description:
+ - An option taken from testcol2
+ type: str
+ version_added: 1.0.0
+ testcol2option2:
+ description:
+ - Another option taken from testcol2
+ type: str
+'''
diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/plugins/doc_fragments/plugin.py b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/plugins/doc_fragments/plugin.py
new file mode 100644
index 0000000..2fe4e4a
--- /dev/null
+++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/plugins/doc_fragments/plugin.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+class ModuleDocFragment(object):
+ DOCUMENTATION = r'''
+options:
+ testcol2option:
+ description:
+ - A plugin option taken from testcol2
+ type: str
+ version_added: 1.0.0
+ ini:
+ - key: foo
+ section: bar
+ version_added: 1.1.0
+ deprecated:
+ alternative: none
+ why: Test deprecation
+ version: '3.0.0'
+ env:
+ - name: FOO_BAR
+ version_added: 1.2.0
+ deprecated:
+ alternative: none
+ why: Test deprecation
+ removed_at_date: 2020-01-31
+ vars:
+ - name: foobar
+ version_added: 1.3.0
+ deprecated:
+ alternative: none
+ why: Test deprecation
+ removed_at_date: 2040-12-31
+ testcol2depr:
+ description:
+ - A plugin option taken from testcol2 that is deprecated
+ type: str
+ deprecated:
+ alternative: none
+ why: Test option deprecation
+ version: '2.0.0'
+'''
diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/plugins/doc_fragments/version_added.py b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/plugins/doc_fragments/version_added.py
new file mode 100644
index 0000000..73e5f2f
--- /dev/null
+++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/plugins/doc_fragments/version_added.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+class ModuleDocFragment(object):
+ DOCUMENTATION = r'''
+options: {}
+version_added: 1.0.0
+'''
diff --git a/test/integration/targets/ansible-doc/fakecollrole.output b/test/integration/targets/ansible-doc/fakecollrole.output
new file mode 100644
index 0000000..3ae9077
--- /dev/null
+++ b/test/integration/targets/ansible-doc/fakecollrole.output
@@ -0,0 +1,15 @@
+> TESTNS.TESTCOL.TESTROLE (/ansible/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol)
+
+ENTRY POINT: alternate - testns.testcol.testrole short description for alternate entry point
+
+ Longer description for testns.testcol.testrole alternate entry
+ point.
+
+OPTIONS (= is mandatory):
+
+= altopt1
+ altopt1 description
+ type: int
+
+
+AUTHOR: Ansible Core (@ansible)
diff --git a/test/integration/targets/ansible-doc/fakemodule.output b/test/integration/targets/ansible-doc/fakemodule.output
new file mode 100644
index 0000000..4fb0776
--- /dev/null
+++ b/test/integration/targets/ansible-doc/fakemodule.output
@@ -0,0 +1,16 @@
+> TESTNS.TESTCOL.FAKEMODULE (./collections/ansible_collections/testns/testcol/plugins/modules/fakemodule.py)
+
+ this is a fake module
+
+ADDED IN: version 1.0.0 of testns.testcol
+
+OPTIONS (= is mandatory):
+
+- _notreal
+ really not a real option
+ default: null
+
+
+AUTHOR: me
+
+SHORT_DESCIPTION: fake module
diff --git a/test/integration/targets/ansible-doc/fakerole.output b/test/integration/targets/ansible-doc/fakerole.output
new file mode 100644
index 0000000..bcb53dc
--- /dev/null
+++ b/test/integration/targets/ansible-doc/fakerole.output
@@ -0,0 +1,32 @@
+> TEST_ROLE1 (/ansible/test/integration/targets/ansible-doc/roles/normal_role1)
+
+ENTRY POINT: main - test_role1 from roles subdir
+
+ In to am attended desirous raptures *declared* diverted
+ confined at. Collected instantly remaining up certainly to
+ `necessary' as. Over walk dull into son boy door went new. At
+ or happiness commanded daughters as. Is `handsome' an declared
+ at received in extended vicinity subjects. Into miss on he
+ over been late pain an. Only week bore boy what fat case left
+ use. Match round scale now style far times. Your me past an
+ much.
+
+OPTIONS (= is mandatory):
+
+= myopt1
+ First option.
+ type: str
+
+- myopt2
+ Second option
+ default: 8000
+ type: int
+
+- myopt3
+ Third option.
+ choices: [choice1, choice2]
+ default: null
+ type: str
+
+
+AUTHOR: John Doe (@john), Jane Doe (@jane)
diff --git a/test/integration/targets/ansible-doc/filter_plugins/donothing.yml b/test/integration/targets/ansible-doc/filter_plugins/donothing.yml
new file mode 100644
index 0000000..87fe2f9
--- /dev/null
+++ b/test/integration/targets/ansible-doc/filter_plugins/donothing.yml
@@ -0,0 +1,21 @@
+DOCUMENTATION:
+ name: donothing
+ author: lazy
+ version_added: 'histerical'
+ short_description: noop
+ description:
+ - don't do anything
+ options:
+ _input:
+ description: Anything you want to get back
+ type: raw
+ required: true
+
+EXAMPLES: |
+ # set first 10 volumes rw, rest as dp
+ meaning: "{{ (stuff|donothing}}"
+
+RETURN:
+ _value:
+ description: guess
+ type: raw
diff --git a/test/integration/targets/ansible-doc/filter_plugins/other.py b/test/integration/targets/ansible-doc/filter_plugins/other.py
new file mode 100644
index 0000000..1bc2e17
--- /dev/null
+++ b/test/integration/targets/ansible-doc/filter_plugins/other.py
@@ -0,0 +1,25 @@
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.utils.display import Display
+
+display = Display()
+
+
+def donothing(a):
+ return a
+
+
+class FilterModule(object):
+ ''' Ansible core jinja2 filters '''
+
+ def filters(self):
+ return {
+ 'donothing': donothing,
+ 'nodocs': donothing,
+ 'split': donothing,
+ 'b64decode': donothing,
+ }
diff --git a/test/integration/targets/ansible-doc/filter_plugins/split.yml b/test/integration/targets/ansible-doc/filter_plugins/split.yml
new file mode 100644
index 0000000..87fe2f9
--- /dev/null
+++ b/test/integration/targets/ansible-doc/filter_plugins/split.yml
@@ -0,0 +1,21 @@
+DOCUMENTATION:
+ name: donothing
+ author: lazy
+ version_added: 'histerical'
+ short_description: noop
+ description:
+ - don't do anything
+ options:
+ _input:
+ description: Anything you want to get back
+ type: raw
+ required: true
+
+EXAMPLES: |
+ # set first 10 volumes rw, rest as dp
+ meaning: "{{ (stuff|donothing}}"
+
+RETURN:
+ _value:
+ description: guess
+ type: raw
diff --git a/test/integration/targets/ansible-doc/inventory b/test/integration/targets/ansible-doc/inventory
new file mode 100644
index 0000000..ab9b62c
--- /dev/null
+++ b/test/integration/targets/ansible-doc/inventory
@@ -0,0 +1 @@
+not_empty # avoid empty empty hosts list warning without defining explicit localhost
diff --git a/test/integration/targets/ansible-doc/library/double_doc.py b/test/integration/targets/ansible-doc/library/double_doc.py
new file mode 100644
index 0000000..6f0412a
--- /dev/null
+++ b/test/integration/targets/ansible-doc/library/double_doc.py
@@ -0,0 +1,19 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+name: double_doc
+description:
+ - module also uses 'DOCUMENTATION' in class
+'''
+
+
+class Foo:
+
+ # 2nd ref to documentation string, used to trip up tokinzer doc reader
+ DOCUMENTATION = None
+
+ def __init__(self):
+ pass
diff --git a/test/integration/targets/ansible-doc/library/test_docs.py b/test/integration/targets/ansible-doc/library/test_docs.py
new file mode 100644
index 0000000..39ae372
--- /dev/null
+++ b/test/integration/targets/ansible-doc/library/test_docs.py
@@ -0,0 +1,39 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['stableinterface'],
+ 'supported_by': 'core'}
+
+DOCUMENTATION = '''
+---
+module: test_docs
+short_description: Test module
+description:
+ - Test module
+author:
+ - Ansible Core Team
+'''
+
+EXAMPLES = '''
+'''
+
+RETURN = '''
+'''
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(),
+ )
+
+ module.exit_json()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-doc/library/test_docs_missing_description.py b/test/integration/targets/ansible-doc/library/test_docs_missing_description.py
new file mode 100644
index 0000000..6ed4183
--- /dev/null
+++ b/test/integration/targets/ansible-doc/library/test_docs_missing_description.py
@@ -0,0 +1,40 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: test_docs_returns
+short_description: Test module
+description:
+ - Test module
+author:
+ - Ansible Core Team
+options:
+ test:
+ type: str
+'''
+
+EXAMPLES = '''
+'''
+
+RETURN = '''
+'''
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ test=dict(type='str'),
+ ),
+ )
+
+ module.exit_json()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-doc/library/test_docs_no_metadata.py b/test/integration/targets/ansible-doc/library/test_docs_no_metadata.py
new file mode 100644
index 0000000..4ea86f0
--- /dev/null
+++ b/test/integration/targets/ansible-doc/library/test_docs_no_metadata.py
@@ -0,0 +1,35 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: test_docs_no_metadata
+short_description: Test module
+description:
+ - Test module
+author:
+ - Ansible Core Team
+'''
+
+EXAMPLES = '''
+'''
+
+RETURN = '''
+'''
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(),
+ )
+
+ module.exit_json()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-doc/library/test_docs_no_status.py b/test/integration/targets/ansible-doc/library/test_docs_no_status.py
new file mode 100644
index 0000000..1b0db4e
--- /dev/null
+++ b/test/integration/targets/ansible-doc/library/test_docs_no_status.py
@@ -0,0 +1,38 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'supported_by': 'core'}
+
+DOCUMENTATION = '''
+---
+module: test_docs_no_status
+short_description: Test module
+description:
+ - Test module
+author:
+ - Ansible Core Team
+'''
+
+EXAMPLES = '''
+'''
+
+RETURN = '''
+'''
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(),
+ )
+
+ module.exit_json()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-doc/library/test_docs_non_iterable_status.py b/test/integration/targets/ansible-doc/library/test_docs_non_iterable_status.py
new file mode 100644
index 0000000..63d080f
--- /dev/null
+++ b/test/integration/targets/ansible-doc/library/test_docs_non_iterable_status.py
@@ -0,0 +1,39 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': 1,
+ 'supported_by': 'core'}
+
+DOCUMENTATION = '''
+---
+module: test_docs_non_iterable_status
+short_description: Test module
+description:
+ - Test module
+author:
+ - Ansible Core Team
+'''
+
+EXAMPLES = '''
+'''
+
+RETURN = '''
+'''
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(),
+ )
+
+ module.exit_json()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-doc/library/test_docs_removed_precedence.py b/test/integration/targets/ansible-doc/library/test_docs_removed_precedence.py
new file mode 100644
index 0000000..3de1c69
--- /dev/null
+++ b/test/integration/targets/ansible-doc/library/test_docs_removed_precedence.py
@@ -0,0 +1,40 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: test_docs_removed_precedence
+short_description: Test module
+description:
+ - Test module
+author:
+ - Ansible Core Team
+deprecated:
+ alternative: new_module
+ why: Updated module released with more functionality
+ removed_at_date: '2022-06-01'
+ removed_in: '2.14'
+'''
+
+EXAMPLES = '''
+'''
+
+RETURN = '''
+'''
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(),
+ )
+
+ module.exit_json()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-doc/library/test_docs_removed_status.py b/test/integration/targets/ansible-doc/library/test_docs_removed_status.py
new file mode 100644
index 0000000..cb48c16
--- /dev/null
+++ b/test/integration/targets/ansible-doc/library/test_docs_removed_status.py
@@ -0,0 +1,39 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['removed'],
+ 'supported_by': 'core'}
+
+DOCUMENTATION = '''
+---
+module: test_docs_removed_status
+short_description: Test module
+description:
+ - Test module
+author:
+ - Ansible Core Team
+'''
+
+EXAMPLES = '''
+'''
+
+RETURN = '''
+'''
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(),
+ )
+
+ module.exit_json()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-doc/library/test_docs_returns.py b/test/integration/targets/ansible-doc/library/test_docs_returns.py
new file mode 100644
index 0000000..77c1376
--- /dev/null
+++ b/test/integration/targets/ansible-doc/library/test_docs_returns.py
@@ -0,0 +1,56 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: test_docs_returns
+short_description: Test module
+description:
+ - Test module
+author:
+ - Ansible Core Team
+'''
+
+EXAMPLES = '''
+'''
+
+RETURN = '''
+z_last:
+ description: A last result.
+ type: str
+ returned: success
+
+m_middle:
+ description:
+ - This should be in the middle.
+ - Has some more data
+ type: dict
+ returned: success and 1st of month
+ contains:
+ suboption:
+ description: A suboption.
+ type: str
+ choices: [ARF, BARN, c_without_capital_first_letter]
+
+a_first:
+ description: A first result.
+ type: str
+ returned: success
+'''
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(),
+ )
+
+ module.exit_json()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-doc/library/test_docs_returns_broken.py b/test/integration/targets/ansible-doc/library/test_docs_returns_broken.py
new file mode 100644
index 0000000..d6d6264
--- /dev/null
+++ b/test/integration/targets/ansible-doc/library/test_docs_returns_broken.py
@@ -0,0 +1,40 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: test_docs_returns_broken
+short_description: Test module
+description:
+ - Test module
+author:
+ - Ansible Core Team
+'''
+
+EXAMPLES = '''
+'''
+
+RETURN = '''
+test:
+ description: A test return value.
+ type: str
+
+broken_key: [
+'''
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(),
+ )
+
+ module.exit_json()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-doc/library/test_docs_suboptions.py b/test/integration/targets/ansible-doc/library/test_docs_suboptions.py
new file mode 100644
index 0000000..c922d1d
--- /dev/null
+++ b/test/integration/targets/ansible-doc/library/test_docs_suboptions.py
@@ -0,0 +1,70 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: test_docs_suboptions
+short_description: Test module
+description:
+ - Test module
+author:
+ - Ansible Core Team
+options:
+ with_suboptions:
+ description:
+ - An option with suboptions.
+ - Use with care.
+ type: dict
+ suboptions:
+ z_last:
+ description: The last suboption.
+ type: str
+ m_middle:
+ description:
+ - The suboption in the middle.
+ - Has its own suboptions.
+ suboptions:
+ a_suboption:
+ description: A sub-suboption.
+ type: str
+ a_first:
+ description: The first suboption.
+ type: str
+'''
+
+EXAMPLES = '''
+'''
+
+RETURN = '''
+'''
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ test_docs_suboptions=dict(
+ type='dict',
+ options=dict(
+ a_first=dict(type='str'),
+ m_middle=dict(
+ type='dict',
+ options=dict(
+ a_suboption=dict(type='str')
+ ),
+ ),
+ z_last=dict(type='str'),
+ ),
+ ),
+ ),
+ )
+
+ module.exit_json()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-doc/library/test_docs_yaml_anchors.py b/test/integration/targets/ansible-doc/library/test_docs_yaml_anchors.py
new file mode 100644
index 0000000..bec0292
--- /dev/null
+++ b/test/integration/targets/ansible-doc/library/test_docs_yaml_anchors.py
@@ -0,0 +1,71 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: test_docs_yaml_anchors
+short_description: Test module with YAML anchors in docs
+description:
+ - Test module
+author:
+ - Ansible Core Team
+options:
+ at_the_top: &toplevel_anchor
+ description:
+ - Short desc
+ default: some string
+ type: str
+
+ last_one: *toplevel_anchor
+
+ egress:
+ description:
+ - Egress firewall rules
+ type: list
+ elements: dict
+ suboptions: &sub_anchor
+ port:
+ description:
+ - Rule port
+ type: int
+ required: true
+
+ ingress:
+ description:
+ - Ingress firewall rules
+ type: list
+ elements: dict
+ suboptions: *sub_anchor
+'''
+
+EXAMPLES = '''
+'''
+
+RETURN = '''
+'''
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ at_the_top=dict(type='str', default='some string'),
+ last_one=dict(type='str', default='some string'),
+ egress=dict(type='list', elements='dict', options=dict(
+ port=dict(type='int', required=True),
+ )),
+ ingress=dict(type='list', elements='dict', options=dict(
+ port=dict(type='int', required=True),
+ )),
+ ),
+ )
+
+ module.exit_json()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-doc/library/test_empty.py b/test/integration/targets/ansible-doc/library/test_empty.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/ansible-doc/library/test_empty.py
diff --git a/test/integration/targets/ansible-doc/library/test_no_docs.py b/test/integration/targets/ansible-doc/library/test_no_docs.py
new file mode 100644
index 0000000..5503aed
--- /dev/null
+++ b/test/integration/targets/ansible-doc/library/test_no_docs.py
@@ -0,0 +1,23 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['stableinterface'],
+ 'supported_by': 'core'}
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(),
+ )
+
+ module.exit_json()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-doc/library/test_no_docs_no_metadata.py b/test/integration/targets/ansible-doc/library/test_no_docs_no_metadata.py
new file mode 100644
index 0000000..4887268
--- /dev/null
+++ b/test/integration/targets/ansible-doc/library/test_no_docs_no_metadata.py
@@ -0,0 +1,18 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(),
+ )
+
+ module.exit_json()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-doc/library/test_no_docs_no_status.py b/test/integration/targets/ansible-doc/library/test_no_docs_no_status.py
new file mode 100644
index 0000000..f90c5c7
--- /dev/null
+++ b/test/integration/targets/ansible-doc/library/test_no_docs_no_status.py
@@ -0,0 +1,22 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'supported_by': 'core'}
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(),
+ )
+
+ module.exit_json()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-doc/library/test_no_docs_non_iterable_status.py b/test/integration/targets/ansible-doc/library/test_no_docs_non_iterable_status.py
new file mode 100644
index 0000000..44fbede
--- /dev/null
+++ b/test/integration/targets/ansible-doc/library/test_no_docs_non_iterable_status.py
@@ -0,0 +1,23 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': 1,
+ 'supported_by': 'core'}
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(),
+ )
+
+ module.exit_json()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-doc/library/test_win_module.ps1 b/test/integration/targets/ansible-doc/library/test_win_module.ps1
new file mode 100644
index 0000000..5653c8b
--- /dev/null
+++ b/test/integration/targets/ansible-doc/library/test_win_module.ps1
@@ -0,0 +1,21 @@
+#!powershell
+# Copyright: (c) Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$spec = @{
+ options = @{
+ hello = @{ type = 'str'; required = $true }
+ }
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$hello = $module.Params.hello
+
+$module.Result.msg = $hello
+$module.Result.changed = $false
+
+$module.ExitJson()
diff --git a/test/integration/targets/ansible-doc/library/test_win_module.yml b/test/integration/targets/ansible-doc/library/test_win_module.yml
new file mode 100644
index 0000000..0547c70
--- /dev/null
+++ b/test/integration/targets/ansible-doc/library/test_win_module.yml
@@ -0,0 +1,9 @@
+DOCUMENTATION:
+ module: test_win_module
+ short_description: Test win module
+ description:
+ - Test win module with sidecar docs
+ author:
+ - Ansible Core Team
+EXAMPLES: ''
+RETURN: ''
diff --git a/test/integration/targets/ansible-doc/lookup_plugins/_deprecated_with_adj_docs.py b/test/integration/targets/ansible-doc/lookup_plugins/_deprecated_with_adj_docs.py
new file mode 100644
index 0000000..81d401d
--- /dev/null
+++ b/test/integration/targets/ansible-doc/lookup_plugins/_deprecated_with_adj_docs.py
@@ -0,0 +1,5 @@
+# Copyright (c) 2022 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
diff --git a/test/integration/targets/ansible-doc/lookup_plugins/_deprecated_with_docs.py b/test/integration/targets/ansible-doc/lookup_plugins/_deprecated_with_docs.py
new file mode 100644
index 0000000..4fd63aa
--- /dev/null
+++ b/test/integration/targets/ansible-doc/lookup_plugins/_deprecated_with_docs.py
@@ -0,0 +1,26 @@
+# Copyright (c) 2022 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+ name: deprecated_with_docs
+ short_description: test lookup
+ description: test lookup
+ author: Ansible Core Team
+ version_added: "2.14"
+ deprecated:
+ why: reasons
+ alternative: other thing
+ removed_in: "2.16"
+ removed_from_collection: "ansible.legacy"
+ options: {}
+'''
+
+EXAMPLE = '''
+'''
+
+RETURN = '''
+'''
diff --git a/test/integration/targets/ansible-doc/lookup_plugins/deprecated_with_adj_docs.yml b/test/integration/targets/ansible-doc/lookup_plugins/deprecated_with_adj_docs.yml
new file mode 100644
index 0000000..6349c39
--- /dev/null
+++ b/test/integration/targets/ansible-doc/lookup_plugins/deprecated_with_adj_docs.yml
@@ -0,0 +1,16 @@
+DOCUMENTATION:
+ name: deprecated_with_adj_docs
+ short_description: test lookup
+ description: test lookup
+ author: Ansible Core Team
+ version_added: "2.14"
+ deprecated:
+ why: reasons
+ alternative: use other thing
+ removed_in: "2.16"
+ removed_from_collection: "ansible.legacy"
+ options: {}
+
+EXAMPLE: ""
+
+RETURN: {}
diff --git a/test/integration/targets/ansible-doc/noop.output b/test/integration/targets/ansible-doc/noop.output
new file mode 100644
index 0000000..567150a
--- /dev/null
+++ b/test/integration/targets/ansible-doc/noop.output
@@ -0,0 +1,32 @@
+{
+ "testns.testcol.noop": {
+ "doc": {
+ "author": "Ansible core team",
+ "collection": "testns.testcol",
+ "deprecated": {
+ "alternative": "Use some other lookup",
+ "removed_from_collection": "testns.testcol",
+ "removed_in": "3.0.0",
+ "why": "Test deprecation"
+ },
+ "description": [
+ "this is a noop"
+ ],
+ "filename": "./collections/ansible_collections/testns/testcol/plugins/lookup/noop.py",
+ "lookup": "noop",
+ "options": {},
+ "short_description": "returns input",
+ "version_added": "1.0.0",
+ "version_added_collection": "testns.testcol2"
+ },
+ "examples": "\n- name: do nothing\n debug: msg=\"{{ lookup('testns.testcol.noop', [1,2,3,4] }}\"\n",
+ "metadata": null,
+ "return": {
+ "_list": {
+ "description": "input given",
+ "version_added": "1.0.0",
+ "version_added_collection": "testns.testcol"
+ }
+ }
+ }
+}
diff --git a/test/integration/targets/ansible-doc/noop_vars_plugin.output b/test/integration/targets/ansible-doc/noop_vars_plugin.output
new file mode 100644
index 0000000..5c42af3
--- /dev/null
+++ b/test/integration/targets/ansible-doc/noop_vars_plugin.output
@@ -0,0 +1,42 @@
+{
+ "testns.testcol.noop_vars_plugin": {
+ "doc": {
+ "collection": "testns.testcol",
+ "deprecated": {
+ "alternative": "Use some other module",
+ "removed_from_collection": "testns.testcol2",
+ "removed_in": "3.0.0",
+ "why": "Test deprecation"
+ },
+ "description": "don't test loading host and group vars from a collection",
+ "filename": "./collections/ansible_collections/testns/testcol/plugins/vars/noop_vars_plugin.py",
+ "options": {
+ "stage": {
+ "choices": [
+ "all",
+ "inventory",
+ "task"
+ ],
+ "default": "all",
+ "env": [
+ {
+ "name": "ANSIBLE_VARS_PLUGIN_STAGE"
+ }
+ ],
+ "ini": [
+ {
+ "key": "stage",
+ "section": "testns.testcol.noop_vars_plugin"
+ }
+ ],
+ "type": "str"
+ }
+ },
+ "short_description": "Do NOT load host and group vars",
+ "vars": "noop_vars_plugin"
+ },
+ "examples": null,
+ "metadata": null,
+ "return": null
+ }
+}
diff --git a/test/integration/targets/ansible-doc/notjsonfile.output b/test/integration/targets/ansible-doc/notjsonfile.output
new file mode 100644
index 0000000..a73b1a9
--- /dev/null
+++ b/test/integration/targets/ansible-doc/notjsonfile.output
@@ -0,0 +1,157 @@
+{
+ "testns.testcol.notjsonfile": {
+ "doc": {
+ "author": "Ansible Core (@ansible-core)",
+ "cache": "notjsonfile",
+ "collection": "testns.testcol",
+ "description": [
+ "This cache uses JSON formatted, per host, files saved to the filesystem."
+ ],
+ "filename": "./collections/ansible_collections/testns/testcol/plugins/cache/notjsonfile.py",
+ "options": {
+ "_prefix": {
+ "deprecated": {
+ "alternative": "none",
+ "collection_name": "testns.testcol",
+ "removed_at_date": "2050-01-01",
+ "why": "Another test deprecation"
+ },
+ "description": "User defined prefix to use when creating the JSON files",
+ "env": [
+ {
+ "name": "ANSIBLE_CACHE_PLUGIN_PREFIX",
+ "version_added": "1.1.0",
+ "version_added_collection": "testns.testcol"
+ }
+ ],
+ "ini": [
+ {
+ "key": "fact_caching_prefix",
+ "section": "defaults"
+ }
+ ]
+ },
+ "_timeout": {
+ "default": 86400,
+ "description": "Expiration timeout for the cache plugin data",
+ "env": [
+ {
+ "name": "ANSIBLE_CACHE_PLUGIN_TIMEOUT"
+ }
+ ],
+ "ini": [
+ {
+ "key": "fact_caching_timeout",
+ "section": "defaults"
+ }
+ ],
+ "type": "integer",
+ "vars": [
+ {
+ "deprecated": {
+ "alternative": "do not use a variable",
+ "collection_name": "testns.testcol",
+ "version": "3.0.0",
+ "why": "Test deprecation"
+ },
+ "name": "notsjonfile_fact_caching_timeout",
+ "version_added": "1.5.0",
+ "version_added_collection": "testns.testcol"
+ }
+ ]
+ },
+ "_uri": {
+ "description": [
+ "Path in which the cache plugin will save the JSON files"
+ ],
+ "env": [
+ {
+ "name": "ANSIBLE_CACHE_PLUGIN_CONNECTION",
+ "version_added": "1.2.0",
+ "version_added_collection": "testns.testcol"
+ }
+ ],
+ "ini": [
+ {
+ "deprecated": {
+ "alternative": "none",
+ "collection_name": "testns.testcol",
+ "version": "2.0.0",
+ "why": "Test deprecation"
+ },
+ "key": "fact_caching_connection",
+ "section": "defaults"
+ }
+ ],
+ "required": true
+ },
+ "testcol2depr": {
+ "deprecated": {
+ "alternative": "none",
+ "collection_name": "testns.testcol2",
+ "version": "2.0.0",
+ "why": "Test option deprecation"
+ },
+ "description": [
+ "A plugin option taken from testcol2 that is deprecated"
+ ],
+ "type": "str"
+ },
+ "testcol2option": {
+ "description": [
+ "A plugin option taken from testcol2"
+ ],
+ "env": [
+ {
+ "deprecated": {
+ "alternative": "none",
+ "collection_name": "testns.testcol2",
+ "removed_at_date": "2020-01-31",
+ "why": "Test deprecation"
+ },
+ "name": "FOO_BAR",
+ "version_added": "1.2.0",
+ "version_added_collection": "testns.testcol2"
+ }
+ ],
+ "ini": [
+ {
+ "deprecated": {
+ "alternative": "none",
+ "collection_name": "testns.testcol2",
+ "version": "3.0.0",
+ "why": "Test deprecation"
+ },
+ "key": "foo",
+ "section": "bar",
+ "version_added": "1.1.0",
+ "version_added_collection": "testns.testcol2"
+ }
+ ],
+ "type": "str",
+ "vars": [
+ {
+ "deprecated": {
+ "alternative": "none",
+ "collection_name": "testns.testcol2",
+ "removed_at_date": "2040-12-31",
+ "why": "Test deprecation"
+ },
+ "name": "foobar",
+ "version_added": "1.3.0",
+ "version_added_collection": "testns.testcol2"
+ }
+ ],
+ "version_added": "1.0.0",
+ "version_added_collection": "testns.testcol2"
+ }
+ },
+ "short_description": "JSON formatted files.",
+ "version_added": "0.7.0",
+ "version_added_collection": "testns.testcol"
+ },
+ "examples": null,
+ "metadata": null,
+ "return": null
+ }
+}
diff --git a/test/integration/targets/ansible-doc/randommodule-text.output b/test/integration/targets/ansible-doc/randommodule-text.output
new file mode 100644
index 0000000..51d7930
--- /dev/null
+++ b/test/integration/targets/ansible-doc/randommodule-text.output
@@ -0,0 +1,101 @@
+> TESTNS.TESTCOL.RANDOMMODULE (./collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py)
+
+ A random module.
+
+ADDED IN: version 1.0.0 of testns.testcol
+
+DEPRECATED:
+
+ Reason: Test deprecation
+ Will be removed in: Ansible 3.0.0
+ Alternatives: Use some other module
+
+
+OPTIONS (= is mandatory):
+
+- sub
+ Suboptions.
+ set_via:
+ env:
+ - deprecated:
+ alternative: none
+ removed_in: 2.0.0
+ version: 2.0.0
+ why: Test deprecation
+ name: TEST_ENV
+ default: null
+ type: dict
+
+ OPTIONS:
+
+ - subtest2
+ Another suboption.
+ default: null
+ type: float
+ added in: version 1.1.0
+
+
+
+ SUBOPTIONS:
+
+ - subtest
+ A suboption.
+ default: null
+ type: int
+ added in: version 1.1.0 of testns.testcol
+
+
+- test
+ Some text.
+ default: null
+ type: str
+ added in: version 1.2.0 of testns.testcol
+
+
+- testcol2option
+ An option taken from testcol2
+ default: null
+ type: str
+ added in: version 1.0.0 of testns.testcol2
+
+
+- testcol2option2
+ Another option taken from testcol2
+ default: null
+ type: str
+
+
+AUTHOR: Ansible Core Team
+
+EXAMPLES:
+
+
+
+
+RETURN VALUES:
+- a_first
+ A first result.
+ returned: success
+ type: str
+
+- m_middle
+ This should be in the middle.
+ Has some more data
+ returned: success and 1st of month
+ type: dict
+
+ CONTAINS:
+
+ - suboption
+ A suboption.
+ choices: [ARF, BARN, c_without_capital_first_letter]
+ type: str
+ added in: version 1.4.0 of testns.testcol
+
+
+- z_last
+ A last result.
+ returned: success
+ type: str
+ added in: version 1.3.0 of testns.testcol
+
diff --git a/test/integration/targets/ansible-doc/randommodule.output b/test/integration/targets/ansible-doc/randommodule.output
new file mode 100644
index 0000000..25f46c3
--- /dev/null
+++ b/test/integration/targets/ansible-doc/randommodule.output
@@ -0,0 +1,115 @@
+{
+ "testns.testcol.randommodule": {
+ "doc": {
+ "author": [
+ "Ansible Core Team"
+ ],
+ "collection": "testns.testcol",
+ "deprecated": {
+ "alternative": "Use some other module",
+ "removed_from_collection": "testns.testcol",
+ "removed_in": "3.0.0",
+ "why": "Test deprecation"
+ },
+ "description": [
+ "A random module."
+ ],
+ "filename": "./collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py",
+ "has_action": false,
+ "module": "randommodule",
+ "options": {
+ "sub": {
+ "description": "Suboptions.",
+ "env": [
+ {
+ "deprecated": {
+ "alternative": "none",
+ "removed_in": "2.0.0",
+ "version": "2.0.0",
+ "why": "Test deprecation"
+ },
+ "name": "TEST_ENV",
+ "version_added": "1.0.0"
+ }
+ ],
+ "options": {
+ "subtest2": {
+ "description": "Another suboption.",
+ "type": "float",
+ "version_added": "1.1.0"
+ }
+ },
+ "suboptions": {
+ "subtest": {
+ "description": "A suboption.",
+ "type": "int",
+ "version_added": "1.1.0",
+ "version_added_collection": "testns.testcol"
+ }
+ },
+ "type": "dict"
+ },
+ "test": {
+ "description": "Some text.",
+ "type": "str",
+ "version_added": "1.2.0",
+ "version_added_collection": "testns.testcol"
+ },
+ "testcol2option": {
+ "description": [
+ "An option taken from testcol2"
+ ],
+ "type": "str",
+ "version_added": "1.0.0",
+ "version_added_collection": "testns.testcol2"
+ },
+ "testcol2option2": {
+ "description": [
+ "Another option taken from testcol2"
+ ],
+ "type": "str"
+ }
+ },
+ "short_description": "A random module",
+ "version_added": "1.0.0",
+ "version_added_collection": "testns.testcol"
+ },
+ "examples": "\n",
+ "metadata": null,
+ "return": {
+ "a_first": {
+ "description": "A first result.",
+ "returned": "success",
+ "type": "str"
+ },
+ "m_middle": {
+ "contains": {
+ "suboption": {
+ "choices": [
+ "ARF",
+ "BARN",
+ "c_without_capital_first_letter"
+ ],
+ "description": "A suboption.",
+ "type": "str",
+ "version_added": "1.4.0",
+ "version_added_collection": "testns.testcol"
+ }
+ },
+ "description": [
+ "This should be in the middle.",
+ "Has some more data"
+ ],
+ "returned": "success and 1st of month",
+ "type": "dict"
+ },
+ "z_last": {
+ "description": "A last result.",
+ "returned": "success",
+ "type": "str",
+ "version_added": "1.3.0",
+ "version_added_collection": "testns.testcol"
+ }
+ }
+ }
+}
diff --git a/test/integration/targets/ansible-doc/roles/test_role1/meta/argument_specs.yml b/test/integration/targets/ansible-doc/roles/test_role1/meta/argument_specs.yml
new file mode 100644
index 0000000..0315a1f
--- /dev/null
+++ b/test/integration/targets/ansible-doc/roles/test_role1/meta/argument_specs.yml
@@ -0,0 +1,34 @@
+---
+argument_specs:
+ main:
+ short_description: test_role1 from roles subdir
+ description:
+ - In to am attended desirous raptures B(declared) diverted confined at. Collected instantly remaining
+ up certainly to C(necessary) as. Over walk dull into son boy door went new.
+ - At or happiness commanded daughters as. Is I(handsome) an declared at received in extended vicinity
+ subjects. Into miss on he over been late pain an. Only week bore boy what fat case left use. Match round
+ scale now style far times. Your me past an much.
+ author:
+ - John Doe (@john)
+ - Jane Doe (@jane)
+
+ options:
+ myopt1:
+ description:
+ - First option.
+ type: "str"
+ required: true
+
+ myopt2:
+ description:
+ - Second option
+ type: "int"
+ default: 8000
+
+ myopt3:
+ description:
+ - Third option.
+ type: "str"
+ choices:
+ - choice1
+ - choice2
diff --git a/test/integration/targets/ansible-doc/roles/test_role1/meta/main.yml b/test/integration/targets/ansible-doc/roles/test_role1/meta/main.yml
new file mode 100644
index 0000000..19f6162
--- /dev/null
+++ b/test/integration/targets/ansible-doc/roles/test_role1/meta/main.yml
@@ -0,0 +1,13 @@
+# This meta/main.yml exists to test that it is NOT read, with preference being
+# given to the meta/argument_specs.yml file. This spec contains additional
+# entry points (groot, foo) that the argument_specs.yml does not. If this file
+# were read, the additional entrypoints would show up in --list output, breaking
+# tests.
+---
+argument_specs:
+ main:
+ short_description: test_role1 from roles subdir
+ groot:
+ short_description: I am Groot
+ foo:
+ short_description: I am Foo
diff --git a/test/integration/targets/ansible-doc/roles/test_role2/meta/empty b/test/integration/targets/ansible-doc/roles/test_role2/meta/empty
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/ansible-doc/roles/test_role2/meta/empty
diff --git a/test/integration/targets/ansible-doc/roles/test_role3/meta/main.yml b/test/integration/targets/ansible-doc/roles/test_role3/meta/main.yml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/ansible-doc/roles/test_role3/meta/main.yml
diff --git a/test/integration/targets/ansible-doc/runme.sh b/test/integration/targets/ansible-doc/runme.sh
new file mode 100755
index 0000000..887d3c4
--- /dev/null
+++ b/test/integration/targets/ansible-doc/runme.sh
@@ -0,0 +1,214 @@
+#!/usr/bin/env bash
+
+set -eux
+ansible-playbook test.yml -i inventory "$@"
+
+# test keyword docs
+ansible-doc -t keyword -l | grep 'vars_prompt: list of variables to prompt for.'
+ansible-doc -t keyword vars_prompt | grep 'description: list of variables to prompt for.'
+ansible-doc -t keyword asldkfjaslidfhals 2>&1 | grep 'Skipping Invalid keyword'
+
+# collections testing
+(
+unset ANSIBLE_PLAYBOOK_DIR
+cd "$(dirname "$0")"
+
+# test module docs from collection
+# we use sed to strip the module path from the first line
+current_out="$(ansible-doc --playbook-dir ./ testns.testcol.fakemodule | sed '1 s/\(^> TESTNS\.TESTCOL\.FAKEMODULE\).*(.*)$/\1/')"
+expected_out="$(sed '1 s/\(^> TESTNS\.TESTCOL\.FAKEMODULE\).*(.*)$/\1/' fakemodule.output)"
+test "$current_out" == "$expected_out"
+
+# we use sed to strip the module path from the first line
+current_out="$(ansible-doc --playbook-dir ./ testns.testcol.randommodule | sed '1 s/\(^> TESTNS\.TESTCOL\.RANDOMMODULE\).*(.*)$/\1/')"
+expected_out="$(sed '1 s/\(^> TESTNS\.TESTCOL\.RANDOMMODULE\).*(.*)$/\1/' randommodule-text.output)"
+test "$current_out" == "$expected_out"
+
+# ensure we do work with valid collection name for list
+ansible-doc --list testns.testcol --playbook-dir ./ 2>&1 | grep -v "Invalid collection name"
+
+# ensure we dont break on invalid collection name for list
+ansible-doc --list testns.testcol.fakemodule --playbook-dir ./ 2>&1 | grep "Invalid collection name"
+
+# test listing diff plugin types from collection
+for ptype in cache inventory lookup vars filter module
+do
+ # each plugin type adds 1 from collection
+ # FIXME pre=$(ansible-doc -l -t ${ptype}|wc -l)
+ # FIXME post=$(ansible-doc -l -t ${ptype} --playbook-dir ./|wc -l)
+ # FIXME test "$pre" -eq $((post - 1))
+ if [ "${ptype}" == "filter" ]; then
+ expected=5
+ expected_names=("b64decode" "filter_subdir.nested" "filter_subdir.noop" "noop" "ultimatequestion")
+ elif [ "${ptype}" == "module" ]; then
+ expected=4
+ expected_names=("fakemodule" "notrealmodule" "randommodule" "database.database_type.subdir_module")
+ else
+ expected=1
+ if [ "${ptype}" == "cache" ]; then expected_names=("notjsonfile");
+ elif [ "${ptype}" == "inventory" ]; then expected_names=("statichost");
+ elif [ "${ptype}" == "lookup" ]; then expected_names=("noop");
+ elif [ "${ptype}" == "vars" ]; then expected_names=("noop_vars_plugin"); fi
+ fi
+ # ensure we ONLY list from the collection
+ justcol=$(ansible-doc -l -t ${ptype} --playbook-dir ./ testns.testcol|wc -l)
+ test "$justcol" -eq "$expected"
+
+ # ensure the right names are displayed
+ list_result=$(ansible-doc -l -t ${ptype} --playbook-dir ./ testns.testcol)
+ metadata_result=$(ansible-doc --metadata-dump --no-fail-on-errors -t ${ptype} --playbook-dir ./ testns.testcol)
+ for name in "${expected_names[@]}"; do
+ echo "${list_result}" | grep "testns.testcol.${name}"
+ echo "${metadata_result}" | grep "testns.testcol.${name}"
+ done
+
+ # ensure we get error if passinginvalid collection, much less any plugins
+ ansible-doc -l -t ${ptype} testns.testcol 2>&1 | grep "unable to locate collection"
+
+ # TODO: do we want per namespace?
+ # ensure we get 1 plugins when restricting namespace
+ #justcol=$(ansible-doc -l -t ${ptype} --playbook-dir ./ testns|wc -l)
+ #test "$justcol" -eq 1
+done
+
+#### test role functionality
+
+# Test role text output
+# we use sed to strip the role path from the first line
+current_role_out="$(ansible-doc -t role -r ./roles test_role1 | sed '1 s/\(^> TEST_ROLE1\).*(.*)$/\1/')"
+expected_role_out="$(sed '1 s/\(^> TEST_ROLE1\).*(.*)$/\1/' fakerole.output)"
+test "$current_role_out" == "$expected_role_out"
+
+# Two collection roles are defined, but only 1 has a role arg spec with 2 entry points
+output=$(ansible-doc -t role -l --playbook-dir . testns.testcol | wc -l)
+test "$output" -eq 2
+
+# Include normal roles (no collection filter)
+output=$(ansible-doc -t role -l --playbook-dir . | wc -l)
+test "$output" -eq 3
+
+# Test that a role in the playbook dir with the same name as a role in the
+# 'roles' subdir of the playbook dir does not appear (lower precedence).
+output=$(ansible-doc -t role -l --playbook-dir . | grep -c "test_role1 from roles subdir")
+test "$output" -eq 1
+output=$(ansible-doc -t role -l --playbook-dir . | grep -c "test_role1 from playbook dir" || true)
+test "$output" -eq 0
+
+# Test entry point filter
+current_role_out="$(ansible-doc -t role --playbook-dir . testns.testcol.testrole -e alternate| sed '1 s/\(^> TESTNS\.TESTCOL\.TESTROLE\).*(.*)$/\1/')"
+expected_role_out="$(sed '1 s/\(^> TESTNS\.TESTCOL\.TESTROLE\).*(.*)$/\1/' fakecollrole.output)"
+test "$current_role_out" == "$expected_role_out"
+
+)
+
+#### test add_collection_to_versions_and_dates()
+
+current_out="$(ansible-doc --json --playbook-dir ./ testns.testcol.randommodule | sed 's/ *$//' | sed 's/ *"filename": "[^"]*",$//')"
+expected_out="$(sed 's/ *"filename": "[^"]*",$//' randommodule.output)"
+test "$current_out" == "$expected_out"
+
+current_out="$(ansible-doc --json --playbook-dir ./ -t cache testns.testcol.notjsonfile | sed 's/ *$//' | sed 's/ *"filename": "[^"]*",$//')"
+expected_out="$(sed 's/ *"filename": "[^"]*",$//' notjsonfile.output)"
+test "$current_out" == "$expected_out"
+
+current_out="$(ansible-doc --json --playbook-dir ./ -t lookup testns.testcol.noop | sed 's/ *$//' | sed 's/ *"filename": "[^"]*",$//')"
+expected_out="$(sed 's/ *"filename": "[^"]*",$//' noop.output)"
+test "$current_out" == "$expected_out"
+
+current_out="$(ansible-doc --json --playbook-dir ./ -t vars testns.testcol.noop_vars_plugin | sed 's/ *$//' | sed 's/ *"filename": "[^"]*",$//')"
+expected_out="$(sed 's/ *"filename": "[^"]*",$//' noop_vars_plugin.output)"
+test "$current_out" == "$expected_out"
+
+# just ensure it runs
+ANSIBLE_LIBRARY='./nolibrary' ansible-doc --metadata-dump --playbook-dir /dev/null >/dev/null
+
+# create broken role argument spec
+mkdir -p broken-docs/collections/ansible_collections/testns/testcol/roles/testrole/meta
+cat <<EOF > broken-docs/collections/ansible_collections/testns/testcol/roles/testrole/meta/main.yml
+---
+dependencies:
+galaxy_info:
+
+argument_specs:
+ main:
+ short_description: testns.testcol.testrole short description for main entry point
+ description:
+ - Longer description for testns.testcol.testrole main entry point.
+ author: Ansible Core (@ansible)
+ options:
+ opt1:
+ description: opt1 description
+ broken:
+ type: "str"
+ required: true
+EOF
+
+# ensure that --metadata-dump does not fail when --no-fail-on-errors is supplied
+ANSIBLE_LIBRARY='./nolibrary' ansible-doc --metadata-dump --no-fail-on-errors --playbook-dir broken-docs testns.testcol >/dev/null
+
+# ensure that --metadata-dump does fail when --no-fail-on-errors is not supplied
+output=$(ANSIBLE_LIBRARY='./nolibrary' ansible-doc --metadata-dump --playbook-dir broken-docs testns.testcol 2>&1 | grep -c 'ERROR!' || true)
+test "${output}" -eq 1
+
+# ensure we list the 'legacy plugins'
+[ "$(ansible-doc -M ./library -l ansible.legacy |wc -l)" -gt "0" ]
+
+# playbook dir should work the same
+[ "$(ansible-doc -l ansible.legacy --playbook-dir ./|wc -l)" -gt "0" ]
+
+# see that we show undocumented when missing docs
+[ "$(ansible-doc -M ./library -l ansible.legacy |grep -c UNDOCUMENTED)" == "6" ]
+
+# ensure filtering works and does not include any 'test_' modules
+[ "$(ansible-doc -M ./library -l ansible.builtin |grep -c test_)" == 0 ]
+[ "$(ansible-doc --playbook-dir ./ -l ansible.builtin |grep -c test_)" == 0 ]
+
+# ensure filtering still shows modules
+count=$(ANSIBLE_LIBRARY='./nolibrary' ansible-doc -l ansible.builtin |wc -l)
+[ "${count}" -gt "0" ]
+[ "$(ansible-doc -M ./library -l ansible.builtin |wc -l)" == "${count}" ]
+[ "$(ansible-doc --playbook-dir ./ -l ansible.builtin |wc -l)" == "${count}" ]
+
+
+# produce 'sidecar' docs for test
+[ "$(ansible-doc -t test --playbook-dir ./ testns.testcol.yolo| wc -l)" -gt "0" ]
+[ "$(ansible-doc -t filter --playbook-dir ./ donothing| wc -l)" -gt "0" ]
+[ "$(ansible-doc -t filter --playbook-dir ./ ansible.legacy.donothing| wc -l)" -gt "0" ]
+
+# no docs and no sidecar
+ansible-doc -t filter --playbook-dir ./ nodocs 2>&1| grep -c 'missing documentation' || true
+
+# produce 'sidecar' docs for module
+[ "$(ansible-doc -M ./library test_win_module| wc -l)" -gt "0" ]
+[ "$(ansible-doc --playbook-dir ./ test_win_module| wc -l)" -gt "0" ]
+
+# test 'double DOCUMENTATION' use
+[ "$(ansible-doc --playbook-dir ./ double_doc| wc -l)" -gt "0" ]
+
+# don't break on module dir
+ansible-doc --list --module-path ./modules > /dev/null
+
+# ensure we dedupe by fqcn and not base name
+[ "$(ansible-doc -l -t filter --playbook-dir ./ |grep -c 'b64decode')" -eq "3" ]
+
+# ensure we don't show duplicates for plugins that only exist in ansible.builtin when listing ansible.legacy plugins
+[ "$(ansible-doc -l -t filter --playbook-dir ./ |grep -c 'b64encode')" -eq "1" ]
+
+# with playbook dir, legacy should override
+ansible-doc -t filter split --playbook-dir ./ |grep histerical
+
+pyc_src="$(pwd)/filter_plugins/other.py"
+pyc_1="$(pwd)/filter_plugins/split.pyc"
+pyc_2="$(pwd)/library/notaplugin.pyc"
+trap 'rm -rf "$pyc_1" "$pyc_2"' EXIT
+
+# test pyc files are not used as adjacent documentation
+python -c "import py_compile; py_compile.compile('$pyc_src', cfile='$pyc_1')"
+ansible-doc -t filter split --playbook-dir ./ |grep histerical
+
+# test pyc files are not listed as plugins
+python -c "import py_compile; py_compile.compile('$pyc_src', cfile='$pyc_2')"
+test "$(ansible-doc -l -t module --playbook-dir ./ 2>&1 1>/dev/null |grep -c "notaplugin")" == 0
+
+# without playbook dir, builtin should return
+ansible-doc -t filter split |grep -v histerical
diff --git a/test/integration/targets/ansible-doc/test.yml b/test/integration/targets/ansible-doc/test.yml
new file mode 100644
index 0000000..a8c992e
--- /dev/null
+++ b/test/integration/targets/ansible-doc/test.yml
@@ -0,0 +1,172 @@
+- hosts: localhost
+ gather_facts: no
+ environment:
+ ANSIBLE_LIBRARY: "{{ playbook_dir }}/library"
+ tasks:
+ - name: module with missing description return docs
+ command: ansible-doc test_docs_missing_description
+ register: result
+ ignore_errors: true
+
+ - assert:
+ that:
+ - result is failed
+ - |
+ "ERROR! Unable to retrieve documentation from 'test_docs_missing_description' due to: All (sub-)options and return values must have a 'description' field"
+ in result.stderr
+
+ - name: module with suboptions (avoid first line as it has full path)
+ shell: ansible-doc test_docs_suboptions| tail -n +2
+ register: result
+ ignore_errors: true
+
+ - set_fact:
+ actual_output: >-
+ {{ result.stdout | regex_replace('^(> [A-Z_]+ +\().+library/([a-z_]+.py)\)$', '\1library/\2)', multiline=true) }}
+ expected_output: "{{ lookup('file', 'test_docs_suboptions.output') }}"
+
+ - assert:
+ that:
+ - result is succeeded
+ - actual_output == expected_output
+
+ - name: module with return docs
+ shell: ansible-doc test_docs_returns| tail -n +2
+ register: result
+ ignore_errors: true
+
+ - set_fact:
+ actual_output: >-
+ {{ result.stdout | regex_replace('^(> [A-Z_]+ +\().+library/([a-z_]+.py)\)$', '\1library/\2)', multiline=true) }}
+ expected_output: "{{ lookup('file', 'test_docs_returns.output') }}"
+
+ - assert:
+ that:
+ - result is succeeded
+ - actual_output == expected_output
+
+ - name: module with broken return docs
+ command: ansible-doc test_docs_returns_broken
+ register: result
+ ignore_errors: true
+
+ - assert:
+ that:
+ - result is failed
+ - '"module test_docs_returns_broken missing documentation (or could not parse documentation)" in result.stderr'
+
+ - name: non-existent module
+ command: ansible-doc test_does_not_exist
+ register: result
+ - assert:
+ that:
+ - '"test_does_not_exist was not found" in result.stderr'
+
+ - name: documented module
+ command: ansible-doc test_docs
+ register: result
+ - assert:
+ that:
+ - '"WARNING" not in result.stderr'
+ - '"TEST_DOCS " in result.stdout'
+ - '"AUTHOR: Ansible Core Team" in result.stdout'
+
+ - name: documented module without metadata
+ command: ansible-doc test_docs_no_metadata
+ register: result
+ - assert:
+ that:
+ - '"WARNING" not in result.stderr'
+ - '"TEST_DOCS_NO_METADATA " in result.stdout'
+ - '"AUTHOR: Ansible Core Team" in result.stdout'
+
+ - name: documented module with no status in metadata
+ command: ansible-doc test_docs_no_status
+ register: result
+ - assert:
+ that:
+ - '"WARNING" not in result.stderr'
+ - '"TEST_DOCS_NO_STATUS " in result.stdout'
+ - '"AUTHOR: Ansible Core Team" in result.stdout'
+
+ - name: documented module with non-iterable status in metadata
+ command: ansible-doc test_docs_non_iterable_status
+ register: result
+ - assert:
+ that:
+ - '"WARNING" not in result.stderr'
+ - '"TEST_DOCS_NON_ITERABLE_STATUS " in result.stdout'
+ - '"AUTHOR: Ansible Core Team" in result.stdout'
+
+ - name: documented module with removed status
+ command: ansible-doc test_docs_removed_status
+ register: result
+
+ - assert:
+ that:
+ - '"WARNING" not in result.stderr'
+ - '"TEST_DOCS_REMOVED_STATUS " in result.stdout'
+ - '"AUTHOR: Ansible Core Team" in result.stdout'
+
+ - name: empty module
+ command: ansible-doc test_empty
+ register: result
+ ignore_errors: true
+
+ - assert:
+ that:
+ - result is failed
+
+ - name: module with no documentation
+ command: ansible-doc test_no_docs
+ register: result
+ ignore_errors: true
+
+ - assert:
+ that:
+ - result is failed
+
+ - name: deprecated module with both removed date and version (date should get precedence)
+ command: ansible-doc test_docs_removed_precedence
+ register: result
+
+ - assert:
+ that:
+ - '"DEPRECATED" in result.stdout'
+ - '"Reason: Updated module released with more functionality" in result.stdout'
+ - '"Will be removed in a release after 2022-06-01" in result.stdout'
+ - '"Alternatives: new_module" in result.stdout'
+
+ - name: documented module with YAML anchors
+ shell: ansible-doc test_docs_yaml_anchors |tail -n +2
+ register: result
+ - set_fact:
+ actual_output: >-
+ {{ result.stdout | regex_replace('^(> [A-Z_]+ +\().+library/([a-z_]+.py)\)$', '\1library/\2)', multiline=true) }}
+ expected_output: "{{ lookup('file', 'test_docs_yaml_anchors.output') }}"
+ - assert:
+ that:
+ - actual_output == expected_output
+
+ - name: ensure 'donothing' adjacent filter is loaded
+ assert:
+ that:
+ - "'x' == ('x'|donothing)"
+
+ - name: docs for deprecated plugin
+ command: ansible-doc deprecated_with_docs -t lookup
+ register: result
+ - assert:
+ that:
+ - '"WARNING" not in result.stderr'
+ - '"DEPRECATED_WITH_DOCS " in result.stdout'
+ - '"AUTHOR: Ansible Core Team" in result.stdout'
+
+ - name: adjacent docs for deprecated plugin
+ command: ansible-doc deprecated_with_adj_docs -t lookup
+ register: result
+ - assert:
+ that:
+ - '"WARNING" not in result.stderr'
+ - '"DEPRECATED_WITH_ADJ_DOCS " in result.stdout'
+ - '"AUTHOR: Ansible Core Team" in result.stdout'
diff --git a/test/integration/targets/ansible-doc/test_docs_returns.output b/test/integration/targets/ansible-doc/test_docs_returns.output
new file mode 100644
index 0000000..3e23645
--- /dev/null
+++ b/test/integration/targets/ansible-doc/test_docs_returns.output
@@ -0,0 +1,33 @@
+
+ Test module
+
+AUTHOR: Ansible Core Team
+
+EXAMPLES:
+
+
+
+
+RETURN VALUES:
+- a_first
+ A first result.
+ returned: success
+ type: str
+
+- m_middle
+ This should be in the middle.
+ Has some more data
+ returned: success and 1st of month
+ type: dict
+
+ CONTAINS:
+
+ - suboption
+ A suboption.
+ choices: [ARF, BARN, c_without_capital_first_letter]
+ type: str
+
+- z_last
+ A last result.
+ returned: success
+ type: str
diff --git a/test/integration/targets/ansible-doc/test_docs_suboptions.output b/test/integration/targets/ansible-doc/test_docs_suboptions.output
new file mode 100644
index 0000000..350f90f
--- /dev/null
+++ b/test/integration/targets/ansible-doc/test_docs_suboptions.output
@@ -0,0 +1,42 @@
+
+ Test module
+
+OPTIONS (= is mandatory):
+
+- with_suboptions
+ An option with suboptions.
+ Use with care.
+ default: null
+ type: dict
+
+ SUBOPTIONS:
+
+ - a_first
+ The first suboption.
+ default: null
+ type: str
+
+ - m_middle
+ The suboption in the middle.
+ Has its own suboptions.
+ default: null
+
+ SUBOPTIONS:
+
+ - a_suboption
+ A sub-suboption.
+ default: null
+ type: str
+
+ - z_last
+ The last suboption.
+ default: null
+ type: str
+
+
+AUTHOR: Ansible Core Team
+
+EXAMPLES:
+
+
+
diff --git a/test/integration/targets/ansible-doc/test_docs_yaml_anchors.output b/test/integration/targets/ansible-doc/test_docs_yaml_anchors.output
new file mode 100644
index 0000000..5eb2eee
--- /dev/null
+++ b/test/integration/targets/ansible-doc/test_docs_yaml_anchors.output
@@ -0,0 +1,46 @@
+
+ Test module
+
+OPTIONS (= is mandatory):
+
+- at_the_top
+ Short desc
+ default: some string
+ type: str
+
+- egress
+ Egress firewall rules
+ default: null
+ elements: dict
+ type: list
+
+ SUBOPTIONS:
+
+ = port
+ Rule port
+ type: int
+
+- ingress
+ Ingress firewall rules
+ default: null
+ elements: dict
+ type: list
+
+ SUBOPTIONS:
+
+ = port
+ Rule port
+ type: int
+
+- last_one
+ Short desc
+ default: some string
+ type: str
+
+
+AUTHOR: Ansible Core Team
+
+EXAMPLES:
+
+
+
diff --git a/test/integration/targets/ansible-doc/test_role1/README.txt b/test/integration/targets/ansible-doc/test_role1/README.txt
new file mode 100644
index 0000000..98983c8
--- /dev/null
+++ b/test/integration/targets/ansible-doc/test_role1/README.txt
@@ -0,0 +1,3 @@
+Test role that exists in the playbook directory so we can validate
+that a role of the same name that exists in the 'roles' subdirectory
+will take precedence over this one.
diff --git a/test/integration/targets/ansible-doc/test_role1/meta/main.yml b/test/integration/targets/ansible-doc/test_role1/meta/main.yml
new file mode 100644
index 0000000..4f7abcc
--- /dev/null
+++ b/test/integration/targets/ansible-doc/test_role1/meta/main.yml
@@ -0,0 +1,8 @@
+---
+dependencies:
+galaxy_info:
+
+argument_specs:
+ main:
+ short_description: test_role1 from playbook dir
+ description: This should not appear in `ansible-doc --list` output.
diff --git a/test/integration/targets/ansible-galaxy-collection-cli/aliases b/test/integration/targets/ansible-galaxy-collection-cli/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-cli/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/ansible-galaxy-collection-cli/files/expected.txt b/test/integration/targets/ansible-galaxy-collection-cli/files/expected.txt
new file mode 100644
index 0000000..110009e
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-cli/files/expected.txt
@@ -0,0 +1,107 @@
+MANIFEST.json
+FILES.json
+README.rst
+changelogs/
+docs/
+playbooks/
+plugins/
+roles/
+tests/
+changelogs/fragments/
+changelogs/fragments/bar.yaml
+changelogs/fragments/foo.yml
+docs/docsite/
+docs/docsite/apple.j2
+docs/docsite/bar.yml
+docs/docsite/baz.yaml
+docs/docsite/foo.rst
+docs/docsite/orange.txt
+docs/docsite/qux.json
+playbooks/bar.yaml
+playbooks/baz.json
+playbooks/foo.yml
+plugins/action/
+plugins/become/
+plugins/cache/
+plugins/callback/
+plugins/cliconf/
+plugins/connection/
+plugins/doc_fragments/
+plugins/filter/
+plugins/httpapi/
+plugins/inventory/
+plugins/lookup/
+plugins/module_utils/
+plugins/modules/
+plugins/netconf/
+plugins/shell/
+plugins/strategy/
+plugins/terminal/
+plugins/test/
+plugins/vars/
+plugins/action/test.py
+plugins/become/bar.yml
+plugins/become/baz.yaml
+plugins/become/test.py
+plugins/cache/bar.yml
+plugins/cache/baz.yaml
+plugins/cache/test.py
+plugins/callback/bar.yml
+plugins/callback/baz.yaml
+plugins/callback/test.py
+plugins/cliconf/bar.yml
+plugins/cliconf/baz.yaml
+plugins/cliconf/test.py
+plugins/connection/bar.yml
+plugins/connection/baz.yaml
+plugins/connection/test.py
+plugins/doc_fragments/test.py
+plugins/filter/bar.yml
+plugins/filter/baz.yaml
+plugins/filter/test.py
+plugins/httpapi/bar.yml
+plugins/httpapi/baz.yaml
+plugins/httpapi/test.py
+plugins/inventory/bar.yml
+plugins/inventory/baz.yaml
+plugins/inventory/test.py
+plugins/lookup/bar.yml
+plugins/lookup/baz.yaml
+plugins/lookup/test.py
+plugins/module_utils/bar.ps1
+plugins/module_utils/test.py
+plugins/modules/bar.yaml
+plugins/modules/test2.py
+plugins/modules/foo.yml
+plugins/modules/qux.ps1
+plugins/netconf/bar.yml
+plugins/netconf/baz.yaml
+plugins/netconf/test.py
+plugins/shell/bar.yml
+plugins/shell/baz.yaml
+plugins/shell/test.py
+plugins/strategy/bar.yml
+plugins/strategy/baz.yaml
+plugins/strategy/test.py
+plugins/terminal/test.py
+plugins/test/bar.yml
+plugins/test/baz.yaml
+plugins/test/test.py
+plugins/vars/bar.yml
+plugins/vars/baz.yaml
+plugins/vars/test.py
+roles/foo/
+roles/foo/tasks/
+roles/foo/templates/
+roles/foo/vars/
+roles/foo/tasks/main.yml
+roles/foo/templates/foo.j2
+roles/foo/vars/main.yaml
+tests/integration/
+tests/units/
+tests/integration/targets/
+tests/integration/targets/foo/
+tests/integration/targets/foo/aliases
+tests/integration/targets/foo/tasks/
+tests/integration/targets/foo/tasks/main.yml
+tests/units/test_foo.py
diff --git a/test/integration/targets/ansible-galaxy-collection-cli/files/expected_full_manifest.txt b/test/integration/targets/ansible-galaxy-collection-cli/files/expected_full_manifest.txt
new file mode 100644
index 0000000..9455d5f
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-cli/files/expected_full_manifest.txt
@@ -0,0 +1,108 @@
+MANIFEST.json
+FILES.json
+README.rst
+galaxy.yml
+changelogs/
+docs/
+playbooks/
+plugins/
+roles/
+tests/
+changelogs/fragments/
+changelogs/fragments/bar.yaml
+changelogs/fragments/foo.yml
+docs/docsite/
+docs/docsite/apple.j2
+docs/docsite/bar.yml
+docs/docsite/baz.yaml
+docs/docsite/foo.rst
+docs/docsite/orange.txt
+docs/docsite/qux.json
+playbooks/bar.yaml
+playbooks/baz.json
+playbooks/foo.yml
+plugins/action/
+plugins/become/
+plugins/cache/
+plugins/callback/
+plugins/cliconf/
+plugins/connection/
+plugins/doc_fragments/
+plugins/filter/
+plugins/httpapi/
+plugins/inventory/
+plugins/lookup/
+plugins/module_utils/
+plugins/modules/
+plugins/netconf/
+plugins/shell/
+plugins/strategy/
+plugins/terminal/
+plugins/test/
+plugins/vars/
+plugins/action/test.py
+plugins/become/bar.yml
+plugins/become/baz.yaml
+plugins/become/test.py
+plugins/cache/bar.yml
+plugins/cache/baz.yaml
+plugins/cache/test.py
+plugins/callback/bar.yml
+plugins/callback/baz.yaml
+plugins/callback/test.py
+plugins/cliconf/bar.yml
+plugins/cliconf/baz.yaml
+plugins/cliconf/test.py
+plugins/connection/bar.yml
+plugins/connection/baz.yaml
+plugins/connection/test.py
+plugins/doc_fragments/test.py
+plugins/filter/bar.yml
+plugins/filter/baz.yaml
+plugins/filter/test.py
+plugins/httpapi/bar.yml
+plugins/httpapi/baz.yaml
+plugins/httpapi/test.py
+plugins/inventory/bar.yml
+plugins/inventory/baz.yaml
+plugins/inventory/test.py
+plugins/lookup/bar.yml
+plugins/lookup/baz.yaml
+plugins/lookup/test.py
+plugins/module_utils/bar.ps1
+plugins/module_utils/test.py
+plugins/modules/bar.yaml
+plugins/modules/test2.py
+plugins/modules/foo.yml
+plugins/modules/qux.ps1
+plugins/netconf/bar.yml
+plugins/netconf/baz.yaml
+plugins/netconf/test.py
+plugins/shell/bar.yml
+plugins/shell/baz.yaml
+plugins/shell/test.py
+plugins/strategy/bar.yml
+plugins/strategy/baz.yaml
+plugins/strategy/test.py
+plugins/terminal/test.py
+plugins/test/bar.yml
+plugins/test/baz.yaml
+plugins/test/test.py
+plugins/vars/bar.yml
+plugins/vars/baz.yaml
+plugins/vars/test.py
+roles/foo/
+roles/foo/tasks/
+roles/foo/templates/
+roles/foo/vars/
+roles/foo/tasks/main.yml
+roles/foo/templates/foo.j2
+roles/foo/vars/main.yaml
+tests/integration/
+tests/units/
+tests/integration/targets/
+tests/integration/targets/foo/
+tests/integration/targets/foo/aliases
+tests/integration/targets/foo/tasks/
+tests/integration/targets/foo/tasks/main.yml
+tests/units/test_foo.py
diff --git a/test/integration/targets/ansible-galaxy-collection-cli/files/full_manifest_galaxy.yml b/test/integration/targets/ansible-galaxy-collection-cli/files/full_manifest_galaxy.yml
new file mode 100644
index 0000000..b893440
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-cli/files/full_manifest_galaxy.yml
@@ -0,0 +1,39 @@
+namespace: ns
+name: col
+version: 2.0.0
+readme: README.rst
+authors:
+ - Ansible
+manifest:
+ omit_default_directives: true
+ directives:
+ - include meta/*.yml
+ - include *.txt *.md *.rst COPYING LICENSE
+ - recursive-include tests **
+ - recursive-include docs **.rst **.yml **.yaml **.json **.j2 **.txt
+ - recursive-include roles **.yml **.yaml **.json **.j2
+ - recursive-include playbooks **.yml **.yaml **.json
+ - recursive-include changelogs **.yml **.yaml
+ - recursive-include plugins */**.py
+ - recursive-include plugins/become **.yml **.yaml
+ - recursive-include plugins/cache **.yml **.yaml
+ - recursive-include plugins/callback **.yml **.yaml
+ - recursive-include plugins/cliconf **.yml **.yaml
+ - recursive-include plugins/connection **.yml **.yaml
+ - recursive-include plugins/filter **.yml **.yaml
+ - recursive-include plugins/httpapi **.yml **.yaml
+ - recursive-include plugins/inventory **.yml **.yaml
+ - recursive-include plugins/lookup **.yml **.yaml
+ - recursive-include plugins/netconf **.yml **.yaml
+ - recursive-include plugins/shell **.yml **.yaml
+ - recursive-include plugins/strategy **.yml **.yaml
+ - recursive-include plugins/test **.yml **.yaml
+ - recursive-include plugins/vars **.yml **.yaml
+ - recursive-include plugins/modules **.ps1 **.yml **.yaml
+ - recursive-include plugins/module_utils **.ps1 **.psm1 **.cs
+ - exclude foo.txt
+ - recursive-exclude docs/foobar **
+ - exclude galaxy.yml galaxy.yaml MANIFEST.json FILES.json ns-col-*.tar.gz
+ - recursive-exclude tests/output **
+ - global-exclude /.* /__pycache__
+ - include galaxy.yml
diff --git a/test/integration/targets/ansible-galaxy-collection-cli/files/galaxy.yml b/test/integration/targets/ansible-galaxy-collection-cli/files/galaxy.yml
new file mode 100644
index 0000000..8f0ada0
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-cli/files/galaxy.yml
@@ -0,0 +1,10 @@
+namespace: ns
+name: col
+version: 1.0.0
+readme: README.rst
+authors:
+ - Ansible
+manifest:
+ directives:
+ - exclude foo.txt
+ - recursive-exclude docs/foobar **
diff --git a/test/integration/targets/ansible-galaxy-collection-cli/files/make_collection_dir.py b/test/integration/targets/ansible-galaxy-collection-cli/files/make_collection_dir.py
new file mode 100644
index 0000000..913a6f7
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-cli/files/make_collection_dir.py
@@ -0,0 +1,114 @@
+import sys
+import pathlib
+
+paths = [
+ 'ns-col-1.0.0.tar.gz',
+ 'foo.txt',
+ 'README.rst',
+ 'artifacts/.gitkeep',
+ 'plugins/vars/bar.yml',
+ 'plugins/vars/baz.yaml',
+ 'plugins/vars/test.py',
+ 'plugins/vars/docs.md',
+ 'plugins/netconf/bar.yml',
+ 'plugins/netconf/baz.yaml',
+ 'plugins/netconf/test.py',
+ 'plugins/netconf/docs.md',
+ 'plugins/cache/bar.yml',
+ 'plugins/cache/baz.yaml',
+ 'plugins/cache/test.py',
+ 'plugins/cache/docs.md',
+ 'plugins/test/bar.yml',
+ 'plugins/test/baz.yaml',
+ 'plugins/test/test.py',
+ 'plugins/test/docs.md',
+ 'plugins/connection/bar.yml',
+ 'plugins/connection/baz.yaml',
+ 'plugins/connection/test.py',
+ 'plugins/connection/docs.md',
+ 'plugins/doc_fragments/bar.yml',
+ 'plugins/doc_fragments/baz.yaml',
+ 'plugins/doc_fragments/test.py',
+ 'plugins/doc_fragments/docs.md',
+ 'plugins/shell/bar.yml',
+ 'plugins/shell/baz.yaml',
+ 'plugins/shell/test.py',
+ 'plugins/shell/docs.md',
+ 'plugins/terminal/bar.yml',
+ 'plugins/terminal/baz.yaml',
+ 'plugins/terminal/test.py',
+ 'plugins/terminal/docs.md',
+ 'plugins/lookup/bar.yml',
+ 'plugins/lookup/baz.yaml',
+ 'plugins/lookup/test.py',
+ 'plugins/lookup/docs.md',
+ 'plugins/httpapi/bar.yml',
+ 'plugins/httpapi/baz.yaml',
+ 'plugins/httpapi/test.py',
+ 'plugins/httpapi/docs.md',
+ 'plugins/action/bar.yml',
+ 'plugins/action/baz.yaml',
+ 'plugins/action/test.py',
+ 'plugins/action/docs.md',
+ 'plugins/inventory/bar.yml',
+ 'plugins/inventory/baz.yaml',
+ 'plugins/inventory/test.py',
+ 'plugins/inventory/docs.md',
+ 'plugins/module_utils/bar.ps1',
+ 'plugins/module_utils/test.py',
+ 'plugins/module_utils/docs.md',
+ 'plugins/module_utils/baz.yml',
+ 'plugins/become/bar.yml',
+ 'plugins/become/baz.yaml',
+ 'plugins/become/test.py',
+ 'plugins/become/docs.md',
+ 'plugins/callback/bar.yml',
+ 'plugins/callback/baz.yaml',
+ 'plugins/callback/test.py',
+ 'plugins/callback/docs.md',
+ 'plugins/filter/bar.yml',
+ 'plugins/filter/baz.yaml',
+ 'plugins/filter/test.py',
+ 'plugins/filter/docs.md',
+ 'plugins/cliconf/bar.yml',
+ 'plugins/cliconf/baz.yaml',
+ 'plugins/cliconf/test.py',
+ 'plugins/cliconf/docs.md',
+ 'plugins/modules/foo.yml',
+ 'plugins/modules/qux.ps1',
+ 'plugins/modules/test2.py',
+ 'plugins/modules/bar.yaml',
+ 'plugins/modules/docs.md',
+ 'plugins/strategy/bar.yml',
+ 'plugins/strategy/baz.yaml',
+ 'plugins/strategy/test.py',
+ 'plugins/strategy/docs.md',
+ 'tests/integration/targets/foo/aliases',
+ 'tests/integration/targets/foo/tasks/main.yml',
+ 'tests/output/foo',
+ 'tests/units/test_foo.py',
+ 'roles/foo/vars/main.yaml',
+ 'roles/foo/tasks/main.yml',
+ 'roles/foo/templates/foo.j2',
+ 'playbooks/baz.json',
+ 'playbooks/foo.yml',
+ 'playbooks/bar.yaml',
+ 'docs/foobar/qux/baz.txt',
+ 'docs/foobar/qux/bar',
+ 'docs/docsite/bar.yml',
+ 'docs/docsite/baz.yaml',
+ 'docs/docsite/apple.j2',
+ 'docs/docsite/qux.json',
+ 'docs/docsite/orange.txt',
+ 'docs/docsite/foo.rst',
+ 'changelogs/fragments/foo.yml',
+ 'changelogs/fragments/bar.yaml'
+]
+
+root = pathlib.Path(sys.argv[1])
+
+for path in paths:
+ print(path)
+ path = root / path
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.touch()
diff --git a/test/integration/targets/ansible-galaxy-collection-cli/tasks/main.yml b/test/integration/targets/ansible-galaxy-collection-cli/tasks/main.yml
new file mode 100644
index 0000000..b74acd3
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-cli/tasks/main.yml
@@ -0,0 +1,19 @@
+- block:
+ - name: Install distlib
+ pip:
+ name: distlib
+ state: present
+ register: distlib
+
+ - set_fact:
+ output_dir: "{{ lookup('env', 'OUTPUT_DIR') }}"
+
+ - import_tasks: manifest.yml
+ environment:
+ ANSIBLE_NOCOLOR: 1
+ always:
+ - name: Uninstall distlib
+ pip:
+ name: distlib
+ state: absent
+ when: distlib is changed
diff --git a/test/integration/targets/ansible-galaxy-collection-cli/tasks/manifest.yml b/test/integration/targets/ansible-galaxy-collection-cli/tasks/manifest.yml
new file mode 100644
index 0000000..5f37c72
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-cli/tasks/manifest.yml
@@ -0,0 +1,57 @@
+- name: Create test collection dir
+ script: make_collection_dir.py "{{ output_dir }}/test_manifest_collection"
+ args:
+ executable: '{{ ansible_facts.python.executable }}'
+
+- name: Copy galaxy.yml with manifest_directives_full
+ copy:
+ src: galaxy.yml
+ dest: '{{ output_dir }}/test_manifest_collection/galaxy.yml'
+
+- name: Build collection
+ command: ansible-galaxy collection build --output-path {{ output_dir|quote }} -vvv
+ args:
+ chdir: '{{ output_dir }}/test_manifest_collection'
+
+- name: Get artifact contents
+ command: tar tzf '{{ output_dir }}/ns-col-1.0.0.tar.gz'
+ register: artifact_contents
+
+- debug:
+ var: artifact_contents.stdout_lines|sort
+
+- debug:
+ var: lookup('file', 'expected.txt').splitlines()|sort
+
+- assert:
+ that:
+ - artifact_contents.stdout_lines|sort == lookup('file', 'expected.txt').splitlines()|sort
+
+- name: Create test collection dir
+ script: make_collection_dir.py "{{ output_dir }}/test_manifest_no_defaults_collection"
+ args:
+ executable: '{{ ansible_facts.python.executable }}'
+
+- name: Copy galaxy.yml with manifest_directives_full
+ copy:
+ src: full_manifest_galaxy.yml
+ dest: '{{ output_dir }}/test_manifest_no_defaults_collection/galaxy.yml'
+
+- name: Build collection
+ command: ansible-galaxy collection build --output-path {{ output_dir|quote }} -vvv
+ args:
+ chdir: '{{ output_dir }}/test_manifest_no_defaults_collection'
+
+- name: Get artifact contents
+ command: tar tzf '{{ output_dir }}/ns-col-2.0.0.tar.gz'
+ register: artifact_contents
+
+- debug:
+ var: artifact_contents.stdout_lines|sort
+
+- debug:
+ var: lookup('file', 'expected_full_manifest.txt').splitlines()|sort
+
+- assert:
+ that:
+ - artifact_contents.stdout_lines|sort == lookup('file', 'expected_full_manifest.txt').splitlines()|sort
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/aliases b/test/integration/targets/ansible-galaxy-collection-scm/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/meta/main.yml b/test/integration/targets/ansible-galaxy-collection-scm/meta/main.yml
new file mode 100644
index 0000000..655a62f
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/meta/main.yml
@@ -0,0 +1,4 @@
+---
+dependencies:
+- setup_remote_tmp_dir
+- setup_gnutar
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/tasks/download.yml b/test/integration/targets/ansible-galaxy-collection-scm/tasks/download.yml
new file mode 100644
index 0000000..6b52bd1
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/tasks/download.yml
@@ -0,0 +1,48 @@
+- name: create test download dir
+ file:
+ path: '{{ galaxy_dir }}/download'
+ state: directory
+
+- name: download a git repository
+ command: >
+ ansible-galaxy collection download
+ git+https://github.com/ansible-collections/amazon.aws.git,37875c5b4ba5bf3cc43e07edf29f3432fd76def5
+ git+https://github.com/AlanCoding/awx.git#awx_collection,750c22a150d04eef1cb625fd4f83cce57949416c
+ --no-deps
+ args:
+ chdir: '{{ galaxy_dir }}/download'
+ register: download_collection
+
+- name: check that the amazon.aws collection was downloaded
+ stat:
+ path: '{{ galaxy_dir }}/download/collections/amazon-aws-1.0.0.tar.gz'
+ register: download_collection_amazon_actual
+
+- name: check that the awx.awx collection was downloaded
+ stat:
+ path: '{{ galaxy_dir }}/download/collections/awx-awx-0.0.1-devel.tar.gz'
+ register: download_collection_awx_actual
+
+- assert:
+ that:
+ - '"Downloading collection ''amazon.aws:1.0.0'' to" in download_collection.stdout'
+ - '"Downloading collection ''awx.awx:0.0.1-devel'' to" in download_collection.stdout'
+ - download_collection_amazon_actual.stat.exists
+ - download_collection_awx_actual.stat.exists
+
+- name: test the downloaded repository can be installed
+ command: 'ansible-galaxy collection install -r requirements.yml --no-deps'
+ args:
+ chdir: '{{ galaxy_dir }}/download/collections/'
+
+- name: list installed collections
+ command: 'ansible-galaxy collection list'
+ register: installed_collections
+
+- assert:
+ that:
+ - "'amazon.aws' in installed_collections.stdout"
+ - "'awx.awx' in installed_collections.stdout"
+
+- include_tasks: ./empty_installed_collections.yml
+ when: cleanup
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/tasks/empty_installed_collections.yml b/test/integration/targets/ansible-galaxy-collection-scm/tasks/empty_installed_collections.yml
new file mode 100644
index 0000000..241107c
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/tasks/empty_installed_collections.yml
@@ -0,0 +1,7 @@
+- name: delete installed collections
+ file:
+ state: absent
+ path: "{{ item }}"
+ loop:
+ - "{{ install_path }}"
+ - "{{ alt_install_path }}"
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/tasks/individual_collection_repo.yml b/test/integration/targets/ansible-galaxy-collection-scm/tasks/individual_collection_repo.yml
new file mode 100644
index 0000000..e51c3a9
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/tasks/individual_collection_repo.yml
@@ -0,0 +1,18 @@
+- name: Clone a git repository
+ git:
+ repo: https://github.com/ansible-collections/amazon.aws.git
+ dest: '{{ scm_path }}/amazon.aws/'
+
+- name: install
+ command: 'ansible-galaxy collection install git+file://{{ scm_path }}/amazon.aws/.git --no-deps'
+
+- name: list installed collections
+ command: 'ansible-galaxy collection list'
+ register: installed_collections
+
+- assert:
+ that:
+ - "'amazon.aws' in installed_collections.stdout"
+
+- include_tasks: ./empty_installed_collections.yml
+ when: cleanup
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/tasks/main.yml b/test/integration/targets/ansible-galaxy-collection-scm/tasks/main.yml
new file mode 100644
index 0000000..dab599b
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/tasks/main.yml
@@ -0,0 +1,61 @@
+---
+- name: set the temp test directory
+ set_fact:
+ galaxy_dir: "{{ remote_tmp_dir }}/galaxy"
+
+- name: Test installing collections from git repositories
+ environment:
+ ANSIBLE_COLLECTIONS_PATHS: "{{ galaxy_dir }}/collections"
+ vars:
+ cleanup: True
+ galaxy_dir: "{{ galaxy_dir }}"
+ block:
+
+ - include_tasks: ./setup.yml
+ - include_tasks: ./requirements.yml
+ - include_tasks: ./individual_collection_repo.yml
+ - include_tasks: ./setup_multi_collection_repo.yml
+ - include_tasks: ./multi_collection_repo_all.yml
+ - include_tasks: ./scm_dependency.yml
+ vars:
+ cleanup: False
+ - include_tasks: ./reinstalling.yml
+ - include_tasks: ./multi_collection_repo_individual.yml
+ - include_tasks: ./setup_recursive_scm_dependency.yml
+ - include_tasks: ./scm_dependency_deduplication.yml
+ - include_tasks: ./test_supported_resolvelib_versions.yml
+ loop: "{{ supported_resolvelib_versions }}"
+ loop_control:
+ loop_var: resolvelib_version
+ - include_tasks: ./download.yml
+ - include_tasks: ./setup_collection_bad_version.yml
+ - include_tasks: ./test_invalid_version.yml
+ - include_tasks: ./test_manifest_metadata.yml
+
+ always:
+
+ - name: Remove the directories for installing collections and git repositories
+ file:
+ path: '{{ item }}'
+ state: absent
+ loop:
+ - "{{ install_path }}"
+ - "{{ alt_install_path }}"
+ - "{{ scm_path }}"
+
+ - name: remove git
+ package:
+ name: git
+ state: absent
+ when: git_install is changed
+
+ # This gets dragged in as a dependency of git on FreeBSD.
+ # We need to remove it too when done.
+ - name: remove python37 if necessary
+ package:
+ name: python37
+ state: absent
+ when:
+ - git_install is changed
+ - ansible_distribution == 'FreeBSD'
+ - ansible_python.version.major == 2
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/tasks/multi_collection_repo_all.yml b/test/integration/targets/ansible-galaxy-collection-scm/tasks/multi_collection_repo_all.yml
new file mode 100644
index 0000000..f22f984
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/tasks/multi_collection_repo_all.yml
@@ -0,0 +1,45 @@
+- name: Install all collections by default
+ command: 'ansible-galaxy collection install git+file://{{ test_repo_path }}/.git'
+
+- name: list installed collections
+ command: 'ansible-galaxy collection list'
+ register: installed_collections
+
+- assert:
+ that:
+ - "'ansible_test.collection_1' in installed_collections.stdout"
+ - "'ansible_test.collection_2' in installed_collections.stdout"
+
+- name: install from artifact to another path to compare contents
+ command: 'ansible-galaxy collection install {{ artifact_path }} -p {{ alt_install_path }} --no-deps'
+ vars:
+ artifact_path: "{{ galaxy_dir }}/ansible_test-collection_1-1.0.0.tar.gz"
+
+- name: check if the files and folders in build_ignore were respected
+ stat:
+ path: "{{ install_path }}/ansible_test/collection_1/{{ item }}"
+ register: result
+ loop:
+ - foo.txt
+ - foobar/baz.txt
+ - foobar/qux
+
+- assert:
+ that: result.results | map(attribute='stat') | map(attribute='exists') is not any
+
+- name: check that directory with ignored files exists and is empty
+ stat:
+ path: "{{ install_path }}/ansible_test/collection_1/foobar"
+ register: result
+
+- assert:
+ that: result.stat.exists
+
+- name: test that there are no diff installing from a repo vs artifact
+ command: "diff -ur {{ collection_from_artifact }} {{ collection_from_repo }} -x *.json"
+ vars:
+ collection_from_repo: "{{ install_path }}/ansible_test/collection_1"
+ collection_from_artifact: "{{ alt_install_path }}/ansible_test/collection_1"
+
+- include_tasks: ./empty_installed_collections.yml
+ when: cleanup
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/tasks/multi_collection_repo_individual.yml b/test/integration/targets/ansible-galaxy-collection-scm/tasks/multi_collection_repo_individual.yml
new file mode 100644
index 0000000..a5a2bdf
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/tasks/multi_collection_repo_individual.yml
@@ -0,0 +1,15 @@
+- name: test installing one collection
+ command: 'ansible-galaxy collection install git+file://{{ test_repo_path }}/.git#collection_2'
+
+- name: list installed collections
+ command: 'ansible-galaxy collection list'
+ register: installed_collections
+
+- assert:
+ that:
+ - "'amazon.aws' not in installed_collections.stdout"
+ - "'ansible_test.collection_1' not in installed_collections.stdout"
+ - "'ansible_test.collection_2' in installed_collections.stdout"
+
+- include_tasks: ./empty_installed_collections.yml
+ when: cleanup
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/tasks/reinstalling.yml b/test/integration/targets/ansible-galaxy-collection-scm/tasks/reinstalling.yml
new file mode 100644
index 0000000..90a70e2
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/tasks/reinstalling.yml
@@ -0,0 +1,31 @@
+- name: Rerun installing a collection with a dep
+ command: 'ansible-galaxy collection install git+file://{{ test_repo_path }}/.git#/collection_1/'
+ register: installed
+
+- name: SCM collections don't have a concrete artifact version so the collection should always be reinstalled
+ assert:
+ that:
+ - "'Created collection for ansible_test.collection_1' in installed.stdout"
+ - "'Created collection for ansible_test.collection_2' in installed.stdout"
+
+- name: The collection should also be reinstalled when --force flag is used
+ command: 'ansible-galaxy collection install git+file://{{ test_repo_path }}/.git#/collection_1/ --force'
+ register: installed
+
+- assert:
+ that:
+ - "'Created collection for ansible_test.collection_1' in installed.stdout"
+ # The dependency is also an SCM collection, so it should also be reinstalled
+ - "'Created collection for ansible_test.collection_2' in installed.stdout"
+
+- name: The collection should also be reinstalled when --force-with-deps is used
+ command: 'ansible-galaxy collection install git+file://{{ test_repo_path }}/.git#/collection_1/ --force-with-deps'
+ register: installed
+
+- assert:
+ that:
+ - "'Created collection for ansible_test.collection_1' in installed.stdout"
+ - "'Created collection for ansible_test.collection_2' in installed.stdout"
+
+- include_tasks: ./empty_installed_collections.yml
+ when: cleanup
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/tasks/requirements.yml b/test/integration/targets/ansible-galaxy-collection-scm/tasks/requirements.yml
new file mode 100644
index 0000000..10070f1
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/tasks/requirements.yml
@@ -0,0 +1,103 @@
+- name: make a requirements directory
+ file:
+ state: directory
+ path: '{{ galaxy_dir }}/requirements'
+
+- name: populate requirement templates
+ template:
+ src: "{{ item }}"
+ dest: "{{ galaxy_dir }}/requirements/{{ item }}"
+ loop:
+ - source_only.yml
+ - source_and_name.yml
+ - source_and_name_and_type.yml
+ - name_without_type.yml
+ - git_prefix_name.yml
+ - name_and_type.yml
+
+- name: test source is not a git repo
+ command: 'ansible-galaxy collection install -r source_only.yml'
+ register: result
+ ignore_errors: true
+ args:
+ chdir: '{{ galaxy_dir }}/requirements'
+
+- assert:
+ that:
+ - result.failed
+ - >-
+ "ERROR! Neither the collection requirement entry key 'name',
+ nor 'source' point to a concrete resolvable collection artifact.
+ Also 'name' is not an FQCN. A valid collection name must be in
+ the format <namespace>.<collection>. Please make sure that the
+ namespace and the collection name contain characters from
+ [a-zA-Z0-9_] only." in result.stderr
+
+- name: test source is not a git repo even if name is provided
+ command: 'ansible-galaxy collection install -r source_and_name.yml'
+ register: result
+ ignore_errors: true
+ args:
+ chdir: '{{ galaxy_dir }}/requirements'
+
+- assert:
+ that:
+ - result.failed
+ - >-
+ result.stderr is search("ERROR! Collections requirement 'source'
+ entry should contain a valid Galaxy API URL but it does not:
+ git\+file:///.*/amazon.aws/.git is not an HTTP URL.")
+
+- name: test source is not a git repo even if name and type is provided
+ command: 'ansible-galaxy collection install -r source_and_name_and_type.yml'
+ register: result
+ ignore_errors: true
+ args:
+ chdir: '{{ galaxy_dir }}/requirements'
+
+- assert:
+ that:
+ - result.failed
+ - >-
+ result.stderr is search("ERROR! Failed to clone a Git repository
+ from `file:///.*/.git`.")
+ - >-
+ result.stderr is search("fatal: '/.*/amazon.aws/.git' does not
+ appear to be a git repository")
+
+- name: test using name as a git repo without git+ prefix
+ command: 'ansible-galaxy collection install -r name_without_type.yml --no-deps'
+ register: result
+ ignore_errors: true
+ args:
+ chdir: '{{ galaxy_dir }}/requirements'
+
+- assert:
+ that:
+ - result.failed
+ - '"name must be in the format <namespace>.<collection>" in result.stderr'
+
+- name: Clone a git repository
+ git:
+ repo: https://github.com/ansible-collections/amazon.aws.git
+ dest: '{{ scm_path }}/amazon.aws/'
+
+- name: test using name as a git repo
+ command: 'ansible-galaxy collection install -r git_prefix_name.yml --no-deps'
+ register: result
+ args:
+ chdir: '{{ galaxy_dir }}/requirements'
+
+- name: test using name plus type as a git repo
+ command: 'ansible-galaxy collection install -r name_and_type.yml --force --no-deps'
+ register: result
+ args:
+ chdir: '{{ galaxy_dir }}/requirements'
+
+- name: remove the test repo and requirements dir
+ file:
+ path: '{{ item }}'
+ state: absent
+ loop:
+ - '{{ scm_path }}/amazon.aws/'
+ - '{{ galaxy_dir }}/requirements'
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/tasks/scm_dependency.yml b/test/integration/targets/ansible-galaxy-collection-scm/tasks/scm_dependency.yml
new file mode 100644
index 0000000..5645aec
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/tasks/scm_dependency.yml
@@ -0,0 +1,29 @@
+- name: test installing one collection that has a SCM dep with --no-deps
+ command: 'ansible-galaxy collection install git+file://{{ test_repo_path }}/.git#/collection_1/ --no-deps'
+
+- name: list installed collections
+ command: 'ansible-galaxy collection list'
+ register: installed_collections
+
+- assert:
+ that:
+ - "'ansible_test.collection_1' in installed_collections.stdout"
+ - "'ansible_test.collection_2' not in installed_collections.stdout"
+
+- name: remove collections to test installing with the dependency
+ include_tasks: ./empty_installed_collections.yml
+
+- name: test installing one collection that has a SCM dep
+ command: 'ansible-galaxy collection install git+file://{{ test_repo_path }}/.git#/collection_1/'
+
+- name: list installed collections
+ command: 'ansible-galaxy collection list'
+ register: installed_collections
+
+- assert:
+ that:
+ - "'ansible_test.collection_1' in installed_collections.stdout"
+ - "'ansible_test.collection_2' in installed_collections.stdout"
+
+- include_tasks: ./empty_installed_collections.yml
+ when: cleanup
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/tasks/scm_dependency_deduplication.yml b/test/integration/targets/ansible-galaxy-collection-scm/tasks/scm_dependency_deduplication.yml
new file mode 100644
index 0000000..f200be1
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/tasks/scm_dependency_deduplication.yml
@@ -0,0 +1,92 @@
+- name: Install all collections in a repo, one of which has a recursive dependency
+ command: 'ansible-galaxy collection install git+file://{{ scm_path }}/namespace_1/.git'
+ register: command
+
+- assert:
+ that:
+ - command.stdout_lines | length == 12
+ - >-
+ 'Starting galaxy collection install process'
+ in command.stdout_lines
+ - >-
+ 'Starting collection install process'
+ in command.stdout_lines
+ - >-
+ "Installing 'namespace_1.collection_1:1.0.0' to
+ '{{ install_path }}/namespace_1/collection_1'"
+ in command.stdout_lines
+ - >-
+ 'Created collection for namespace_1.collection_1:1.0.0 at
+ {{ install_path }}/namespace_1/collection_1'
+ in command.stdout_lines
+ - >-
+ 'namespace_1.collection_1:1.0.0 was installed successfully'
+ in command.stdout_lines
+ - >-
+ "Installing 'namespace_2.collection_2:1.0.0' to
+ '{{ install_path }}/namespace_2/collection_2'"
+ in command.stdout_lines
+ - >-
+ 'Created collection for namespace_2.collection_2:1.0.0 at
+ {{ install_path }}/namespace_2/collection_2'
+ in command.stdout_lines
+ - >-
+ 'namespace_2.collection_2:1.0.0 was installed successfully'
+ in command.stdout_lines
+
+- name: list installed collections
+ command: 'ansible-galaxy collection list'
+ register: installed_collections
+
+- assert:
+ that:
+ - "'namespace_1.collection_1' in installed_collections.stdout"
+ - "'namespace_2.collection_2' in installed_collections.stdout"
+
+- name: Install a specific collection in a repo with a recursive dependency
+ command: 'ansible-galaxy collection install git+file://{{ scm_path }}/namespace_1/.git#/collection_1/ --force-with-deps'
+ register: command
+
+- assert:
+ that:
+ - command.stdout_lines | length == 12
+ - >-
+ 'Starting galaxy collection install process'
+ in command.stdout_lines
+ - >-
+ 'Starting collection install process'
+ in command.stdout_lines
+ - >-
+ "Installing 'namespace_1.collection_1:1.0.0' to
+ '{{ install_path }}/namespace_1/collection_1'"
+ in command.stdout_lines
+ - >-
+ 'Created collection for namespace_1.collection_1:1.0.0 at
+ {{ install_path }}/namespace_1/collection_1'
+ in command.stdout_lines
+ - >-
+ 'namespace_1.collection_1:1.0.0 was installed successfully'
+ in command.stdout_lines
+ - >-
+ "Installing 'namespace_2.collection_2:1.0.0' to
+ '{{ install_path }}/namespace_2/collection_2'"
+ in command.stdout_lines
+ - >-
+ 'Created collection for namespace_2.collection_2:1.0.0 at
+ {{ install_path }}/namespace_2/collection_2'
+ in command.stdout_lines
+ - >-
+ 'namespace_2.collection_2:1.0.0 was installed successfully'
+ in command.stdout_lines
+
+- name: list installed collections
+ command: 'ansible-galaxy collection list'
+ register: installed_collections
+
+- assert:
+ that:
+ - "'namespace_1.collection_1' in installed_collections.stdout"
+ - "'namespace_2.collection_2' in installed_collections.stdout"
+
+- include_tasks: ./empty_installed_collections.yml
+ when: cleanup
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/tasks/setup.yml b/test/integration/targets/ansible-galaxy-collection-scm/tasks/setup.yml
new file mode 100644
index 0000000..bb882c9
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/tasks/setup.yml
@@ -0,0 +1,19 @@
+- name: ensure git is installed
+ package:
+ name: git
+ when: ansible_distribution not in ["MacOSX", "Alpine"]
+ register: git_install
+
+- name: set git global user.email if not already set
+ shell: git config --global user.email || git config --global user.email "noreply@example.com"
+
+- name: set git global user.name if not already set
+ shell: git config --global user.name || git config --global user.name "Ansible Test Runner"
+
+- name: Create a directory for installing collections and creating git repositories
+ file:
+ path: '{{ item }}'
+ state: directory
+ loop:
+ - '{{ install_path }}'
+ - '{{ test_repo_path }}'
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/tasks/setup_collection_bad_version.yml b/test/integration/targets/ansible-galaxy-collection-scm/tasks/setup_collection_bad_version.yml
new file mode 100644
index 0000000..0ef406e
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/tasks/setup_collection_bad_version.yml
@@ -0,0 +1,47 @@
+- name: Initialize a git repo
+ command: 'git init {{ test_error_repo_path }}'
+
+- stat:
+ path: "{{ test_error_repo_path }}"
+
+- name: Add a collection to the repository
+ command: 'ansible-galaxy collection init {{ item }}'
+ args:
+ chdir: '{{ scm_path }}'
+ loop:
+ - error_test.float_version_collection
+ - error_test.not_semantic_version_collection
+ - error_test.list_version_collection
+ - error_test.dict_version_collection
+
+- name: Add an invalid float version to a collection
+ lineinfile:
+ path: '{{ test_error_repo_path }}/float_version_collection/galaxy.yml'
+ regexp: '^version'
+ line: "version: 1.0" # Version is a float, not a string as expected
+
+- name: Add an invalid non-semantic string version a collection
+ lineinfile:
+ path: '{{ test_error_repo_path }}/not_semantic_version_collection/galaxy.yml'
+ regexp: '^version'
+ line: "version: '1.0'" # Version is a string, but not a semantic version as expected
+
+- name: Add an invalid list version to a collection
+ lineinfile:
+ path: '{{ test_error_repo_path }}/list_version_collection/galaxy.yml'
+ regexp: '^version'
+ line: "version: ['1.0.0']" # Version is a list, not a string as expected
+
+- name: Add an invalid version to a collection
+ lineinfile:
+ path: '{{ test_error_repo_path }}/dict_version_collection/galaxy.yml'
+ regexp: '^version'
+ line: "version: {'broken': 'version'}" # Version is a dict, not a string as expected
+
+- name: Commit the changes
+ command: '{{ item }}'
+ args:
+ chdir: '{{ test_error_repo_path }}'
+ loop:
+ - git add ./
+ - git commit -m 'add collections'
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/tasks/setup_multi_collection_repo.yml b/test/integration/targets/ansible-galaxy-collection-scm/tasks/setup_multi_collection_repo.yml
new file mode 100644
index 0000000..e733a84
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/tasks/setup_multi_collection_repo.yml
@@ -0,0 +1,70 @@
+- name: Initialize a git repo
+ command: 'git init {{ test_repo_path }}'
+
+- stat:
+ path: "{{ test_repo_path }}"
+
+- name: Add a couple collections to the repository
+ command: 'ansible-galaxy collection init {{ item }}'
+ args:
+ chdir: '{{ scm_path }}'
+ loop:
+ - 'ansible_test.collection_1'
+ - 'ansible_test.collection_2'
+
+- name: Preserve the (empty) docs directory for the SCM collection
+ file:
+ path: '{{ test_repo_path }}/{{ item }}/docs/README.md'
+ state: touch
+ loop:
+ - collection_1
+ - collection_2
+
+- name: Preserve the (empty) roles directory for the SCM collection
+ file:
+ path: '{{ test_repo_path }}/{{ item }}/roles/README.md'
+ state: touch
+ loop:
+ - collection_1
+ - collection_2
+
+- name: create extra files and folders to test build_ignore
+ file:
+ path: '{{ test_repo_path }}/collection_1/{{ item.name }}'
+ state: '{{ item.state }}'
+ loop:
+ - name: foo.txt
+ state: touch
+ - name: foobar
+ state: directory
+ - name: foobar/qux
+ state: directory
+ - name: foobar/qux/bar
+ state: touch
+ - name: foobar/baz.txt
+ state: touch
+
+- name: Add collection_2 as a dependency of collection_1
+ lineinfile:
+ path: '{{ test_repo_path }}/collection_1/galaxy.yml'
+ regexp: '^dependencies'
+ line: "dependencies: {'git+file://{{ test_repo_path }}/.git#collection_2/': '*'}"
+
+- name: Ignore a directory and files
+ lineinfile:
+ path: '{{ test_repo_path }}/collection_1/galaxy.yml'
+ regexp: '^build_ignore'
+ line: "build_ignore: ['foo.txt', 'foobar/*']"
+
+- name: Commit the changes
+ command: '{{ item }}'
+ args:
+ chdir: '{{ test_repo_path }}'
+ loop:
+ - git add ./
+ - git commit -m 'add collections'
+
+- name: Build the actual artifact for ansible_test.collection_1 for comparison
+ command: "ansible-galaxy collection build ansible_test/collection_1 --output-path {{ galaxy_dir }}"
+ args:
+ chdir: "{{ scm_path }}"
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/tasks/setup_recursive_scm_dependency.yml b/test/integration/targets/ansible-galaxy-collection-scm/tasks/setup_recursive_scm_dependency.yml
new file mode 100644
index 0000000..dd307d7
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/tasks/setup_recursive_scm_dependency.yml
@@ -0,0 +1,33 @@
+- name: Initialize git repositories
+ command: 'git init {{ scm_path }}/{{ item }}'
+ loop:
+ - namespace_1
+ - namespace_2
+
+- name: Add a couple collections to the repository
+ command: 'ansible-galaxy collection init {{ item }}'
+ args:
+ chdir: '{{ scm_path }}'
+ loop:
+ - 'namespace_1.collection_1'
+ - 'namespace_2.collection_2'
+
+- name: Add collection_2 as a dependency of collection_1
+ lineinfile:
+ path: '{{ scm_path }}/namespace_1/collection_1/galaxy.yml'
+ regexp: '^dependencies'
+ line: "dependencies: {'git+file://{{ scm_path }}/namespace_2/.git#collection_2/': '*'}"
+
+- name: Add collection_1 as a dependency on collection_2
+ lineinfile:
+ path: '{{ scm_path }}/namespace_2/collection_2/galaxy.yml'
+ regexp: '^dependencies'
+ line: "dependencies: {'git+file://{{ scm_path }}/namespace_1/.git#collection_1/': 'master'}"
+
+- name: Commit the changes
+ shell: git add ./; git commit -m 'add collection'
+ args:
+ chdir: '{{ scm_path }}/{{ item }}'
+ loop:
+ - namespace_1
+ - namespace_2
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/tasks/test_invalid_version.yml b/test/integration/targets/ansible-galaxy-collection-scm/tasks/test_invalid_version.yml
new file mode 100644
index 0000000..1f22bb8
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/tasks/test_invalid_version.yml
@@ -0,0 +1,58 @@
+- block:
+ - name: test installing a collection with an invalid float version value
+ command: 'ansible-galaxy collection install git+file://{{ test_error_repo_path }}/.git#float_version_collection -vvvvvv'
+ ignore_errors: yes
+ register: invalid_version_result
+
+ - assert:
+ that:
+ - invalid_version_result is failed
+ - msg in invalid_version_result.stderr
+ vars:
+ req: error_test.float_version_collection:1.0
+ ver: "1.0 (<class 'float'>)"
+ msg: "Invalid version found for the collection '{{ req }}': {{ ver }}. A SemVer-compliant version or '*' is required."
+
+ - name: test installing a collection with an invalid non-SemVer string version value
+ command: 'ansible-galaxy collection install git+file://{{ test_error_repo_path }}/.git#not_semantic_version_collection -vvvvvv'
+ ignore_errors: yes
+ register: invalid_version_result
+
+ - assert:
+ that:
+ - invalid_version_result is failed
+ - msg in invalid_version_result.stderr
+ vars:
+ req: error_test.not_semantic_version_collection:1.0
+ ver: "1.0 (<class 'str'>)"
+ msg: "Invalid version found for the collection '{{ req }}': {{ ver }}. A SemVer-compliant version or '*' is required."
+
+ - name: test installing a collection with an invalid list version value
+ command: 'ansible-galaxy collection install git+file://{{ test_error_repo_path }}/.git#list_version_collection -vvvvvv'
+ ignore_errors: yes
+ register: invalid_version_result
+
+ - assert:
+ that:
+ - invalid_version_result is failed
+ - msg in invalid_version_result.stderr
+ vars:
+ req: "error_test.list_version_collection:['1.0.0']"
+ msg: "Invalid version found for the collection '{{ req }}'. A SemVer-compliant version or '*' is required."
+
+ - name: test installing a collection with an invalid dict version value
+ command: 'ansible-galaxy collection install git+file://{{ test_error_repo_path }}/.git#dict_version_collection -vvvvvv'
+ ignore_errors: yes
+ register: invalid_version_result
+
+ - assert:
+ that:
+ - invalid_version_result is failed
+ - msg in invalid_version_result.stderr
+ vars:
+ req: "error_test.dict_version_collection:{'broken': 'version'}"
+ msg: "Invalid version found for the collection '{{ req }}'. A SemVer-compliant version or '*' is required."
+
+ always:
+ - include_tasks: ./empty_installed_collections.yml
+ when: cleanup
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/tasks/test_manifest_metadata.yml b/test/integration/targets/ansible-galaxy-collection-scm/tasks/test_manifest_metadata.yml
new file mode 100644
index 0000000..a01551c
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/tasks/test_manifest_metadata.yml
@@ -0,0 +1,55 @@
+- name: Test installing a collection from a git repo containing a MANIFEST.json
+ block:
+ - name: Create a temp directory for building the collection
+ file:
+ path: '{{ galaxy_dir }}/scratch'
+ state: directory
+
+ - name: Initialize a collection
+ command: 'ansible-galaxy collection init namespace_3.collection_1'
+ args:
+ chdir: '{{ galaxy_dir }}/scratch'
+
+ - name: Build the collection
+ command: 'ansible-galaxy collection build namespace_3/collection_1'
+ args:
+ chdir: '{{ galaxy_dir }}/scratch'
+
+ - name: Initialize git repository
+ command: 'git init {{ scm_path }}/namespace_3'
+
+ - name: Create the destination for the collection
+ file:
+ path: '{{ scm_path }}/namespace_3/collection_1'
+ state: directory
+
+ - name: Unarchive the collection in the git repo
+ unarchive:
+ dest: '{{ scm_path }}/namespace_3/collection_1'
+ src: '{{ galaxy_dir }}/scratch/namespace_3-collection_1-1.0.0.tar.gz'
+ remote_src: yes
+
+ - name: Commit the changes
+ shell: git add ./; git commit -m 'add collection'
+ args:
+ chdir: '{{ scm_path }}/namespace_3'
+
+ - name: Install the collection in the git repository
+ command: 'ansible-galaxy collection install git+file://{{ scm_path }}/namespace_3/.git'
+ register: result
+
+ - name: Assert the collection was installed successfully
+ assert:
+ that:
+ - '"namespace_3.collection_1:1.0.0 was installed successfully" in result.stdout_lines'
+
+ always:
+ - name: Clean up directories from test
+ file:
+ path: '{{ galaxy_dir }}/scratch'
+ state: absent
+ loop:
+ - '{{ galaxy_dir }}/scratch'
+ - '{{ scm_path }}/namespace_3'
+
+ - include_tasks: ./empty_installed_collections.yml
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/tasks/test_supported_resolvelib_versions.yml b/test/integration/targets/ansible-galaxy-collection-scm/tasks/test_supported_resolvelib_versions.yml
new file mode 100644
index 0000000..029cbb3
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/tasks/test_supported_resolvelib_versions.yml
@@ -0,0 +1,25 @@
+- vars:
+ venv_cmd: "{{ ansible_python_interpreter ~ ' -m venv' }}"
+ venv_dest: "{{ galaxy_dir }}/test_venv_{{ resolvelib_version }}"
+ block:
+ - name: install another version of resolvelib that is supported by ansible-galaxy
+ pip:
+ name: resolvelib
+ version: "{{ resolvelib_version }}"
+ state: present
+ virtualenv_command: "{{ venv_cmd }}"
+ virtualenv: "{{ venv_dest }}"
+ virtualenv_site_packages: True
+
+ - include_tasks: ./scm_dependency_deduplication.yml
+ args:
+ apply:
+ environment:
+ PATH: "{{ venv_dest }}/bin:{{ ansible_env.PATH }}"
+ ANSIBLE_CONFIG: '{{ galaxy_dir }}/ansible.cfg'
+
+ always:
+ - name: remove test venv
+ file:
+ path: "{{ venv_dest }}"
+ state: absent
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/templates/git_prefix_name.yml b/test/integration/targets/ansible-galaxy-collection-scm/templates/git_prefix_name.yml
new file mode 100644
index 0000000..8b0e788
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/templates/git_prefix_name.yml
@@ -0,0 +1,2 @@
+collections:
+ - name: git+file://{{ scm_path }}/amazon.aws/.git
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/templates/name_and_type.yml b/test/integration/targets/ansible-galaxy-collection-scm/templates/name_and_type.yml
new file mode 100644
index 0000000..8f6be46
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/templates/name_and_type.yml
@@ -0,0 +1,3 @@
+collections:
+ - name: file://{{ scm_path }}/amazon.aws/.git
+ type: git
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/templates/name_without_type.yml b/test/integration/targets/ansible-galaxy-collection-scm/templates/name_without_type.yml
new file mode 100644
index 0000000..9d340b5
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/templates/name_without_type.yml
@@ -0,0 +1,3 @@
+collections:
+ # should not work: git prefix or type is required
+ - name: file://{{ scm_path }}/amazon.aws/.git
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/templates/source_and_name.yml b/test/integration/targets/ansible-galaxy-collection-scm/templates/source_and_name.yml
new file mode 100644
index 0000000..b7bdb27
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/templates/source_and_name.yml
@@ -0,0 +1,4 @@
+collections:
+ # should not work: source is expected to be a galaxy server name or URL
+ - source: git+file://{{ scm_path }}/amazon.aws/.git
+ name: ansible.nope
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/templates/source_and_name_and_type.yml b/test/integration/targets/ansible-galaxy-collection-scm/templates/source_and_name_and_type.yml
new file mode 100644
index 0000000..01a2ea2
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/templates/source_and_name_and_type.yml
@@ -0,0 +1,5 @@
+collections:
+ # should not work: source is expected to be a galaxy server name or URL
+ - source: git+file://{{ scm_path }}/amazon.aws/.git
+ name: ansible.nope
+ type: git
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/templates/source_only.yml b/test/integration/targets/ansible-galaxy-collection-scm/templates/source_only.yml
new file mode 100644
index 0000000..74f8708
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/templates/source_only.yml
@@ -0,0 +1,3 @@
+collections:
+ # should not work: source is expected to be a galaxy server name or URL
+ - source: git+file://{{ scm_path }}/amazon.aws/.git
diff --git a/test/integration/targets/ansible-galaxy-collection-scm/vars/main.yml b/test/integration/targets/ansible-galaxy-collection-scm/vars/main.yml
new file mode 100644
index 0000000..cd198c6
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection-scm/vars/main.yml
@@ -0,0 +1,11 @@
+install_path: "{{ galaxy_dir }}/collections/ansible_collections"
+alt_install_path: "{{ galaxy_dir }}/other_collections/ansible_collections"
+scm_path: "{{ galaxy_dir }}/development"
+test_repo_path: "{{ galaxy_dir }}/development/ansible_test"
+test_error_repo_path: "{{ galaxy_dir }}/development/error_test"
+
+supported_resolvelib_versions:
+ - "0.5.3" # Oldest supported
+ - "0.6.0"
+ - "0.7.0"
+ - "0.8.0"
diff --git a/test/integration/targets/ansible-galaxy-collection/aliases b/test/integration/targets/ansible-galaxy-collection/aliases
new file mode 100644
index 0000000..6c57208
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection/aliases
@@ -0,0 +1,4 @@
+shippable/galaxy/group1
+shippable/galaxy/smoketest
+cloud/galaxy
+context/controller
diff --git a/test/integration/targets/ansible-galaxy-collection/files/build_bad_tar.py b/test/integration/targets/ansible-galaxy-collection/files/build_bad_tar.py
new file mode 100644
index 0000000..6182e86
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection/files/build_bad_tar.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python
+
+# Copyright: (c) 2020, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import hashlib
+import io
+import json
+import os
+import sys
+import tarfile
+
+manifest = {
+ 'collection_info': {
+ 'namespace': 'suspicious',
+ 'name': 'test',
+ 'version': '1.0.0',
+ 'dependencies': {},
+ },
+ 'file_manifest_file': {
+ 'name': 'FILES.json',
+ 'ftype': 'file',
+ 'chksum_type': 'sha256',
+ 'chksum_sha256': None,
+ 'format': 1
+ },
+ 'format': 1,
+}
+
+files = {
+ 'files': [
+ {
+ 'name': '.',
+ 'ftype': 'dir',
+ 'chksum_type': None,
+ 'chksum_sha256': None,
+ 'format': 1,
+ },
+ ],
+ 'format': 1,
+}
+
+
+def add_file(tar_file, filename, b_content, update_files=True):
+ tar_info = tarfile.TarInfo(filename)
+ tar_info.size = len(b_content)
+ tar_info.mode = 0o0755
+ tar_file.addfile(tarinfo=tar_info, fileobj=io.BytesIO(b_content))
+
+ if update_files:
+ sha256 = hashlib.sha256()
+ sha256.update(b_content)
+
+ files['files'].append({
+ 'name': filename,
+ 'ftype': 'file',
+ 'chksum_type': 'sha256',
+ 'chksum_sha256': sha256.hexdigest(),
+ 'format': 1
+ })
+
+
+collection_tar = os.path.join(sys.argv[1], 'suspicious-test-1.0.0.tar.gz')
+with tarfile.open(collection_tar, mode='w:gz') as tar_file:
+ add_file(tar_file, '../../outside.sh', b"#!/usr/bin/env bash\necho \"you got pwned\"")
+
+ b_files = json.dumps(files).encode('utf-8')
+ b_files_hash = hashlib.sha256()
+ b_files_hash.update(b_files)
+ manifest['file_manifest_file']['chksum_sha256'] = b_files_hash.hexdigest()
+ add_file(tar_file, 'FILES.json', b_files)
+ add_file(tar_file, 'MANIFEST.json', json.dumps(manifest).encode('utf-8'))
+
+ b_manifest = json.dumps(manifest).encode('utf-8')
+
+ for name, b in [('MANIFEST.json', b_manifest), ('FILES.json', b_files)]:
+ b_io = io.BytesIO(b)
+ tar_info = tarfile.TarInfo(name)
+ tar_info.size = len(b)
+ tar_info.mode = 0o0644
+ tar_file.addfile(tarinfo=tar_info, fileobj=b_io)
diff --git a/test/integration/targets/ansible-galaxy-collection/files/test_module.py b/test/integration/targets/ansible-galaxy-collection/files/test_module.py
new file mode 100644
index 0000000..d7e4814
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection/files/test_module.py
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# (c) 2016, Toshio Kuratomi <tkuratomi@ansible.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: ping
+version_added: historical
+short_description: Try to connect to host, verify a usable python and return C(pong) on success
+description:
+ - A trivial test module, this module always returns C(pong) on successful
+ contact. It does not make sense in playbooks, but it is useful from
+ C(/usr/bin/ansible) to verify the ability to login and that a usable Python is configured.
+ - This is NOT ICMP ping, this is just a trivial test module that requires Python on the remote-node.
+ - For Windows targets, use the M(ansible.windows.win_ping) module instead.
+ - For Network targets, use the M(ansible.netcommon.net_ping) module instead.
+options:
+ data:
+ description:
+ - Data to return for the C(ping) return value.
+ - If this parameter is set to C(crash), the module will cause an exception.
+ type: str
+ default: pong
+seealso:
+- module: ansible.netcommon.net_ping
+- module: ansible.windows.win_ping
+author:
+ - Ansible Core Team
+ - Michael DeHaan
+'''
+
+EXAMPLES = '''
+# Test we can logon to 'webservers' and execute python with json lib.
+# ansible webservers -m ping
+
+- name: Example from an Ansible Playbook
+ ping:
+
+- name: Induce an exception to see what happens
+ ping:
+ data: crash
+'''
+
+RETURN = '''
+ping:
+ description: value provided with the data parameter
+ returned: success
+ type: str
+ sample: pong
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ data=dict(type='str', default='pong'),
+ ),
+ supports_check_mode=True
+ )
+
+ if module.params['data'] == 'crash':
+ raise Exception("boom")
+
+ result = dict(
+ ping=module.params['data'],
+ )
+
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-galaxy-collection/library/reset_pulp.py b/test/integration/targets/ansible-galaxy-collection/library/reset_pulp.py
new file mode 100644
index 0000000..53c29f7
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection/library/reset_pulp.py
@@ -0,0 +1,211 @@
+#!/usr/bin/python
+
+# Copyright: (c) 2020, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+---
+module: reset_pulp
+short_description: Resets pulp back to the initial state
+description:
+- See short_description
+options:
+ pulp_api:
+ description:
+ - The Pulp API endpoint.
+ required: yes
+ type: str
+ galaxy_ng_server:
+ description:
+ - The Galaxy NG API endpoint.
+ required: yes
+ type: str
+ url_username:
+ description:
+ - The username to use when authenticating against Pulp.
+ required: yes
+ type: str
+ url_password:
+ description:
+ - The password to use when authenticating against Pulp.
+ required: yes
+ type: str
+ repositories:
+ description:
+ - A list of pulp repositories to create.
+ - Galaxy NG expects a repository that matches C(GALAXY_API_DEFAULT_DISTRIBUTION_BASE_PATH) in
+ C(/etc/pulp/settings.py) or the default of C(published).
+ required: yes
+ type: list
+ elements: str
+ namespaces:
+ description:
+ - A list of namespaces to create for Galaxy NG.
+ required: yes
+ type: list
+ elements: str
+author:
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = '''
+- name: reset pulp content
+ reset_pulp:
+ pulp_api: http://galaxy:24817
+ galaxy_ng_server: http://galaxy/api/galaxy/
+ url_username: username
+ url_password: password
+ repository: published
+ namespaces:
+ - namespace1
+ - namespace2
+'''
+
+RETURN = '''
+#
+'''
+
+import json
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.urls import fetch_url
+from ansible.module_utils.common.text.converters import to_text
+
+
+def invoke_api(module, url, method='GET', data=None, status_codes=None):
+ status_codes = status_codes or [200]
+ headers = {}
+ if data:
+ headers['Content-Type'] = 'application/json'
+ data = json.dumps(data)
+
+ resp, info = fetch_url(module, url, method=method, data=data, headers=headers)
+ if info['status'] not in status_codes:
+ module.fail_json(url=url, **info)
+
+ data = to_text(resp.read())
+ if data:
+ return json.loads(data)
+
+
+def delete_galaxy_namespace(namespace, module):
+ """ Deletes the galaxy ng namespace specified. """
+ ns_uri = '%sv3/namespaces/%s/' % (module.params['galaxy_ng_server'], namespace)
+ invoke_api(module, ns_uri, method='DELETE', status_codes=[204])
+
+
+def delete_pulp_distribution(distribution, module):
+ """ Deletes the pulp distribution at the URI specified. """
+ task_info = invoke_api(module, distribution, method='DELETE', status_codes=[202])
+ wait_pulp_task(task_info['task'], module)
+
+
+def delete_pulp_orphans(module):
+ """ Deletes any orphaned pulp objects. """
+ orphan_uri = module.params['pulp_api'] + '/pulp/api/v3/orphans/'
+ task_info = invoke_api(module, orphan_uri, method='DELETE', status_codes=[202])
+ wait_pulp_task(task_info['task'], module)
+
+
+def delete_pulp_repository(repository, module):
+ """ Deletes the pulp repository at the URI specified. """
+ task_info = invoke_api(module, repository, method='DELETE', status_codes=[202])
+ wait_pulp_task(task_info['task'], module)
+
+
+def get_galaxy_namespaces(module):
+ """ Gets a list of galaxy namespaces. """
+ # No pagination has been implemented, shouldn't need unless we ever exceed 100 namespaces.
+ namespace_uri = module.params['galaxy_ng_server'] + 'v3/namespaces/?limit=100&offset=0'
+ ns_info = invoke_api(module, namespace_uri)
+
+ return [n['name'] for n in ns_info['data']]
+
+
+def get_pulp_distributions(module):
+ """ Gets a list of all the pulp distributions. """
+ distro_uri = module.params['pulp_api'] + '/pulp/api/v3/distributions/ansible/ansible/'
+ distro_info = invoke_api(module, distro_uri)
+ return [module.params['pulp_api'] + r['pulp_href'] for r in distro_info['results']]
+
+
+def get_pulp_repositories(module):
+ """ Gets a list of all the pulp repositories. """
+ repo_uri = module.params['pulp_api'] + '/pulp/api/v3/repositories/ansible/ansible/'
+ repo_info = invoke_api(module, repo_uri)
+ return [module.params['pulp_api'] + r['pulp_href'] for r in repo_info['results']]
+
+
+def new_galaxy_namespace(name, module):
+ """ Creates a new namespace in Galaxy NG. """
+ ns_uri = module.params['galaxy_ng_server'] + 'v3/_ui/namespaces/'
+ data = {'name': name, 'groups': [{'name': 'system:partner-engineers', 'object_permissions':
+ ['add_namespace', 'change_namespace', 'upload_to_namespace']}]}
+ ns_info = invoke_api(module, ns_uri, method='POST', data=data, status_codes=[201])
+
+ return ns_info['id']
+
+
+def new_pulp_repository(name, module):
+ """ Creates a new pulp repository. """
+ repo_uri = module.params['pulp_api'] + '/pulp/api/v3/repositories/ansible/ansible/'
+ data = {'name': name}
+ repo_info = invoke_api(module, repo_uri, method='POST', data=data, status_codes=[201])
+
+ return module.params['pulp_api'] + repo_info['pulp_href']
+
+
+def new_pulp_distribution(name, base_path, repository, module):
+ """ Creates a new pulp distribution for a repository. """
+ distro_uri = module.params['pulp_api'] + '/pulp/api/v3/distributions/ansible/ansible/'
+ data = {'name': name, 'base_path': base_path, 'repository': repository}
+ task_info = invoke_api(module, distro_uri, method='POST', data=data, status_codes=[202])
+ task_info = wait_pulp_task(task_info['task'], module)
+
+ return module.params['pulp_api'] + task_info['created_resources'][0]
+
+
+def wait_pulp_task(task, module):
+ """ Waits for a pulp import task to finish. """
+ while True:
+ task_info = invoke_api(module, module.params['pulp_api'] + task)
+ if task_info['finished_at'] is not None:
+ break
+
+ return task_info
+
+
+def main():
+ module_args = dict(
+ pulp_api=dict(type='str', required=True),
+ galaxy_ng_server=dict(type='str', required=True),
+ url_username=dict(type='str', required=True),
+ url_password=dict(type='str', required=True, no_log=True),
+ repositories=dict(type='list', elements='str', required=True),
+ namespaces=dict(type='list', elements='str', required=True),
+ )
+
+ module = AnsibleModule(
+ argument_spec=module_args,
+ supports_check_mode=False
+ )
+ module.params['force_basic_auth'] = True
+
+ [delete_pulp_distribution(d, module) for d in get_pulp_distributions(module)]
+ [delete_pulp_repository(r, module) for r in get_pulp_repositories(module)]
+ delete_pulp_orphans(module)
+ [delete_galaxy_namespace(n, module) for n in get_galaxy_namespaces(module)]
+
+ for repository in module.params['repositories']:
+ repo_href = new_pulp_repository(repository, module)
+ new_pulp_distribution(repository, repository, repo_href, module)
+ [new_galaxy_namespace(n, module) for n in module.params['namespaces']]
+
+ module.exit_json(changed=True)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-galaxy-collection/library/setup_collections.py b/test/integration/targets/ansible-galaxy-collection/library/setup_collections.py
new file mode 100644
index 0000000..35b18de
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection/library/setup_collections.py
@@ -0,0 +1,269 @@
+#!/usr/bin/python
+
+# Copyright: (c) 2020, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = '''
+---
+module: setup_collections
+short_description: Set up test collections based on the input
+description:
+- Builds and publishes a whole bunch of collections used for testing in bulk.
+options:
+ server:
+ description:
+ - The Galaxy server to upload the collections to.
+ required: yes
+ type: str
+ token:
+ description:
+ - The token used to authenticate with the Galaxy server.
+ required: yes
+ type: str
+ collections:
+ description:
+ - A list of collection details to use for the build.
+ required: yes
+ type: list
+ elements: dict
+ options:
+ namespace:
+ description:
+ - The namespace of the collection.
+ required: yes
+ type: str
+ name:
+ description:
+ - The name of the collection.
+ required: yes
+ type: str
+ version:
+ description:
+ - The version of the collection.
+ type: str
+ default: '1.0.0'
+ dependencies:
+ description:
+ - The dependencies of the collection.
+ type: dict
+ default: '{}'
+author:
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = '''
+- name: Build test collections
+ setup_collections:
+ path: ~/ansible/collections/ansible_collections
+ collections:
+ - namespace: namespace1
+ name: name1
+ version: 0.0.1
+ - namespace: namespace1
+ name: name1
+ version: 0.0.2
+'''
+
+RETURN = '''
+#
+'''
+
+import os
+import subprocess
+import tarfile
+import tempfile
+import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils._text import to_bytes
+from functools import partial
+from multiprocessing import dummy as threading
+from multiprocessing import TimeoutError
+
+
+COLLECTIONS_BUILD_AND_PUBLISH_TIMEOUT = 120
+
+
+def publish_collection(module, collection):
+ namespace = collection['namespace']
+ name = collection['name']
+ version = collection['version']
+ dependencies = collection['dependencies']
+ use_symlink = collection['use_symlink']
+
+ result = {}
+ collection_dir = os.path.join(module.tmpdir, "%s-%s-%s" % (namespace, name, version))
+ b_collection_dir = to_bytes(collection_dir, errors='surrogate_or_strict')
+ os.mkdir(b_collection_dir)
+
+ with open(os.path.join(b_collection_dir, b'README.md'), mode='wb') as fd:
+ fd.write(b"Collection readme")
+
+ galaxy_meta = {
+ 'namespace': namespace,
+ 'name': name,
+ 'version': version,
+ 'readme': 'README.md',
+ 'authors': ['Collection author <name@email.com'],
+ 'dependencies': dependencies,
+ 'license': ['GPL-3.0-or-later'],
+ 'repository': 'https://ansible.com/',
+ }
+ with open(os.path.join(b_collection_dir, b'galaxy.yml'), mode='wb') as fd:
+ fd.write(to_bytes(yaml.safe_dump(galaxy_meta), errors='surrogate_or_strict'))
+
+ with tempfile.NamedTemporaryFile(mode='wb') as temp_fd:
+ temp_fd.write(b"data")
+
+ if use_symlink:
+ os.mkdir(os.path.join(b_collection_dir, b'docs'))
+ os.mkdir(os.path.join(b_collection_dir, b'plugins'))
+ b_target_file = b'RE\xc3\x85DM\xc3\x88.md'
+ with open(os.path.join(b_collection_dir, b_target_file), mode='wb') as fd:
+ fd.write(b'data')
+
+ os.symlink(b_target_file, os.path.join(b_collection_dir, b_target_file + b'-link'))
+ os.symlink(temp_fd.name, os.path.join(b_collection_dir, b_target_file + b'-outside-link'))
+ os.symlink(os.path.join(b'..', b_target_file), os.path.join(b_collection_dir, b'docs', b_target_file))
+ os.symlink(os.path.join(b_collection_dir, b_target_file),
+ os.path.join(b_collection_dir, b'plugins', b_target_file))
+ os.symlink(b'docs', os.path.join(b_collection_dir, b'docs-link'))
+
+ release_filename = '%s-%s-%s.tar.gz' % (namespace, name, version)
+ collection_path = os.path.join(collection_dir, release_filename)
+ rc, stdout, stderr = module.run_command(['ansible-galaxy', 'collection', 'build'], cwd=collection_dir)
+ result['build'] = {
+ 'rc': rc,
+ 'stdout': stdout,
+ 'stderr': stderr,
+ }
+
+ if module.params['signature_dir'] is not None:
+ # To test user-provided signatures, we need to sign the MANIFEST.json before publishing
+
+ # Extract the tarfile to sign the MANIFEST.json
+ with tarfile.open(collection_path, mode='r') as collection_tar:
+ collection_tar.extractall(path=os.path.join(collection_dir, '%s-%s-%s' % (namespace, name, version)))
+
+ manifest_path = os.path.join(collection_dir, '%s-%s-%s' % (namespace, name, version), 'MANIFEST.json')
+ signature_path = os.path.join(module.params['signature_dir'], '%s-%s-%s-MANIFEST.json.asc' % (namespace, name, version))
+ sign_manifest(signature_path, manifest_path, module, result)
+
+ # Create the tarfile containing the signed MANIFEST.json
+ with tarfile.open(collection_path, "w:gz") as tar:
+ tar.add(os.path.join(collection_dir, '%s-%s-%s' % (namespace, name, version)), arcname=os.path.sep)
+
+ publish_args = ['ansible-galaxy', 'collection', 'publish', collection_path, '--server', module.params['server']]
+ if module.params['token']:
+ publish_args.extend(['--token', module.params['token']])
+
+ rc, stdout, stderr = module.run_command(publish_args)
+ result['publish'] = {
+ 'rc': rc,
+ 'stdout': stdout,
+ 'stderr': stderr,
+ }
+
+ return result
+
+
+def sign_manifest(signature_path, manifest_path, module, collection_setup_result):
+ collection_setup_result['gpg_detach_sign'] = {'signature_path': signature_path}
+
+ status_fd_read, status_fd_write = os.pipe()
+ gpg_cmd = [
+ "gpg",
+ "--batch",
+ "--pinentry-mode",
+ "loopback",
+ "--yes",
+ "--homedir",
+ module.params['signature_dir'],
+ "--detach-sign",
+ "--armor",
+ "--output",
+ signature_path,
+ manifest_path,
+ ]
+ try:
+ p = subprocess.Popen(
+ gpg_cmd,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ pass_fds=(status_fd_write,),
+ encoding='utf8',
+ )
+ except (FileNotFoundError, subprocess.SubprocessError) as err:
+ collection_setup_result['gpg_detach_sign']['error'] = "Failed during GnuPG verification with command '{gpg_cmd}': {err}".format(
+ gpg_cmd=gpg_cmd, err=err
+ )
+ else:
+ stdout, stderr = p.communicate()
+ collection_setup_result['gpg_detach_sign']['stdout'] = stdout
+ if stderr:
+ error = "Failed during GnuPG verification with command '{gpg_cmd}':\n{stderr}".format(gpg_cmd=gpg_cmd, stderr=stderr)
+ collection_setup_result['gpg_detach_sign']['error'] = error
+ finally:
+ os.close(status_fd_write)
+
+
+def run_module():
+ module_args = dict(
+ server=dict(type='str', required=True),
+ token=dict(type='str'),
+ collections=dict(
+ type='list',
+ elements='dict',
+ required=True,
+ options=dict(
+ namespace=dict(type='str', required=True),
+ name=dict(type='str', required=True),
+ version=dict(type='str', default='1.0.0'),
+ dependencies=dict(type='dict', default={}),
+ use_symlink=dict(type='bool', default=False),
+ ),
+ ),
+ signature_dir=dict(type='path', default=None),
+ )
+
+ module = AnsibleModule(
+ argument_spec=module_args,
+ supports_check_mode=False
+ )
+
+ result = dict(changed=True, results=[])
+
+ pool = threading.Pool(4)
+ publish_func = partial(publish_collection, module)
+ try:
+ result['results'] = pool.map_async(
+ publish_func, module.params['collections'],
+ ).get(timeout=COLLECTIONS_BUILD_AND_PUBLISH_TIMEOUT)
+ except TimeoutError as timeout_err:
+ module.fail_json(
+ 'Timed out waiting for collections to be provisioned.',
+ )
+
+ failed = bool(sum(
+ r['build']['rc'] + r['publish']['rc'] for r in result['results']
+ ))
+
+ module.exit_json(failed=failed, **result)
+
+
+def main():
+ run_module()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-galaxy-collection/meta/main.yml b/test/integration/targets/ansible-galaxy-collection/meta/main.yml
new file mode 100644
index 0000000..ca46bea
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection/meta/main.yml
@@ -0,0 +1,4 @@
+---
+dependencies:
+- setup_remote_tmp_dir
+- setup_pexpect
diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/build.yml b/test/integration/targets/ansible-galaxy-collection/tasks/build.yml
new file mode 100644
index 0000000..8140d46
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection/tasks/build.yml
@@ -0,0 +1,74 @@
+---
+- name: build basic collection based on current directory
+ command: ansible-galaxy collection build {{ galaxy_verbosity }}
+ args:
+ chdir: '{{ galaxy_dir }}/scratch/ansible_test/my_collection'
+ register: build_current_dir
+
+- name: get result of build basic collection on current directory
+ stat:
+ path: '{{ galaxy_dir }}/scratch/ansible_test/my_collection/ansible_test-my_collection-1.0.0.tar.gz'
+ register: build_current_dir_actual
+
+- name: assert build basic collection based on current directory
+ assert:
+ that:
+ - '"Created collection for ansible_test.my_collection" in build_current_dir.stdout'
+ - build_current_dir_actual.stat.exists
+
+- name: build basic collection based on relative dir
+ command: ansible-galaxy collection build scratch/ansible_test/my_collection {{ galaxy_verbosity }}
+ args:
+ chdir: '{{ galaxy_dir }}'
+ register: build_relative_dir
+
+- name: get result of build basic collection based on relative dir
+ stat:
+ path: '{{ galaxy_dir }}/ansible_test-my_collection-1.0.0.tar.gz'
+ register: build_relative_dir_actual
+
+- name: assert build basic collection based on relative dir
+ assert:
+ that:
+ - '"Created collection for ansible_test.my_collection" in build_relative_dir.stdout'
+ - build_relative_dir_actual.stat.exists
+
+- name: fail to build existing collection without force
+ command: ansible-galaxy collection build scratch/ansible_test/my_collection {{ galaxy_verbosity }}
+ args:
+ chdir: '{{ galaxy_dir }}'
+ ignore_errors: yes
+ register: build_existing_no_force
+
+- name: build existing collection with force
+ command: ansible-galaxy collection build scratch/ansible_test/my_collection --force {{ galaxy_verbosity }}
+ args:
+ chdir: '{{ galaxy_dir }}'
+ register: build_existing_force
+
+- name: assert build existing collection
+ assert:
+ that:
+ - '"use --force to re-create the collection artifact" in build_existing_no_force.stderr'
+ - '"Created collection for ansible_test.my_collection" in build_existing_force.stdout'
+
+- name: build collection containing ignored files
+ command: ansible-galaxy collection build
+ args:
+ chdir: '{{ galaxy_dir }}/scratch/ansible_test/ignore'
+
+- name: list the created tar contents
+ command: tar -tf {{ galaxy_dir }}/scratch/ansible_test/ignore/ansible_test-ignore-1.0.0.tar.gz
+ register: tar_output
+
+- name: assert ignored files are not present in the archive
+ assert:
+ that:
+ - item not in tar_output.stdout_lines
+ loop: '{{ collection_ignored_files }}'
+
+- name: assert ignored directories are not present in the archive
+ assert:
+ that:
+ - item not in tar_output.stdout_lines
+ loop: '{{ collection_ignored_directories }}'
diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/download.yml b/test/integration/targets/ansible-galaxy-collection/tasks/download.yml
new file mode 100644
index 0000000..b651a73
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection/tasks/download.yml
@@ -0,0 +1,176 @@
+---
+- name: create test download dir
+ file:
+ path: '{{ galaxy_dir }}/download'
+ state: directory
+
+- name: download collection with multiple dependencies with --no-deps
+ command: ansible-galaxy collection download parent_dep.parent_collection:1.0.0 --no-deps -s pulp_v2 {{ galaxy_verbosity }}
+ register: download_collection
+ args:
+ chdir: '{{ galaxy_dir }}/download'
+
+- name: get result of download collection with multiple dependencies
+ find:
+ path: '{{ galaxy_dir }}/download/collections'
+ file_type: file
+ register: download_collection_actual
+
+- name: assert download collection with multiple dependencies --no-deps
+ assert:
+ that:
+ - >-
+ "Downloading collection 'parent_dep.parent_collection:1.0.0' to '/tmp/"
+ in download_collection.stdout
+ - >-
+ "Downloading collection 'child_dep.child_collection"
+ not in download_collection.stdout
+ - >-
+ "Downloading collection 'child_dep.child_dep2"
+ not in download_collection.stdout
+ - download_collection_actual.examined == 2
+ - download_collection_actual.matched == 2
+ - (download_collection_actual.files[0].path | basename) in ['requirements.yml', 'parent_dep-parent_collection-1.0.0.tar.gz']
+ - (download_collection_actual.files[1].path | basename) in ['requirements.yml', 'parent_dep-parent_collection-1.0.0.tar.gz']
+
+- name: download collection with multiple dependencies
+ command: ansible-galaxy collection download parent_dep.parent_collection:1.0.0 -s pulp_v2 {{ galaxy_verbosity }}
+ register: download_collection
+ args:
+ chdir: '{{ galaxy_dir }}/download'
+
+- name: get result of download collection with multiple dependencies
+ find:
+ path: '{{ galaxy_dir }}/download/collections'
+ file_type: file
+ register: download_collection_actual
+
+- name: assert download collection with multiple dependencies
+ assert:
+ that:
+ - '"Downloading collection ''parent_dep.parent_collection:1.0.0'' to" in download_collection.stdout'
+ - '"Downloading collection ''child_dep.child_collection:0.9.9'' to" in download_collection.stdout'
+ - '"Downloading collection ''child_dep.child_dep2:1.2.2'' to" in download_collection.stdout'
+ - download_collection_actual.examined == 4
+ - download_collection_actual.matched == 4
+ - (download_collection_actual.files[0].path | basename) in ['requirements.yml', 'child_dep-child_dep2-1.2.2.tar.gz', 'child_dep-child_collection-0.9.9.tar.gz', 'parent_dep-parent_collection-1.0.0.tar.gz']
+ - (download_collection_actual.files[1].path | basename) in ['requirements.yml', 'child_dep-child_dep2-1.2.2.tar.gz', 'child_dep-child_collection-0.9.9.tar.gz', 'parent_dep-parent_collection-1.0.0.tar.gz']
+ - (download_collection_actual.files[2].path | basename) in ['requirements.yml', 'child_dep-child_dep2-1.2.2.tar.gz', 'child_dep-child_collection-0.9.9.tar.gz', 'parent_dep-parent_collection-1.0.0.tar.gz']
+ - (download_collection_actual.files[3].path | basename) in ['requirements.yml', 'child_dep-child_dep2-1.2.2.tar.gz', 'child_dep-child_collection-0.9.9.tar.gz', 'parent_dep-parent_collection-1.0.0.tar.gz']
+
+- name: test install of download requirements file
+ command: ansible-galaxy collection install -r requirements.yml -p '{{ galaxy_dir }}/download' {{ galaxy_verbosity }}
+ args:
+ chdir: '{{ galaxy_dir }}/download/collections'
+ register: install_download
+
+- name: get result of test install of download requirements file
+ slurp:
+ path: '{{ galaxy_dir }}/download/ansible_collections/{{ collection.namespace }}/{{ collection.name }}/MANIFEST.json'
+ register: install_download_actual
+ loop_control:
+ loop_var: collection
+ loop:
+ - namespace: parent_dep
+ name: parent_collection
+ - namespace: child_dep
+ name: child_collection
+ - namespace: child_dep
+ name: child_dep2
+
+- name: assert test install of download requirements file
+ assert:
+ that:
+ - '"Installing ''parent_dep.parent_collection:1.0.0'' to" in install_download.stdout'
+ - '"Installing ''child_dep.child_collection:0.9.9'' to" in install_download.stdout'
+ - '"Installing ''child_dep.child_dep2:1.2.2'' to" in install_download.stdout'
+ - (install_download_actual.results[0].content | b64decode | from_json).collection_info.version == '1.0.0'
+ - (install_download_actual.results[1].content | b64decode | from_json).collection_info.version == '0.9.9'
+ - (install_download_actual.results[2].content | b64decode | from_json).collection_info.version == '1.2.2'
+
+- name: create test requirements file for download
+ copy:
+ content: |
+ collections:
+ - name: namespace1.name1
+ version: 1.1.0-beta.1
+
+ dest: '{{ galaxy_dir }}/download/download.yml'
+
+- name: download collection with req to custom dir
+ command: ansible-galaxy collection download -r '{{ galaxy_dir }}/download/download.yml' -s galaxy_ng -p '{{ galaxy_dir }}/download/collections-custom' {{ galaxy_verbosity }}
+ register: download_req_custom_path
+
+- name: get result of download collection with req to custom dir
+ find:
+ path: '{{ galaxy_dir }}/download/collections-custom'
+ file_type: file
+ register: download_req_custom_path_actual
+
+- name: assert download collection with multiple dependencies
+ assert:
+ that:
+ - '"Downloading collection ''namespace1.name1:1.1.0-beta.1'' to" in download_req_custom_path.stdout'
+ - download_req_custom_path_actual.examined == 2
+ - download_req_custom_path_actual.matched == 2
+ - (download_req_custom_path_actual.files[0].path | basename) in ['requirements.yml', 'namespace1-name1-1.1.0-beta.1.tar.gz']
+ - (download_req_custom_path_actual.files[1].path | basename) in ['requirements.yml', 'namespace1-name1-1.1.0-beta.1.tar.gz']
+
+# https://github.com/ansible/ansible/issues/68186
+- name: create test requirements file without roles and collections
+ copy:
+ content: |
+ collections:
+ roles:
+
+ dest: '{{ galaxy_dir }}/download/no_roles_no_collections.yml'
+
+- name: install collection with requirements
+ command: ansible-galaxy collection install -r '{{ galaxy_dir }}/download/no_roles_no_collections.yml' {{ galaxy_verbosity }}
+ register: install_no_requirements
+
+- name: assert install collection with no roles and no collections in requirements
+ assert:
+ that:
+ - '"Skipping install, no requirements found" in install_no_requirements.stdout'
+
+- name: Test downloading a tar.gz collection artifact
+ block:
+
+ - name: get result of build basic collection on current directory
+ stat:
+ path: '{{ galaxy_dir }}/scratch/ansible_test/my_collection/ansible_test-my_collection-1.0.0.tar.gz'
+ register: result
+
+ - name: create default skeleton
+ command: ansible-galaxy collection init ansible_test.my_collection {{ galaxy_verbosity }}
+ args:
+ chdir: '{{ galaxy_dir }}/scratch'
+ when: not result.stat.exists
+
+ - name: build the tar.gz
+ command: ansible-galaxy collection build {{ galaxy_verbosity }}
+ args:
+ chdir: '{{ galaxy_dir }}/scratch/ansible_test/my_collection'
+ when: not result.stat.exists
+
+ - name: download a tar.gz file
+ command: ansible-galaxy collection download '{{ galaxy_dir }}/scratch/ansible_test/my_collection/ansible_test-my_collection-1.0.0.tar.gz'
+ args:
+ chdir: '{{ galaxy_dir }}/download'
+ register: download_collection
+
+ - name: get result of downloaded tar.gz
+ stat:
+ path: '{{ galaxy_dir }}/download/collections/ansible_test-my_collection-1.0.0.tar.gz'
+ register: download_collection_actual
+
+ - assert:
+ that:
+ - '"Downloading collection ''ansible_test.my_collection:1.0.0'' to" in download_collection.stdout'
+ - download_collection_actual.stat.exists
+
+- name: remove test download dir
+ file:
+ path: '{{ galaxy_dir }}/download'
+ state: absent
diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/fail_fast_resolvelib.yml b/test/integration/targets/ansible-galaxy-collection/tasks/fail_fast_resolvelib.yml
new file mode 100644
index 0000000..eb471f8
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection/tasks/fail_fast_resolvelib.yml
@@ -0,0 +1,45 @@
+# resolvelib>=0.6.0 added an 'incompatibilities' parameter to find_matches
+# If incompatibilities aren't removed from the viable candidates, this example causes infinite resursion
+- name: test resolvelib removes incompatibilites in find_matches and errors quickly (prevent infinite recursion)
+ block:
+ - name: create collection dir
+ file:
+ dest: "{{ galaxy_dir }}/resolvelib/ns/coll"
+ state: directory
+
+ - name: create galaxy.yml with a dependecy on a galaxy-sourced collection
+ copy:
+ dest: "{{ galaxy_dir }}/resolvelib/ns/coll/galaxy.yml"
+ content: |
+ namespace: ns
+ name: coll
+ authors:
+ - ansible-core
+ readme: README.md
+ version: "1.0.0"
+ dependencies:
+ namespace1.name1: "0.0.5"
+
+ - name: build the collection
+ command: ansible-galaxy collection build ns/coll
+ args:
+ chdir: "{{ galaxy_dir }}/resolvelib"
+
+ - name: install a conflicting version of the dep with the tarfile (expected failure)
+ command: ansible-galaxy collection install namespace1.name1:1.0.9 ns-coll-1.0.0.tar.gz -vvvvv -s {{ test_name }} -p collections/
+ args:
+ chdir: "{{ galaxy_dir }}/resolvelib"
+ timeout: 30
+ ignore_errors: yes
+ register: incompatible
+
+ - assert:
+ that:
+ - incompatible.failed
+ - not incompatible.msg.startswith("The command action failed to execute in the expected time frame")
+
+ always:
+ - name: cleanup resolvelib test
+ file:
+ dest: "{{ galaxy_dir }}/resolvelib"
+ state: absent
diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/init.yml b/test/integration/targets/ansible-galaxy-collection/tasks/init.yml
new file mode 100644
index 0000000..17a000d
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection/tasks/init.yml
@@ -0,0 +1,124 @@
+---
+- name: create default skeleton
+ command: ansible-galaxy collection init ansible_test.my_collection {{ galaxy_verbosity }}
+ args:
+ chdir: '{{ galaxy_dir }}/scratch'
+ register: init_relative
+
+- name: get result of create default skeleton
+ find:
+ path: '{{ galaxy_dir }}/scratch/ansible_test/my_collection'
+ recurse: yes
+ file_type: directory
+ register: init_relative_actual
+
+- debug:
+ var: init_relative_actual.files | map(attribute='path') | list
+
+- name: assert create default skeleton
+ assert:
+ that:
+ - '"Collection ansible_test.my_collection was created successfully" in init_relative.stdout'
+ - init_relative_actual.files | length == 4
+ - (init_relative_actual.files | map(attribute='path') | list)[0] | basename in ['docs', 'plugins', 'roles', 'meta']
+ - (init_relative_actual.files | map(attribute='path') | list)[1] | basename in ['docs', 'plugins', 'roles', 'meta']
+ - (init_relative_actual.files | map(attribute='path') | list)[2] | basename in ['docs', 'plugins', 'roles', 'meta']
+ - (init_relative_actual.files | map(attribute='path') | list)[3] | basename in ['docs', 'plugins', 'roles', 'meta']
+
+- name: create collection with custom init path
+ command: ansible-galaxy collection init ansible_test2.my_collection --init-path "{{ galaxy_dir }}/scratch/custom-init-dir" {{ galaxy_verbosity }}
+ register: init_custom_path
+
+- name: get result of create default skeleton
+ find:
+ path: '{{ galaxy_dir }}/scratch/custom-init-dir/ansible_test2/my_collection'
+ file_type: directory
+ register: init_custom_path_actual
+
+- name: assert create collection with custom init path
+ assert:
+ that:
+ - '"Collection ansible_test2.my_collection was created successfully" in init_custom_path.stdout'
+ - init_custom_path_actual.files | length == 4
+ - (init_custom_path_actual.files | map(attribute='path') | list)[0] | basename in ['docs', 'plugins', 'roles', 'meta']
+ - (init_custom_path_actual.files | map(attribute='path') | list)[1] | basename in ['docs', 'plugins', 'roles', 'meta']
+ - (init_custom_path_actual.files | map(attribute='path') | list)[2] | basename in ['docs', 'plugins', 'roles', 'meta']
+ - (init_custom_path_actual.files | map(attribute='path') | list)[3] | basename in ['docs', 'plugins', 'roles', 'meta']
+
+- name: add a directory to the init collection path to test that --force removes it
+ file:
+ state: directory
+ path: "{{ galaxy_dir }}/scratch/custom-init-dir/ansible_test2/my_collection/remove_me"
+
+- name: create collection with custom init path
+ command: ansible-galaxy collection init ansible_test2.my_collection --init-path "{{ galaxy_dir }}/scratch/custom-init-dir" --force {{ galaxy_verbosity }}
+ register: init_custom_path
+
+- name: get result of create default skeleton
+ find:
+ path: '{{ galaxy_dir }}/scratch/custom-init-dir/ansible_test2/my_collection'
+ file_type: directory
+ register: init_custom_path_actual
+
+- name: assert create collection with custom init path
+ assert:
+ that:
+ - '"Collection ansible_test2.my_collection was created successfully" in init_custom_path.stdout'
+ - init_custom_path_actual.files | length == 4
+ - (init_custom_path_actual.files | map(attribute='path') | list)[0] | basename in ['docs', 'plugins', 'roles', 'meta']
+ - (init_custom_path_actual.files | map(attribute='path') | list)[1] | basename in ['docs', 'plugins', 'roles', 'meta']
+ - (init_custom_path_actual.files | map(attribute='path') | list)[2] | basename in ['docs', 'plugins', 'roles', 'meta']
+ - (init_custom_path_actual.files | map(attribute='path') | list)[3] | basename in ['docs', 'plugins', 'roles', 'meta']
+
+- name: create collection in cwd with custom init path
+ command: ansible-galaxy collection init ansible_test2.my_collection --init-path ../../ --force {{ galaxy_verbosity }}
+ args:
+ chdir: "{{ galaxy_dir }}/scratch/custom-init-dir/ansible_test2/my_collection"
+ register: init_custom_path
+
+- name: get result of create default skeleton
+ find:
+ path: '{{ galaxy_dir }}/scratch/custom-init-dir/ansible_test2/my_collection'
+ file_type: directory
+ register: init_custom_path_actual
+
+- name: assert create collection with custom init path
+ assert:
+ that:
+ - '"Collection ansible_test2.my_collection was created successfully" in init_custom_path.stdout'
+ - init_custom_path_actual.files | length == 4
+ - (init_custom_path_actual.files | map(attribute='path') | list)[0] | basename in ['docs', 'plugins', 'roles', 'meta']
+ - (init_custom_path_actual.files | map(attribute='path') | list)[1] | basename in ['docs', 'plugins', 'roles', 'meta']
+ - (init_custom_path_actual.files | map(attribute='path') | list)[2] | basename in ['docs', 'plugins', 'roles', 'meta']
+ - (init_custom_path_actual.files | map(attribute='path') | list)[3] | basename in ['docs', 'plugins', 'roles', 'meta']
+
+- name: create collection for ignored files and folders
+ command: ansible-galaxy collection init ansible_test.ignore
+ args:
+ chdir: '{{ galaxy_dir }}/scratch'
+
+- name: create list of ignored files
+ set_fact:
+ collection_ignored_files:
+ - plugins/compiled.pyc
+ - something.retry
+ - .git
+
+- name: plant ignored files into the ansible_test.ignore collection
+ copy:
+ dest: '{{ galaxy_dir }}/scratch/ansible_test/ignore/{{ item }}'
+ content: '{{ item }}'
+ loop: '{{ collection_ignored_files }}'
+
+- name: create list of ignored directories
+ set_fact:
+ collection_ignored_directories:
+ - docs/.git
+ - plugins/doc_fragments/__pycache__
+ - .svn
+
+- name: plant ignored folders into the ansible_test.ignore collection
+ file:
+ path: '{{ galaxy_dir }}/scratch/ansible_test/ignore/{{ item }}'
+ state: directory
+ loop: '{{ collection_ignored_directories }}'
diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/install.yml b/test/integration/targets/ansible-galaxy-collection/tasks/install.yml
new file mode 100644
index 0000000..8916faf
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection/tasks/install.yml
@@ -0,0 +1,1035 @@
+---
+- name: create test collection install directory - {{ test_id }}
+ file:
+ path: '{{ galaxy_dir }}/ansible_collections'
+ state: directory
+
+- name: install simple collection from first accessible server
+ command: ansible-galaxy collection install namespace1.name1 {{ galaxy_verbosity }}
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+ register: from_first_good_server
+
+- name: get installed files of install simple collection from first good server
+ find:
+ path: '{{ galaxy_dir }}/ansible_collections/namespace1/name1'
+ file_type: file
+ register: install_normal_files
+
+- name: get the manifest of install simple collection from first good server
+ slurp:
+ path: '{{ galaxy_dir }}/ansible_collections/namespace1/name1/MANIFEST.json'
+ register: install_normal_manifest
+
+- name: assert install simple collection from first good server
+ assert:
+ that:
+ - '"Installing ''namespace1.name1:1.0.9'' to" in from_first_good_server.stdout'
+ - install_normal_files.files | length == 3
+ - install_normal_files.files[0].path | basename in ['MANIFEST.json', 'FILES.json', 'README.md']
+ - install_normal_files.files[1].path | basename in ['MANIFEST.json', 'FILES.json', 'README.md']
+ - install_normal_files.files[2].path | basename in ['MANIFEST.json', 'FILES.json', 'README.md']
+ - (install_normal_manifest.content | b64decode | from_json).collection_info.version == '1.0.9'
+
+- name: Remove the collection
+ file:
+ path: '{{ galaxy_dir }}/ansible_collections/namespace1'
+ state: absent
+
+- name: install simple collection with implicit path - {{ test_id }}
+ command: ansible-galaxy collection install namespace1.name1 -s '{{ test_name }}' {{ galaxy_verbosity }}
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+ register: install_normal
+
+- name: get installed files of install simple collection with implicit path - {{ test_id }}
+ find:
+ path: '{{ galaxy_dir }}/ansible_collections/namespace1/name1'
+ file_type: file
+ register: install_normal_files
+
+- name: get the manifest of install simple collection with implicit path - {{ test_id }}
+ slurp:
+ path: '{{ galaxy_dir }}/ansible_collections/namespace1/name1/MANIFEST.json'
+ register: install_normal_manifest
+
+- name: assert install simple collection with implicit path - {{ test_id }}
+ assert:
+ that:
+ - '"Installing ''namespace1.name1:1.0.9'' to" in install_normal.stdout'
+ - install_normal_files.files | length == 3
+ - install_normal_files.files[0].path | basename in ['MANIFEST.json', 'FILES.json', 'README.md']
+ - install_normal_files.files[1].path | basename in ['MANIFEST.json', 'FILES.json', 'README.md']
+ - install_normal_files.files[2].path | basename in ['MANIFEST.json', 'FILES.json', 'README.md']
+ - (install_normal_manifest.content | b64decode | from_json).collection_info.version == '1.0.9'
+
+- name: install existing without --force - {{ test_id }}
+ command: ansible-galaxy collection install namespace1.name1 -s '{{ test_name }}' {{ galaxy_verbosity }}
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+ register: install_existing_no_force
+
+- name: assert install existing without --force - {{ test_id }}
+ assert:
+ that:
+ - '"Nothing to do. All requested collections are already installed" in install_existing_no_force.stdout'
+
+- name: install existing with --force - {{ test_id }}
+ command: ansible-galaxy collection install namespace1.name1 -s '{{ test_name }}' --force {{ galaxy_verbosity }}
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+ register: install_existing_force
+
+- name: assert install existing with --force - {{ test_id }}
+ assert:
+ that:
+ - '"Installing ''namespace1.name1:1.0.9'' to" in install_existing_force.stdout'
+
+- name: remove test installed collection - {{ test_id }}
+ file:
+ path: '{{ galaxy_dir }}/ansible_collections/namespace1'
+ state: absent
+
+- name: install pre-release as explicit version to custom dir - {{ test_id }}
+ command: ansible-galaxy collection install 'namespace1.name1:1.1.0-beta.1' -s '{{ test_name }}' -p '{{ galaxy_dir }}/ansible_collections' {{ galaxy_verbosity }}
+ register: install_prerelease
+
+- name: get result of install pre-release as explicit version to custom dir - {{ test_id }}
+ slurp:
+ path: '{{ galaxy_dir }}/ansible_collections/namespace1/name1/MANIFEST.json'
+ register: install_prerelease_actual
+
+- name: assert install pre-release as explicit version to custom dir - {{ test_id }}
+ assert:
+ that:
+ - '"Installing ''namespace1.name1:1.1.0-beta.1'' to" in install_prerelease.stdout'
+ - (install_prerelease_actual.content | b64decode | from_json).collection_info.version == '1.1.0-beta.1'
+
+- name: Remove beta
+ file:
+ path: '{{ galaxy_dir }}/ansible_collections/namespace1/name1'
+ state: absent
+
+- name: install pre-release version with --pre to custom dir - {{ test_id }}
+ command: ansible-galaxy collection install --pre 'namespace1.name1' -s '{{ test_name }}' -p '{{ galaxy_dir }}/ansible_collections' {{ galaxy_verbosity }}
+ register: install_prerelease
+
+- name: get result of install pre-release version with --pre to custom dir - {{ test_id }}
+ slurp:
+ path: '{{ galaxy_dir }}/ansible_collections/namespace1/name1/MANIFEST.json'
+ register: install_prerelease_actual
+
+- name: assert install pre-release version with --pre to custom dir - {{ test_id }}
+ assert:
+ that:
+ - '"Installing ''namespace1.name1:1.1.0-beta.1'' to" in install_prerelease.stdout'
+ - (install_prerelease_actual.content | b64decode | from_json).collection_info.version == '1.1.0-beta.1'
+
+- name: install multiple collections with dependencies - {{ test_id }}
+ command: ansible-galaxy collection install parent_dep.parent_collection:1.0.0 namespace2.name -s {{ test_name }} {{ galaxy_verbosity }}
+ args:
+ chdir: '{{ galaxy_dir }}/ansible_collections'
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+ ANSIBLE_CONFIG: '{{ galaxy_dir }}/ansible.cfg'
+ register: install_multiple_with_dep
+
+- name: get result of install multiple collections with dependencies - {{ test_id }}
+ slurp:
+ path: '{{ galaxy_dir }}/ansible_collections/{{ collection.namespace }}/{{ collection.name }}/MANIFEST.json'
+ register: install_multiple_with_dep_actual
+ loop_control:
+ loop_var: collection
+ loop:
+ - namespace: namespace2
+ name: name
+ - namespace: parent_dep
+ name: parent_collection
+ - namespace: child_dep
+ name: child_collection
+ - namespace: child_dep
+ name: child_dep2
+
+- name: assert install multiple collections with dependencies - {{ test_id }}
+ assert:
+ that:
+ - (install_multiple_with_dep_actual.results[0].content | b64decode | from_json).collection_info.version == '1.0.0'
+ - (install_multiple_with_dep_actual.results[1].content | b64decode | from_json).collection_info.version == '1.0.0'
+ - (install_multiple_with_dep_actual.results[2].content | b64decode | from_json).collection_info.version == '0.9.9'
+ - (install_multiple_with_dep_actual.results[3].content | b64decode | from_json).collection_info.version == '1.2.2'
+
+- name: expect failure with dep resolution failure - {{ test_id }}
+ command: ansible-galaxy collection install fail_namespace.fail_collection -s {{ test_name }} {{ galaxy_verbosity }}
+ register: fail_dep_mismatch
+ failed_when:
+ - '"Could not satisfy the following requirements" not in fail_dep_mismatch.stderr'
+ - '" fail_dep2.name:<0.0.5 (dependency of fail_namespace.fail_collection:2.1.2)" not in fail_dep_mismatch.stderr'
+
+- name: Find artifact url for namespace3.name
+ uri:
+ url: '{{ test_server }}{{ vX }}collections/namespace3/name/versions/1.0.0/'
+ user: '{{ pulp_user }}'
+ password: '{{ pulp_password }}'
+ force_basic_auth: true
+ register: artifact_url_response
+
+- name: download a collection for an offline install - {{ test_id }}
+ get_url:
+ url: '{{ artifact_url_response.json.download_url }}'
+ dest: '{{ galaxy_dir }}/namespace3.tar.gz'
+
+- name: install a collection from a tarball - {{ test_id }}
+ command: ansible-galaxy collection install '{{ galaxy_dir }}/namespace3.tar.gz' {{ galaxy_verbosity }}
+ register: install_tarball
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+
+- name: get result of install collection from a tarball - {{ test_id }}
+ slurp:
+ path: '{{ galaxy_dir }}/ansible_collections/namespace3/name/MANIFEST.json'
+ register: install_tarball_actual
+
+- name: assert install a collection from a tarball - {{ test_id }}
+ assert:
+ that:
+ - '"Installing ''namespace3.name:1.0.0'' to" in install_tarball.stdout'
+ - (install_tarball_actual.content | b64decode | from_json).collection_info.version == '1.0.0'
+
+- name: write a requirements file using the artifact and a conflicting version
+ copy:
+ content: |
+ collections:
+ - name: {{ galaxy_dir }}/namespace3.tar.gz
+ version: 1.2.0
+ dest: '{{ galaxy_dir }}/test_req.yml'
+
+- name: install the requirements file with mismatched versions
+ command: ansible-galaxy collection install -r '{{ galaxy_dir }}/test_req.yml' {{ galaxy_verbosity }}
+ ignore_errors: True
+ register: result
+ environment:
+ ANSIBLE_NOCOLOR: True
+ ANSIBLE_FORCE_COLOR: False
+
+- name: remove the requirements file
+ file:
+ path: '{{ galaxy_dir }}/test_req.yml'
+ state: absent
+
+- assert:
+ that: error == expected_error
+ vars:
+ error: "{{ result.stderr | regex_replace('\\n', ' ') }}"
+ expected_error: >-
+ ERROR! Failed to resolve the requested dependencies map.
+ Got the candidate namespace3.name:1.0.0 (direct request)
+ which didn't satisfy all of the following requirements:
+ * namespace3.name:1.2.0
+
+- name: test error for mismatched dependency versions
+ vars:
+ error: "{{ result.stderr | regex_replace('\\n', ' ') }}"
+ expected_error: >-
+ ERROR! Failed to resolve the requested dependencies map.
+ Got the candidate namespace3.name:1.0.0 (dependency of tmp_parent.name:1.0.0)
+ which didn't satisfy all of the following requirements:
+ * namespace3.name:1.2.0
+ environment:
+ ANSIBLE_NOCOLOR: True
+ ANSIBLE_FORCE_COLOR: False
+ block:
+ - name: init a new parent collection
+ command: ansible-galaxy collection init tmp_parent.name --init-path '{{ galaxy_dir }}/scratch'
+
+ - name: replace the dependencies
+ lineinfile:
+ path: "{{ galaxy_dir }}/scratch/tmp_parent/name/galaxy.yml"
+ regexp: "^dependencies:*"
+ line: "dependencies: { '{{ galaxy_dir }}/namespace3.tar.gz': '1.2.0' }"
+
+ - name: build the new artifact
+ command: ansible-galaxy collection build {{ galaxy_dir }}/scratch/tmp_parent/name
+ args:
+ chdir: "{{ galaxy_dir }}"
+
+ - name: install the artifact to verify the error is handled
+ command: ansible-galaxy collection install '{{ galaxy_dir }}/tmp_parent-name-1.0.0.tar.gz'
+ ignore_errors: yes
+ register: result
+
+ - debug: msg="Actual - {{ error }}"
+
+ - debug: msg="Expected - {{ expected_error }}"
+
+ - assert:
+ that: error == expected_error
+ always:
+ - name: clean up collection skeleton and artifact
+ file:
+ state: absent
+ path: "{{ item }}"
+ loop:
+ - "{{ galaxy_dir }}/scratch/tmp_parent/"
+ - "{{ galaxy_dir }}/tmp_parent-name-1.0.0.tar.gz"
+
+- name: setup bad tarball - {{ test_id }}
+ script: build_bad_tar.py {{ galaxy_dir | quote }}
+
+- name: fail to install a collection from a bad tarball - {{ test_id }}
+ command: ansible-galaxy collection install '{{ galaxy_dir }}/suspicious-test-1.0.0.tar.gz' {{ galaxy_verbosity }}
+ register: fail_bad_tar
+ failed_when: fail_bad_tar.rc != 1 and "Cannot extract tar entry '../../outside.sh' as it will be placed outside the collection directory" not in fail_bad_tar.stderr
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+
+- name: get result of failed collection install - {{ test_id }}
+ stat:
+ path: '{{ galaxy_dir }}/ansible_collections\suspicious'
+ register: fail_bad_tar_actual
+
+- name: assert result of failed collection install - {{ test_id }}
+ assert:
+ that:
+ - not fail_bad_tar_actual.stat.exists
+
+- name: Find artifact url for namespace4.name
+ uri:
+ url: '{{ test_server }}{{ vX }}collections/namespace4/name/versions/1.0.0/'
+ user: '{{ pulp_user }}'
+ password: '{{ pulp_password }}'
+ force_basic_auth: true
+ register: artifact_url_response
+
+- name: install a collection from a URI - {{ test_id }}
+ command: ansible-galaxy collection install {{ artifact_url_response.json.download_url}} {{ galaxy_verbosity }}
+ register: install_uri
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+
+- name: get result of install collection from a URI - {{ test_id }}
+ slurp:
+ path: '{{ galaxy_dir }}/ansible_collections/namespace4/name/MANIFEST.json'
+ register: install_uri_actual
+
+- name: assert install a collection from a URI - {{ test_id }}
+ assert:
+ that:
+ - '"Installing ''namespace4.name:1.0.0'' to" in install_uri.stdout'
+ - (install_uri_actual.content | b64decode | from_json).collection_info.version == '1.0.0'
+
+- name: fail to install a collection with an undefined URL - {{ test_id }}
+ command: ansible-galaxy collection install namespace5.name {{ galaxy_verbosity }}
+ register: fail_undefined_server
+ failed_when: '"No setting was provided for required configuration plugin_type: galaxy_server plugin: undefined" not in fail_undefined_server.stderr'
+ environment:
+ ANSIBLE_GALAXY_SERVER_LIST: undefined
+
+- when: not requires_auth
+ block:
+ - name: install a collection with an empty server list - {{ test_id }}
+ command: ansible-galaxy collection install namespace5.name -s '{{ test_server }}' {{ galaxy_verbosity }}
+ register: install_empty_server_list
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+ ANSIBLE_GALAXY_SERVER_LIST: ''
+
+ - name: get result of a collection with an empty server list - {{ test_id }}
+ slurp:
+ path: '{{ galaxy_dir }}/ansible_collections/namespace5/name/MANIFEST.json'
+ register: install_empty_server_list_actual
+
+ - name: assert install a collection with an empty server list - {{ test_id }}
+ assert:
+ that:
+ - '"Installing ''namespace5.name:1.0.0'' to" in install_empty_server_list.stdout'
+ - (install_empty_server_list_actual.content | b64decode | from_json).collection_info.version == '1.0.0'
+
+- name: create test requirements file with both roles and collections - {{ test_id }}
+ copy:
+ content: |
+ collections:
+ - namespace6.name
+ - name: namespace7.name
+ roles:
+ - skip.me
+ dest: '{{ galaxy_dir }}/ansible_collections/requirements-with-role.yml'
+
+- name: install roles from requirements file with collection-only keyring option
+ command: ansible-galaxy role install -r {{ req_file }} -s {{ test_name }} --keyring {{ keyring }}
+ vars:
+ req_file: '{{ galaxy_dir }}/ansible_collections/requirements-with-role.yml'
+ keyring: "{{ gpg_homedir }}/pubring.kbx"
+ ignore_errors: yes
+ register: invalid_opt
+
+- assert:
+ that:
+ - invalid_opt is failed
+ - "'unrecognized arguments: --keyring' in invalid_opt.stderr"
+
+# Need to run with -vvv to validate the roles will be skipped msg
+- name: install collections only with requirements-with-role.yml - {{ test_id }}
+ command: ansible-galaxy collection install -r '{{ galaxy_dir }}/ansible_collections/requirements-with-role.yml' -s '{{ test_name }}' -vvv
+ register: install_req_collection
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+
+- name: get result of install collections only with requirements-with-roles.yml - {{ test_id }}
+ slurp:
+ path: '{{ galaxy_dir }}/ansible_collections/{{ collection }}/name/MANIFEST.json'
+ register: install_req_collection_actual
+ loop_control:
+ loop_var: collection
+ loop:
+ - namespace6
+ - namespace7
+
+- name: assert install collections only with requirements-with-role.yml - {{ test_id }}
+ assert:
+ that:
+ - '"contains roles which will be ignored" in install_req_collection.stdout'
+ - '"Installing ''namespace6.name:1.0.0'' to" in install_req_collection.stdout'
+ - '"Installing ''namespace7.name:1.0.0'' to" in install_req_collection.stdout'
+ - (install_req_collection_actual.results[0].content | b64decode | from_json).collection_info.version == '1.0.0'
+ - (install_req_collection_actual.results[1].content | b64decode | from_json).collection_info.version == '1.0.0'
+
+- name: create test requirements file with just collections - {{ test_id }}
+ copy:
+ content: |
+ collections:
+ - namespace8.name
+ - name: namespace9.name
+ dest: '{{ galaxy_dir }}/ansible_collections/requirements.yaml'
+
+- name: install collections with ansible-galaxy install - {{ test_id }}
+ command: ansible-galaxy install -r '{{ galaxy_dir }}/ansible_collections/requirements.yaml' -s '{{ test_name }}'
+ register: install_req
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+
+- name: get result of install collections with ansible-galaxy install - {{ test_id }}
+ slurp:
+ path: '{{ galaxy_dir }}/ansible_collections/{{ collection }}/name/MANIFEST.json'
+ register: install_req_actual
+ loop_control:
+ loop_var: collection
+ loop:
+ - namespace8
+ - namespace9
+
+- name: assert install collections with ansible-galaxy install - {{ test_id }}
+ assert:
+ that:
+ - '"Installing ''namespace8.name:1.0.0'' to" in install_req.stdout'
+ - '"Installing ''namespace9.name:1.0.0'' to" in install_req.stdout'
+ - (install_req_actual.results[0].content | b64decode | from_json).collection_info.version == '1.0.0'
+ - (install_req_actual.results[1].content | b64decode | from_json).collection_info.version == '1.0.0'
+
+- name: Test deviations on -r and --role-file without collection or role sub command
+ command: '{{ cmd }}'
+ loop:
+ - ansible-galaxy install -vr '{{ galaxy_dir }}/ansible_collections/requirements.yaml' -s '{{ test_name }}' -vv
+ - ansible-galaxy install --role-file '{{ galaxy_dir }}/ansible_collections/requirements.yaml' -s '{{ test_name }}' -vvv
+ - ansible-galaxy install --role-file='{{ galaxy_dir }}/ansible_collections/requirements.yaml' -s '{{ test_name }}' -vvv
+ loop_control:
+ loop_var: cmd
+
+- name: uninstall collections for next requirements file test
+ file:
+ path: '{{ galaxy_dir }}/ansible_collections/{{ collection }}/name'
+ state: absent
+ loop_control:
+ loop_var: collection
+ loop:
+ - namespace7
+ - namespace8
+ - namespace9
+
+- name: rewrite requirements file with collections and signatures
+ copy:
+ content: |
+ collections:
+ - name: namespace7.name
+ version: "1.0.0"
+ signatures:
+ - "{{ not_mine }}"
+ - "{{ also_not_mine }}"
+ - "file://{{ gpg_homedir }}/namespace7-name-1.0.0-MANIFEST.json.asc"
+ - namespace8.name
+ - name: namespace9.name
+ signatures:
+ - "file://{{ gpg_homedir }}/namespace9-name-1.0.0-MANIFEST.json.asc"
+ dest: '{{ galaxy_dir }}/ansible_collections/requirements.yaml'
+ vars:
+ not_mine: "file://{{ gpg_homedir }}/namespace1-name1-1.0.0-MANIFEST.json.asc"
+ also_not_mine: "file://{{ gpg_homedir }}/namespace1-name1-1.0.9-MANIFEST.json.asc"
+
+- name: installing only roles does not fail if keyring for collections is not provided
+ command: ansible-galaxy role install -r {{ galaxy_dir }}/ansible_collections/requirements.yaml
+ register: roles_only
+
+- assert:
+ that:
+ - roles_only is success
+
+- name: installing only roles implicitly does not fail if keyring for collections is not provided
+ # if -p/--roles-path are specified, only roles are installed
+ command: ansible-galaxy install -r {{ galaxy_dir }}/ansible_collections/requirements.yaml }} -p {{ galaxy_dir }}
+ register: roles_only
+
+- assert:
+ that:
+ - roles_only is success
+
+- name: installing roles and collections requires keyring if collections have signatures
+ command: ansible-galaxy install -r {{ galaxy_dir }}/ansible_collections/requirements.yaml }}
+ ignore_errors: yes
+ register: collections_and_roles
+
+- assert:
+ that:
+ - collections_and_roles is failed
+ - "'no keyring was configured' in collections_and_roles.stderr"
+
+- name: install collection with mutually exclusive options
+ command: ansible-galaxy collection install -r {{ req_file }} -s {{ test_name }} {{ cli_signature }}
+ vars:
+ req_file: "{{ galaxy_dir }}/ansible_collections/requirements.yaml"
+ # --signature is an ansible-galaxy collection install subcommand, but mutually exclusive with -r
+ cli_signature: "--signature file://{{ gpg_homedir }}/namespace7-name-1.0.0-MANIFEST.json.asc"
+ ignore_errors: yes
+ register: mutually_exclusive_opts
+
+- assert:
+ that:
+ - mutually_exclusive_opts is failed
+ - expected_error in actual_error
+ vars:
+ expected_error: >-
+ The --signatures option and --requirements-file are mutually exclusive.
+ Use the --signatures with positional collection_name args or provide a
+ 'signatures' key for requirements in the --requirements-file.
+ actual_error: "{{ mutually_exclusive_opts.stderr }}"
+
+- name: install a collection with user-supplied signatures for verification but no keyring
+ command: ansible-galaxy collection install namespace1.name1:1.0.0 {{ cli_signature }}
+ vars:
+ cli_signature: "--signature file://{{ gpg_homedir }}/namespace1-name1-1.0.0-MANIFEST.json.asc"
+ ignore_errors: yes
+ register: required_together
+
+- assert:
+ that:
+ - required_together is failed
+ - '"ERROR! Signatures were provided to verify namespace1.name1 but no keyring was configured." in required_together.stderr'
+
+- name: install collections with ansible-galaxy install -r with invalid signatures - {{ test_id }}
+ # Note that --keyring is a valid option for 'ansible-galaxy install -r ...', not just 'ansible-galaxy collection ...'
+ command: ansible-galaxy install -r {{ req_file }} -s {{ test_name }} --keyring {{ keyring }} {{ galaxy_verbosity }}
+ register: install_req
+ ignore_errors: yes
+ vars:
+ req_file: "{{ galaxy_dir }}/ansible_collections/requirements.yaml"
+ keyring: "{{ gpg_homedir }}/pubring.kbx"
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+ ANSIBLE_GALAXY_REQUIRED_VALID_SIGNATURE_COUNT: all
+
+- name: assert invalid signature is fatal with ansible-galaxy install - {{ test_id }}
+ assert:
+ that:
+ - install_req is failed
+ - '"Installing ''namespace7.name:1.0.0'' to" in install_req.stdout'
+ - '"Not installing namespace7.name because GnuPG signature verification failed" in install_req.stderr'
+ # The other collections shouldn't be installed because they're listed
+ # after the failing collection and --ignore-errors was not provided
+ - '"Installing ''namespace8.name:1.0.0'' to" not in install_req.stdout'
+ - '"Installing ''namespace9.name:1.0.0'' to" not in install_req.stdout'
+
+# This command is hardcoded with -vvvv purposefully to evaluate extra verbosity messages
+- name: install collections with ansible-galaxy install and --ignore-errors - {{ test_id }}
+ command: ansible-galaxy install -r {{ req_file }} {{ cli_opts }} -vvvv
+ register: install_req
+ vars:
+ req_file: "{{ galaxy_dir }}/ansible_collections/requirements.yaml"
+ cli_opts: "-s {{ test_name }} --keyring {{ keyring }} --ignore-errors"
+ keyring: "{{ gpg_homedir }}/pubring.kbx"
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+ ANSIBLE_GALAXY_REQUIRED_VALID_SIGNATURE_COUNT: all
+ ANSIBLE_NOCOLOR: True
+ ANSIBLE_FORCE_COLOR: False
+
+- name: get result of install collections with ansible-galaxy install - {{ test_id }}
+ slurp:
+ path: '{{ galaxy_dir }}/ansible_collections/{{ collection }}/name/MANIFEST.json'
+ register: install_req_actual
+ loop_control:
+ loop_var: collection
+ loop:
+ - namespace8
+ - namespace9
+
+# SIVEL
+- name: assert invalid signature is not fatal with ansible-galaxy install --ignore-errors - {{ test_id }}
+ assert:
+ that:
+ - install_req is success
+ - '"Installing ''namespace7.name:1.0.0'' to" in install_req.stdout'
+ - '"Signature verification failed for ''namespace7.name'' (return code 1)" in install_req.stdout'
+ - '"Not installing namespace7.name because GnuPG signature verification failed." in install_stderr'
+ - '"Failed to install collection namespace7.name:1.0.0 but skipping due to --ignore-errors being set." in install_stderr'
+ - '"Installing ''namespace8.name:1.0.0'' to" in install_req.stdout'
+ - '"Installing ''namespace9.name:1.0.0'' to" in install_req.stdout'
+ - (install_req_actual.results[0].content | b64decode | from_json).collection_info.version == '1.0.0'
+ - (install_req_actual.results[1].content | b64decode | from_json).collection_info.version == '1.0.0'
+ vars:
+ install_stderr: "{{ install_req.stderr | regex_replace('\\n', ' ') }}"
+
+- name: clean up collections from last test
+ file:
+ path: '{{ galaxy_dir }}/ansible_collections/{{ collection }}/name'
+ state: absent
+ loop_control:
+ loop_var: collection
+ loop:
+ - namespace8
+ - namespace9
+
+- name: install collections with only one valid signature using ansible-galaxy install - {{ test_id }}
+ command: ansible-galaxy install -r {{ req_file }} {{ cli_opts }} {{ galaxy_verbosity }}
+ register: install_req
+ vars:
+ req_file: "{{ galaxy_dir }}/ansible_collections/requirements.yaml"
+ cli_opts: "-s {{ test_name }} --keyring {{ keyring }}"
+ keyring: "{{ gpg_homedir }}/pubring.kbx"
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+ ANSIBLE_NOCOLOR: True
+ ANSIBLE_FORCE_COLOR: False
+
+- name: get result of install collections with ansible-galaxy install - {{ test_id }}
+ slurp:
+ path: '{{ galaxy_dir }}/ansible_collections/{{ collection }}/name/MANIFEST.json'
+ register: install_req_actual
+ loop_control:
+ loop_var: collection
+ loop:
+ - namespace7
+ - namespace8
+ - namespace9
+
+- name: assert just one valid signature is not fatal with ansible-galaxy install - {{ test_id }}
+ assert:
+ that:
+ - install_req is success
+ - '"Installing ''namespace7.name:1.0.0'' to" in install_req.stdout'
+ - '"Signature verification failed for ''namespace7.name'' (return code 1)" not in install_req.stdout'
+ - '"Not installing namespace7.name because GnuPG signature verification failed." not in install_stderr'
+ - '"Installing ''namespace8.name:1.0.0'' to" in install_req.stdout'
+ - '"Installing ''namespace9.name:1.0.0'' to" in install_req.stdout'
+ - (install_req_actual.results[0].content | b64decode | from_json).collection_info.version == '1.0.0'
+ - (install_req_actual.results[1].content | b64decode | from_json).collection_info.version == '1.0.0'
+ - (install_req_actual.results[2].content | b64decode | from_json).collection_info.version == '1.0.0'
+ vars:
+ install_stderr: "{{ install_req.stderr | regex_replace('\\n', ' ') }}"
+
+- name: clean up collections from last test
+ file:
+ path: '{{ galaxy_dir }}/ansible_collections/{{ collection }}/name'
+ state: absent
+ loop_control:
+ loop_var: collection
+ loop:
+ - namespace7
+ - namespace8
+ - namespace9
+
+- name: install collections with only one valid signature by ignoring the other errors
+ command: ansible-galaxy install -r {{ req_file }} {{ cli_opts }} {{ galaxy_verbosity }} --ignore-signature-status-code FAILURE
+ register: install_req
+ vars:
+ req_file: "{{ galaxy_dir }}/ansible_collections/requirements.yaml"
+ cli_opts: "-s {{ test_name }} --keyring {{ keyring }}"
+ keyring: "{{ gpg_homedir }}/pubring.kbx"
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+ ANSIBLE_GALAXY_REQUIRED_VALID_SIGNATURE_COUNT: all
+ ANSIBLE_GALAXY_IGNORE_SIGNATURE_STATUS_CODES: BADSIG # cli option is appended and both status codes are ignored
+ ANSIBLE_NOCOLOR: True
+ ANSIBLE_FORCE_COLOR: False
+
+- name: get result of install collections with ansible-galaxy install - {{ test_id }}
+ slurp:
+ path: '{{ galaxy_dir }}/ansible_collections/{{ collection }}/name/MANIFEST.json'
+ register: install_req_actual
+ loop_control:
+ loop_var: collection
+ loop:
+ - namespace7
+ - namespace8
+ - namespace9
+
+- name: assert invalid signature is not fatal with ansible-galaxy install - {{ test_id }}
+ assert:
+ that:
+ - install_req is success
+ - '"Installing ''namespace7.name:1.0.0'' to" in install_req.stdout'
+ - '"Signature verification failed for ''namespace7.name'' (return code 1)" not in install_req.stdout'
+ - '"Not installing namespace7.name because GnuPG signature verification failed." not in install_stderr'
+ - '"Installing ''namespace8.name:1.0.0'' to" in install_req.stdout'
+ - '"Installing ''namespace9.name:1.0.0'' to" in install_req.stdout'
+ - (install_req_actual.results[0].content | b64decode | from_json).collection_info.version == '1.0.0'
+ - (install_req_actual.results[1].content | b64decode | from_json).collection_info.version == '1.0.0'
+ - (install_req_actual.results[2].content | b64decode | from_json).collection_info.version == '1.0.0'
+ vars:
+ install_stderr: "{{ install_req.stderr | regex_replace('\\n', ' ') }}"
+
+- name: clean up collections from last test
+ file:
+ path: '{{ galaxy_dir }}/ansible_collections/{{ collection }}/name'
+ state: absent
+ loop_control:
+ loop_var: collection
+ loop:
+ - namespace7
+ - namespace8
+ - namespace9
+
+# Uncomment once pulp container is at pulp>=0.5.0
+#- name: install cache.cache at the current latest version
+# command: ansible-galaxy collection install cache.cache -s '{{ test_name }}' -vvv
+# environment:
+# ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+#
+#- set_fact:
+# cache_version_build: '{{ (cache_version_build | int) + 1 }}'
+#
+#- name: publish update for cache.cache test
+# setup_collections:
+# server: galaxy_ng
+# collections:
+# - namespace: cache
+# name: cache
+# version: 1.0.{{ cache_version_build }}
+#
+#- name: make sure the cache version list is ignored on a collection version change - {{ test_id }}
+# command: ansible-galaxy collection install cache.cache -s '{{ test_name }}' --force -vvv
+# register: install_cached_update
+# environment:
+# ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+#
+#- name: get result of cache version list is ignored on a collection version change - {{ test_id }}
+# slurp:
+# path: '{{ galaxy_dir }}/ansible_collections/cache/cache/MANIFEST.json'
+# register: install_cached_update_actual
+#
+#- name: assert cache version list is ignored on a collection version change - {{ test_id }}
+# assert:
+# that:
+# - '"Installing ''cache.cache:1.0.{{ cache_version_build }}'' to" in install_cached_update.stdout'
+# - (install_cached_update_actual.content | b64decode | from_json).collection_info.version == '1.0.' ~ cache_version_build
+
+- name: install collection with symlink - {{ test_id }}
+ command: ansible-galaxy collection install symlink.symlink -s '{{ test_name }}' {{ galaxy_verbosity }}
+ environment:
+ ANSIBLE_COLLECTIONS_PATHS: '{{ galaxy_dir }}/ansible_collections'
+ register: install_symlink
+
+- find:
+ paths: '{{ galaxy_dir }}/ansible_collections/symlink/symlink'
+ recurse: yes
+ file_type: any
+
+- name: get result of install collection with symlink - {{ test_id }}
+ stat:
+ path: '{{ galaxy_dir }}/ansible_collections/symlink/symlink/{{ path }}'
+ register: install_symlink_actual
+ loop_control:
+ loop_var: path
+ loop:
+ - REÅDMÈ.md-link
+ - docs/REÅDMÈ.md
+ - plugins/REÅDMÈ.md
+ - REÅDMÈ.md-outside-link
+ - docs-link
+ - docs-link/REÅDMÈ.md
+
+- name: assert install collection with symlink - {{ test_id }}
+ assert:
+ that:
+ - '"Installing ''symlink.symlink:1.0.0'' to" in install_symlink.stdout'
+ - install_symlink_actual.results[0].stat.islnk
+ - install_symlink_actual.results[0].stat.lnk_target == 'REÅDMÈ.md'
+ - install_symlink_actual.results[1].stat.islnk
+ - install_symlink_actual.results[1].stat.lnk_target == '../REÅDMÈ.md'
+ - install_symlink_actual.results[2].stat.islnk
+ - install_symlink_actual.results[2].stat.lnk_target == '../REÅDMÈ.md'
+ - install_symlink_actual.results[3].stat.isreg
+ - install_symlink_actual.results[4].stat.islnk
+ - install_symlink_actual.results[4].stat.lnk_target == 'docs'
+ - install_symlink_actual.results[5].stat.islnk
+ - install_symlink_actual.results[5].stat.lnk_target == '../REÅDMÈ.md'
+
+- name: remove install directory for the next test because parent_dep.parent_collection was installed - {{ test_id }}
+ file:
+ path: '{{ galaxy_dir }}/ansible_collections'
+ state: absent
+
+- name: install collection and dep compatible with multiple requirements - {{ test_id }}
+ command: ansible-galaxy collection install parent_dep.parent_collection parent_dep2.parent_collection
+ environment:
+ ANSIBLE_COLLECTIONS_PATHS: '{{ galaxy_dir }}/ansible_collections'
+ register: install_req
+
+- name: assert install collections with ansible-galaxy install - {{ test_id }}
+ assert:
+ that:
+ - '"Installing ''parent_dep.parent_collection:1.0.0'' to" in install_req.stdout'
+ - '"Installing ''parent_dep2.parent_collection:1.0.0'' to" in install_req.stdout'
+ - '"Installing ''child_dep.child_collection:0.5.0'' to" in install_req.stdout'
+
+- name: install a collection to a directory that contains another collection with no metadata
+ block:
+
+ # Collections are usable in ansible without a galaxy.yml or MANIFEST.json
+ - name: create a collection directory
+ file:
+ state: directory
+ path: '{{ galaxy_dir }}/ansible_collections/unrelated_namespace/collection_without_metadata/plugins'
+
+ - name: install a collection to the same installation directory - {{ test_id }}
+ command: ansible-galaxy collection install namespace1.name1
+ environment:
+ ANSIBLE_COLLECTIONS_PATHS: '{{ galaxy_dir }}/ansible_collections'
+ register: install_req
+
+ - name: assert installed collections with ansible-galaxy install - {{ test_id }}
+ assert:
+ that:
+ - '"Installing ''namespace1.name1:1.0.9'' to" in install_req.stdout'
+
+- name: remove test collection install directory - {{ test_id }}
+ file:
+ path: '{{ galaxy_dir }}/ansible_collections'
+ state: absent
+
+# This command is hardcoded with -vvvv purposefully to evaluate extra verbosity messages
+- name: install collection with signature with invalid keyring
+ command: ansible-galaxy collection install namespace1.name1 -vvvv {{ signature_option }} {{ keyring_option }}
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+ ANSIBLE_NOCOLOR: True
+ ANSIBLE_FORCE_COLOR: False
+ vars:
+ signature_option: "--signature file://{{ gpg_homedir }}/namespace1-name1-1.0.9-MANIFEST.json.asc"
+ keyring_option: '--keyring {{ gpg_homedir }}/i_do_not_exist.kbx'
+ ignore_errors: yes
+ register: keyring_error
+
+- assert:
+ that:
+ - keyring_error is failed
+ - expected_errors[0] in actual_error
+ - expected_errors[1] in actual_error
+ - expected_errors[2] in actual_error
+ - unexpected_warning not in actual_warning
+ vars:
+ keyring: "{{ gpg_homedir }}/i_do_not_exist.kbx"
+ expected_errors:
+ - "Signature verification failed for 'namespace1.name1' (return code 2):"
+ - "* The public key is not available."
+ - >-
+ * It was not possible to check the signature. This may be caused
+ by a missing public key or an unsupported algorithm. A RC of 4
+ indicates unknown algorithm, a 9 indicates a missing public key.
+ unexpected_warning: >-
+ The GnuPG keyring used for collection signature
+ verification was not configured but signatures were
+ provided by the Galaxy server to verify authenticity.
+ Configure a keyring for ansible-galaxy to use
+ or disable signature verification.
+ Skipping signature verification.
+ actual_warning: "{{ keyring_error.stderr | regex_replace('\\n', ' ') }}"
+ # Remove formatting from the reason so it's one line
+ actual_error: "{{ keyring_error.stdout | regex_replace('\"') | regex_replace('\\n') | regex_replace(' ', ' ') }}"
+
+# TODO: Uncomment once signatures are provided by pulp-galaxy-ng
+#- name: install collection with signature provided by Galaxy server (no keyring)
+# command: ansible-galaxy collection install namespace1.name1 {{ galaxy_verbosity }}
+# environment:
+# ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+# ANSIBLE_NOCOLOR: True
+# ANSIBLE_FORCE_COLOR: False
+# ignore_errors: yes
+# register: keyring_warning
+#
+#- name: assert a warning was given but signature verification did not occur without configuring the keyring
+# assert:
+# that:
+# - keyring_warning is not failed
+# - - '"Installing ''namespace1.name1:1.0.9'' to" in keyring_warning.stdout'
+# # TODO: Don't just check the stdout, make sure the collection was installed.
+# - expected_warning in actual_warning
+# vars:
+# expected_warning: >-
+# The GnuPG keyring used for collection signature
+# verification was not configured but signatures were
+# provided by the Galaxy server to verify authenticity.
+# Configure a keyring for ansible-galaxy to use
+# or disable signature verification.
+# Skipping signature verification.
+# actual_warning: "{{ keyring_warning.stderr | regex_replace('\\n', ' ') }}"
+
+- name: install simple collection from first accessible server with valid detached signature
+ command: ansible-galaxy collection install namespace1.name1 {{ galaxy_verbosity }} {{ signature_options }}
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+ vars:
+ signature_options: "--signature {{ signature }} --keyring {{ keyring }}"
+ signature: "file://{{ gpg_homedir }}/namespace1-name1-1.0.9-MANIFEST.json.asc"
+ keyring: "{{ gpg_homedir }}/pubring.kbx"
+ register: from_first_good_server
+
+- name: get installed files of install simple collection from first good server
+ find:
+ path: '{{ galaxy_dir }}/ansible_collections/namespace1/name1'
+ file_type: file
+ register: install_normal_files
+
+- name: get the manifest of install simple collection from first good server
+ slurp:
+ path: '{{ galaxy_dir }}/ansible_collections/namespace1/name1/MANIFEST.json'
+ register: install_normal_manifest
+
+- name: assert install simple collection from first good server
+ assert:
+ that:
+ - '"Installing ''namespace1.name1:1.0.9'' to" in from_first_good_server.stdout'
+ - install_normal_files.files | length == 3
+ - install_normal_files.files[0].path | basename in ['MANIFEST.json', 'FILES.json', 'README.md']
+ - install_normal_files.files[1].path | basename in ['MANIFEST.json', 'FILES.json', 'README.md']
+ - install_normal_files.files[2].path | basename in ['MANIFEST.json', 'FILES.json', 'README.md']
+ - (install_normal_manifest.content | b64decode | from_json).collection_info.version == '1.0.9'
+
+- name: Remove the collection
+ file:
+ path: '{{ galaxy_dir }}/ansible_collections/namespace1'
+ state: absent
+
+# This command is hardcoded with -vvvv purposefully to evaluate extra verbosity messages
+- name: install simple collection with invalid detached signature
+ command: ansible-galaxy collection install namespace1.name1 -vvvv {{ signature_options }}
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+ ANSIBLE_NOCOLOR: True
+ ANSIBLE_FORCE_COLOR: False
+ vars:
+ signature_options: "--signature {{ signature }} --keyring {{ keyring }}"
+ signature: "file://{{ gpg_homedir }}/namespace2-name-1.0.0-MANIFEST.json.asc"
+ keyring: "{{ gpg_homedir }}/pubring.kbx"
+ ignore_errors: yes
+ register: invalid_signature
+
+- assert:
+ that:
+ - invalid_signature is failed
+ - "'Not installing namespace1.name1 because GnuPG signature verification failed.' in invalid_signature.stderr"
+ - expected_errors[0] in install_stdout
+ - expected_errors[1] in install_stdout
+ vars:
+ expected_errors:
+ - "* This is the counterpart to SUCCESS and used to indicate a program failure."
+ - "* The signature with the keyid has not been verified okay."
+ # Remove formatting from the reason so it's one line
+ install_stdout: "{{ invalid_signature.stdout | regex_replace('\"') | regex_replace('\\n') | regex_replace(' ', ' ') }}"
+
+- name: validate collection directory was not created
+ file:
+ path: '{{ galaxy_dir }}/ansible_collections/namespace1/name1'
+ state: absent
+ register: collection_dir
+ check_mode: yes
+ failed_when: collection_dir is changed
+
+- name: disable signature verification and install simple collection with invalid detached signature
+ command: ansible-galaxy collection install namespace1.name1 {{ galaxy_verbosity }} {{ signature_options }}
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+ vars:
+ signature_options: "--signature {{ signature }} --keyring {{ keyring }} --disable-gpg-verify"
+ signature: "file://{{ gpg_homedir }}/namespace2-name-1.0.0-MANIFEST.json.asc"
+ keyring: "{{ gpg_homedir }}/pubring.kbx"
+ ignore_errors: yes
+ register: ignore_invalid_signature
+
+- assert:
+ that:
+ - ignore_invalid_signature is success
+ - '"Installing ''namespace1.name1:1.0.9'' to" in ignore_invalid_signature.stdout'
+
+- name: use lenient signature verification (default) without providing signatures
+ command: ansible-galaxy collection install namespace1.name1:1.0.0 -vvvv --keyring {{ gpg_homedir }}/pubring.kbx --force
+ environment:
+ ANSIBLE_GALAXY_REQUIRED_VALID_SIGNATURE_COUNT: "all"
+ register: missing_signature
+
+- assert:
+ that:
+ - missing_signature is success
+ - missing_signature.rc == 0
+ - '"namespace1.name1:1.0.0 was installed successfully" in missing_signature.stdout'
+ - '"Signature verification failed for ''namespace1.name1'': no successful signatures" not in missing_signature.stdout'
+
+- name: use strict signature verification without providing signatures
+ command: ansible-galaxy collection install namespace1.name1:1.0.0 -vvvv --keyring {{ gpg_homedir }}/pubring.kbx --force
+ environment:
+ ANSIBLE_GALAXY_REQUIRED_VALID_SIGNATURE_COUNT: "+1"
+ ignore_errors: yes
+ register: missing_signature
+
+- assert:
+ that:
+ - missing_signature is failed
+ - missing_signature.rc == 1
+ - '"Signature verification failed for ''namespace1.name1'': no successful signatures" in missing_signature.stdout'
+ - '"Not installing namespace1.name1 because GnuPG signature verification failed" in missing_signature.stderr'
+
+- name: Remove the collection
+ file:
+ path: '{{ galaxy_dir }}/ansible_collections/namespace1'
+ state: absent
+
+- name: download collections with pre-release dep - {{ test_id }}
+ command: ansible-galaxy collection download dep_with_beta.parent namespace1.name1:1.1.0-beta.1 -p '{{ galaxy_dir }}/scratch'
+
+- name: install collection with concrete pre-release dep - {{ test_id }}
+ command: ansible-galaxy collection install -r '{{ galaxy_dir }}/scratch/requirements.yml'
+ args:
+ chdir: '{{ galaxy_dir }}/scratch'
+ environment:
+ ANSIBLE_COLLECTIONS_PATHS: '{{ galaxy_dir }}/ansible_collections'
+ register: install_concrete_pre
+
+- name: get result of install collections with concrete pre-release dep - {{ test_id }}
+ slurp:
+ path: '{{ galaxy_dir }}/ansible_collections/{{ collection }}/MANIFEST.json'
+ register: install_concrete_pre_actual
+ loop_control:
+ loop_var: collection
+ loop:
+ - namespace1/name1
+ - dep_with_beta/parent
+
+- name: assert install collections with ansible-galaxy install - {{ test_id }}
+ assert:
+ that:
+ - '"Installing ''namespace1.name1:1.1.0-beta.1'' to" in install_concrete_pre.stdout'
+ - '"Installing ''dep_with_beta.parent:1.0.0'' to" in install_concrete_pre.stdout'
+ - (install_concrete_pre_actual.results[0].content | b64decode | from_json).collection_info.version == '1.1.0-beta.1'
+ - (install_concrete_pre_actual.results[1].content | b64decode | from_json).collection_info.version == '1.0.0'
+
+- name: remove collection dir after round of testing - {{ test_id }}
+ file:
+ path: '{{ galaxy_dir }}/ansible_collections'
+ state: absent
diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/install_offline.yml b/test/integration/targets/ansible-galaxy-collection/tasks/install_offline.yml
new file mode 100644
index 0000000..74c9983
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection/tasks/install_offline.yml
@@ -0,0 +1,137 @@
+- name: test tarfile dependency resolution without querying distribution servers
+ vars:
+ init_dir: "{{ galaxy_dir }}/offline/setup"
+ build_dir: "{{ galaxy_dir }}/offline/build"
+ install_dir: "{{ galaxy_dir }}/offline/collections"
+ block:
+ - name: create test directories
+ file:
+ path: "{{ item }}"
+ state: directory
+ loop:
+ - "{{ init_dir }}"
+ - "{{ build_dir }}"
+ - "{{ install_dir }}"
+
+ - name: initialize two collections
+ command: ansible-galaxy collection init ns.{{ item }} --init-path {{ init_dir }}
+ loop:
+ - coll1
+ - coll2
+
+ - name: add one collection as the dependency of the other
+ lineinfile:
+ path: "{{ galaxy_dir }}/offline/setup/ns/coll1/galaxy.yml"
+ regexp: "^dependencies:*"
+ line: "dependencies: {'ns.coll2': '>=1.0.0'}"
+
+ - name: build both collections
+ command: ansible-galaxy collection build {{ init_dir }}/ns/{{ item }}
+ args:
+ chdir: "{{ build_dir }}"
+ loop:
+ - coll1
+ - coll2
+
+ - name: install the dependency from the tarfile
+ command: ansible-galaxy collection install {{ build_dir }}/ns-coll2-1.0.0.tar.gz -p {{ install_dir }} -s offline
+
+ - name: install the tarfile with the installed dependency
+ command: ansible-galaxy collection install {{ build_dir }}/ns-coll1-1.0.0.tar.gz -p {{ install_dir }} -s offline
+
+ - name: empty the installed collections directory
+ file:
+ path: "{{ install_dir }}"
+ state: "{{ item }}"
+ loop:
+ - absent
+ - directory
+
+ - name: edit skeleton with new versions to test upgrading
+ lineinfile:
+ path: "{{ galaxy_dir }}/offline/setup/ns/{{ item }}/galaxy.yml"
+ regexp: "^version:.*$"
+ line: "version: 2.0.0"
+ loop:
+ - coll1
+ - coll2
+
+ - name: build the tarfiles to test upgrading
+ command: ansible-galaxy collection build {{ init_dir }}/ns/{{ item }}
+ args:
+ chdir: "{{ build_dir }}"
+ loop:
+ - coll1
+ - coll2
+
+ - name: install the tarfile and its dep with an unreachable server (expected error)
+ command: ansible-galaxy collection install {{ build_dir }}/ns-coll1-1.0.0.tar.gz -p {{ install_dir }} -s offline
+ environment:
+ ANSIBLE_FORCE_COLOR: False
+ ANSIBLE_NOCOLOR: True
+ ignore_errors: yes
+ register: missing_dep
+
+ - name: install the tarfile with a missing dependency and --offline
+ command: ansible-galaxy collection install --offline {{ build_dir }}/ns-coll1-1.0.0.tar.gz -p {{ install_dir }} -s offline
+ environment:
+ ANSIBLE_FORCE_COLOR: False
+ ANSIBLE_NOCOLOR: True
+ ignore_errors: yes
+ register: missing_dep_offline
+
+ - assert:
+ that:
+ - missing_dep.failed
+ - missing_dep_offline.failed
+ - galaxy_err in missing_dep.stderr
+ - missing_err in missing_dep_offline.stderr
+ vars:
+ galaxy_err: "ERROR! Unknown error when attempting to call Galaxy at '{{ offline_server }}'"
+ missing_err: |-
+ ERROR! Failed to resolve the requested dependencies map. Could not satisfy the following requirements:
+ * ns.coll2:>=1.0.0 (dependency of ns.coll1:1.0.0)
+
+ - name: install the dependency from the tarfile
+ command: ansible-galaxy collection install {{ build_dir }}/ns-coll2-1.0.0.tar.gz -p {{ install_dir }}
+
+ - name: install the tarfile with --offline for dep resolution
+ command: ansible-galaxy collection install {{ build_dir }}/ns-coll1-1.0.0.tar.gz {{ cli_opts }}
+ vars:
+ cli_opts: "--offline -p {{ install_dir }} -s offline"
+ register: offline_install
+
+ - name: upgrade using tarfile with --offline for dep resolution
+ command: ansible-galaxy collection install {{ build_dir }}/ns-coll1-2.0.0.tar.gz {{ cli_opts }}
+ vars:
+ cli_opts: "--offline --upgrade -p {{ install_dir }} -s offline"
+ register: offline_upgrade
+
+ - name: reinstall ns-coll1-1.0.0 to test upgrading the dependency too
+ command: ansible-galaxy collection install {{ build_dir }}/ns-coll1-1.0.0.tar.gz {{ cli_opts }}
+ vars:
+ cli_opts: "--offline --force -p {{ install_dir }} -s offline"
+
+ - name: upgrade both collections using tarfiles and --offline
+ command: ansible-galaxy collection install {{ collections }} {{ cli_opts }}
+ vars:
+ collections: "{{ build_dir }}/ns-coll1-2.0.0.tar.gz {{ build_dir }}/ns-coll2-2.0.0.tar.gz"
+ cli_opts: "--offline --upgrade -s offline"
+ register: upgrade_all
+
+ - assert:
+ that:
+ - '"ns.coll1:1.0.0 was installed successfully" in offline_install.stdout'
+ - '"ns.coll1:2.0.0 was installed successfully" in offline_upgrade.stdout'
+ - '"ns.coll1:2.0.0 was installed successfully" in upgrade_all.stdout'
+ - '"ns.coll2:2.0.0 was installed successfully" in upgrade_all.stdout'
+
+ always:
+ - name: clean up test directories
+ file:
+ path: "{{ item }}"
+ state: absent
+ loop:
+ - "{{ init_dir }}"
+ - "{{ build_dir }}"
+ - "{{ install_dir }}"
diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/list.yml b/test/integration/targets/ansible-galaxy-collection/tasks/list.yml
new file mode 100644
index 0000000..b8d6349
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection/tasks/list.yml
@@ -0,0 +1,167 @@
+- name: initialize collection structure
+ command: ansible-galaxy collection init {{ item }} --init-path "{{ galaxy_dir }}/dev/ansible_collections" {{ galaxy_verbosity }}
+ loop:
+ - 'dev.collection1'
+ - 'dev.collection2'
+ - 'dev.collection3'
+ - 'dev.collection4'
+ - 'dev.collection5'
+ - 'dev.collection6'
+
+- name: replace the default version of the collections
+ lineinfile:
+ path: "{{ galaxy_dir }}/dev/ansible_collections/dev/{{ item.name }}/galaxy.yml"
+ line: "{{ item.version }}"
+ regexp: "version: .*"
+ loop:
+ - name: "collection1"
+ version: "version: null"
+ - name: "collection2"
+ version: "version: placeholder"
+ - name: "collection3"
+ version: "version: ''"
+
+- name: set the namespace, name, and version keys to None
+ lineinfile:
+ path: "{{ galaxy_dir }}/dev/ansible_collections/dev/collection4/galaxy.yml"
+ line: "{{ item.after }}"
+ regexp: "{{ item.before }}"
+ loop:
+ - before: "^namespace: dev"
+ after: "namespace:"
+ - before: "^name: collection4"
+ after: "name:"
+ - before: "^version: 1.0.0"
+ after: "version:"
+
+- name: replace galaxy.yml content with a string
+ copy:
+ content: "invalid"
+ dest: "{{ galaxy_dir }}/dev/ansible_collections/dev/collection5/galaxy.yml"
+
+- name: remove galaxy.yml key required by build
+ lineinfile:
+ path: "{{ galaxy_dir }}/dev/ansible_collections/dev/collection6/galaxy.yml"
+ line: "version: 1.0.0"
+ state: absent
+
+- name: list collections in development without semver versions
+ command: ansible-galaxy collection list {{ galaxy_verbosity }}
+ register: list_result
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: "{{ galaxy_dir }}/dev:{{ galaxy_dir }}/prod"
+
+- assert:
+ that:
+ - "'dev.collection1 *' in list_result.stdout"
+ # Note the version displayed is the 'placeholder' string rather than "*" since it is not falsey
+ - "'dev.collection2 placeholder' in list_result.stdout"
+ - "'dev.collection3 *' in list_result.stdout"
+ - "'dev.collection4 *' in list_result.stdout"
+ - "'dev.collection5 *' in list_result.stdout"
+ - "'dev.collection6 *' in list_result.stdout"
+
+- name: list collections in human format
+ command: ansible-galaxy collection list --format human
+ register: list_result_human
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: "{{ galaxy_dir }}/dev:{{ galaxy_dir }}/prod"
+
+- assert:
+ that:
+ - "'dev.collection1 *' in list_result_human.stdout"
+ # Note the version displayed is the 'placeholder' string rather than "*" since it is not falsey
+ - "'dev.collection2 placeholder' in list_result_human.stdout"
+ - "'dev.collection3 *' in list_result_human.stdout"
+ - "'dev.collection5 *' in list_result.stdout"
+ - "'dev.collection6 *' in list_result.stdout"
+
+- name: list collections in yaml format
+ command: ansible-galaxy collection list --format yaml
+ register: list_result_yaml
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: "{{ galaxy_dir }}/dev:{{ galaxy_dir }}/prod"
+
+- assert:
+ that:
+ - "item.value | length == 6"
+ - "item.value['dev.collection1'].version == '*'"
+ - "item.value['dev.collection2'].version == 'placeholder'"
+ - "item.value['dev.collection3'].version == '*'"
+ - "item.value['dev.collection5'].version == '*'"
+ - "item.value['dev.collection6'].version == '*'"
+ with_dict: "{{ list_result_yaml.stdout | from_yaml }}"
+
+- name: list collections in json format
+ command: ansible-galaxy collection list --format json
+ register: list_result_json
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: "{{ galaxy_dir }}/dev:{{ galaxy_dir }}/prod"
+
+- assert:
+ that:
+ - "item.value | length == 6"
+ - "item.value['dev.collection1'].version == '*'"
+ - "item.value['dev.collection2'].version == 'placeholder'"
+ - "item.value['dev.collection3'].version == '*'"
+ - "item.value['dev.collection5'].version == '*'"
+ - "item.value['dev.collection6'].version == '*'"
+ with_dict: "{{ list_result_json.stdout | from_json }}"
+
+- name: list single collection in json format
+ command: "ansible-galaxy collection list {{ item.key }} --format json"
+ register: list_single_result_json
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: "{{ galaxy_dir }}/dev:{{ galaxy_dir }}/prod"
+ with_dict: "{{ { 'dev.collection1': '*', 'dev.collection2': 'placeholder', 'dev.collection3': '*' } }}"
+
+- assert:
+ that:
+ - "(item.stdout | from_json)[galaxy_dir + '/dev/ansible_collections'][item.item.key].version == item.item.value"
+ with_items: "{{ list_single_result_json.results }}"
+
+- name: list single collection in yaml format
+ command: "ansible-galaxy collection list {{ item.key }} --format yaml"
+ register: list_single_result_yaml
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: "{{ galaxy_dir }}/dev:{{ galaxy_dir }}/prod"
+ with_dict: "{{ { 'dev.collection1': '*', 'dev.collection2': 'placeholder', 'dev.collection3': '*' } }}"
+
+- assert:
+ that:
+ - "(item.stdout | from_yaml)[galaxy_dir + '/dev/ansible_collections'][item.item.key].version == item.item.value"
+ with_items: "{{ list_single_result_json.results }}"
+
+- name: test that no json is emitted when no collection paths are usable
+ command: "ansible-galaxy collection list --format json"
+ register: list_result_error
+ ignore_errors: True
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: ""
+
+- assert:
+ that:
+ - "'{}' not in list_result_error.stdout"
+
+- name: install an artifact to the second collections path
+ command: ansible-galaxy collection install namespace1.name1 -s galaxy_ng {{ galaxy_verbosity }} -p "{{ galaxy_dir }}/prod"
+ environment:
+ ANSIBLE_CONFIG: '{{ galaxy_dir }}/ansible.cfg'
+
+- name: replace the artifact version
+ lineinfile:
+ path: "{{ galaxy_dir }}/prod/ansible_collections/namespace1/name1/MANIFEST.json"
+ line: ' "version": null,'
+ regexp: ' "version": .*'
+
+- name: test listing collections in all paths
+ command: ansible-galaxy collection list {{ galaxy_verbosity }}
+ register: list_result
+ ignore_errors: True
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: "{{ galaxy_dir }}/dev:{{ galaxy_dir }}/prod"
+
+- assert:
+ that:
+ - list_result is failed
+ - "'is expected to have a valid SemVer version value but got None' in list_result.stderr"
diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/main.yml b/test/integration/targets/ansible-galaxy-collection/tasks/main.yml
new file mode 100644
index 0000000..724c861
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection/tasks/main.yml
@@ -0,0 +1,220 @@
+---
+- name: set some facts for tests
+ set_fact:
+ galaxy_dir: "{{ remote_tmp_dir }}/galaxy"
+
+- name: create scratch dir used for testing
+ file:
+ path: '{{ galaxy_dir }}/scratch'
+ state: directory
+
+- name: run ansible-galaxy collection init tests
+ import_tasks: init.yml
+
+- name: run ansible-galaxy collection build tests
+ import_tasks: build.yml
+
+# The pulp container has a long start up time
+# The first task to interact with pulp needs to wait until it responds appropriately
+- name: list pulp distributions
+ uri:
+ url: '{{ pulp_api }}/pulp/api/v3/distributions/ansible/ansible/'
+ status_code:
+ - 200
+ user: '{{ pulp_user }}'
+ password: '{{ pulp_password }}'
+ force_basic_auth: true
+ register: pulp_distributions
+ until: pulp_distributions is successful
+ delay: 1
+ retries: 60
+
+- name: configure pulp
+ include_tasks: pulp.yml
+
+- name: Get galaxy_ng token
+ uri:
+ url: '{{ galaxy_ng_server }}v3/auth/token/'
+ method: POST
+ body_format: json
+ body: {}
+ status_code:
+ - 200
+ user: '{{ pulp_user }}'
+ password: '{{ pulp_password }}'
+ force_basic_auth: true
+ register: galaxy_ng_token
+
+- name: create test ansible.cfg that contains the Galaxy server list
+ template:
+ src: ansible.cfg.j2
+ dest: '{{ galaxy_dir }}/ansible.cfg'
+
+- name: test install command using an unsupported version of resolvelib
+ include_tasks: unsupported_resolvelib.yml
+ loop: "{{ unsupported_resolvelib_versions }}"
+ loop_control:
+ loop_var: resolvelib_version
+
+- name: run ansible-galaxy collection offline installation tests
+ include_tasks: install_offline.yml
+ args:
+ apply:
+ environment:
+ ANSIBLE_CONFIG: '{{ galaxy_dir }}/ansible.cfg'
+
+- name: run ansible-galaxy collection publish tests for {{ test_name }}
+ include_tasks: publish.yml
+ args:
+ apply:
+ environment:
+ ANSIBLE_CONFIG: '{{ galaxy_dir }}/ansible.cfg'
+ vars:
+ test_name: '{{ item.name }}'
+ test_server: '{{ item.server }}'
+ vX: '{{ "v3/" if item.v3|default(false) else "v2/" }}'
+ loop:
+ - name: pulp_v2
+ server: '{{ pulp_server }}published/api/'
+ - name: pulp_v3
+ server: '{{ pulp_server }}published/api/'
+ v3: true
+ - name: galaxy_ng
+ server: '{{ galaxy_ng_server }}'
+ v3: true
+
+- include_tasks: setup_gpg.yml
+
+# We use a module for this so we can speed up the test time.
+# For pulp interactions, we only upload to galaxy_ng which shares
+# the same repo and distribution with pulp_ansible
+# However, we use galaxy_ng only, since collections are unique across
+# pulp repositories, and galaxy_ng maintains a 2nd list of published collections
+- name: setup test collections for install and download test
+ setup_collections:
+ server: galaxy_ng
+ collections: '{{ collection_list }}'
+ signature_dir: '{{ gpg_homedir }}'
+ environment:
+ ANSIBLE_CONFIG: '{{ galaxy_dir }}/ansible.cfg'
+
+# Stores the cached test version number index as we run install many times
+- set_fact:
+ cache_version_build: 0
+
+- name: run ansible-galaxy collection install tests for {{ test_name }}
+ include_tasks: install.yml
+ vars:
+ test_id: '{{ item.name }}'
+ test_name: '{{ item.name }}'
+ test_server: '{{ item.server }}'
+ vX: '{{ "v3/" if item.v3|default(false) else "v2/" }}'
+ requires_auth: '{{ item.requires_auth|default(false) }}'
+ args:
+ apply:
+ environment:
+ ANSIBLE_CONFIG: '{{ galaxy_dir }}/ansible.cfg'
+ loop:
+ - name: galaxy_ng
+ server: '{{ galaxy_ng_server }}'
+ v3: true
+ requires_auth: true
+ - name: pulp_v2
+ server: '{{ pulp_server }}published/api/'
+ - name: pulp_v3
+ server: '{{ pulp_server }}published/api/'
+ v3: true
+
+- name: test installing and downloading collections with the range of supported resolvelib versions
+ include_tasks: supported_resolvelib.yml
+ args:
+ apply:
+ environment:
+ ANSIBLE_CONFIG: '{{ galaxy_dir }}/ansible.cfg'
+ loop: '{{ supported_resolvelib_versions }}'
+ loop_control:
+ loop_var: resolvelib_version
+
+- name: publish collection with a dep on another server
+ setup_collections:
+ server: secondary
+ collections:
+ - namespace: secondary
+ name: name
+ # parent_dep.parent_collection does not exist on the secondary server
+ dependencies:
+ parent_dep.parent_collection: '1.0.0'
+ environment:
+ ANSIBLE_CONFIG: '{{ galaxy_dir }}/ansible.cfg'
+
+- name: install collection with dep on another server
+ command: ansible-galaxy collection install secondary.name -vvv # 3 -v's will show the source in the stdout
+ register: install_cross_dep
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+ ANSIBLE_CONFIG: '{{ galaxy_dir }}/ansible.cfg'
+
+- name: get result of install collection with dep on another server
+ slurp:
+ path: '{{ galaxy_dir }}/ansible_collections/{{ item.namespace }}/{{ item.name }}/MANIFEST.json'
+ register: install_cross_dep_actual
+ loop:
+ - namespace: secondary
+ name: name
+ - namespace: parent_dep
+ name: parent_collection
+ - namespace: child_dep
+ name: child_collection
+ - namespace: child_dep
+ name: child_dep2
+
+- name: assert result of install collection with dep on another server
+ assert:
+ that:
+ - >-
+ "'secondary.name:1.0.0' obtained from server secondary"
+ in install_cross_dep.stdout
+ # pulp_v2 is highest in the list so it will find it there first
+ - >-
+ "'parent_dep.parent_collection:1.0.0' obtained from server pulp_v2"
+ in install_cross_dep.stdout
+ - >-
+ "'child_dep.child_collection:0.9.9' obtained from server pulp_v2"
+ in install_cross_dep.stdout
+ - >-
+ "'child_dep.child_dep2:1.2.2' obtained from server pulp_v2"
+ in install_cross_dep.stdout
+ - (install_cross_dep_actual.results[0].content | b64decode | from_json).collection_info.version == '1.0.0'
+ - (install_cross_dep_actual.results[1].content | b64decode | from_json).collection_info.version == '1.0.0'
+ - (install_cross_dep_actual.results[2].content | b64decode | from_json).collection_info.version == '0.9.9'
+ - (install_cross_dep_actual.results[3].content | b64decode | from_json).collection_info.version == '1.2.2'
+
+- name: run ansible-galaxy collection download tests
+ include_tasks: download.yml
+ args:
+ apply:
+ environment:
+ ANSIBLE_CONFIG: '{{ galaxy_dir }}/ansible.cfg'
+
+- name: run ansible-galaxy collection verify tests for {{ test_name }}
+ include_tasks: verify.yml
+ args:
+ apply:
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}'
+ ANSIBLE_CONFIG: '{{ galaxy_dir }}/ansible.cfg'
+ vars:
+ test_api_fallback: 'pulp_v2'
+ test_api_fallback_versions: 'v1, v2'
+ test_name: 'galaxy_ng'
+ test_server: '{{ galaxy_ng_server }}'
+
+- name: run ansible-galaxy collection list tests
+ include_tasks: list.yml
+
+- include_tasks: upgrade.yml
+ args:
+ apply:
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}'
+ ANSIBLE_CONFIG: '{{ galaxy_dir }}/ansible.cfg'
diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/publish.yml b/test/integration/targets/ansible-galaxy-collection/tasks/publish.yml
new file mode 100644
index 0000000..241eae6
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection/tasks/publish.yml
@@ -0,0 +1,33 @@
+---
+- name: publish collection - {{ test_name }}
+ command: ansible-galaxy collection publish ansible_test-my_collection-1.0.0.tar.gz -s {{ test_name }} {{ galaxy_verbosity }}
+ args:
+ chdir: '{{ galaxy_dir }}'
+ register: publish_collection
+
+- name: get result of publish collection - {{ test_name }}
+ uri:
+ url: '{{ test_server }}{{ vX }}collections/ansible_test/my_collection/versions/1.0.0/'
+ return_content: yes
+ user: '{{ pulp_user }}'
+ password: '{{ pulp_password }}'
+ force_basic_auth: true
+ register: publish_collection_actual
+
+- name: assert publish collection - {{ test_name }}
+ assert:
+ that:
+ - '"Collection has been successfully published and imported to the Galaxy server" in publish_collection.stdout'
+ - publish_collection_actual.json.collection.name == 'my_collection'
+ - publish_collection_actual.json.namespace.name == 'ansible_test'
+ - publish_collection_actual.json.version == '1.0.0'
+
+- name: fail to publish existing collection version - {{ test_name }}
+ command: ansible-galaxy collection publish ansible_test-my_collection-1.0.0.tar.gz -s {{ test_name }} {{ galaxy_verbosity }}
+ args:
+ chdir: '{{ galaxy_dir }}'
+ register: fail_publish_existing
+ failed_when: fail_publish_existing is not failed
+
+- name: reset published collections - {{ test_name }}
+ include_tasks: pulp.yml
diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/pulp.yml b/test/integration/targets/ansible-galaxy-collection/tasks/pulp.yml
new file mode 100644
index 0000000..7b6c5f8
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection/tasks/pulp.yml
@@ -0,0 +1,11 @@
+# These tasks configure pulp/pulp_ansible so that we can use the container
+# This will also reset pulp between iterations
+# A module is used to make the tests run quicker as this will send lots of API requests.
+- name: reset pulp content
+ reset_pulp:
+ pulp_api: '{{ pulp_api }}'
+ galaxy_ng_server: '{{ galaxy_ng_server }}'
+ url_username: '{{ pulp_user }}'
+ url_password: '{{ pulp_password }}'
+ repositories: '{{ pulp_repositories }}'
+ namespaces: '{{ collection_list|map(attribute="namespace")|unique + publish_namespaces }}'
diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/revoke_gpg_key.yml b/test/integration/targets/ansible-galaxy-collection/tasks/revoke_gpg_key.yml
new file mode 100644
index 0000000..7a49eee
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection/tasks/revoke_gpg_key.yml
@@ -0,0 +1,14 @@
+- name: generate revocation certificate
+ expect:
+ command: "gpg --homedir {{ gpg_homedir }} --pinentry-mode loopback --output {{ gpg_homedir }}/revoke.asc --gen-revoke {{ fingerprint }}"
+ responses:
+ "Create a revocation certificate for this key": "y"
+ "Please select the reason for the revocation": "0"
+ "Enter an optional description": ""
+ "Is this okay": "y"
+
+- name: revoke key
+ command: "gpg --no-tty --homedir {{ gpg_homedir }} --import {{ gpg_homedir }}/revoke.asc"
+
+- name: list keys for debugging
+ command: "gpg --no-tty --homedir {{ gpg_homedir }} --list-keys {{ gpg_user }}"
diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/setup_gpg.yml b/test/integration/targets/ansible-galaxy-collection/tasks/setup_gpg.yml
new file mode 100644
index 0000000..ddc4d8a
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection/tasks/setup_gpg.yml
@@ -0,0 +1,24 @@
+- name: create empty gpg homedir
+ file:
+ state: "{{ item }}"
+ path: "{{ gpg_homedir }}"
+ mode: 0700
+ loop:
+ - absent
+ - directory
+
+- name: get username for generating key
+ command: whoami
+ register: user
+
+- name: generate key for user with gpg
+ command: "gpg --no-tty --homedir {{ gpg_homedir }} --passphrase '' --pinentry-mode loopback --quick-gen-key {{ user.stdout }} default default"
+
+- name: list gpg keys for user
+ command: "gpg --no-tty --homedir {{ gpg_homedir }} --list-keys {{ user.stdout }}"
+ register: gpg_list_keys
+
+- name: save gpg user and fingerprint of new key
+ set_fact:
+ gpg_user: "{{ user.stdout }}"
+ fingerprint: "{{ gpg_list_keys.stdout_lines[1] | trim }}"
diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/supported_resolvelib.yml b/test/integration/targets/ansible-galaxy-collection/tasks/supported_resolvelib.yml
new file mode 100644
index 0000000..763c5a1
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection/tasks/supported_resolvelib.yml
@@ -0,0 +1,44 @@
+- vars:
+ venv_cmd: "{{ ansible_python_interpreter ~ ' -m venv' }}"
+ venv_dest: "{{ galaxy_dir }}/test_venv_{{ resolvelib_version }}"
+ block:
+ - name: install another version of resolvelib that is supported by ansible-galaxy
+ pip:
+ name: resolvelib
+ version: "{{ resolvelib_version }}"
+ state: present
+ virtualenv_command: "{{ venv_cmd }}"
+ virtualenv: "{{ venv_dest }}"
+ virtualenv_site_packages: True
+
+ - include_tasks: fail_fast_resolvelib.yml
+ args:
+ apply:
+ environment:
+ PATH: "{{ venv_dest }}/bin:{{ ansible_env.PATH }}"
+ ANSIBLE_CONFIG: '{{ galaxy_dir }}/ansible.cfg'
+
+ - include_tasks: install.yml
+ vars:
+ test_name: pulp_v3
+ test_id: '{{ test_name }} (resolvelib {{ resolvelib_version }})'
+ test_server: '{{ pulp_server }}published/api/'
+ vX: "v3/"
+ requires_auth: false
+ args:
+ apply:
+ environment:
+ PATH: "{{ venv_dest }}/bin:{{ ansible_env.PATH }}"
+ ANSIBLE_CONFIG: '{{ galaxy_dir }}/ansible.cfg'
+
+ - include_tasks: download.yml
+ args:
+ apply:
+ environment:
+ PATH: "{{ venv_dest }}/bin:{{ ansible_env.PATH }}"
+ ANSIBLE_CONFIG: '{{ galaxy_dir }}/ansible.cfg'
+ always:
+ - name: remove test venv
+ file:
+ path: "{{ venv_dest }}"
+ state: absent
diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/unsupported_resolvelib.yml b/test/integration/targets/ansible-galaxy-collection/tasks/unsupported_resolvelib.yml
new file mode 100644
index 0000000..a208b29
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection/tasks/unsupported_resolvelib.yml
@@ -0,0 +1,44 @@
+- vars:
+ venv_cmd: "{{ ansible_python_interpreter ~ ' -m venv' }}"
+ venv_dest: "{{ galaxy_dir }}/test_resolvelib_{{ resolvelib_version }}"
+ block:
+ - name: install another version of resolvelib that is unsupported by ansible-galaxy
+ pip:
+ name: resolvelib
+ version: "{{ resolvelib_version }}"
+ state: present
+ virtualenv_command: "{{ venv_cmd }}"
+ virtualenv: "{{ venv_dest }}"
+ virtualenv_site_packages: True
+
+ - name: create test collection install directory - {{ test_name }}
+ file:
+ path: '{{ galaxy_dir }}/ansible_collections'
+ state: directory
+
+ - name: install simple collection from first accessible server (expected failure)
+ command: "ansible-galaxy collection install namespace1.name1 {{ galaxy_verbosity }}"
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+ PATH: "{{ venv_dest }}/bin:{{ ansible_env.PATH }}"
+ register: resolvelib_version_error
+ ignore_errors: yes
+
+ - assert:
+ that:
+ - resolvelib_version_error is failed
+ - resolvelib_version_error.stderr | regex_search(error)
+ vars:
+ error: "({{ import_error }}|{{ compat_error }})"
+ import_error: "Failed to import resolvelib"
+ compat_error: "ansible-galaxy requires resolvelib<{{major_minor_patch}},>={{major_minor_patch}}"
+ major_minor_patch: "[0-9]\\d*\\.[0-9]\\d*\\.[0-9]\\d*"
+
+ always:
+ - name: cleanup venv and install directory
+ file:
+ path: '{{ galaxy_dir }}/ansible_collections'
+ state: absent
+ loop:
+ - '{{ galaxy_dir }}/ansible_collections'
+ - '{{ venv_dest }}'
diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/upgrade.yml b/test/integration/targets/ansible-galaxy-collection/tasks/upgrade.yml
new file mode 100644
index 0000000..893ea80
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection/tasks/upgrade.yml
@@ -0,0 +1,282 @@
+##### Updating collections with a new version constraint
+
+# No deps
+
+- name: reset installation directory
+ file:
+ state: "{{ item }}"
+ path: "{{ galaxy_dir }}/ansible_collections"
+ loop:
+ - absent
+ - directory
+
+- name: install a collection
+ command: ansible-galaxy collection install namespace1.name1:0.0.1 {{ galaxy_verbosity }}
+ register: result
+ failed_when:
+ - '"namespace1.name1:0.0.1 was installed successfully" not in result.stdout_lines'
+
+- name: install a new version of the collection without --force
+ command: ansible-galaxy collection install namespace1.name1:>0.0.4,<=0.0.5 {{ galaxy_verbosity }}
+ register: result
+
+- name: check the MANIFEST.json to verify the version
+ slurp:
+ path: '{{ galaxy_dir }}/ansible_collections/namespace1/name1/MANIFEST.json'
+ register: metadata
+
+- assert:
+ that:
+ - '"namespace1.name1:0.0.5 was installed successfully" in result.stdout_lines'
+ - (metadata.content | b64decode | from_json).collection_info.version == '0.0.5'
+
+- name: don't reinstall the collection in the requirement is compatible
+ command: ansible-galaxy collection install namespace1.name1:>=0.0.5 {{ galaxy_verbosity }}
+ register: result
+
+- assert:
+ that: "\"Nothing to do. All requested collections are already installed.\" in result.stdout"
+
+- name: install a pre-release of the collection without --force
+ command: ansible-galaxy collection install namespace1.name1:1.1.0-beta.1 {{ galaxy_verbosity }}
+ register: result
+
+- name: check the MANIFEST.json to verify the version
+ slurp:
+ path: '{{ galaxy_dir }}/ansible_collections/namespace1/name1/MANIFEST.json'
+ register: metadata
+
+- assert:
+ that:
+ - '"namespace1.name1:1.1.0-beta.1 was installed successfully" in result.stdout_lines'
+ - (metadata.content | b64decode | from_json).collection_info.version == '1.1.0-beta.1'
+
+# With deps
+
+- name: reset installation directory
+ file:
+ state: "{{ item }}"
+ path: "{{ galaxy_dir }}/ansible_collections"
+ loop:
+ - absent
+ - directory
+
+- name: install a dep that will need to be upgraded to be compatible with the parent
+ command: ansible-galaxy collection install child_dep.child_collection:0.4.0 --no-deps {{ galaxy_verbosity }}
+ register: result
+ failed_when:
+ - '"child_dep.child_collection:0.4.0 was installed successfully" not in result.stdout_lines'
+
+- name: install a dep of the dep that will need to be upgraded to be compatible
+ command: ansible-galaxy collection install child_dep.child_dep2:>1.2.2 {{ galaxy_verbosity }}
+ register: result
+ failed_when:
+ - '"child_dep.child_dep2:1.2.3 was installed successfully" not in result.stdout_lines'
+
+- name: install the parent without deps to test dep reconciliation during upgrade
+ command: ansible-galaxy collection install parent_dep.parent_collection:0.0.1 {{ galaxy_verbosity }}
+ register: result
+ failed_when:
+ - '"parent_dep.parent_collection:0.0.1 was installed successfully" not in result.stdout_lines'
+
+- name: test upgrading the parent collection and dependencies
+ command: ansible-galaxy collection install parent_dep.parent_collection:>=1.0.0,<1.1.0 {{ galaxy_verbosity }}
+ register: result
+
+- name: check the MANIFEST.json to verify the versions
+ slurp:
+ path: '{{ galaxy_dir }}/ansible_collections/{{ item.namespace }}/{{ item.name }}/MANIFEST.json'
+ register: metadata
+ loop:
+ - namespace: parent_dep
+ name: parent_collection
+ - namespace: child_dep
+ name: child_collection
+ - namespace: child_dep
+ name: child_dep2
+
+- assert:
+ that:
+ - '"parent_dep.parent_collection:1.0.0 was installed successfully" in result.stdout_lines'
+ - (metadata.results[0].content | b64decode | from_json).collection_info.version == '1.0.0'
+ - '"child_dep.child_collection:0.9.9 was installed successfully" in result.stdout_lines'
+ - (metadata.results[1].content | b64decode | from_json).collection_info.version == '0.9.9'
+ - '"child_dep.child_dep2:1.2.2 was installed successfully" in result.stdout_lines'
+ - (metadata.results[2].content | b64decode | from_json).collection_info.version == '1.2.2'
+
+- name: test upgrading a collection only upgrades dependencies if necessary
+ command: ansible-galaxy collection install parent_dep.parent_collection:1.1.0 {{ galaxy_verbosity }}
+ register: result
+
+- name: check the MANIFEST.json to verify the versions
+ slurp:
+ path: '{{ galaxy_dir }}/ansible_collections/{{ item.namespace }}/{{ item.name }}/MANIFEST.json'
+ register: metadata
+ loop:
+ - namespace: parent_dep
+ name: parent_collection
+ - namespace: child_dep
+ name: child_collection
+ - namespace: child_dep
+ name: child_dep2
+
+- assert:
+ that:
+ - '"parent_dep.parent_collection:1.1.0 was installed successfully" in result.stdout_lines'
+ - (metadata.results[0].content | b64decode | from_json).collection_info.version == '1.1.0'
+ - "\"'child_dep.child_collection:0.9.9' is already installed, skipping.\" in result.stdout_lines"
+ - (metadata.results[1].content | b64decode | from_json).collection_info.version == '0.9.9'
+ - "\"'child_dep.child_dep2:1.2.2' is already installed, skipping.\" in result.stdout_lines"
+ - (metadata.results[2].content | b64decode | from_json).collection_info.version == '1.2.2'
+
+##### Updating collections with --upgrade
+
+# No deps
+
+- name: reset installation directory
+ file:
+ state: "{{ item }}"
+ path: "{{ galaxy_dir }}/ansible_collections"
+ loop:
+ - absent
+ - directory
+
+- name: install a collection
+ command: ansible-galaxy collection install namespace1.name1:0.0.1 {{ galaxy_verbosity }}
+ register: result
+ failed_when:
+ - '"namespace1.name1:0.0.1 was installed successfully" not in result.stdout_lines'
+
+- name: install a new version of the collection with --upgrade
+ command: ansible-galaxy collection install namespace1.name1:<=0.0.5 --upgrade {{ galaxy_verbosity }}
+ register: result
+
+- name: check the MANIFEST.json to verify the version
+ slurp:
+ path: '{{ galaxy_dir }}/ansible_collections/namespace1/name1/MANIFEST.json'
+ register: metadata
+
+- assert:
+ that:
+ - '"namespace1.name1:0.0.5 was installed successfully" in result.stdout_lines'
+ - (metadata.content | b64decode | from_json).collection_info.version == '0.0.5'
+
+- name: upgrade the collection
+ command: ansible-galaxy collection install namespace1.name1:<0.0.7 -U {{ galaxy_verbosity }}
+ register: result
+
+- assert:
+ that: '"namespace1.name1:0.0.6 was installed successfully" in result.stdout_lines'
+
+- name: upgrade the collection to the last version excluding prereleases
+ command: ansible-galaxy collection install namespace1.name1 -U {{ galaxy_verbosity }}
+ register: result
+
+- assert:
+ that: '"namespace1.name1:1.0.9 was installed successfully" in result.stdout_lines'
+
+- name: upgrade the collection to the latest available version including prereleases
+ command: ansible-galaxy collection install namespace1.name1 --pre -U {{ galaxy_verbosity }}
+ register: result
+
+- assert:
+ that: '"namespace1.name1:1.1.0-beta.1 was installed successfully" in result.stdout_lines'
+
+- name: run the same command again
+ command: ansible-galaxy collection install namespace1.name1 --pre -U {{ galaxy_verbosity }}
+ register: result
+
+- assert:
+ that: "\"'namespace1.name1:1.1.0-beta.1' is already installed, skipping.\" in result.stdout"
+
+# With deps
+
+- name: reset installation directory
+ file:
+ state: "{{ item }}"
+ path: "{{ galaxy_dir }}/ansible_collections"
+ loop:
+ - absent
+ - directory
+
+- name: install a parent collection and a particular version of its dependency
+ command: ansible-galaxy collection install parent_dep.parent_collection:1.0.0 child_dep.child_collection:0.5.0 {{ galaxy_verbosity }}
+ register: result
+ failed_when:
+ - '"parent_dep.parent_collection:1.0.0 was installed successfully" not in result.stdout_lines'
+ - '"child_dep.child_collection:0.5.0 was installed successfully" not in result.stdout_lines'
+
+- name: upgrade the parent and child - the child's new version has a dependency that should be installed
+ command: ansible-galaxy collection install parent_dep.parent_collection -U {{ galaxy_verbosity }}
+ register: result
+
+- name: check the MANIFEST.json to verify the versions
+ slurp:
+ path: '{{ galaxy_dir }}/ansible_collections/{{ item.namespace }}/{{ item.name }}/MANIFEST.json'
+ register: metadata
+ loop:
+ - namespace: parent_dep
+ name: parent_collection
+ - namespace: child_dep
+ name: child_collection
+ - namespace: child_dep
+ name: child_dep2
+
+- assert:
+ that:
+ - '"parent_dep.parent_collection:2.0.0 was installed successfully" in result.stdout_lines'
+ - (metadata.results[0].content | b64decode | from_json).collection_info.version == '2.0.0'
+ - '"child_dep.child_collection:1.0.0 was installed successfully" in result.stdout_lines'
+ - (metadata.results[1].content | b64decode | from_json).collection_info.version == '1.0.0'
+ - '"child_dep.child_dep2:1.2.2 was installed successfully" in result.stdout_lines'
+ - (metadata.results[2].content | b64decode | from_json).collection_info.version == '1.2.2'
+
+# Test using a requirements.yml file for upgrade
+
+- name: reset installation directory
+ file:
+ state: "{{ item }}"
+ path: "{{ galaxy_dir }}/ansible_collections"
+ loop:
+ - absent
+ - directory
+
+- name: install upgradeable collections
+ command: ansible-galaxy collection install namespace1.name1:1.0.0 parent_dep.parent_collection:0.0.1 {{ galaxy_verbosity }}
+ register: result
+ failed_when:
+ - '"namespace1.name1:1.0.0 was installed successfully" not in result.stdout_lines'
+ - '"parent_dep.parent_collection:0.0.1 was installed successfully" not in result.stdout_lines'
+ - '"child_dep.child_collection:0.4.0 was installed successfully" not in result.stdout_lines'
+
+- name: create test requirements file with both roles and collections - {{ test_name }}
+ copy:
+ content: |
+ collections:
+ - namespace1.name1
+ - name: parent_dep.parent_collection
+ version: <=2.0.0
+ roles:
+ - skip.me
+ dest: '{{ galaxy_dir }}/ansible_collections/requirements.yml'
+
+- name: upgrade collections with the requirements.yml
+ command: ansible-galaxy collection install -r {{ requirements_path }} --upgrade {{ galaxy_verbosity }}
+ vars:
+ requirements_path: '{{ galaxy_dir }}/ansible_collections/requirements.yml'
+ register: result
+
+- assert:
+ that:
+ - '"namespace1.name1:1.0.9 was installed successfully" in result.stdout_lines'
+ - '"parent_dep.parent_collection:2.0.0 was installed successfully" in result.stdout_lines'
+ - '"child_dep.child_collection:1.0.0 was installed successfully" in result.stdout_lines'
+ - '"child_dep.child_dep2:1.2.2 was installed successfully" in result.stdout_lines'
+
+- name: cleanup
+ file:
+ state: "{{ item }}"
+ path: "{{ galaxy_dir }}/ansible_collections"
+ loop:
+ - absent
+ - directory
diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/verify.yml b/test/integration/targets/ansible-galaxy-collection/tasks/verify.yml
new file mode 100644
index 0000000..dfe3d0f
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection/tasks/verify.yml
@@ -0,0 +1,475 @@
+- name: create an empty collection skeleton
+ command: ansible-galaxy collection init ansible_test.verify
+ args:
+ chdir: '{{ galaxy_dir }}/scratch'
+
+- name: build the collection
+ command: ansible-galaxy collection build scratch/ansible_test/verify
+ args:
+ chdir: '{{ galaxy_dir }}'
+
+- name: publish collection - {{ test_name }}
+ command: ansible-galaxy collection publish ansible_test-verify-1.0.0.tar.gz -s {{ test_name }} {{ galaxy_verbosity }}
+ args:
+ chdir: '{{ galaxy_dir }}'
+
+- name: test verifying a tarfile
+ command: ansible-galaxy collection verify {{ galaxy_dir }}/ansible_test-verify-1.0.0.tar.gz
+ register: verify
+ failed_when: verify.rc == 0
+
+- assert:
+ that:
+ - verify.rc != 0
+ - >-
+ "ERROR! 'file' type is not supported. The format namespace.name is expected." in verify.stderr
+
+- name: install the collection from the server
+ command: ansible-galaxy collection install ansible_test.verify:1.0.0 -s {{ test_api_fallback }} {{ galaxy_verbosity }}
+
+# This command is hardcoded with -vvvv purposefully to evaluate extra verbosity messages
+- name: verify the collection against the first valid server
+ command: ansible-galaxy collection verify ansible_test.verify:1.0.0 -vvvv {{ galaxy_verbosity }}
+ register: verify
+
+- assert:
+ that:
+ - verify is success
+ - >-
+ "Found API version '{{ test_api_fallback_versions }}' with Galaxy server {{ test_api_fallback }}" in verify.stdout
+
+- name: verify the installed collection against the server
+ command: ansible-galaxy collection verify ansible_test.verify:1.0.0 -s {{ test_name }} {{ galaxy_verbosity }}
+ register: verify
+
+- assert:
+ that:
+ - verify is success
+ - "'Collection ansible_test.verify contains modified content' not in verify.stdout"
+
+- name: verify the installed collection against the server, with unspecified version in CLI
+ command: ansible-galaxy collection verify ansible_test.verify -s {{ test_name }} {{ galaxy_verbosity }}
+
+- name: verify a collection that doesn't appear to be installed
+ command: ansible-galaxy collection verify ansible_test.verify:1.0.0 -s {{ test_name }} {{ galaxy_verbosity }}
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/nonexistent_dir'
+ register: verify
+ failed_when: verify.rc == 0
+
+- assert:
+ that:
+ - verify.rc != 0
+ - "'Collection ansible_test.verify is not installed in any of the collection paths.' in verify.stderr"
+
+- name: create a modules directory
+ file:
+ state: directory
+ path: '{{ galaxy_dir }}/scratch/ansible_test/verify/plugins/modules'
+
+- name: add a module to the collection
+ copy:
+ src: test_module.py
+ dest: '{{ galaxy_dir }}/scratch/ansible_test/verify/plugins/modules/test_module.py'
+
+- name: update the collection version
+ lineinfile:
+ regexp: "version: .*"
+ line: "version: '2.0.0'"
+ path: '{{ galaxy_dir }}/scratch/ansible_test/verify/galaxy.yml'
+
+- name: build the new version
+ command: ansible-galaxy collection build scratch/ansible_test/verify
+ args:
+ chdir: '{{ galaxy_dir }}'
+
+- name: publish the new version
+ command: ansible-galaxy collection publish ansible_test-verify-2.0.0.tar.gz -s {{ test_name }} {{ galaxy_verbosity }}
+ args:
+ chdir: '{{ galaxy_dir }}'
+
+- name: verify a version of a collection that isn't installed
+ command: ansible-galaxy collection verify ansible_test.verify:2.0.0 -s {{ test_name }} {{ galaxy_verbosity }}
+ register: verify
+ failed_when: verify.rc == 0
+
+- assert:
+ that:
+ - verify.rc != 0
+ - '"ansible_test.verify has the version ''1.0.0'' but is being compared to ''2.0.0''" in verify.stdout'
+
+- name: install the new version from the server
+ command: ansible-galaxy collection install ansible_test.verify:2.0.0 --force -s {{ test_name }} {{ galaxy_verbosity }}
+
+- name: verify the installed collection against the server
+ command: ansible-galaxy collection verify ansible_test.verify:2.0.0 -s {{ test_name }} {{ galaxy_verbosity }}
+ register: verify
+
+- assert:
+ that:
+ - "'Collection ansible_test.verify contains modified content' not in verify.stdout"
+
+# Test a modified collection
+
+- set_fact:
+ manifest_path: '{{ galaxy_dir }}/ansible_collections/ansible_test/verify/MANIFEST.json'
+ file_manifest_path: '{{ galaxy_dir }}/ansible_collections/ansible_test/verify/FILES.json'
+ module_path: '{{ galaxy_dir }}/ansible_collections/ansible_test/verify/plugins/modules/test_module.py'
+
+- name: load the FILES.json
+ set_fact:
+ files_manifest: "{{ lookup('file', file_manifest_path) | from_json }}"
+
+- name: get the real checksum of a particular module
+ stat:
+ path: "{{ module_path }}"
+ checksum_algorithm: sha256
+ register: file
+
+- assert:
+ that:
+ - "file.stat.checksum == item.chksum_sha256"
+ loop: "{{ files_manifest.files }}"
+ when: "item.name == 'plugins/modules/aws_s3.py'"
+
+- name: append a newline to the module to modify the checksum
+ shell: "echo '' >> {{ module_path }}"
+
+- name: get the new checksum
+ stat:
+ path: "{{ module_path }}"
+ checksum_algorithm: sha256
+ register: updated_file
+
+- assert:
+ that:
+ - "updated_file.stat.checksum != file.stat.checksum"
+
+- name: test verifying checksumes of the modified collection
+ command: ansible-galaxy collection verify ansible_test.verify:2.0.0 -s {{ test_name }} {{ galaxy_verbosity }}
+ register: verify
+ failed_when: verify.rc == 0
+
+- assert:
+ that:
+ - verify.rc != 0
+ - "'Collection ansible_test.verify contains modified content in the following files:\n plugins/modules/test_module.py' in verify.stdout"
+
+- name: modify the FILES.json to match the new checksum
+ lineinfile:
+ path: "{{ file_manifest_path }}"
+ regexp: ' "chksum_sha256": "{{ file.stat.checksum }}",'
+ line: ' "chksum_sha256": "{{ updated_file.stat.checksum }}",'
+ state: present
+ diff: true
+
+- name: ensure a modified FILES.json is validated
+ command: ansible-galaxy collection verify ansible_test.verify:2.0.0 -s {{ test_name }} {{ galaxy_verbosity }}
+ register: verify
+ failed_when: verify.rc == 0
+
+- assert:
+ that:
+ - verify.rc != 0
+ - "'Collection ansible_test.verify contains modified content in the following files:\n FILES.json' in verify.stdout"
+
+- name: get the checksum of the FILES.json
+ stat:
+ path: "{{ file_manifest_path }}"
+ checksum_algorithm: sha256
+ register: manifest_info
+
+- name: modify the MANIFEST.json to contain a different checksum for FILES.json
+ lineinfile:
+ regexp: ' "chksum_sha256": *'
+ path: "{{ manifest_path }}"
+ line: ' "chksum_sha256": "{{ manifest_info.stat.checksum }}",'
+
+- name: ensure the MANIFEST.json is validated against the uncorrupted file from the server
+ command: ansible-galaxy collection verify ansible_test.verify:2.0.0 -s {{ test_name }} {{ galaxy_verbosity }}
+ register: verify
+ failed_when: verify.rc == 0
+
+- assert:
+ that:
+ - verify.rc != 0
+ - "'Collection ansible_test.verify contains modified content in the following files:\n MANIFEST.json' in verify.stdout"
+
+- name: remove the artifact metadata to test verifying a collection without it
+ file:
+ path: "{{ item }}"
+ state: absent
+ loop:
+ - "{{ manifest_path }}"
+ - "{{ file_manifest_path }}"
+
+- name: add some development metadata
+ copy:
+ content: |
+ namespace: 'ansible_test'
+ name: 'verify'
+ version: '2.0.0'
+ readme: 'README.md'
+ authors: ['Ansible']
+ dest: '{{ galaxy_dir }}/ansible_collections/ansible_test/verify/galaxy.yml'
+
+- name: test we only verify collections containing a MANIFEST.json with the version on the server
+ command: ansible-galaxy collection verify ansible_test.verify:2.0.0 -s {{ test_name }} {{ galaxy_verbosity }}
+ register: verify
+ failed_when: verify.rc == 0
+
+- assert:
+ that:
+ - verify.rc != 0
+ - "'Collection ansible_test.verify does not have a MANIFEST.json' in verify.stderr"
+
+- name: update the collection version to something not present on the server
+ lineinfile:
+ regexp: "version: .*"
+ line: "version: '3.0.0'"
+ path: '{{ galaxy_dir }}/scratch/ansible_test/verify/galaxy.yml'
+
+- name: build the new version
+ command: ansible-galaxy collection build scratch/ansible_test/verify
+ args:
+ chdir: '{{ galaxy_dir }}'
+
+- name: force-install from local artifact
+ command: ansible-galaxy collection install '{{ galaxy_dir }}/ansible_test-verify-3.0.0.tar.gz' --force
+
+- name: verify locally only, no download or server manifest hash check
+ command: ansible-galaxy collection verify --offline ansible_test.verify
+ register: verify
+
+- assert:
+ that:
+ - >-
+ "Verifying 'ansible_test.verify:3.0.0'." in verify.stdout
+ - '"MANIFEST.json hash: " in verify.stdout'
+ - >-
+ "Successfully verified that checksums for 'ansible_test.verify:3.0.0' are internally consistent with its manifest." in verify.stdout
+
+- name: append a newline to a module to modify the checksum
+ shell: "echo '' >> {{ module_path }}"
+
+- name: create a new module file
+ file:
+ path: '{{ galaxy_dir }}/ansible_collections/ansible_test/verify/plugins/modules/test_new_file.py'
+ state: touch
+
+- name: create a new directory
+ file:
+ path: '{{ galaxy_dir }}/ansible_collections/ansible_test/verify/plugins/modules/test_new_dir'
+ state: directory
+
+- name: verify modified collection locally-only (should fail)
+ command: ansible-galaxy collection verify --offline ansible_test.verify
+ register: verify
+ failed_when: verify.rc == 0
+
+- assert:
+ that:
+ - verify.rc != 0
+ - "'Collection ansible_test.verify contains modified content in the following files:' in verify.stdout"
+ - "'plugins/modules/test_module.py' in verify.stdout"
+ - "'plugins/modules/test_new_file.py' in verify.stdout"
+ - "'plugins/modules/test_new_dir' in verify.stdout"
+
+# TODO: add a test for offline Galaxy signature metadata
+
+- name: install a collection that was signed by setup_collections
+ command: ansible-galaxy collection install namespace1.name1:1.0.0
+
+- name: verify the installed collection with a detached signature
+ command: ansible-galaxy collection verify namespace1.name1:1.0.0 {{ galaxy_verbosity }} {{ signature_options }}
+ vars:
+ signature_options: "--signature {{ signature }} --keyring {{ keyring }}"
+ signature: "file://{{ gpg_homedir }}/namespace1-name1-1.0.0-MANIFEST.json.asc"
+ keyring: "{{ gpg_homedir }}/pubring.kbx"
+ register: verify
+
+- assert:
+ that:
+ - verify.rc == 0
+
+# This command is hardcoded with -vvvv purposefully to evaluate extra verbosity messages
+- name: verify the installed collection with invalid detached signature
+ command: ansible-galaxy collection verify namespace1.name1:1.0.0 -vvvv {{ signature_options }}
+ vars:
+ signature_options: "--signature {{ signature }} --keyring {{ keyring }}"
+ signature: "file://{{ gpg_homedir }}/namespace1-name1-1.0.9-MANIFEST.json.asc"
+ keyring: "{{ gpg_homedir }}/pubring.kbx"
+ register: verify
+ ignore_errors: yes
+ environment:
+ ANSIBLE_NOCOLOR: True
+ ANSIBLE_FORCE_COLOR: False
+
+- assert:
+ that:
+ - verify.rc != 0
+ - '"Signature verification failed for ''namespace1.name1'' (return code 1)" in verify.stdout'
+ - expected_errors[0] in verify_stdout
+ - expected_errors[1] in verify_stdout
+ vars:
+ expected_errors:
+ - "* This is the counterpart to SUCCESS and used to indicate a program failure."
+ - "* The signature with the keyid has not been verified okay."
+ # Remove formatting from the reason so it's one line
+ verify_stdout: "{{ verify.stdout | regex_replace('\"') | regex_replace('\\n') | regex_replace(' ', ' ') }}"
+
+# This command is hardcoded with -vvvv purposefully to evaluate extra verbosity messages
+- name: verify the installed collection with invalid detached signature offline
+ command: ansible-galaxy collection verify namespace1.name1:1.0.0 -vvvv {{ signature_options }} --offline
+ vars:
+ signature_options: "--signature {{ signature }} --keyring {{ keyring }}"
+ signature: "file://{{ gpg_homedir }}/namespace1-name1-1.0.9-MANIFEST.json.asc"
+ keyring: "{{ gpg_homedir }}/pubring.kbx"
+ register: verify
+ ignore_errors: yes
+ environment:
+ ANSIBLE_NOCOLOR: True
+ ANSIBLE_FORCE_COLOR: False
+
+- assert:
+ that:
+ - verify.rc != 0
+ - '"Signature verification failed for ''namespace1.name1'' (return code 1)" in verify.stdout'
+ - expected_errors[0] in verify_stdout
+ - expected_errors[1] in verify_stdout
+ vars:
+ expected_errors:
+ - "* This is the counterpart to SUCCESS and used to indicate a program failure."
+ - "* The signature with the keyid has not been verified okay."
+ # Remove formatting from the reason so it's one line
+ verify_stdout: "{{ verify.stdout | regex_replace('\"') | regex_replace('\\n') | regex_replace(' ', ' ') }}"
+
+- include_tasks: revoke_gpg_key.yml
+
+# This command is hardcoded with -vvvv purposefully to evaluate extra verbosity messages
+- name: verify the installed collection with a revoked detached signature
+ command: ansible-galaxy collection verify namespace1.name1:1.0.0 -vvvv {{ signature_options }}
+ vars:
+ signature_options: "--signature {{ signature }} --keyring {{ keyring }}"
+ signature: "file://{{ gpg_homedir }}/namespace1-name1-1.0.0-MANIFEST.json.asc"
+ keyring: "{{ gpg_homedir }}/pubring.kbx"
+ register: verify
+ ignore_errors: yes
+ environment:
+ ANSIBLE_NOCOLOR: True
+ ANSIBLE_FORCE_COLOR: False
+
+- assert:
+ that:
+ - verify.rc != 0
+ - '"Signature verification failed for ''namespace1.name1'' (return code 0)" in verify.stdout'
+ - expected_errors[0] in verify_stdout
+ - expected_errors[1] in verify_stdout
+ vars:
+ expected_errors:
+ - "* The used key has been revoked by its owner."
+ - "* The signature with the keyid is good, but the signature was made by a revoked key."
+ # Remove formatting from the reason so it's one line
+ verify_stdout: "{{ verify.stdout | regex_replace('\"') | regex_replace('\\n') | regex_replace(' ', ' ') }}"
+
+# This command is hardcoded with no verbosity purposefully to evaluate overall gpg failure
+- name: verify that ignoring the signature error and no successful signatures is not successful verification
+ command: ansible-galaxy collection verify namespace1.name1:1.0.0 {{ signature_options }}
+ vars:
+ signature_options: "--signature {{ signature }} --keyring {{ keyring }}"
+ signature: "file://{{ gpg_homedir }}/namespace1-name1-1.0.0-MANIFEST.json.asc"
+ keyring: "{{ gpg_homedir }}/pubring.kbx"
+ register: verify
+ ignore_errors: yes
+ environment:
+ ANSIBLE_GALAXY_IGNORE_SIGNATURE_STATUS_CODES: REVKEYSIG,KEYREVOKED
+ ANSIBLE_NOCOLOR: True
+ ANSIBLE_FORCE_COLOR: False
+
+- assert:
+ that:
+ - verify.rc != 0
+ - '"Signature verification failed for ''namespace1.name1'': fewer successful signatures than required" in verify.stdout'
+ - ignored_errors[0] not in verify_stdout
+ - ignored_errors[1] not in verify_stdout
+ vars:
+ ignored_errors:
+ - "* The used key has been revoked by its owner."
+ - "* The signature with the keyid is good, but the signature was made by a revoked key."
+ # Remove formatting from the reason so it's one line
+ verify_stdout: "{{ verify.stdout | regex_replace('\"') | regex_replace('\\n') | regex_replace(' ', ' ') }}"
+
+# This command is hardcoded with -vvvv purposefully to evaluate extra verbosity messages
+- name: verify that ignoring the signature error and no successful signatures and required signature count all is successful verification
+ command: ansible-galaxy collection verify namespace1.name1:1.0.0 -vvvv {{ signature_options }}
+ vars:
+ signature_options: "--signature {{ signature }} --keyring {{ keyring }}"
+ signature: "file://{{ gpg_homedir }}/namespace1-name1-1.0.0-MANIFEST.json.asc"
+ keyring: "{{ gpg_homedir }}/pubring.kbx"
+ register: verify
+ ignore_errors: yes
+ environment:
+ ANSIBLE_GALAXY_IGNORE_SIGNATURE_STATUS_CODES: REVKEYSIG,KEYREVOKED
+ ANSIBLE_GALAXY_REQUIRED_VALID_SIGNATURE_COUNT: all
+ ANSIBLE_NOCOLOR: True
+ ANSIBLE_FORCE_COLOR: False
+
+- assert:
+ that:
+ - verify is success
+ - verify.rc == 0
+ - '"Signature verification failed for ''namespace1.name1'': fewer successful signatures than required" not in verify.stdout'
+ - success_messages[0] in verify_stdout
+ - success_messages[1] in verify_stdout
+ - ignored_errors[0] not in verify_stdout
+ - ignored_errors[1] not in verify_stdout
+ vars:
+ success_messages:
+ - "GnuPG signature verification succeeded, verifying contents of namespace1.name1:1.0.0"
+ - "Successfully verified that checksums for 'namespace1.name1:1.0.0' match the remote collection."
+ ignored_errors:
+ - "* The used key has been revoked by its owner."
+ - "* The signature with the keyid is good, but the signature was made by a revoked key."
+ # Remove formatting from the reason so it's one line
+ verify_stdout: "{{ verify.stdout | regex_replace('\"') | regex_replace('\\n') | regex_replace(' ', ' ') }}"
+
+- name: use lenient signature verification (default) without providing signatures
+ command: ansible-galaxy collection verify namespace1.name1:1.0.0 -vvvv --keyring {{ gpg_homedir }}/pubring.kbx
+ environment:
+ ANSIBLE_GALAXY_REQUIRED_VALID_SIGNATURE_COUNT: "1"
+ register: verify
+ ignore_errors: yes
+
+- assert:
+ that:
+ - verify is success
+ - verify.rc == 0
+ - error_message not in verify.stdout
+ - success_messages[0] in verify.stdout
+ - success_messages[1] in verify.stdout
+ vars:
+ error_message: "Signature verification failed for 'namespace1.name1': fewer successful signatures than required"
+ success_messages:
+ - "GnuPG signature verification succeeded, verifying contents of namespace1.name1:1.0.0"
+ - "Successfully verified that checksums for 'namespace1.name1:1.0.0' match the remote collection."
+
+- name: use strict signature verification without providing signatures
+ command: ansible-galaxy collection verify namespace1.name1:1.0.0 -vvvv --keyring {{ gpg_homedir }}/pubring.kbx
+ environment:
+ ANSIBLE_GALAXY_REQUIRED_VALID_SIGNATURE_COUNT: "+1"
+ register: verify
+ ignore_errors: yes
+
+- assert:
+ that:
+ - verify is failed
+ - verify.rc == 1
+ - '"Signature verification failed for ''namespace1.name1'': no successful signatures" in verify.stdout'
+
+- name: empty installed collections
+ file:
+ path: "{{ galaxy_dir }}/ansible_collections"
+ state: "{{ item }}"
+ loop:
+ - absent
+ - directory
diff --git a/test/integration/targets/ansible-galaxy-collection/templates/ansible.cfg.j2 b/test/integration/targets/ansible-galaxy-collection/templates/ansible.cfg.j2
new file mode 100644
index 0000000..9bff527
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection/templates/ansible.cfg.j2
@@ -0,0 +1,28 @@
+[galaxy]
+# Ensures subsequent unstable reruns don't use the cached information causing another failure
+cache_dir={{ remote_tmp_dir }}/galaxy_cache
+server_list=offline,pulp_v2,pulp_v3,galaxy_ng,secondary
+
+[galaxy_server.offline]
+url={{ offline_server }}
+
+[galaxy_server.pulp_v2]
+url={{ pulp_server }}published/api/
+username={{ pulp_user }}
+password={{ pulp_password }}
+
+[galaxy_server.pulp_v3]
+url={{ pulp_server }}published/api/
+v3=true
+username={{ pulp_user }}
+password={{ pulp_password }}
+
+[galaxy_server.galaxy_ng]
+url={{ galaxy_ng_server }}
+token={{ galaxy_ng_token.json.token }}
+
+[galaxy_server.secondary]
+url={{ pulp_server }}secondary/api/
+v3=true
+username={{ pulp_user }}
+password={{ pulp_password }}
diff --git a/test/integration/targets/ansible-galaxy-collection/vars/main.yml b/test/integration/targets/ansible-galaxy-collection/vars/main.yml
new file mode 100644
index 0000000..175d669
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-collection/vars/main.yml
@@ -0,0 +1,164 @@
+galaxy_verbosity: "{{ '' if not ansible_verbosity else '-' ~ ('v' * ansible_verbosity) }}"
+
+gpg_homedir: "{{ galaxy_dir }}/gpg"
+
+offline_server: https://test-hub.demolab.local/api/galaxy/content/api/
+
+supported_resolvelib_versions:
+ - "0.5.3" # Oldest supported
+ - "0.6.0"
+ - "0.7.0"
+ - "0.8.0"
+
+unsupported_resolvelib_versions:
+ - "0.2.0" # Fails on import
+ - "0.5.1"
+
+pulp_repositories:
+ - published
+ - secondary
+
+publish_namespaces:
+ - ansible_test
+
+collection_list:
+ # Scenario to test out pre-release being ignored unless explicitly set and version pagination.
+ - namespace: namespace1
+ name: name1
+ version: 0.0.1
+ - namespace: namespace1
+ name: name1
+ version: 0.0.2
+ - namespace: namespace1
+ name: name1
+ version: 0.0.3
+ - namespace: namespace1
+ name: name1
+ version: 0.0.4
+ - namespace: namespace1
+ name: name1
+ version: 0.0.5
+ - namespace: namespace1
+ name: name1
+ version: 0.0.6
+ - namespace: namespace1
+ name: name1
+ version: 0.0.7
+ - namespace: namespace1
+ name: name1
+ version: 0.0.8
+ - namespace: namespace1
+ name: name1
+ version: 0.0.9
+ - namespace: namespace1
+ name: name1
+ version: 0.0.10
+ - namespace: namespace1
+ name: name1
+ version: 0.1.0
+ - namespace: namespace1
+ name: name1
+ version: 1.0.0
+ - namespace: namespace1
+ name: name1
+ version: 1.0.9
+ - namespace: namespace1
+ name: name1
+ version: 1.1.0-beta.1
+
+ # Pad out number of namespaces for pagination testing
+ - namespace: namespace2
+ name: name
+ - namespace: namespace3
+ name: name
+ - namespace: namespace4
+ name: name
+ - namespace: namespace5
+ name: name
+ - namespace: namespace6
+ name: name
+ - namespace: namespace7
+ name: name
+ - namespace: namespace8
+ name: name
+ - namespace: namespace9
+ name: name
+
+ # Complex dependency resolution
+ - namespace: parent_dep
+ name: parent_collection
+ version: 0.0.1
+ dependencies:
+ child_dep.child_collection: '<0.5.0'
+ - namespace: parent_dep
+ name: parent_collection
+ version: 1.0.0
+ dependencies:
+ child_dep.child_collection: '>=0.5.0,<1.0.0'
+ - namespace: parent_dep
+ name: parent_collection
+ version: 1.1.0
+ dependencies:
+ child_dep.child_collection: '>=0.9.9,<=1.0.0'
+ - namespace: parent_dep
+ name: parent_collection
+ version: 2.0.0
+ dependencies:
+ child_dep.child_collection: '>=1.0.0'
+ - namespace: parent_dep2
+ name: parent_collection
+ dependencies:
+ child_dep.child_collection: '0.5.0'
+ - namespace: child_dep
+ name: child_collection
+ version: 0.4.0
+ - namespace: child_dep
+ name: child_collection
+ version: 0.5.0
+ - namespace: child_dep
+ name: child_collection
+ version: 0.9.9
+ dependencies:
+ child_dep.child_dep2: '!=1.2.3'
+ - namespace: child_dep
+ name: child_collection
+ version: 1.0.0
+ dependencies:
+ child_dep.child_dep2: '!=1.2.3'
+ - namespace: child_dep
+ name: child_dep2
+ version: 1.2.2
+ - namespace: child_dep
+ name: child_dep2
+ version: 1.2.3
+
+ # Dep resolution failure
+ - namespace: fail_namespace
+ name: fail_collection
+ version: 2.1.2
+ dependencies:
+ fail_dep.name: '0.0.5'
+ fail_dep2.name: '<0.0.5'
+ - namespace: fail_dep
+ name: name
+ version: '0.0.5'
+ dependencies:
+ fail_dep2.name: '>0.0.5'
+ - namespace: fail_dep2
+ name: name
+
+ # Symlink tests
+ - namespace: symlink
+ name: symlink
+ use_symlink: yes
+
+ # Caching update tests
+ - namespace: cache
+ name: cache
+ version: 1.0.0
+
+ # Dep with beta version
+ - namespace: dep_with_beta
+ name: parent
+ dependencies:
+ namespace1.name1: '*'
diff --git a/test/integration/targets/ansible-galaxy-role/aliases b/test/integration/targets/ansible-galaxy-role/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-role/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/ansible-galaxy-role/meta/main.yml b/test/integration/targets/ansible-galaxy-role/meta/main.yml
new file mode 100644
index 0000000..e6bc9cc
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-role/meta/main.yml
@@ -0,0 +1 @@
+dependencies: [setup_remote_tmp_dir]
diff --git a/test/integration/targets/ansible-galaxy-role/tasks/main.yml b/test/integration/targets/ansible-galaxy-role/tasks/main.yml
new file mode 100644
index 0000000..03d0b3c
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy-role/tasks/main.yml
@@ -0,0 +1,61 @@
+- name: Install role from Galaxy (should not fail with AttributeError)
+ command: ansible-galaxy role install ansible.nope -vvvv --ignore-errors
+
+- name: Archive directories
+ file:
+ state: directory
+ path: "{{ remote_tmp_dir }}/role.d/{{item}}"
+ loop:
+ - meta
+ - tasks
+
+- name: Metadata file
+ copy:
+ content: "'galaxy_info': {}"
+ dest: "{{ remote_tmp_dir }}/role.d/meta/main.yml"
+
+- name: Valid files
+ copy:
+ content: ""
+ dest: "{{ remote_tmp_dir }}/role.d/tasks/{{item}}"
+ loop:
+ - "main.yml"
+ - "valid~file.yml"
+
+- name: Valid role archive
+ command: "tar cf {{ remote_tmp_dir }}/valid-role.tar {{ remote_tmp_dir }}/role.d"
+
+- name: Invalid file
+ copy:
+ content: ""
+ dest: "{{ remote_tmp_dir }}/role.d/tasks/~invalid.yml"
+
+- name: Valid requirements file
+ copy:
+ dest: valid-requirements.yml
+ content: "[{'src': '{{ remote_tmp_dir }}/valid-role.tar', 'name': 'valid-testrole'}]"
+
+- name: Invalid role archive
+ command: "tar cf {{ remote_tmp_dir }}/invalid-role.tar {{ remote_tmp_dir }}/role.d"
+
+- name: Invalid requirements file
+ copy:
+ dest: invalid-requirements.yml
+ content: "[{'src': '{{ remote_tmp_dir }}/invalid-role.tar', 'name': 'invalid-testrole'}]"
+
+- name: Install valid role
+ command: ansible-galaxy install -r valid-requirements.yml
+
+- name: Uninstall valid role
+ command: ansible-galaxy role remove valid-testrole
+
+- name: Install invalid role
+ command: ansible-galaxy install -r invalid-requirements.yml
+ ignore_errors: yes
+ register: invalid
+
+- assert:
+ that: "invalid.rc != 0"
+
+- name: Uninstall invalid role
+ command: ansible-galaxy role remove invalid-testrole
diff --git a/test/integration/targets/ansible-galaxy/aliases b/test/integration/targets/ansible-galaxy/aliases
new file mode 100644
index 0000000..90edbd9
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy/aliases
@@ -0,0 +1,3 @@
+destructive
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/ansible-galaxy/cleanup-default.yml b/test/integration/targets/ansible-galaxy/cleanup-default.yml
new file mode 100644
index 0000000..8060079
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy/cleanup-default.yml
@@ -0,0 +1,13 @@
+- name: remove git package
+ package:
+ name: git
+ state: absent
+ when: git_install.changed
+- name: remove openssl package
+ package:
+ name: openssl
+ state: absent
+ when: ansible_distribution not in ["MacOSX", "Alpine"] and openssl_install.changed
+- name: remove openssl package
+ command: apk del openssl
+ when: ansible_distribution == "Alpine" and openssl_install.changed
diff --git a/test/integration/targets/ansible-galaxy/cleanup-freebsd.yml b/test/integration/targets/ansible-galaxy/cleanup-freebsd.yml
new file mode 100644
index 0000000..87b987d
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy/cleanup-freebsd.yml
@@ -0,0 +1,12 @@
+- name: remove git from FreeBSD
+ pkgng:
+ name: git
+ state: absent
+ autoremove: yes
+ when: git_install.changed
+- name: remove openssl from FreeBSD
+ pkgng:
+ name: openssl
+ state: absent
+ autoremove: yes
+ when: openssl_install.changed
diff --git a/test/integration/targets/ansible-galaxy/cleanup.yml b/test/integration/targets/ansible-galaxy/cleanup.yml
new file mode 100644
index 0000000..e80eeef
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy/cleanup.yml
@@ -0,0 +1,26 @@
+- hosts: localhost
+ vars:
+ git_install: '{{ lookup("file", lookup("env", "OUTPUT_DIR") + "/git_install.json") | from_json }}'
+ openssl_install: '{{ lookup("file", lookup("env", "OUTPUT_DIR") + "/openssl_install.json") | from_json }}'
+ ws_dir: '{{ lookup("file", lookup("env", "OUTPUT_DIR") + "/ws_dir.json") | from_json }}'
+ tasks:
+ - name: cleanup
+ include_tasks: "{{ cleanup_filename }}"
+ with_first_found:
+ - "cleanup-{{ ansible_distribution | lower }}.yml"
+ - "cleanup-default.yml"
+ loop_control:
+ loop_var: cleanup_filename
+
+ - name: Remove default collection directories
+ file:
+ path: "{{ item }}"
+ state: absent
+ loop:
+ - "~/.ansible/collections/ansible_collections"
+ - /usr/share/ansible/collections/ansible_collections
+
+ - name: Remove webserver directory
+ file:
+ path: "{{ ws_dir }}"
+ state: absent
diff --git a/test/integration/targets/ansible-galaxy/files/testserver.py b/test/integration/targets/ansible-galaxy/files/testserver.py
new file mode 100644
index 0000000..1359850
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy/files/testserver.py
@@ -0,0 +1,20 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import sys
+import ssl
+
+if __name__ == '__main__':
+ if sys.version_info[0] >= 3:
+ import http.server
+ import socketserver
+ Handler = http.server.SimpleHTTPRequestHandler
+ httpd = socketserver.TCPServer(("", 4443), Handler)
+ else:
+ import BaseHTTPServer
+ import SimpleHTTPServer
+ Handler = SimpleHTTPServer.SimpleHTTPRequestHandler
+ httpd = BaseHTTPServer.HTTPServer(("", 4443), Handler)
+
+ httpd.socket = ssl.wrap_socket(httpd.socket, certfile='./cert.pem', keyfile='./key.pem', server_side=True)
+ httpd.serve_forever()
diff --git a/test/integration/targets/ansible-galaxy/runme.sh b/test/integration/targets/ansible-galaxy/runme.sh
new file mode 100755
index 0000000..7d966e2
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy/runme.sh
@@ -0,0 +1,571 @@
+#!/usr/bin/env bash
+
+set -eux -o pipefail
+
+galaxy_testdir="${OUTPUT_DIR}/galaxy-test-dir"
+role_testdir="${OUTPUT_DIR}/role-test-dir"
+# Prep the local git repos with role and make a tar archive so we can test
+# different things
+galaxy_local_test_role="test-role"
+galaxy_local_test_role_dir="${OUTPUT_DIR}/galaxy-role-test-root"
+galaxy_local_test_role_git_repo="${galaxy_local_test_role_dir}/${galaxy_local_test_role}"
+galaxy_local_test_role_tar="${galaxy_local_test_role_dir}/${galaxy_local_test_role}.tar"
+galaxy_webserver_root="${OUTPUT_DIR}/ansible-galaxy-webserver"
+
+mkdir -p "${galaxy_local_test_role_dir}"
+mkdir -p "${role_testdir}"
+mkdir -p "${galaxy_webserver_root}"
+
+ansible-playbook setup.yml "$@"
+
+trap 'ansible-playbook ${ANSIBLE_PLAYBOOK_DIR}/cleanup.yml' EXIT
+
+# Very simple version test
+ansible-galaxy --version
+
+# Need a relative custom roles path for testing various scenarios of -p
+galaxy_relative_rolespath="my/custom/roles/path"
+
+# Status message function (f_ to designate that it's a function)
+f_ansible_galaxy_status()
+{
+ printf "\n\n\n### Testing ansible-galaxy: %s\n" "${@}"
+}
+
+# Use to initialize a repository. Must call the post function too.
+f_ansible_galaxy_create_role_repo_pre()
+{
+ repo_name=$1
+ repo_dir=$2
+
+ pushd "${repo_dir}"
+ ansible-galaxy init "${repo_name}"
+ pushd "${repo_name}"
+ git init .
+
+ # Prep git, because it doesn't work inside a docker container without it
+ git config user.email "tester@ansible.com"
+ git config user.name "Ansible Tester"
+
+ # f_ansible_galaxy_create_role_repo_post
+}
+
+# Call after f_ansible_galaxy_create_repo_pre.
+f_ansible_galaxy_create_role_repo_post()
+{
+ repo_name=$1
+ repo_tar=$2
+
+ # f_ansible_galaxy_create_role_repo_pre
+
+ git add .
+ git commit -m "local testing ansible galaxy role"
+
+ git archive \
+ --format=tar \
+ --prefix="${repo_name}/" \
+ master > "${repo_tar}"
+ # Configure basic (insecure) HTTPS-accessible repository
+ galaxy_local_test_role_http_repo="${galaxy_webserver_root}/${galaxy_local_test_role}.git"
+ if [[ ! -d "${galaxy_local_test_role_http_repo}" ]]; then
+ git clone --bare "${galaxy_local_test_role_git_repo}" "${galaxy_local_test_role_http_repo}"
+ pushd "${galaxy_local_test_role_http_repo}"
+ touch "git-daemon-export-ok"
+ git --bare update-server-info
+ mv "hooks/post-update.sample" "hooks/post-update"
+ popd # ${galaxy_local_test_role_http_repo}
+ fi
+ popd # "${repo_name}"
+ popd # "${repo_dir}"
+}
+
+f_ansible_galaxy_create_role_repo_pre "${galaxy_local_test_role}" "${galaxy_local_test_role_dir}"
+f_ansible_galaxy_create_role_repo_post "${galaxy_local_test_role}" "${galaxy_local_test_role_tar}"
+
+galaxy_local_parent_role="parent-role"
+galaxy_local_parent_role_dir="${OUTPUT_DIR}/parent-role"
+mkdir -p "${galaxy_local_parent_role_dir}"
+galaxy_local_parent_role_git_repo="${galaxy_local_parent_role_dir}/${galaxy_local_parent_role}"
+galaxy_local_parent_role_tar="${galaxy_local_parent_role_dir}/${galaxy_local_parent_role}.tar"
+
+# Create parent-role repository
+f_ansible_galaxy_create_role_repo_pre "${galaxy_local_parent_role}" "${galaxy_local_parent_role_dir}"
+
+ cat <<EOF > meta/requirements.yml
+- src: git+file:///${galaxy_local_test_role_git_repo}
+EOF
+f_ansible_galaxy_create_role_repo_post "${galaxy_local_parent_role}" "${galaxy_local_parent_role_tar}"
+
+# Galaxy install test case
+#
+# Install local git repo
+f_ansible_galaxy_status "install of local git repo"
+mkdir -p "${galaxy_testdir}"
+pushd "${galaxy_testdir}"
+
+ # minimum verbosity is hardcoded to include calls to Galaxy
+ ansible-galaxy install git+file:///"${galaxy_local_test_role_git_repo}" "$@" -vvvv 2>&1 | tee out.txt
+
+ # Test no initial call is made to Galaxy
+ grep out.txt -e "https://galaxy.ansible.com" && cat out.txt && exit 1
+
+ # Test that the role was installed to the expected directory
+ [[ -d "${HOME}/.ansible/roles/${galaxy_local_test_role}" ]]
+popd # ${galaxy_testdir}
+rm -fr "${galaxy_testdir}"
+rm -fr "${HOME}/.ansible/roles/${galaxy_local_test_role}"
+
+# Galaxy install test case
+#
+# Install local git repo and ensure that if a role_path is passed, it is in fact used
+f_ansible_galaxy_status "install of local git repo with -p \$role_path"
+mkdir -p "${galaxy_testdir}"
+pushd "${galaxy_testdir}"
+ mkdir -p "${galaxy_relative_rolespath}"
+
+ ansible-galaxy install git+file:///"${galaxy_local_test_role_git_repo}" -p "${galaxy_relative_rolespath}" "$@"
+
+ # Test that the role was installed to the expected directory
+ [[ -d "${galaxy_relative_rolespath}/${galaxy_local_test_role}" ]]
+popd # ${galaxy_testdir}
+rm -fr "${galaxy_testdir}"
+
+# Galaxy install test case - skipping cert verification
+#
+# Install from remote git repo and ensure that cert validation is skipped
+#
+# Protect against regression (GitHub Issue #41077)
+# https://github.com/ansible/ansible/issues/41077
+f_ansible_galaxy_status "install of role from untrusted repository"
+mkdir -p "${galaxy_testdir}"
+pushd "${galaxy_testdir}"
+ mkdir -p "${galaxy_relative_rolespath}"
+
+ # Without --ignore-certs, installing a role from an untrusted repository should fail
+ set +e
+ ansible-galaxy install --verbose git+https://localhost:4443/"${galaxy_local_test_role}.git" -p "${galaxy_relative_rolespath}" "$@" 2>&1 | tee out.txt
+ ansible_exit_code="$?"
+ set -e
+ cat out.txt
+
+ if [[ "$ansible_exit_code" -ne 1 ]]; then echo "Exit code ($ansible_exit_code) is expected to be 1" && exit "$ansible_exit_code"; fi
+ [[ $(grep -c 'ERROR' out.txt) -eq 1 ]]
+ [[ ! -d "${galaxy_relative_rolespath}/${galaxy_local_test_role}" ]]
+
+ ansible-galaxy install --verbose --ignore-certs git+https://localhost:4443/"${galaxy_local_test_role}.git" -p "${galaxy_relative_rolespath}" "$@"
+
+ # Test that the role was installed to the expected directory
+ [[ -d "${galaxy_relative_rolespath}/${galaxy_local_test_role}" ]]
+popd # ${galaxy_testdir}
+rm -fr "${galaxy_testdir}"
+
+# Galaxy install test case
+#
+# Install local git repo with a meta/requirements.yml
+f_ansible_galaxy_status "install of local git repo with meta/requirements.yml"
+mkdir -p "${galaxy_testdir}"
+pushd "${galaxy_testdir}"
+
+ ansible-galaxy install git+file:///"${galaxy_local_parent_role_git_repo}" "$@"
+
+ # Test that the role was installed to the expected directory
+ [[ -d "${HOME}/.ansible/roles/${galaxy_local_parent_role}" ]]
+
+ # Test that the dependency was also installed
+ [[ -d "${HOME}/.ansible/roles/${galaxy_local_test_role}" ]]
+
+popd # ${galaxy_testdir}
+rm -fr "${galaxy_testdir}"
+rm -fr "${HOME}/.ansible/roles/${galaxy_local_parent_role}"
+rm -fr "${HOME}/.ansible/roles/${galaxy_local_test_role}"
+
+# Galaxy install test case
+#
+# Install local git repo with a meta/requirements.yml + --no-deps argument
+f_ansible_galaxy_status "install of local git repo with meta/requirements.yml + --no-deps argument"
+mkdir -p "${galaxy_testdir}"
+pushd "${galaxy_testdir}"
+
+ ansible-galaxy install git+file:///"${galaxy_local_parent_role_git_repo}" --no-deps "$@"
+
+ # Test that the role was installed to the expected directory
+ [[ -d "${HOME}/.ansible/roles/${galaxy_local_parent_role}" ]]
+
+ # Test that the dependency was not installed
+ [[ ! -d "${HOME}/.ansible/roles/${galaxy_local_test_role}" ]]
+
+popd # ${galaxy_testdir}
+rm -fr "${galaxy_testdir}"
+rm -fr "${HOME}/.ansible/roles/${galaxy_local_test_role}"
+
+# Galaxy install test case (expected failure)
+#
+# Install role with a meta/requirements.yml that is not a list of roles
+mkdir -p "${role_testdir}"
+pushd "${role_testdir}"
+
+ ansible-galaxy role init --init-path . unsupported_requirements_format
+ cat <<EOF > ./unsupported_requirements_format/meta/requirements.yml
+roles:
+ - src: git+file:///${galaxy_local_test_role_git_repo}
+EOF
+ tar czvf unsupported_requirements_format.tar.gz unsupported_requirements_format
+
+ set +e
+ ansible-galaxy role install -p ./roles unsupported_requirements_format.tar.gz 2>&1 | tee out.txt
+ rc="$?"
+ set -e
+
+ # Test that installing the role was an error
+ [[ ! "$rc" == 0 ]]
+ grep out.txt -qe 'Expected role dependencies to be a list.'
+
+ # Test that the role was not installed to the expected directory
+ [[ ! -d "${HOME}/.ansible/roles/unsupported_requirements_format" ]]
+
+popd # ${role_testdir}
+rm -rf "${role_testdir}"
+
+# Galaxy install test case (expected failure)
+#
+# Install role with meta/main.yml dependencies that is not a list of roles
+mkdir -p "${role_testdir}"
+pushd "${role_testdir}"
+
+ ansible-galaxy role init --init-path . unsupported_requirements_format
+ cat <<EOF > ./unsupported_requirements_format/meta/main.yml
+galaxy_info:
+ author: Ansible
+ description: test unknown dependency format (expected failure)
+ company: your company (optional)
+ license: license (GPL-2.0-or-later, MIT, etc)
+ min_ansible_version: 2.1
+ galaxy_tags: []
+dependencies:
+ roles:
+ - src: git+file:///${galaxy_local_test_role_git_repo}
+EOF
+ tar czvf unsupported_requirements_format.tar.gz unsupported_requirements_format
+
+ set +e
+ ansible-galaxy role install -p ./roles unsupported_requirements_format.tar.gz 2>&1 | tee out.txt
+ rc="$?"
+ set -e
+
+ # Test that installing the role was an error
+ [[ ! "$rc" == 0 ]]
+ grep out.txt -qe 'Expected role dependencies to be a list.'
+
+ # Test that the role was not installed to the expected directory
+ [[ ! -d "${HOME}/.ansible/roles/unsupported_requirements_format" ]]
+
+popd # ${role_testdir}
+rm -rf "${role_testdir}"
+
+# Galaxy install test case
+#
+# Ensure that if both a role_file and role_path is provided, they are both
+# honored
+#
+# Protect against regression (GitHub Issue #35217)
+# https://github.com/ansible/ansible/issues/35217
+
+f_ansible_galaxy_status \
+ "install of local git repo and local tarball with -p \$role_path and -r \$role_file" \
+ "Protect against regression (Issue #35217)"
+mkdir -p "${galaxy_testdir}"
+pushd "${galaxy_testdir}"
+
+ git clone "${galaxy_local_test_role_git_repo}" "${galaxy_local_test_role}"
+ ansible-galaxy init roles-path-bug "$@"
+ pushd roles-path-bug
+ cat <<EOF > ansible.cfg
+[defaults]
+roles_path = ../:../../:../roles:roles/
+EOF
+ cat <<EOF > requirements.yml
+---
+- src: ${galaxy_local_test_role_tar}
+ name: ${galaxy_local_test_role}
+EOF
+
+ ansible-galaxy install -r requirements.yml -p roles/ "$@"
+ popd # roles-path-bug
+
+ # Test that the role was installed to the expected directory
+ [[ -d "${galaxy_testdir}/roles-path-bug/roles/${galaxy_local_test_role}" ]]
+
+popd # ${galaxy_testdir}
+rm -fr "${galaxy_testdir}"
+
+
+# Galaxy role list tests
+#
+# Basic tests to ensure listing roles works
+
+f_ansible_galaxy_status "role list"
+mkdir -p "${galaxy_testdir}"
+pushd "${galaxy_testdir}"
+ ansible-galaxy install git+file:///"${galaxy_local_test_role_git_repo}" "$@"
+
+ ansible-galaxy role list | tee out.txt
+ ansible-galaxy role list test-role | tee -a out.txt
+
+ [[ $(grep -c '^- test-role' out.txt ) -eq 2 ]]
+popd # ${galaxy_testdir}
+
+# Galaxy role test case
+#
+# Test listing a specific role that is not in the first path in ANSIBLE_ROLES_PATH.
+# https://github.com/ansible/ansible/issues/60167#issuecomment-585460706
+
+f_ansible_galaxy_status \
+ "list specific role not in the first path in ANSIBLE_ROLES_PATH"
+
+mkdir -p "${role_testdir}"
+pushd "${role_testdir}"
+
+ mkdir testroles
+ ansible-galaxy role init --init-path ./local-roles quark
+ ANSIBLE_ROLES_PATH=./local-roles:${HOME}/.ansible/roles ansible-galaxy role list quark | tee out.txt
+
+ [[ $(grep -c 'not found' out.txt) -eq 0 ]]
+
+ ANSIBLE_ROLES_PATH=${HOME}/.ansible/roles:./local-roles ansible-galaxy role list quark | tee out.txt
+
+ [[ $(grep -c 'not found' out.txt) -eq 0 ]]
+
+popd # ${role_testdir}
+rm -fr "${role_testdir}"
+
+
+# Galaxy role info tests
+
+# Get info about role that is not installed
+
+f_ansible_galaxy_status "role info"
+mkdir -p "${galaxy_testdir}"
+pushd "${galaxy_testdir}"
+ ansible-galaxy role info samdoran.fish | tee out.txt
+
+ [[ $(grep -c 'not found' out.txt ) -eq 0 ]]
+ [[ $(grep -c 'Role:.*samdoran\.fish' out.txt ) -eq 1 ]]
+
+popd # ${galaxy_testdir}
+
+f_ansible_galaxy_status \
+ "role info non-existant role"
+
+mkdir -p "${role_testdir}"
+pushd "${role_testdir}"
+
+ ansible-galaxy role info notaroll | tee out.txt
+
+ grep -- '- the role notaroll was not found' out.txt
+
+f_ansible_galaxy_status \
+ "role info description offline"
+
+ mkdir testroles
+ ansible-galaxy role init testdesc --init-path ./testroles
+
+ # Only galaxy_info['description'] exists in file
+ sed -i -e 's#[[:space:]]\{1,\}description:.*$# description: Description in galaxy_info#' ./testroles/testdesc/meta/main.yml
+ ansible-galaxy role info -p ./testroles --offline testdesc | tee out.txt
+ grep 'description: Description in galaxy_info' out.txt
+
+ # Both top level 'description' and galaxy_info['description'] exist in file
+ # Use shell-fu instead of sed to prepend a line to a file because BSD
+ # and macOS sed don't work the same as GNU sed.
+ echo 'description: Top level' | \
+ cat - ./testroles/testdesc/meta/main.yml > tmp.yml && \
+ mv tmp.yml ./testroles/testdesc/meta/main.yml
+ ansible-galaxy role info -p ./testroles --offline testdesc | tee out.txt
+ grep 'description: Top level' out.txt
+
+ # Only top level 'description' exists in file
+ sed -i.bak '/^[[:space:]]\{1,\}description: Description in galaxy_info/d' ./testroles/testdesc/meta/main.yml
+ ansible-galaxy role info -p ./testroles --offline testdesc | tee out.txt
+ grep 'description: Top level' out.txt
+
+ # test multiple role listing
+ ansible-galaxy role init otherrole --init-path ./testroles
+ ansible-galaxy role info -p ./testroles --offline testdesc otherrole | tee out.txt
+ grep 'Role: testdesc' out.txt
+ grep 'Role: otherrole' out.txt
+
+
+popd # ${role_testdir}
+rm -fr "${role_testdir}"
+
+# Properly list roles when the role name is a subset of the path, or the role
+# name is the same name as the parent directory of the role. Issue #67365
+#
+# ./parrot/parrot
+# ./parrot/arr
+# ./testing-roles/test
+
+f_ansible_galaxy_status \
+ "list roles where the role name is the same or a subset of the role path (#67365)"
+
+mkdir -p "${role_testdir}"
+pushd "${role_testdir}"
+
+ mkdir parrot
+ ansible-galaxy role init --init-path ./parrot parrot
+ ansible-galaxy role init --init-path ./parrot parrot-ship
+ ansible-galaxy role init --init-path ./parrot arr
+
+ ansible-galaxy role list -p ./parrot | tee out.txt
+
+ [[ $(grep -Ec '\- (parrot|arr)' out.txt) -eq 3 ]]
+ ansible-galaxy role list test-role | tee -a out.txt
+
+popd # ${role_testdir}
+rm -rf "${role_testdir}"
+
+f_ansible_galaxy_status \
+ "Test role with non-ascii characters"
+
+mkdir -p "${role_testdir}"
+pushd "${role_testdir}"
+
+ mkdir nonascii
+ ansible-galaxy role init --init-path ./nonascii nonascii
+ touch nonascii/ÅÑŚÌβÅÈ.txt
+ tar czvf nonascii.tar.gz nonascii
+ ansible-galaxy role install -p ./roles nonascii.tar.gz
+
+popd # ${role_testdir}
+rm -rf "${role_testdir}"
+
+f_ansible_galaxy_status \
+ "Test if git hidden directories are skipped while using role skeleton (#71977)"
+
+role_testdir=$(mktemp -d)
+pushd "${role_testdir}"
+
+ ansible-galaxy role init sample-role-skeleton
+ git init ./sample-role-skeleton
+ ansible-galaxy role init --role-skeleton=sample-role-skeleton example
+
+popd # ${role_testdir}
+rm -rf "${role_testdir}"
+
+#################################
+# ansible-galaxy collection tests
+#################################
+# TODO: Move these to ansible-galaxy-collection
+
+mkdir -p "${galaxy_testdir}"
+pushd "${galaxy_testdir}"
+
+## ansible-galaxy collection list tests
+
+# Create more collections and put them in various places
+f_ansible_galaxy_status \
+ "setting up for collection list tests"
+
+rm -rf ansible_test/* install/*
+
+NAMES=(zoo museum airport)
+for n in "${NAMES[@]}"; do
+ ansible-galaxy collection init "ansible_test.$n"
+ ansible-galaxy collection build "ansible_test/$n"
+done
+
+ansible-galaxy collection install ansible_test-zoo-1.0.0.tar.gz
+ansible-galaxy collection install ansible_test-museum-1.0.0.tar.gz -p ./install
+ansible-galaxy collection install ansible_test-airport-1.0.0.tar.gz -p ./local
+
+# Change the collection version and install to another location
+sed -i -e 's#^version:.*#version: 2.5.0#' ansible_test/zoo/galaxy.yml
+ansible-galaxy collection build ansible_test/zoo
+ansible-galaxy collection install ansible_test-zoo-2.5.0.tar.gz -p ./local
+
+# Test listing a collection that contains a galaxy.yml
+ansible-galaxy collection init "ansible_test.development"
+mv ./ansible_test/development "${galaxy_testdir}/local/ansible_collections/ansible_test/"
+
+export ANSIBLE_COLLECTIONS_PATH=~/.ansible/collections:${galaxy_testdir}/local
+
+f_ansible_galaxy_status \
+ "collection list all collections"
+
+ ansible-galaxy collection list -p ./install | tee out.txt
+
+ [[ $(grep -c ansible_test out.txt) -eq 5 ]]
+
+f_ansible_galaxy_status \
+ "collection list specific collection"
+
+ ansible-galaxy collection list -p ./install ansible_test.airport | tee out.txt
+
+ [[ $(grep -c 'ansible_test\.airport' out.txt) -eq 1 ]]
+
+f_ansible_galaxy_status \
+ "collection list specific collection which contains galaxy.yml"
+
+ ansible-galaxy collection list -p ./install ansible_test.development 2>&1 | tee out.txt
+
+ [[ $(grep -c 'ansible_test\.development' out.txt) -eq 1 ]]
+ [[ $(grep -c 'WARNING' out.txt) -eq 0 ]]
+
+f_ansible_galaxy_status \
+ "collection list specific collection found in multiple places"
+
+ ansible-galaxy collection list -p ./install ansible_test.zoo | tee out.txt
+
+ [[ $(grep -c 'ansible_test\.zoo' out.txt) -eq 2 ]]
+
+f_ansible_galaxy_status \
+ "collection list all with duplicate paths"
+
+ ansible-galaxy collection list -p ~/.ansible/collections | tee out.txt
+
+ [[ $(grep -c '# /root/.ansible/collections/ansible_collections' out.txt) -eq 1 ]]
+
+f_ansible_galaxy_status \
+ "collection list invalid collection name"
+
+ ansible-galaxy collection list -p ./install dirty.wraughten.name "$@" 2>&1 | tee out.txt || echo "expected failure"
+
+ grep 'ERROR! Invalid collection name' out.txt
+
+f_ansible_galaxy_status \
+ "collection list path not found"
+
+ ansible-galaxy collection list -p ./nope "$@" 2>&1 | tee out.txt || echo "expected failure"
+
+ grep '\[WARNING\]: - the configured path' out.txt
+
+f_ansible_galaxy_status \
+ "collection list missing ansible_collections dir inside path"
+
+ mkdir emptydir
+
+ ansible-galaxy collection list -p ./emptydir "$@"
+
+ rmdir emptydir
+
+unset ANSIBLE_COLLECTIONS_PATH
+
+f_ansible_galaxy_status \
+ "collection list with collections installed from python package"
+
+ mkdir -p test-site-packages
+ ln -s "${galaxy_testdir}/local/ansible_collections" test-site-packages/ansible_collections
+ ansible-galaxy collection list
+ PYTHONPATH="./test-site-packages/:$PYTHONPATH" ansible-galaxy collection list | tee out.txt
+
+ grep ".ansible/collections/ansible_collections" out.txt
+ grep "test-site-packages/ansible_collections" out.txt
+
+## end ansible-galaxy collection list
+
+
+popd # ${galaxy_testdir}
+
+rm -fr "${galaxy_testdir}"
+
+rm -fr "${galaxy_local_test_role_dir}"
diff --git a/test/integration/targets/ansible-galaxy/setup.yml b/test/integration/targets/ansible-galaxy/setup.yml
new file mode 100644
index 0000000..b4fb6d3
--- /dev/null
+++ b/test/integration/targets/ansible-galaxy/setup.yml
@@ -0,0 +1,57 @@
+- hosts: localhost
+ vars:
+ ws_dir: '{{ lookup("env", "OUTPUT_DIR") }}/ansible-galaxy-webserver'
+ tasks:
+ - name: install git & OpenSSL
+ package:
+ name: git
+ when: ansible_distribution not in ["MacOSX", "Alpine"]
+ register: git_install
+
+ - name: install OpenSSL
+ package:
+ name: openssl
+ when: ansible_distribution not in ["MacOSX", "Alpine"]
+ register: openssl_install
+
+ - name: install OpenSSL
+ command: apk add openssl
+ when: ansible_distribution == "Alpine"
+ register: openssl_install
+
+ - name: setup webserver dir
+ file:
+ state: directory
+ path: "{{ ws_dir }}"
+
+ - name: copy webserver
+ copy:
+ src: testserver.py
+ dest: "{{ ws_dir }}"
+
+ - name: Create rand file
+ command: dd if=/dev/urandom of="{{ ws_dir }}/.rnd" bs=256 count=1
+
+ - name: Create self-signed cert
+ shell: RANDFILE={{ ws_dir }}/.rnd openssl req -x509 -newkey rsa:2048 \
+ -nodes -days 365 -keyout "{{ ws_dir }}/key.pem" -out "{{ ws_dir }}/cert.pem" \
+ -subj "/C=GB/O=Red Hat/OU=Ansible/CN=ansible-test-cert"
+
+ - name: start SimpleHTTPServer
+ shell: cd {{ ws_dir }} && {{ ansible_python.executable }} {{ ws_dir }}/testserver.py
+ async: 120 # this test set can take ~1m to run on FreeBSD (via Shippable)
+ poll: 0
+
+ - wait_for: port=4443
+
+ - name: save results
+ copy:
+ content: "{{ item.content }}"
+ dest: '{{ lookup("env", "OUTPUT_DIR") }}/{{ item.key }}.json'
+ loop:
+ - key: git_install
+ content: "{{ git_install }}"
+ - key: openssl_install
+ content: "{{ openssl_install }}"
+ - key: ws_dir
+ content: "{{ ws_dir | to_json }}"
diff --git a/test/integration/targets/ansible-inventory/aliases b/test/integration/targets/ansible-inventory/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/ansible-inventory/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/ansible-inventory/files/invalid_sample.yml b/test/integration/targets/ansible-inventory/files/invalid_sample.yml
new file mode 100644
index 0000000..f7bbe0c
--- /dev/null
+++ b/test/integration/targets/ansible-inventory/files/invalid_sample.yml
@@ -0,0 +1,7 @@
+all:
+ children:
+ somegroup:
+ hosts:
+ something:
+ 7.2: bar
+ ungrouped: {}
diff --git a/test/integration/targets/ansible-inventory/files/unicode.yml b/test/integration/targets/ansible-inventory/files/unicode.yml
new file mode 100644
index 0000000..ff95db0
--- /dev/null
+++ b/test/integration/targets/ansible-inventory/files/unicode.yml
@@ -0,0 +1,3 @@
+all:
+ hosts:
+ příbor:
diff --git a/test/integration/targets/ansible-inventory/files/valid_sample.toml b/test/integration/targets/ansible-inventory/files/valid_sample.toml
new file mode 100644
index 0000000..6d83b6f
--- /dev/null
+++ b/test/integration/targets/ansible-inventory/files/valid_sample.toml
@@ -0,0 +1,2 @@
+[somegroup.hosts.something]
+foo = "bar"
diff --git a/test/integration/targets/ansible-inventory/files/valid_sample.yml b/test/integration/targets/ansible-inventory/files/valid_sample.yml
new file mode 100644
index 0000000..477f82f
--- /dev/null
+++ b/test/integration/targets/ansible-inventory/files/valid_sample.yml
@@ -0,0 +1,7 @@
+all:
+ children:
+ somegroup:
+ hosts:
+ something:
+ foo: bar
+ ungrouped: {} \ No newline at end of file
diff --git a/test/integration/targets/ansible-inventory/runme.sh b/test/integration/targets/ansible-inventory/runme.sh
new file mode 100755
index 0000000..6f3e342
--- /dev/null
+++ b/test/integration/targets/ansible-inventory/runme.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+source virtualenv.sh
+export ANSIBLE_ROLES_PATH=../
+set -euvx
+
+ansible-playbook test.yml "$@"
diff --git a/test/integration/targets/ansible-inventory/tasks/main.yml b/test/integration/targets/ansible-inventory/tasks/main.yml
new file mode 100644
index 0000000..84ac2c3
--- /dev/null
+++ b/test/integration/targets/ansible-inventory/tasks/main.yml
@@ -0,0 +1,147 @@
+- name: "No command supplied"
+ command: ansible-inventory
+ ignore_errors: true
+ register: result
+
+- assert:
+ that:
+ - result is failed
+ - '"ERROR! No action selected, at least one of --host, --graph or --list needs to be specified." in result.stderr'
+
+- name: "test option: --list --export"
+ command: ansible-inventory --list --export
+ register: result
+
+- assert:
+ that:
+ - result is succeeded
+
+- name: "test option: --list --yaml --export"
+ command: ansible-inventory --list --yaml --export
+ register: result
+
+- assert:
+ that:
+ - result is succeeded
+
+- name: "test option: --list --output"
+ command: ansible-inventory --list --output junk.txt
+ register: result
+
+- name: stat output file
+ stat:
+ path: junk.txt
+ register: st
+
+- assert:
+ that:
+ - result is succeeded
+ - st.stat.exists
+
+- name: "test option: --graph"
+ command: ansible-inventory --graph
+ register: result
+
+- assert:
+ that:
+ - result is succeeded
+
+- name: "test option: --graph --vars"
+ command: ansible-inventory --graph --vars
+ register: result
+
+- assert:
+ that:
+ - result is succeeded
+
+- name: "test option: --graph with bad pattern"
+ command: ansible-inventory --graph invalid
+ ignore_errors: true
+ register: result
+
+- assert:
+ that:
+ - result is failed
+ - '"ERROR! Pattern must be valid group name when using --graph" in result.stderr'
+
+- name: "test option: --host localhost"
+ command: ansible-inventory --host localhost
+ register: result
+
+- assert:
+ that:
+ - result is succeeded
+
+- name: "test option: --host with invalid host"
+ command: ansible-inventory --host invalid
+ ignore_errors: true
+ register: result
+
+- assert:
+ that:
+ - result is failed
+ - '"ERROR! Could not match supplied host pattern, ignoring: invalid" in result.stderr'
+
+- name: "test json output with unicode characters"
+ command: ansible-inventory --list -i {{ role_path }}/files/unicode.yml
+ register: result
+
+- assert:
+ that:
+ - result is succeeded
+ - result.stdout is contains('příbor')
+
+- block:
+ - name: "test json output file with unicode characters"
+ command: ansible-inventory --list --output unicode_inventory.json -i {{ role_path }}/files/unicode.yml
+
+ - set_fact:
+ json_inventory_file: "{{ lookup('file', 'unicode_inventory.json') }}"
+
+ - assert:
+ that:
+ - json_inventory_file|string is contains('příbor')
+ always:
+ - file:
+ name: unicode_inventory.json
+ state: absent
+
+- name: "test yaml output with unicode characters"
+ command: ansible-inventory --list --yaml -i {{ role_path }}/files/unicode.yml
+ register: result
+
+- assert:
+ that:
+ - result is succeeded
+ - result.stdout is contains('příbor')
+
+- block:
+ - name: "test yaml output file with unicode characters"
+ command: ansible-inventory --list --yaml --output unicode_inventory.yaml -i {{ role_path }}/files/unicode.yml
+
+ - set_fact:
+ yaml_inventory_file: "{{ lookup('file', 'unicode_inventory.yaml') | string }}"
+
+ - assert:
+ that:
+ - yaml_inventory_file is contains('příbor')
+ always:
+ - file:
+ name: unicode_inventory.yaml
+ state: absent
+
+- include_tasks: toml.yml
+ loop:
+ -
+ - toml<0.10.0
+ -
+ - toml
+ -
+ - tomli
+ - tomli-w
+ -
+ - tomllib
+ - tomli-w
+ loop_control:
+ loop_var: toml_package
+ when: toml_package is not contains 'tomllib' or (toml_package is contains 'tomllib' and ansible_facts.python.version_info >= [3, 11])
diff --git a/test/integration/targets/ansible-inventory/tasks/toml.yml b/test/integration/targets/ansible-inventory/tasks/toml.yml
new file mode 100644
index 0000000..4a227dd
--- /dev/null
+++ b/test/integration/targets/ansible-inventory/tasks/toml.yml
@@ -0,0 +1,66 @@
+- name: Ensure no toml packages are installed
+ pip:
+ name:
+ - tomli
+ - tomli-w
+ - toml
+ state: absent
+
+- name: Install toml package
+ pip:
+ name: '{{ toml_package|difference(["tomllib"]) }}'
+ state: present
+
+- name: test toml parsing
+ command: ansible-inventory --list --toml -i {{ role_path }}/files/valid_sample.toml
+ register: toml_in
+
+- assert:
+ that:
+ - >
+ 'foo = "bar"' in toml_in.stdout
+
+- name: "test option: --toml with valid group name"
+ command: ansible-inventory --list --toml -i {{ role_path }}/files/valid_sample.yml
+ register: result
+
+- assert:
+ that:
+ - result is succeeded
+
+- name: "test option: --toml with invalid group name"
+ command: ansible-inventory --list --toml -i {{ role_path }}/files/invalid_sample.yml
+ ignore_errors: true
+ register: result
+
+- assert:
+ that:
+ - result is failed
+ - >
+ "ERROR! The source inventory contains" in result.stderr
+
+- block:
+ - name: "test toml output with unicode characters"
+ command: ansible-inventory --list --toml -i {{ role_path }}/files/unicode.yml
+ register: result
+
+ - assert:
+ that:
+ - result is succeeded
+ - result.stdout is contains('příbor')
+
+ - block:
+ - name: "test toml output file with unicode characters"
+ command: ansible-inventory --list --toml --output unicode_inventory.toml -i {{ role_path }}/files/unicode.yml
+
+ - set_fact:
+ toml_inventory_file: "{{ lookup('file', 'unicode_inventory.toml') | string }}"
+
+ - assert:
+ that:
+ - toml_inventory_file is contains('příbor')
+ always:
+ - file:
+ name: unicode_inventory.toml
+ state: absent
+ when: ansible_python.version.major|int == 3
diff --git a/test/integration/targets/ansible-inventory/test.yml b/test/integration/targets/ansible-inventory/test.yml
new file mode 100644
index 0000000..38b3686
--- /dev/null
+++ b/test/integration/targets/ansible-inventory/test.yml
@@ -0,0 +1,3 @@
+- hosts: localhost
+ roles:
+ - ansible-inventory
diff --git a/test/integration/targets/ansible-pull/aliases b/test/integration/targets/ansible-pull/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/ansible-pull/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/ansible-pull/cleanup.yml b/test/integration/targets/ansible-pull/cleanup.yml
new file mode 100644
index 0000000..32a6602
--- /dev/null
+++ b/test/integration/targets/ansible-pull/cleanup.yml
@@ -0,0 +1,16 @@
+- hosts: localhost
+ vars:
+ git_install: '{{ lookup("file", lookup("env", "OUTPUT_DIR") + "/git_install.json") }}'
+ tasks:
+ - name: remove unwanted packages
+ package:
+ name: git
+ state: absent
+ when: git_install.changed
+
+ - name: remove auto-installed packages from FreeBSD
+ package:
+ name: git
+ state: absent
+ autoremove: yes
+ when: git_install.changed and ansible_distribution == "FreeBSD"
diff --git a/test/integration/targets/ansible-pull/pull-integration-test/ansible.cfg b/test/integration/targets/ansible-pull/pull-integration-test/ansible.cfg
new file mode 100644
index 0000000..f8fc6cd
--- /dev/null
+++ b/test/integration/targets/ansible-pull/pull-integration-test/ansible.cfg
@@ -0,0 +1,2 @@
+[defaults]
+inventory = inventory
diff --git a/test/integration/targets/ansible-pull/pull-integration-test/inventory b/test/integration/targets/ansible-pull/pull-integration-test/inventory
new file mode 100644
index 0000000..72644ce
--- /dev/null
+++ b/test/integration/targets/ansible-pull/pull-integration-test/inventory
@@ -0,0 +1,2 @@
+testhost1.example.com
+localhost
diff --git a/test/integration/targets/ansible-pull/pull-integration-test/local.yml b/test/integration/targets/ansible-pull/pull-integration-test/local.yml
new file mode 100644
index 0000000..d358ee8
--- /dev/null
+++ b/test/integration/targets/ansible-pull/pull-integration-test/local.yml
@@ -0,0 +1,20 @@
+- name: test playbook for ansible-pull
+ hosts: all
+ gather_facts: False
+ tasks:
+ - name: debug output
+ debug: msg="test task"
+ - name: check for correct inventory
+ debug: msg="testing for inventory content"
+ failed_when: "'testhost1.example.com' not in groups['all']"
+ - name: check for correct limit
+ debug: msg="testing for limit"
+ failed_when: "'testhost1.example.com' == inventory_hostname"
+ - name: final task, has to be reached for the test to succeed
+ debug: msg="MAGICKEYWORD"
+
+ - name: check that extra vars are correclty passed
+ assert:
+ that:
+ - docker_registries_login is defined
+ tags: ['never', 'test_ev']
diff --git a/test/integration/targets/ansible-pull/pull-integration-test/multi_play_1.yml b/test/integration/targets/ansible-pull/pull-integration-test/multi_play_1.yml
new file mode 100644
index 0000000..0ec0da6
--- /dev/null
+++ b/test/integration/targets/ansible-pull/pull-integration-test/multi_play_1.yml
@@ -0,0 +1,6 @@
+- name: test multiple playbooks for ansible-pull
+ hosts: all
+ gather_facts: False
+ tasks:
+ - name: debug output
+ debug: msg="test multi_play_1"
diff --git a/test/integration/targets/ansible-pull/pull-integration-test/multi_play_2.yml b/test/integration/targets/ansible-pull/pull-integration-test/multi_play_2.yml
new file mode 100644
index 0000000..1fe5a58
--- /dev/null
+++ b/test/integration/targets/ansible-pull/pull-integration-test/multi_play_2.yml
@@ -0,0 +1,6 @@
+- name: test multiple playbooks for ansible-pull
+ hosts: all
+ gather_facts: False
+ tasks:
+ - name: debug output
+ debug: msg="test multi_play_2"
diff --git a/test/integration/targets/ansible-pull/runme.sh b/test/integration/targets/ansible-pull/runme.sh
new file mode 100755
index 0000000..347971a
--- /dev/null
+++ b/test/integration/targets/ansible-pull/runme.sh
@@ -0,0 +1,87 @@
+#!/usr/bin/env bash
+
+set -eux
+set -o pipefail
+
+# http://unix.stackexchange.com/questions/30091/fix-or-alternative-for-mktemp-in-os-x
+temp_dir=$(shell mktemp -d 2>/dev/null || mktemp -d -t 'ansible-testing-XXXXXXXXXX')
+trap 'rm -rf "${temp_dir}"' EXIT
+
+repo_dir="${temp_dir}/repo"
+pull_dir="${temp_dir}/pull"
+temp_log="${temp_dir}/pull.log"
+
+ansible-playbook setup.yml -i ../../inventory
+
+cleanup="$(pwd)/cleanup.yml"
+
+trap 'ansible-playbook "${cleanup}" -i ../../inventory' EXIT
+
+cp -av "pull-integration-test" "${repo_dir}"
+cd "${repo_dir}"
+(
+ git init
+ git config user.email "ansible@ansible.com"
+ git config user.name "Ansible Test Runner"
+ git add .
+ git commit -m "Initial commit."
+)
+
+function pass_tests {
+ # test for https://github.com/ansible/ansible/issues/13688
+ if ! grep MAGICKEYWORD "${temp_log}"; then
+ cat "${temp_log}"
+ echo "Missing MAGICKEYWORD in output."
+ exit 1
+ fi
+
+ # test for https://github.com/ansible/ansible/issues/13681
+ if grep -E '127\.0\.0\.1.*ok' "${temp_log}"; then
+ cat "${temp_log}"
+ echo "Found host 127.0.0.1 in output. Only localhost should be present."
+ exit 1
+ fi
+ # make sure one host was run
+ if ! grep -E 'localhost.*ok' "${temp_log}"; then
+ cat "${temp_log}"
+ echo "Did not find host localhost in output."
+ exit 1
+ fi
+}
+
+function pass_tests_multi {
+ # test for https://github.com/ansible/ansible/issues/72708
+ if ! grep 'test multi_play_1' "${temp_log}"; then
+ cat "${temp_log}"
+ echo "Did not run multiple playbooks"
+ exit 1
+ fi
+ if ! grep 'test multi_play_2' "${temp_log}"; then
+ cat "${temp_log}"
+ echo "Did not run multiple playbooks"
+ exit 1
+ fi
+}
+
+export ANSIBLE_INVENTORY
+export ANSIBLE_HOST_PATTERN_MISMATCH
+
+unset ANSIBLE_INVENTORY
+unset ANSIBLE_HOST_PATTERN_MISMATCH
+
+ANSIBLE_CONFIG='' ansible-pull -d "${pull_dir}" -U "${repo_dir}" "$@" | tee "${temp_log}"
+
+pass_tests
+
+# ensure complex extra vars work
+PASSWORD='test'
+USER=${USER:-'broken_docker'}
+JSON_EXTRA_ARGS='{"docker_registries_login": [{ "docker_password": "'"${PASSWORD}"'", "docker_username": "'"${USER}"'", "docker_registry_url":"repository-manager.company.com:5001"}], "docker_registries_logout": [{ "docker_password": "'"${PASSWORD}"'", "docker_username": "'"${USER}"'", "docker_registry_url":"repository-manager.company.com:5001"}] }'
+
+ANSIBLE_CONFIG='' ansible-pull -d "${pull_dir}" -U "${repo_dir}" -e "${JSON_EXTRA_ARGS}" "$@" --tags untagged,test_ev | tee "${temp_log}"
+
+pass_tests
+
+ANSIBLE_CONFIG='' ansible-pull -d "${pull_dir}" -U "${repo_dir}" "$@" multi_play_1.yml multi_play_2.yml | tee "${temp_log}"
+
+pass_tests_multi \ No newline at end of file
diff --git a/test/integration/targets/ansible-pull/setup.yml b/test/integration/targets/ansible-pull/setup.yml
new file mode 100644
index 0000000..ebd5a1c
--- /dev/null
+++ b/test/integration/targets/ansible-pull/setup.yml
@@ -0,0 +1,11 @@
+- hosts: localhost
+ tasks:
+ - name: install git
+ package:
+ name: git
+ when: ansible_distribution not in ["MacOSX", "Alpine"]
+ register: git_install
+ - name: save install result
+ copy:
+ content: '{{ git_install }}'
+ dest: '{{ lookup("env", "OUTPUT_DIR") }}/git_install.json'
diff --git a/test/integration/targets/ansible-runner/aliases b/test/integration/targets/ansible-runner/aliases
new file mode 100644
index 0000000..13e7d78
--- /dev/null
+++ b/test/integration/targets/ansible-runner/aliases
@@ -0,0 +1,5 @@
+shippable/posix/group5
+context/controller
+skip/osx
+skip/macos
+skip/freebsd
diff --git a/test/integration/targets/ansible-runner/files/adhoc_example1.py b/test/integration/targets/ansible-runner/files/adhoc_example1.py
new file mode 100644
index 0000000..ab24bca
--- /dev/null
+++ b/test/integration/targets/ansible-runner/files/adhoc_example1.py
@@ -0,0 +1,29 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+import os
+import sys
+import ansible_runner
+
+# the first positional arg should be where the artifacts live
+output_dir = sys.argv[1]
+
+# this calls a single module directly, aka "adhoc" mode
+r = ansible_runner.run(
+ private_data_dir=output_dir,
+ host_pattern='localhost',
+ module='shell',
+ module_args='whoami'
+)
+
+data = {
+ 'rc': r.rc,
+ 'status': r.status,
+ 'events': [x['event'] for x in r.events],
+ 'stats': r.stats
+}
+
+# insert this header for the flask controller
+print('#STARTJSON')
+json.dump(data, sys.stdout)
diff --git a/test/integration/targets/ansible-runner/files/playbook_example1.py b/test/integration/targets/ansible-runner/files/playbook_example1.py
new file mode 100644
index 0000000..550c185
--- /dev/null
+++ b/test/integration/targets/ansible-runner/files/playbook_example1.py
@@ -0,0 +1,41 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+import os
+import sys
+import ansible_runner
+
+
+PLAYBOOK = '''
+- hosts: localhost
+ gather_facts: False
+ tasks:
+ - set_fact:
+ foo: bar
+'''
+
+# the first positional arg should be where the artifacts live
+output_dir = sys.argv[1]
+
+invdir = os.path.join(output_dir, 'inventory')
+if not os.path.isdir(invdir):
+ os.makedirs(invdir)
+with open(os.path.join(invdir, 'hosts'), 'w') as f:
+ f.write('localhost\n')
+pbfile = os.path.join(output_dir, 'test.yml')
+with open(pbfile, 'w') as f:
+ f.write(PLAYBOOK)
+
+r = ansible_runner.run(private_data_dir=output_dir, playbook='test.yml')
+
+data = {
+ 'rc': r.rc,
+ 'status': r.status,
+ 'events': [x['event'] for x in r.events],
+ 'stats': r.stats
+}
+
+# insert this header for the flask controller
+print('#STARTJSON')
+json.dump(data, sys.stdout)
diff --git a/test/integration/targets/ansible-runner/filter_plugins/parse.py b/test/integration/targets/ansible-runner/filter_plugins/parse.py
new file mode 100644
index 0000000..7842f6c
--- /dev/null
+++ b/test/integration/targets/ansible-runner/filter_plugins/parse.py
@@ -0,0 +1,17 @@
+from __future__ import (absolute_import, division, print_function)
+
+__metaclass__ = type
+
+import re
+import json
+
+
+def parse_json(value):
+ return json.dumps(json.loads(re.sub('^.*\n#STARTJSON\n', '', value, flags=re.DOTALL)), indent=4, sort_keys=True)
+
+
+class FilterModule(object):
+ def filters(self):
+ return {
+ 'parse_json': parse_json,
+ }
diff --git a/test/integration/targets/ansible-runner/inventory b/test/integration/targets/ansible-runner/inventory
new file mode 100644
index 0000000..009f6c3
--- /dev/null
+++ b/test/integration/targets/ansible-runner/inventory
@@ -0,0 +1 @@
+# no hosts required, test only requires implicit localhost
diff --git a/test/integration/targets/ansible-runner/runme.sh b/test/integration/targets/ansible-runner/runme.sh
new file mode 100755
index 0000000..97e6f4d
--- /dev/null
+++ b/test/integration/targets/ansible-runner/runme.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+set -eux
+
+source virtualenv.sh
+
+ANSIBLE_ROLES_PATH=../ ansible-playbook test.yml -i inventory "$@"
diff --git a/test/integration/targets/ansible-runner/tasks/adhoc_example1.yml b/test/integration/targets/ansible-runner/tasks/adhoc_example1.yml
new file mode 100644
index 0000000..ce174f1
--- /dev/null
+++ b/test/integration/targets/ansible-runner/tasks/adhoc_example1.yml
@@ -0,0 +1,14 @@
+- name: execute the script
+ command: "'{{ ansible_python_interpreter }}' '{{ role_path }}/files/adhoc_example1.py' '{{ lookup('env', 'OUTPUT_DIR') }}'"
+ register: script
+
+- name: parse script output
+ # work around for ansible-runner showing ansible warnings on stdout
+ set_fact:
+ adexec1_json: "{{ script.stdout | parse_json }}"
+
+- assert:
+ that:
+ - "adexec1_json.rc == 0"
+ - "adexec1_json.events|length == 4"
+ - "'localhost' in adexec1_json.stats.ok"
diff --git a/test/integration/targets/ansible-runner/tasks/main.yml b/test/integration/targets/ansible-runner/tasks/main.yml
new file mode 100644
index 0000000..ba6a3a2
--- /dev/null
+++ b/test/integration/targets/ansible-runner/tasks/main.yml
@@ -0,0 +1,4 @@
+- block:
+ - include_tasks: setup.yml
+ - include_tasks: adhoc_example1.yml
+ - include_tasks: playbook_example1.yml
diff --git a/test/integration/targets/ansible-runner/tasks/playbook_example1.yml b/test/integration/targets/ansible-runner/tasks/playbook_example1.yml
new file mode 100644
index 0000000..1fedb53
--- /dev/null
+++ b/test/integration/targets/ansible-runner/tasks/playbook_example1.yml
@@ -0,0 +1,21 @@
+- name: execute the script
+ command: "'{{ ansible_python_interpreter }}' '{{ role_path }}/files/playbook_example1.py' '{{ lookup('env', 'OUTPUT_DIR') }}'"
+ register: script
+
+- name: parse script output
+ # work around for ansible-runner showing ansible warnings on stdout
+ set_fact:
+ pbexec_json: "{{ script.stdout | parse_json }}"
+ expected_events:
+ - playbook_on_start
+ - playbook_on_play_start
+ - playbook_on_task_start
+ - runner_on_start
+ - runner_on_ok
+ - playbook_on_stats
+
+- assert:
+ that:
+ - "pbexec_json.rc == 0"
+ - "pbexec_json.events == expected_events"
+ - "'localhost' in pbexec_json.stats.ok"
diff --git a/test/integration/targets/ansible-runner/tasks/setup.yml b/test/integration/targets/ansible-runner/tasks/setup.yml
new file mode 100644
index 0000000..7ee66b2
--- /dev/null
+++ b/test/integration/targets/ansible-runner/tasks/setup.yml
@@ -0,0 +1,4 @@
+- name: Install ansible-runner
+ pip:
+ name: ansible-runner
+ version: 2.2.0
diff --git a/test/integration/targets/ansible-runner/test.yml b/test/integration/targets/ansible-runner/test.yml
new file mode 100644
index 0000000..113f8e7
--- /dev/null
+++ b/test/integration/targets/ansible-runner/test.yml
@@ -0,0 +1,3 @@
+- hosts: localhost
+ roles:
+ - ansible-runner
diff --git a/test/integration/targets/ansible-test-cloud-acme/aliases b/test/integration/targets/ansible-test-cloud-acme/aliases
new file mode 100644
index 0000000..db3ab68
--- /dev/null
+++ b/test/integration/targets/ansible-test-cloud-acme/aliases
@@ -0,0 +1,3 @@
+cloud/acme
+shippable/generic/group1
+context/controller
diff --git a/test/integration/targets/ansible-test-cloud-acme/tasks/main.yml b/test/integration/targets/ansible-test-cloud-acme/tasks/main.yml
new file mode 100644
index 0000000..42ebc28
--- /dev/null
+++ b/test/integration/targets/ansible-test-cloud-acme/tasks/main.yml
@@ -0,0 +1,7 @@
+- name: Verify endpoints respond
+ uri:
+ url: "{{ item }}"
+ validate_certs: no
+ with_items:
+ - http://{{ acme_host }}:5000/
+ - https://{{ acme_host }}:14000/dir
diff --git a/test/integration/targets/ansible-test-cloud-aws/aliases b/test/integration/targets/ansible-test-cloud-aws/aliases
new file mode 100644
index 0000000..9442e88
--- /dev/null
+++ b/test/integration/targets/ansible-test-cloud-aws/aliases
@@ -0,0 +1,3 @@
+cloud/aws
+shippable/generic/group1
+context/controller
diff --git a/test/integration/targets/ansible-test-cloud-aws/tasks/main.yml b/test/integration/targets/ansible-test-cloud-aws/tasks/main.yml
new file mode 100644
index 0000000..4f7c4c4
--- /dev/null
+++ b/test/integration/targets/ansible-test-cloud-aws/tasks/main.yml
@@ -0,0 +1,17 @@
+- name: Verify variables are set
+ assert:
+ that:
+ - aws_access_key
+ - aws_region
+ - aws_secret_key
+ - resource_prefix
+ - security_token
+ - tiny_prefix
+- name: Show variables
+ debug:
+ msg: "{{ lookup('vars', item) }}"
+ with_items:
+ - aws_access_key
+ - aws_region
+ - resource_prefix
+ - tiny_prefix
diff --git a/test/integration/targets/ansible-test-cloud-azure/aliases b/test/integration/targets/ansible-test-cloud-azure/aliases
new file mode 100644
index 0000000..a5526fe
--- /dev/null
+++ b/test/integration/targets/ansible-test-cloud-azure/aliases
@@ -0,0 +1,3 @@
+cloud/azure
+shippable/generic/group1
+context/controller
diff --git a/test/integration/targets/ansible-test-cloud-azure/tasks/main.yml b/test/integration/targets/ansible-test-cloud-azure/tasks/main.yml
new file mode 100644
index 0000000..c9201ba
--- /dev/null
+++ b/test/integration/targets/ansible-test-cloud-azure/tasks/main.yml
@@ -0,0 +1,18 @@
+- name: Verify variables are set
+ assert:
+ that:
+ - azure_client_id
+ - azure_secret
+ - azure_subscription_id
+ - azure_tenant
+ - resource_group
+ - resource_group_secondary
+- name: Show variables
+ debug:
+ msg: "{{ lookup('vars', item) }}"
+ with_items:
+ - azure_client_id
+ - azure_subscription_id
+ - azure_tenant
+ - resource_group
+ - resource_group_secondary
diff --git a/test/integration/targets/ansible-test-cloud-cs/aliases b/test/integration/targets/ansible-test-cloud-cs/aliases
new file mode 100644
index 0000000..cf43ff1
--- /dev/null
+++ b/test/integration/targets/ansible-test-cloud-cs/aliases
@@ -0,0 +1,3 @@
+cloud/cs
+shippable/generic/group1
+context/controller
diff --git a/test/integration/targets/ansible-test-cloud-cs/tasks/main.yml b/test/integration/targets/ansible-test-cloud-cs/tasks/main.yml
new file mode 100644
index 0000000..3b219c7
--- /dev/null
+++ b/test/integration/targets/ansible-test-cloud-cs/tasks/main.yml
@@ -0,0 +1,8 @@
+- name: Verify endpoints respond
+ uri:
+ url: "{{ item }}"
+ validate_certs: no
+ register: this
+ failed_when: "this.status != 401" # authentication is required, but not provided (requests must be signed)
+ with_items:
+ - "{{ ansible_env.CLOUDSTACK_ENDPOINT }}"
diff --git a/test/integration/targets/ansible-test-cloud-foreman/aliases b/test/integration/targets/ansible-test-cloud-foreman/aliases
new file mode 100644
index 0000000..a4bdcea
--- /dev/null
+++ b/test/integration/targets/ansible-test-cloud-foreman/aliases
@@ -0,0 +1,3 @@
+cloud/foreman
+shippable/generic/group1
+context/controller
diff --git a/test/integration/targets/ansible-test-cloud-foreman/tasks/main.yml b/test/integration/targets/ansible-test-cloud-foreman/tasks/main.yml
new file mode 100644
index 0000000..4170d83
--- /dev/null
+++ b/test/integration/targets/ansible-test-cloud-foreman/tasks/main.yml
@@ -0,0 +1,6 @@
+- name: Verify endpoints respond
+ uri:
+ url: "{{ item }}"
+ validate_certs: no
+ with_items:
+ - http://{{ ansible_env.FOREMAN_HOST }}:{{ ansible_env.FOREMAN_PORT }}/ping
diff --git a/test/integration/targets/ansible-test-cloud-galaxy/aliases b/test/integration/targets/ansible-test-cloud-galaxy/aliases
new file mode 100644
index 0000000..6c57208
--- /dev/null
+++ b/test/integration/targets/ansible-test-cloud-galaxy/aliases
@@ -0,0 +1,4 @@
+shippable/galaxy/group1
+shippable/galaxy/smoketest
+cloud/galaxy
+context/controller
diff --git a/test/integration/targets/ansible-test-cloud-galaxy/tasks/main.yml b/test/integration/targets/ansible-test-cloud-galaxy/tasks/main.yml
new file mode 100644
index 0000000..8ae15ea
--- /dev/null
+++ b/test/integration/targets/ansible-test-cloud-galaxy/tasks/main.yml
@@ -0,0 +1,25 @@
+# The pulp container has a long start up time.
+# The first task to interact with pulp needs to wait until it responds appropriately.
+- name: Wait for Pulp API
+ uri:
+ url: '{{ pulp_api }}/pulp/api/v3/distributions/ansible/ansible/'
+ user: '{{ pulp_user }}'
+ password: '{{ pulp_password }}'
+ force_basic_auth: true
+ register: this
+ until: this is successful
+ delay: 1
+ retries: 60
+
+- name: Verify Galaxy NG server
+ uri:
+ url: "{{ galaxy_ng_server }}"
+ user: '{{ pulp_user }}'
+ password: '{{ pulp_password }}'
+ force_basic_auth: true
+
+- name: Verify Pulp server
+ uri:
+ url: "{{ pulp_server }}"
+ status_code:
+ - 404 # endpoint responds without authentication
diff --git a/test/integration/targets/ansible-test-cloud-httptester-windows/aliases b/test/integration/targets/ansible-test-cloud-httptester-windows/aliases
new file mode 100644
index 0000000..f45a162
--- /dev/null
+++ b/test/integration/targets/ansible-test-cloud-httptester-windows/aliases
@@ -0,0 +1,4 @@
+cloud/httptester
+windows
+shippable/windows/group1
+context/target
diff --git a/test/integration/targets/ansible-test-cloud-httptester-windows/tasks/main.yml b/test/integration/targets/ansible-test-cloud-httptester-windows/tasks/main.yml
new file mode 100644
index 0000000..a78b28c
--- /dev/null
+++ b/test/integration/targets/ansible-test-cloud-httptester-windows/tasks/main.yml
@@ -0,0 +1,15 @@
+- name: Verify HTTPTESTER environment variable
+ assert:
+ that:
+ - "lookup('env', 'HTTPTESTER') == '1'"
+
+- name: Verify endpoints respond
+ ansible.windows.win_uri:
+ url: "{{ item }}"
+ validate_certs: no
+ with_items:
+ - http://ansible.http.tests/
+ - https://ansible.http.tests/
+ - https://sni1.ansible.http.tests/
+ - https://fail.ansible.http.tests/
+ - https://self-signed.ansible.http.tests/
diff --git a/test/integration/targets/ansible-test-cloud-httptester/aliases b/test/integration/targets/ansible-test-cloud-httptester/aliases
new file mode 100644
index 0000000..db811dd
--- /dev/null
+++ b/test/integration/targets/ansible-test-cloud-httptester/aliases
@@ -0,0 +1,3 @@
+needs/httptester # using legacy alias for testing purposes
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/ansible-test-cloud-httptester/tasks/main.yml b/test/integration/targets/ansible-test-cloud-httptester/tasks/main.yml
new file mode 100644
index 0000000..16b632f
--- /dev/null
+++ b/test/integration/targets/ansible-test-cloud-httptester/tasks/main.yml
@@ -0,0 +1,15 @@
+- name: Verify HTTPTESTER environment variable
+ assert:
+ that:
+ - "lookup('env', 'HTTPTESTER') == '1'"
+
+- name: Verify endpoints respond
+ uri:
+ url: "{{ item }}"
+ validate_certs: no
+ with_items:
+ - http://ansible.http.tests/
+ - https://ansible.http.tests/
+ - https://sni1.ansible.http.tests/
+ - https://fail.ansible.http.tests/
+ - https://self-signed.ansible.http.tests/
diff --git a/test/integration/targets/ansible-test-cloud-nios/aliases b/test/integration/targets/ansible-test-cloud-nios/aliases
new file mode 100644
index 0000000..136344a
--- /dev/null
+++ b/test/integration/targets/ansible-test-cloud-nios/aliases
@@ -0,0 +1,3 @@
+cloud/nios
+shippable/generic/group1
+context/controller
diff --git a/test/integration/targets/ansible-test-cloud-nios/tasks/main.yml b/test/integration/targets/ansible-test-cloud-nios/tasks/main.yml
new file mode 100644
index 0000000..b4d447d
--- /dev/null
+++ b/test/integration/targets/ansible-test-cloud-nios/tasks/main.yml
@@ -0,0 +1,10 @@
+- name: Verify endpoints respond
+ uri:
+ url: "{{ item }}"
+ url_username: "{{ nios_provider.username }}"
+ url_password: "{{ nios_provider.password }}"
+ validate_certs: no
+ register: this
+ failed_when: "this.status != 404" # authentication succeeded, but the requested path was not found
+ with_items:
+ - https://{{ nios_provider.host }}/
diff --git a/test/integration/targets/ansible-test-cloud-openshift/aliases b/test/integration/targets/ansible-test-cloud-openshift/aliases
new file mode 100644
index 0000000..6e32db7
--- /dev/null
+++ b/test/integration/targets/ansible-test-cloud-openshift/aliases
@@ -0,0 +1,4 @@
+cloud/openshift
+shippable/generic/group1
+disabled # disabled due to requirements conflict: botocore 1.20.6 has requirement urllib3<1.27,>=1.25.4, but you have urllib3 1.24.3.
+context/controller
diff --git a/test/integration/targets/ansible-test-cloud-openshift/tasks/main.yml b/test/integration/targets/ansible-test-cloud-openshift/tasks/main.yml
new file mode 100644
index 0000000..c3b5190
--- /dev/null
+++ b/test/integration/targets/ansible-test-cloud-openshift/tasks/main.yml
@@ -0,0 +1,6 @@
+- name: Verify endpoints respond
+ uri:
+ url: "{{ item }}"
+ validate_certs: no
+ with_items:
+ - https://openshift-origin:8443/
diff --git a/test/integration/targets/ansible-test-cloud-vcenter/aliases b/test/integration/targets/ansible-test-cloud-vcenter/aliases
new file mode 100644
index 0000000..0cd8ad2
--- /dev/null
+++ b/test/integration/targets/ansible-test-cloud-vcenter/aliases
@@ -0,0 +1,3 @@
+cloud/vcenter
+shippable/generic/group1
+context/controller
diff --git a/test/integration/targets/ansible-test-cloud-vcenter/tasks/main.yml b/test/integration/targets/ansible-test-cloud-vcenter/tasks/main.yml
new file mode 100644
index 0000000..49e5c16
--- /dev/null
+++ b/test/integration/targets/ansible-test-cloud-vcenter/tasks/main.yml
@@ -0,0 +1,6 @@
+- name: Verify endpoints respond
+ uri:
+ url: "{{ item }}"
+ validate_certs: no
+ with_items:
+ - http://{{ vcenter_hostname }}:5000/ # control endpoint for the simulator
diff --git a/test/integration/targets/ansible-test-config-invalid/aliases b/test/integration/targets/ansible-test-config-invalid/aliases
new file mode 100644
index 0000000..7741d44
--- /dev/null
+++ b/test/integration/targets/ansible-test-config-invalid/aliases
@@ -0,0 +1,4 @@
+shippable/posix/group3 # runs in the distro test containers
+shippable/generic/group1 # runs in the default test container
+context/controller
+needs/target/collection
diff --git a/test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/config.yml b/test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/config.yml
new file mode 100644
index 0000000..9977a28
--- /dev/null
+++ b/test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/config.yml
@@ -0,0 +1 @@
+invalid
diff --git a/test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/integration/targets/test/aliases b/test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/integration/targets/test/aliases
new file mode 100644
index 0000000..1af1cf9
--- /dev/null
+++ b/test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/integration/targets/test/aliases
@@ -0,0 +1 @@
+context/controller
diff --git a/test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/integration/targets/test/runme.sh b/test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/integration/targets/test/runme.sh
new file mode 100755
index 0000000..f1f641a
--- /dev/null
+++ b/test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/integration/targets/test/runme.sh
@@ -0,0 +1 @@
+#!/usr/bin/env bash
diff --git a/test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_test.py b/test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_test.py
new file mode 100644
index 0000000..06e7782
--- /dev/null
+++ b/test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_test.py
@@ -0,0 +1,2 @@
+def test_me():
+ pass
diff --git a/test/integration/targets/ansible-test-config-invalid/runme.sh b/test/integration/targets/ansible-test-config-invalid/runme.sh
new file mode 100755
index 0000000..6ff2d40
--- /dev/null
+++ b/test/integration/targets/ansible-test-config-invalid/runme.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+# Make sure that ansible-test continues to work when content config is invalid.
+
+set -eu
+
+source ../collection/setup.sh
+
+set -x
+
+ansible-test sanity --test import --python "${ANSIBLE_TEST_PYTHON_VERSION}" --color --venv -v
+ansible-test units --python "${ANSIBLE_TEST_PYTHON_VERSION}" --color --venv -v
+ansible-test integration --color --venv -v
diff --git a/test/integration/targets/ansible-test-config/aliases b/test/integration/targets/ansible-test-config/aliases
new file mode 100644
index 0000000..7741d44
--- /dev/null
+++ b/test/integration/targets/ansible-test-config/aliases
@@ -0,0 +1,4 @@
+shippable/posix/group3 # runs in the distro test containers
+shippable/generic/group1 # runs in the default test container
+context/controller
+needs/target/collection
diff --git a/test/integration/targets/ansible-test-config/ansible_collections/ns/col/plugins/module_utils/test.py b/test/integration/targets/ansible-test-config/ansible_collections/ns/col/plugins/module_utils/test.py
new file mode 100644
index 0000000..962dba2
--- /dev/null
+++ b/test/integration/targets/ansible-test-config/ansible_collections/ns/col/plugins/module_utils/test.py
@@ -0,0 +1,14 @@
+import sys
+import os
+
+
+def version_to_str(value):
+ return '.'.join(str(v) for v in value)
+
+
+controller_min_python_version = tuple(int(v) for v in os.environ['ANSIBLE_CONTROLLER_MIN_PYTHON_VERSION'].split('.'))
+current_python_version = sys.version_info[:2]
+
+if current_python_version < controller_min_python_version:
+ raise Exception('Current Python version %s is lower than the minimum controller Python version of %s. '
+ 'Did the collection config get ignored?' % (version_to_str(current_python_version), version_to_str(controller_min_python_version)))
diff --git a/test/integration/targets/ansible-test-config/ansible_collections/ns/col/tests/config.yml b/test/integration/targets/ansible-test-config/ansible_collections/ns/col/tests/config.yml
new file mode 100644
index 0000000..7772d7d
--- /dev/null
+++ b/test/integration/targets/ansible-test-config/ansible_collections/ns/col/tests/config.yml
@@ -0,0 +1,2 @@
+modules:
+ python_requires: controller # allow tests to pass when run against a Python version not supported by the controller
diff --git a/test/integration/targets/ansible-test-config/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_test.py b/test/integration/targets/ansible-test-config/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_test.py
new file mode 100644
index 0000000..b320a15
--- /dev/null
+++ b/test/integration/targets/ansible-test-config/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_test.py
@@ -0,0 +1,5 @@
+from ansible_collections.ns.col.plugins.module_utils import test
+
+
+def test_me():
+ assert test
diff --git a/test/integration/targets/ansible-test-config/runme.sh b/test/integration/targets/ansible-test-config/runme.sh
new file mode 100755
index 0000000..9636d04
--- /dev/null
+++ b/test/integration/targets/ansible-test-config/runme.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+# Make sure that ansible-test is able to parse collection config when using a venv.
+
+set -eu
+
+source ../collection/setup.sh
+
+set -x
+
+# On systems with a Python version below the minimum controller Python version, such as the default container, this test
+# will verify that the content config is working properly after delegation. Otherwise it will only verify that no errors
+# occur while trying to access content config (such as missing requirements).
+
+ansible-test sanity --test import --color --venv -v
+ansible-test units --color --venv -v
diff --git a/test/integration/targets/ansible-test-container/aliases b/test/integration/targets/ansible-test-container/aliases
new file mode 100644
index 0000000..65a0509
--- /dev/null
+++ b/test/integration/targets/ansible-test-container/aliases
@@ -0,0 +1,5 @@
+shippable/posix/group6
+context/controller
+needs/root
+destructive
+retry/never # tests on some platforms run too long to make retries useful
diff --git a/test/integration/targets/ansible-test-container/runme.py b/test/integration/targets/ansible-test-container/runme.py
new file mode 100755
index 0000000..6871280
--- /dev/null
+++ b/test/integration/targets/ansible-test-container/runme.py
@@ -0,0 +1,1090 @@
+#!/usr/bin/env python
+"""Test suite used to verify ansible-test is able to run its containers on various container hosts."""
+
+from __future__ import annotations
+
+import abc
+import dataclasses
+import datetime
+import errno
+import functools
+import json
+import os
+import pathlib
+import pwd
+import re
+import secrets
+import shlex
+import shutil
+import signal
+import subprocess
+import sys
+import time
+import typing as t
+
+UNPRIVILEGED_USER_NAME = 'ansible-test'
+CGROUP_SYSTEMD = pathlib.Path('/sys/fs/cgroup/systemd')
+LOG_PATH = pathlib.Path('/tmp/results')
+
+# The value of /proc/*/loginuid when it is not set.
+# It is a reserved UID, which is the maximum 32-bit unsigned integer value.
+# See: https://access.redhat.com/solutions/25404
+LOGINUID_NOT_SET = 4294967295
+
+UID = os.getuid()
+
+try:
+ LOGINUID = int(pathlib.Path('/proc/self/loginuid').read_text())
+ LOGINUID_MISMATCH = LOGINUID != LOGINUID_NOT_SET and LOGINUID != UID
+except FileNotFoundError:
+ LOGINUID = None
+ LOGINUID_MISMATCH = False
+
+
+def main() -> None:
+ """Main program entry point."""
+ display.section('Startup check')
+
+ try:
+ bootstrap_type = pathlib.Path('/etc/ansible-test.bootstrap').read_text().strip()
+ except FileNotFoundError:
+ bootstrap_type = 'undefined'
+
+ display.info(f'Bootstrap type: {bootstrap_type}')
+
+ if bootstrap_type != 'remote':
+ display.warning('Skipping destructive test on system which is not an ansible-test remote provisioned instance.')
+ return
+
+ display.info(f'UID: {UID} / {LOGINUID}')
+
+ if UID != 0:
+ raise Exception('This test must be run as root.')
+
+ if not LOGINUID_MISMATCH:
+ if LOGINUID is None:
+ display.warning('Tests involving loginuid mismatch will be skipped on this host since it does not have audit support.')
+ elif LOGINUID == LOGINUID_NOT_SET:
+ display.warning('Tests involving loginuid mismatch will be skipped on this host since it is not set.')
+ elif LOGINUID == 0:
+ raise Exception('Use sudo, su, etc. as a non-root user to become root before running this test.')
+ else:
+ raise Exception()
+
+ display.section(f'Bootstrapping {os_release}')
+
+ bootstrapper = Bootstrapper.init()
+ bootstrapper.run()
+
+ result_dir = LOG_PATH
+
+ if result_dir.exists():
+ shutil.rmtree(result_dir)
+
+ result_dir.mkdir()
+ result_dir.chmod(0o777)
+
+ scenarios = get_test_scenarios()
+ results = [run_test(scenario) for scenario in scenarios]
+ error_total = 0
+
+ for name in sorted(result_dir.glob('*.log')):
+ lines = name.read_text().strip().splitlines()
+ error_count = len([line for line in lines if line.startswith('FAIL: ')])
+ error_total += error_count
+
+ display.section(f'Log ({error_count=}/{len(lines)}): {name.name}')
+
+ for line in lines:
+ if line.startswith('FAIL: '):
+ display.show(line, display.RED)
+ else:
+ display.show(line)
+
+ error_count = len([result for result in results if result.message])
+ error_total += error_count
+
+ duration = datetime.timedelta(seconds=int(sum(result.duration.total_seconds() for result in results)))
+
+ display.section(f'Test Results ({error_count=}/{len(results)}) [{duration}]')
+
+ for result in results:
+ notes = f' <cleanup: {", ".join(result.cleanup)}>' if result.cleanup else ''
+
+ if result.cgroup_dirs:
+ notes += f' <cgroup_dirs: {len(result.cgroup_dirs)}>'
+
+ notes += f' [{result.duration}]'
+
+ if result.message:
+ display.show(f'FAIL: {result.scenario} {result.message}{notes}', display.RED)
+ elif result.duration.total_seconds() >= 90:
+ display.show(f'SLOW: {result.scenario}{notes}', display.YELLOW)
+ else:
+ display.show(f'PASS: {result.scenario}{notes}')
+
+ if error_total:
+ sys.exit(1)
+
+
+def get_test_scenarios() -> list[TestScenario]:
+ """Generate and return a list of test scenarios."""
+
+ supported_engines = ('docker', 'podman')
+ available_engines = [engine for engine in supported_engines if shutil.which(engine)]
+
+ if not available_engines:
+ raise ApplicationError(f'No supported container engines found: {", ".join(supported_engines)}')
+
+ completion_lines = pathlib.Path(os.environ['PYTHONPATH'], '../test/lib/ansible_test/_data/completion/docker.txt').read_text().splitlines()
+
+ # TODO: consider including testing for the collection default image
+ entries = {name: value for name, value in (parse_completion_entry(line) for line in completion_lines) if name != 'default'}
+
+ unprivileged_user = User.get(UNPRIVILEGED_USER_NAME)
+
+ scenarios: list[TestScenario] = []
+
+ for container_name, settings in entries.items():
+ image = settings['image']
+ cgroup = settings.get('cgroup', 'v1-v2')
+
+ if container_name == 'centos6' and os_release.id == 'alpine':
+ # Alpine kernels do not emulate vsyscall by default, which causes the centos6 container to fail during init.
+ # See: https://unix.stackexchange.com/questions/478387/running-a-centos-docker-image-on-arch-linux-exits-with-code-139
+ # Other distributions enable settings which trap vsyscall by default.
+ # See: https://www.kernelconfig.io/config_legacy_vsyscall_xonly
+ # See: https://www.kernelconfig.io/config_legacy_vsyscall_emulate
+ continue
+
+ for engine in available_engines:
+ # TODO: figure out how to get tests passing using docker without disabling selinux
+ disable_selinux = os_release.id == 'fedora' and engine == 'docker' and cgroup != 'none'
+ expose_cgroup_v1 = cgroup == 'v1-only' and get_docker_info(engine).cgroup_version != 1
+ debug_systemd = cgroup != 'none'
+
+ # The sleep+pkill used to support the cgroup probe causes problems with the centos6 container.
+ # It results in sshd connections being refused or reset for many, but not all, container instances.
+ # The underlying cause of this issue is unknown.
+ probe_cgroups = container_name != 'centos6'
+
+ # The default RHEL 9 crypto policy prevents use of SHA-1.
+ # This results in SSH errors with centos6 containers: ssh_dispatch_run_fatal: Connection to 1.2.3.4 port 22: error in libcrypto
+ # See: https://access.redhat.com/solutions/6816771
+ enable_sha1 = os_release.id == 'rhel' and os_release.version_id.startswith('9.') and container_name == 'centos6'
+
+ if cgroup != 'none' and get_docker_info(engine).cgroup_version == 1 and not have_cgroup_systemd():
+ expose_cgroup_v1 = True # the host uses cgroup v1 but there is no systemd cgroup and the container requires cgroup support
+
+ user_scenarios = [
+ # TODO: test rootless docker
+ UserScenario(ssh=unprivileged_user),
+ ]
+
+ if engine == 'podman':
+ user_scenarios.append(UserScenario(ssh=ROOT_USER))
+
+ # TODO: test podman remote on Alpine and Ubuntu hosts
+ # TODO: combine remote with ssh using different unprivileged users
+ if os_release.id not in ('alpine', 'ubuntu'):
+ user_scenarios.append(UserScenario(remote=unprivileged_user))
+
+ if LOGINUID_MISMATCH:
+ user_scenarios.append(UserScenario())
+
+ for user_scenario in user_scenarios:
+ scenarios.append(
+ TestScenario(
+ user_scenario=user_scenario,
+ engine=engine,
+ container_name=container_name,
+ image=image,
+ disable_selinux=disable_selinux,
+ expose_cgroup_v1=expose_cgroup_v1,
+ enable_sha1=enable_sha1,
+ debug_systemd=debug_systemd,
+ probe_cgroups=probe_cgroups,
+ )
+ )
+
+ return scenarios
+
+
+def run_test(scenario: TestScenario) -> TestResult:
+ """Run a test scenario and return the test results."""
+ display.section(f'Testing {scenario} Started')
+
+ start = time.monotonic()
+
+ integration = ['ansible-test', 'integration', 'split']
+ integration_options = ['--target', f'docker:{scenario.container_name}', '--color', '--truncate', '0', '-v']
+ target_only_options = []
+
+ if scenario.debug_systemd:
+ integration_options.append('--dev-systemd-debug')
+
+ if scenario.probe_cgroups:
+ target_only_options = ['--dev-probe-cgroups', str(LOG_PATH)]
+
+ commands = [
+ # The cgroup probe is only performed for the first test of the target.
+ # There's no need to repeat the probe again for the same target.
+ # The controller will be tested separately as a target.
+ # This ensures that both the probe and no-probe code paths are functional.
+ [*integration, *integration_options, *target_only_options],
+ # For the split test we'll use alpine3 as the controller. There are two reasons for this:
+ # 1) It doesn't require the cgroup v1 hack, so we can test a target that doesn't need that.
+ # 2) It doesn't require disabling selinux, so we can test a target that doesn't need that.
+ [*integration, '--controller', 'docker:alpine3', *integration_options],
+ ]
+
+ common_env: dict[str, str] = {}
+ test_env: dict[str, str] = {}
+
+ if scenario.engine == 'podman':
+ if scenario.user_scenario.remote:
+ common_env.update(
+ # Podman 4.3.0 has a regression which requires a port for remote connections to work.
+ # See: https://github.com/containers/podman/issues/16509
+ CONTAINER_HOST=f'ssh://{scenario.user_scenario.remote.name}@localhost:22'
+ f'/run/user/{scenario.user_scenario.remote.pwnam.pw_uid}/podman/podman.sock',
+ CONTAINER_SSHKEY=str(pathlib.Path('~/.ssh/id_rsa').expanduser()), # TODO: add support for ssh + remote when the ssh user is not root
+ )
+
+ test_env.update(ANSIBLE_TEST_PREFER_PODMAN='1')
+
+ test_env.update(common_env)
+
+ if scenario.user_scenario.ssh:
+ client_become_cmd = ['ssh', f'{scenario.user_scenario.ssh.name}@localhost']
+ test_commands = [client_become_cmd + [f'cd ~/ansible; {format_env(test_env)}{sys.executable} bin/{shlex.join(command)}'] for command in commands]
+ else:
+ client_become_cmd = ['sh', '-c']
+ test_commands = [client_become_cmd + [f'{format_env(test_env)}{shlex.join(command)}'] for command in commands]
+
+ prime_storage_command = []
+
+ if scenario.engine == 'podman' and scenario.user_scenario.actual.name == UNPRIVILEGED_USER_NAME:
+ # When testing podman we need to make sure that the overlay filesystem is used instead of vfs.
+ # Using the vfs filesystem will result in running out of disk space during the tests.
+ # To change the filesystem used, the existing storage directory must be removed before "priming" the storage database.
+ #
+ # Without this change the following message may be displayed:
+ #
+ # User-selected graph driver "overlay" overwritten by graph driver "vfs" from database - delete libpod local files to resolve
+ #
+ # However, with this change it may be replaced with the following message:
+ #
+ # User-selected graph driver "vfs" overwritten by graph driver "overlay" from database - delete libpod local files to resolve
+
+ actual_become_cmd = ['ssh', f'{scenario.user_scenario.actual.name}@localhost']
+ prime_storage_command = actual_become_cmd + prepare_prime_podman_storage()
+
+ message = ''
+
+ if scenario.expose_cgroup_v1:
+ prepare_cgroup_systemd(scenario.user_scenario.actual.name, scenario.engine)
+
+ try:
+ if prime_storage_command:
+ retry_command(lambda: run_command(*prime_storage_command), retry_any_error=True)
+
+ if scenario.disable_selinux:
+ run_command('setenforce', 'permissive')
+
+ if scenario.enable_sha1:
+ run_command('update-crypto-policies', '--set', 'DEFAULT:SHA1')
+
+ for test_command in test_commands:
+ retry_command(lambda: run_command(*test_command))
+ except SubprocessError as ex:
+ message = str(ex)
+ display.error(f'{scenario} {message}')
+ finally:
+ if scenario.enable_sha1:
+ run_command('update-crypto-policies', '--set', 'DEFAULT')
+
+ if scenario.disable_selinux:
+ run_command('setenforce', 'enforcing')
+
+ if scenario.expose_cgroup_v1:
+ dirs = remove_cgroup_systemd()
+ else:
+ dirs = list_group_systemd()
+
+ cleanup_command = [scenario.engine, 'rmi', '-f', scenario.image]
+
+ try:
+ retry_command(lambda: run_command(*client_become_cmd + [f'{format_env(common_env)}{shlex.join(cleanup_command)}']), retry_any_error=True)
+ except SubprocessError as ex:
+ display.error(str(ex))
+
+ cleanup = cleanup_podman() if scenario.engine == 'podman' else tuple()
+
+ finish = time.monotonic()
+ duration = datetime.timedelta(seconds=int(finish - start))
+
+ display.section(f'Testing {scenario} Completed in {duration}')
+
+ return TestResult(
+ scenario=scenario,
+ message=message,
+ cleanup=cleanup,
+ duration=duration,
+ cgroup_dirs=tuple(str(path) for path in dirs),
+ )
+
+
+def prepare_prime_podman_storage() -> list[str]:
+ """Partially prime podman storage and return a command to complete the remainder."""
+ prime_storage_command = ['rm -rf ~/.local/share/containers; STORAGE_DRIVER=overlay podman pull quay.io/bedrock/alpine:3.16.2']
+
+ test_containers = pathlib.Path(f'~{UNPRIVILEGED_USER_NAME}/.local/share/containers').expanduser()
+
+ if test_containers.is_dir():
+ # First remove the directory as root, since the user may not have permissions on all the files.
+ # The directory will be removed again after login, before initializing the database.
+ rmtree(test_containers)
+
+ return prime_storage_command
+
+
+def cleanup_podman() -> tuple[str, ...]:
+ """Cleanup podman processes and files on disk."""
+ cleanup = []
+
+ for remaining in range(3, -1, -1):
+ processes = [(int(item[0]), item[1]) for item in
+ [item.split(maxsplit=1) for item in run_command('ps', '-A', '-o', 'pid,comm', capture=True).stdout.splitlines()]
+ if pathlib.Path(item[1].split()[0]).name in ('catatonit', 'podman', 'conmon')]
+
+ if not processes:
+ break
+
+ for pid, name in processes:
+ display.info(f'Killing "{name}" ({pid}) ...')
+
+ try:
+ os.kill(pid, signal.SIGTERM if remaining > 1 else signal.SIGKILL)
+ except ProcessLookupError:
+ pass
+
+ cleanup.append(name)
+
+ time.sleep(1)
+ else:
+ raise Exception('failed to kill all matching processes')
+
+ uid = pwd.getpwnam(UNPRIVILEGED_USER_NAME).pw_uid
+
+ container_tmp = pathlib.Path(f'/tmp/containers-user-{uid}')
+ podman_tmp = pathlib.Path(f'/tmp/podman-run-{uid}')
+
+ user_config = pathlib.Path(f'~{UNPRIVILEGED_USER_NAME}/.config').expanduser()
+ user_local = pathlib.Path(f'~{UNPRIVILEGED_USER_NAME}/.local').expanduser()
+
+ if container_tmp.is_dir():
+ rmtree(container_tmp)
+
+ if podman_tmp.is_dir():
+ rmtree(podman_tmp)
+
+ if user_config.is_dir():
+ rmtree(user_config)
+
+ if user_local.is_dir():
+ rmtree(user_local)
+
+ return tuple(sorted(set(cleanup)))
+
+
+def have_cgroup_systemd() -> bool:
+ """Return True if the container host has a systemd cgroup."""
+ return pathlib.Path(CGROUP_SYSTEMD).is_dir()
+
+
+def prepare_cgroup_systemd(username: str, engine: str) -> None:
+ """Prepare the systemd cgroup."""
+ CGROUP_SYSTEMD.mkdir()
+
+ run_command('mount', 'cgroup', '-t', 'cgroup', str(CGROUP_SYSTEMD), '-o', 'none,name=systemd,xattr', capture=True)
+
+ if engine == 'podman':
+ run_command('chown', '-R', f'{username}:{username}', str(CGROUP_SYSTEMD))
+
+ run_command('find', str(CGROUP_SYSTEMD), '-type', 'd', '-exec', 'ls', '-l', '{}', ';')
+
+
+def list_group_systemd() -> list[pathlib.Path]:
+ """List the systemd cgroup."""
+ dirs = set()
+
+ for dirpath, dirnames, filenames in os.walk(CGROUP_SYSTEMD, topdown=False):
+ for dirname in dirnames:
+ target_path = pathlib.Path(dirpath, dirname)
+ display.info(f'dir: {target_path}')
+ dirs.add(target_path)
+
+ return sorted(dirs)
+
+
+def remove_cgroup_systemd() -> list[pathlib.Path]:
+ """Remove the systemd cgroup."""
+ dirs = set()
+
+ for sleep_seconds in range(1, 10):
+ try:
+ for dirpath, dirnames, filenames in os.walk(CGROUP_SYSTEMD, topdown=False):
+ for dirname in dirnames:
+ target_path = pathlib.Path(dirpath, dirname)
+ display.info(f'rmdir: {target_path}')
+ dirs.add(target_path)
+ target_path.rmdir()
+ except OSError as ex:
+ if ex.errno != errno.EBUSY:
+ raise
+
+ error = str(ex)
+ else:
+ break
+
+ display.warning(f'{error} -- sleeping for {sleep_seconds} second(s) before trying again ...') # pylint: disable=used-before-assignment
+
+ time.sleep(sleep_seconds)
+
+ time.sleep(1) # allow time for cgroups to be fully removed before unmounting
+
+ run_command('umount', str(CGROUP_SYSTEMD))
+
+ CGROUP_SYSTEMD.rmdir()
+
+ time.sleep(1) # allow time for cgroup hierarchy to be removed after unmounting
+
+ cgroup = pathlib.Path('/proc/self/cgroup').read_text()
+
+ if 'systemd' in cgroup:
+ raise Exception('systemd hierarchy detected')
+
+ return sorted(dirs)
+
+
+def rmtree(path: pathlib.Path) -> None:
+ """Wrapper around shutil.rmtree with additional error handling."""
+ for retries in range(10, -1, -1):
+ try:
+ display.info(f'rmtree: {path} ({retries} attempts remaining) ... ')
+ shutil.rmtree(path)
+ except Exception:
+ if not path.exists():
+ display.info(f'rmtree: {path} (not found)')
+ return
+
+ if not path.is_dir():
+ display.info(f'rmtree: {path} (not a directory)')
+ return
+
+ if retries:
+ continue
+
+ raise
+ else:
+ display.info(f'rmtree: {path} (done)')
+ return
+
+
+def format_env(env: dict[str, str]) -> str:
+ """Format an env dict for injection into a shell command and return the resulting string."""
+ if env:
+ return ' '.join(f'{shlex.quote(key)}={shlex.quote(value)}' for key, value in env.items()) + ' '
+
+ return ''
+
+
+class DockerInfo:
+ """The results of `docker info` for the container runtime."""
+
+ def __init__(self, data: dict[str, t.Any]) -> None:
+ self.data = data
+
+ @property
+ def cgroup_version(self) -> int:
+ """The cgroup version of the container host."""
+ data = self.data
+ host = data.get('host')
+
+ if host:
+ version = int(host['cgroupVersion'].lstrip('v')) # podman
+ else:
+ version = int(data['CgroupVersion']) # docker
+
+ return version
+
+
+@functools.lru_cache
+def get_docker_info(engine: str) -> DockerInfo:
+ """Return info for the current container runtime. The results are cached."""
+ return DockerInfo(json.loads(run_command(engine, 'info', '--format', '{{ json . }}', capture=True).stdout))
+
+
+@dataclasses.dataclass(frozen=True)
+class User:
+ name: str
+ pwnam: pwd.struct_passwd
+
+ @classmethod
+ def get(cls, name: str) -> User:
+ return User(
+ name=name,
+ pwnam=pwd.getpwnam(name),
+ )
+
+
+@dataclasses.dataclass(frozen=True)
+class UserScenario:
+ ssh: User = None
+ remote: User = None
+
+ @property
+ def actual(self) -> User:
+ return self.remote or self.ssh or ROOT_USER
+
+
+@dataclasses.dataclass(frozen=True)
+class TestScenario:
+ user_scenario: UserScenario
+ engine: str
+ container_name: str
+ image: str
+ disable_selinux: bool
+ expose_cgroup_v1: bool
+ enable_sha1: bool
+ debug_systemd: bool
+ probe_cgroups: bool
+
+ @property
+ def tags(self) -> tuple[str, ...]:
+ tags = []
+
+ if self.user_scenario.ssh:
+ tags.append(f'ssh: {self.user_scenario.ssh.name}')
+
+ if self.user_scenario.remote:
+ tags.append(f'remote: {self.user_scenario.remote.name}')
+
+ if self.disable_selinux:
+ tags.append('selinux: permissive')
+
+ if self.expose_cgroup_v1:
+ tags.append('cgroup: v1')
+
+ if self.enable_sha1:
+ tags.append('sha1: enabled')
+
+ return tuple(tags)
+
+ @property
+ def tag_label(self) -> str:
+ return ' '.join(f'[{tag}]' for tag in self.tags)
+
+ def __str__(self):
+ return f'[{self.container_name}] ({self.engine}) {self.tag_label}'.strip()
+
+
+@dataclasses.dataclass(frozen=True)
+class TestResult:
+ scenario: TestScenario
+ message: str
+ cleanup: tuple[str, ...]
+ duration: datetime.timedelta
+ cgroup_dirs: tuple[str, ...]
+
+
+def parse_completion_entry(value: str) -> tuple[str, dict[str, str]]:
+ """Parse the given completion entry, returning the entry name and a dictionary of key/value settings."""
+ values = value.split()
+
+ name = values[0]
+ data = {kvp[0]: kvp[1] if len(kvp) > 1 else '' for kvp in [item.split('=', 1) for item in values[1:]]}
+
+ return name, data
+
+
+@dataclasses.dataclass(frozen=True)
+class SubprocessResult:
+ """Result from execution of a subprocess."""
+
+ command: list[str]
+ stdout: str
+ stderr: str
+ status: int
+
+
+class ApplicationError(Exception):
+ """An application error."""
+
+ def __init__(self, message: str) -> None:
+ self.message = message
+
+ super().__init__(message)
+
+
+class SubprocessError(ApplicationError):
+ """An error from executing a subprocess."""
+
+ def __init__(self, result: SubprocessResult) -> None:
+ self.result = result
+
+ message = f'Command `{shlex.join(result.command)}` exited with status: {result.status}'
+
+ stdout = (result.stdout or '').strip()
+ stderr = (result.stderr or '').strip()
+
+ if stdout:
+ message += f'\n>>> Standard Output\n{stdout}'
+
+ if stderr:
+ message += f'\n>>> Standard Error\n{stderr}'
+
+ super().__init__(message)
+
+
+class ProgramNotFoundError(ApplicationError):
+ """A required program was not found."""
+
+ def __init__(self, name: str) -> None:
+ self.name = name
+
+ super().__init__(f'Missing program: {name}')
+
+
+class Display:
+ """Display interface for sending output to the console."""
+
+ CLEAR = '\033[0m'
+ RED = '\033[31m'
+ GREEN = '\033[32m'
+ YELLOW = '\033[33m'
+ BLUE = '\033[34m'
+ PURPLE = '\033[35m'
+ CYAN = '\033[36m'
+
+ def __init__(self) -> None:
+ self.sensitive: set[str] = set()
+
+ def section(self, message: str) -> None:
+ """Print a section message to the console."""
+ self.show(f'==> {message}', color=self.BLUE)
+
+ def subsection(self, message: str) -> None:
+ """Print a subsection message to the console."""
+ self.show(f'--> {message}', color=self.CYAN)
+
+ def fatal(self, message: str) -> None:
+ """Print a fatal message to the console."""
+ self.show(f'FATAL: {message}', color=self.RED)
+
+ def error(self, message: str) -> None:
+ """Print an error message to the console."""
+ self.show(f'ERROR: {message}', color=self.RED)
+
+ def warning(self, message: str) -> None:
+ """Print a warning message to the console."""
+ self.show(f'WARNING: {message}', color=self.PURPLE)
+
+ def info(self, message: str) -> None:
+ """Print an info message to the console."""
+ self.show(f'INFO: {message}', color=self.YELLOW)
+
+ def show(self, message: str, color: str | None = None) -> None:
+ """Print a message to the console."""
+ for item in self.sensitive:
+ message = message.replace(item, '*' * len(item))
+
+ print(f'{color or self.CLEAR}{message}{self.CLEAR}', flush=True)
+
+
+def run_module(
+ module: str,
+ args: dict[str, t.Any],
+) -> SubprocessResult:
+ """Run the specified Ansible module and return the result."""
+ return run_command('ansible', '-m', module, '-v', '-a', json.dumps(args), 'localhost')
+
+
+def retry_command(func: t.Callable[[], SubprocessResult], attempts: int = 3, retry_any_error: bool = False) -> SubprocessResult:
+ """Run the given command function up to the specified number of attempts when the failure is due to an SSH error."""
+ for attempts_remaining in range(attempts - 1, -1, -1):
+ try:
+ return func()
+ except SubprocessError as ex:
+ if ex.result.command[0] == 'ssh' and ex.result.status == 255 and attempts_remaining:
+ # SSH connections on our Ubuntu 22.04 host sometimes fail for unknown reasons.
+ # This retry should allow the test suite to continue, maintaining CI stability.
+ # TODO: Figure out why local SSH connections sometimes fail during the test run.
+ display.warning('Command failed due to an SSH error. Waiting a few seconds before retrying.')
+ time.sleep(3)
+ continue
+
+ if retry_any_error:
+ display.warning('Command failed. Waiting a few seconds before retrying.')
+ time.sleep(3)
+ continue
+
+ raise
+
+
+def run_command(
+ *command: str,
+ data: str | None = None,
+ stdin: int | t.IO[bytes] | None = None,
+ env: dict[str, str] | None = None,
+ capture: bool = False,
+) -> SubprocessResult:
+ """Run the specified command and return the result."""
+ stdin = subprocess.PIPE if data else stdin or subprocess.DEVNULL
+ stdout = subprocess.PIPE if capture else None
+ stderr = subprocess.PIPE if capture else None
+
+ display.subsection(f'Run command: {shlex.join(command)}')
+
+ try:
+ with subprocess.Popen(args=command, stdin=stdin, stdout=stdout, stderr=stderr, env=env, text=True) as process:
+ process_stdout, process_stderr = process.communicate(data)
+ process_status = process.returncode
+ except FileNotFoundError:
+ raise ProgramNotFoundError(command[0]) from None
+
+ result = SubprocessResult(
+ command=list(command),
+ stdout=process_stdout,
+ stderr=process_stderr,
+ status=process_status,
+ )
+
+ if process.returncode != 0:
+ raise SubprocessError(result)
+
+ return result
+
+
+class Bootstrapper(metaclass=abc.ABCMeta):
+ """Bootstrapper for remote instances."""
+
+ @classmethod
+ def install_podman(cls) -> bool:
+ """Return True if podman will be installed."""
+ return False
+
+ @classmethod
+ def install_docker(cls) -> bool:
+ """Return True if docker will be installed."""
+ return False
+
+ @classmethod
+ def usable(cls) -> bool:
+ """Return True if the bootstrapper can be used, otherwise False."""
+ return False
+
+ @classmethod
+ def init(cls) -> t.Type[Bootstrapper]:
+ """Return a bootstrapper type appropriate for the current system."""
+ for bootstrapper in cls.__subclasses__():
+ if bootstrapper.usable():
+ return bootstrapper
+
+ display.warning('No supported bootstrapper found.')
+ return Bootstrapper
+
+ @classmethod
+ def run(cls) -> None:
+ """Run the bootstrapper."""
+ cls.configure_root_user()
+ cls.configure_unprivileged_user()
+ cls.configure_source_trees()
+ cls.configure_ssh_keys()
+ cls.configure_podman_remote()
+
+ @classmethod
+ def configure_root_user(cls) -> None:
+ """Configure the root user to run tests."""
+ root_password_status = run_command('passwd', '--status', 'root', capture=True)
+ root_password_set = root_password_status.stdout.split()[1]
+
+ if root_password_set not in ('P', 'PS'):
+ root_password = run_command('openssl', 'passwd', '-5', '-stdin', data=secrets.token_hex(8), capture=True).stdout.strip()
+
+ run_module(
+ 'user',
+ dict(
+ user='root',
+ password=root_password,
+ ),
+ )
+
+ @classmethod
+ def configure_unprivileged_user(cls) -> None:
+ """Configure the unprivileged user to run tests."""
+ unprivileged_password = run_command('openssl', 'passwd', '-5', '-stdin', data=secrets.token_hex(8), capture=True).stdout.strip()
+
+ run_module(
+ 'user',
+ dict(
+ user=UNPRIVILEGED_USER_NAME,
+ password=unprivileged_password,
+ groups=['docker'] if cls.install_docker() else [],
+ append=True,
+ ),
+ )
+
+ if os_release.id == 'alpine':
+ # Most distros handle this automatically, but not Alpine.
+ # See: https://www.redhat.com/sysadmin/rootless-podman
+ start = 165535
+ end = start + 65535
+ id_range = f'{start}-{end}'
+
+ run_command(
+ 'usermod',
+ '--add-subuids',
+ id_range,
+ '--add-subgids',
+ id_range,
+ UNPRIVILEGED_USER_NAME,
+ )
+
+ @classmethod
+ def configure_source_trees(cls):
+ """Configure the source trees needed to run tests for both root and the unprivileged user."""
+ current_ansible = pathlib.Path(os.environ['PYTHONPATH']).parent
+
+ root_ansible = pathlib.Path('~').expanduser() / 'ansible'
+ test_ansible = pathlib.Path(f'~{UNPRIVILEGED_USER_NAME}').expanduser() / 'ansible'
+
+ if current_ansible != root_ansible:
+ display.info(f'copying {current_ansible} -> {root_ansible} ...')
+ rmtree(root_ansible)
+ shutil.copytree(current_ansible, root_ansible)
+ run_command('chown', '-R', 'root:root', str(root_ansible))
+
+ display.info(f'copying {current_ansible} -> {test_ansible} ...')
+ rmtree(test_ansible)
+ shutil.copytree(current_ansible, test_ansible)
+ run_command('chown', '-R', f'{UNPRIVILEGED_USER_NAME}:{UNPRIVILEGED_USER_NAME}', str(test_ansible))
+
+ paths = [pathlib.Path(test_ansible)]
+
+ for root, dir_names, file_names in os.walk(test_ansible):
+ paths.extend(pathlib.Path(root, dir_name) for dir_name in dir_names)
+ paths.extend(pathlib.Path(root, file_name) for file_name in file_names)
+
+ user = pwd.getpwnam(UNPRIVILEGED_USER_NAME)
+ uid = user.pw_uid
+ gid = user.pw_gid
+
+ for path in paths:
+ os.chown(path, uid, gid)
+
+ @classmethod
+ def configure_ssh_keys(cls) -> None:
+ """Configure SSH keys needed to run tests."""
+ user = pwd.getpwnam(UNPRIVILEGED_USER_NAME)
+ uid = user.pw_uid
+ gid = user.pw_gid
+
+ current_rsa_pub = pathlib.Path('~/.ssh/id_rsa.pub').expanduser()
+
+ test_authorized_keys = pathlib.Path(f'~{UNPRIVILEGED_USER_NAME}/.ssh/authorized_keys').expanduser()
+
+ test_authorized_keys.parent.mkdir(mode=0o755, parents=True, exist_ok=True)
+ os.chown(test_authorized_keys.parent, uid, gid)
+
+ shutil.copyfile(current_rsa_pub, test_authorized_keys)
+ os.chown(test_authorized_keys, uid, gid)
+ test_authorized_keys.chmod(mode=0o644)
+
+ @classmethod
+ def configure_podman_remote(cls) -> None:
+ """Configure podman remote support."""
+ # TODO: figure out how to support remote podman without systemd (Alpine)
+ # TODO: figure out how to support remote podman on Ubuntu
+ if os_release.id in ('alpine', 'ubuntu'):
+ return
+
+ # Support podman remote on any host with systemd available.
+ retry_command(lambda: run_command('ssh', f'{UNPRIVILEGED_USER_NAME}@localhost', 'systemctl', '--user', 'enable', '--now', 'podman.socket'))
+ run_command('loginctl', 'enable-linger', UNPRIVILEGED_USER_NAME)
+
+
+class DnfBootstrapper(Bootstrapper):
+ """Bootstrapper for dnf based systems."""
+
+ @classmethod
+ def install_podman(cls) -> bool:
+ """Return True if podman will be installed."""
+ return True
+
+ @classmethod
+ def install_docker(cls) -> bool:
+ """Return True if docker will be installed."""
+ return os_release.id != 'rhel'
+
+ @classmethod
+ def usable(cls) -> bool:
+ """Return True if the bootstrapper can be used, otherwise False."""
+ return bool(shutil.which('dnf'))
+
+ @classmethod
+ def run(cls) -> None:
+ """Run the bootstrapper."""
+ # NOTE: Install crun to make it available to podman, otherwise installing moby-engine can cause podman to use runc instead.
+ packages = ['podman', 'crun']
+
+ if cls.install_docker():
+ packages.append('moby-engine')
+
+ if os_release.id == 'fedora' and os_release.version_id == '36':
+ # In Fedora 36 the current version of netavark, 1.2.0, causes TCP connect to hang between rootfull containers.
+ # The previously tested version, 1.1.0, did not have this issue.
+ # Unfortunately, with the release of 1.2.0 the 1.1.0 package was removed from the repositories.
+ # Thankfully the 1.0.2 version is available and also works, so we'll use that here until a fixed version is available.
+ # See: https://github.com/containers/netavark/issues/491
+ packages.append('netavark-1.0.2')
+
+ if os_release.id == 'rhel':
+ # As of the release of RHEL 9.1, installing podman on RHEL 9.0 results in a non-fatal error at install time:
+ #
+ # libsemanage.semanage_pipe_data: Child process /usr/libexec/selinux/hll/pp failed with code: 255. (No such file or directory).
+ # container: libsepol.policydb_read: policydb module version 21 does not match my version range 4-20
+ # container: libsepol.sepol_module_package_read: invalid module in module package (at section 0)
+ # container: Failed to read policy package
+ # libsemanage.semanage_direct_commit: Failed to compile hll files into cil files.
+ # (No such file or directory).
+ # /usr/sbin/semodule: Failed!
+ #
+ # Unfortunately this is then fatal when running podman, resulting in no error message and a 127 return code.
+ # The solution is to update the policycoreutils package *before* installing podman.
+ #
+ # NOTE: This work-around can probably be removed once we're testing on RHEL 9.1, as the updated packages should already be installed.
+ # Unfortunately at this time there is no RHEL 9.1 AMI available (other than the Beta release).
+
+ run_command('dnf', 'update', '-y', 'policycoreutils')
+
+ run_command('dnf', 'install', '-y', *packages)
+
+ if cls.install_docker():
+ run_command('systemctl', 'start', 'docker')
+
+ if os_release.id == 'rhel' and os_release.version_id.startswith('8.'):
+ # RHEL 8 defaults to using runc instead of crun.
+ # Unfortunately runc seems to have issues with podman remote.
+ # Specifically, it tends to cause conmon to burn CPU until it reaches the specified exit delay.
+ # So we'll just change the system default to crun instead.
+ # Unfortunately we can't do this with the `--runtime` option since that doesn't work with podman remote.
+
+ conf = pathlib.Path('/usr/share/containers/containers.conf').read_text()
+
+ conf = re.sub('^runtime .*', 'runtime = "crun"', conf, flags=re.MULTILINE)
+
+ pathlib.Path('/etc/containers/containers.conf').write_text(conf)
+
+ super().run()
+
+
+class AptBootstrapper(Bootstrapper):
+ """Bootstrapper for apt based systems."""
+
+ @classmethod
+ def install_podman(cls) -> bool:
+ """Return True if podman will be installed."""
+ return not (os_release.id == 'ubuntu' and os_release.version_id == '20.04')
+
+ @classmethod
+ def install_docker(cls) -> bool:
+ """Return True if docker will be installed."""
+ return True
+
+ @classmethod
+ def usable(cls) -> bool:
+ """Return True if the bootstrapper can be used, otherwise False."""
+ return bool(shutil.which('apt-get'))
+
+ @classmethod
+ def run(cls) -> None:
+ """Run the bootstrapper."""
+ apt_env = os.environ.copy()
+ apt_env.update(
+ DEBIAN_FRONTEND='noninteractive',
+ )
+
+ packages = ['docker.io']
+
+ if cls.install_podman():
+ # NOTE: Install crun to make it available to podman, otherwise installing docker.io can cause podman to use runc instead.
+ # Using podman rootless requires the `newuidmap` and `slirp4netns` commands.
+ packages.extend(('podman', 'crun', 'uidmap', 'slirp4netns'))
+
+ run_command('apt-get', 'install', *packages, '-y', '--no-install-recommends', env=apt_env)
+
+ super().run()
+
+
+class ApkBootstrapper(Bootstrapper):
+ """Bootstrapper for apk based systems."""
+
+ @classmethod
+ def install_podman(cls) -> bool:
+ """Return True if podman will be installed."""
+ return True
+
+ @classmethod
+ def install_docker(cls) -> bool:
+ """Return True if docker will be installed."""
+ return True
+
+ @classmethod
+ def usable(cls) -> bool:
+ """Return True if the bootstrapper can be used, otherwise False."""
+ return bool(shutil.which('apk'))
+
+ @classmethod
+ def run(cls) -> None:
+ """Run the bootstrapper."""
+ # The `openssl` package is used to generate hashed passwords.
+ packages = ['docker', 'podman', 'openssl']
+
+ run_command('apk', 'add', *packages)
+ run_command('service', 'docker', 'start')
+ run_command('modprobe', 'tun')
+
+ super().run()
+
+
+@dataclasses.dataclass(frozen=True)
+class OsRelease:
+ """Operating system identification."""
+
+ id: str
+ version_id: str
+
+ @staticmethod
+ def init() -> OsRelease:
+ """Detect the current OS release and return the result."""
+ lines = run_command('sh', '-c', '. /etc/os-release && echo $ID && echo $VERSION_ID', capture=True).stdout.splitlines()
+
+ result = OsRelease(
+ id=lines[0],
+ version_id=lines[1],
+ )
+
+ display.show(f'Detected OS "{result.id}" version "{result.version_id}".')
+
+ return result
+
+
+display = Display()
+os_release = OsRelease.init()
+
+ROOT_USER = User.get('root')
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-test-container/runme.sh b/test/integration/targets/ansible-test-container/runme.sh
new file mode 100755
index 0000000..56fd669
--- /dev/null
+++ b/test/integration/targets/ansible-test-container/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eu
+
+./runme.py
diff --git a/test/integration/targets/ansible-test-coverage/aliases b/test/integration/targets/ansible-test-coverage/aliases
new file mode 100644
index 0000000..7741d44
--- /dev/null
+++ b/test/integration/targets/ansible-test-coverage/aliases
@@ -0,0 +1,4 @@
+shippable/posix/group3 # runs in the distro test containers
+shippable/generic/group1 # runs in the default test container
+context/controller
+needs/target/collection
diff --git a/test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/plugins/module_utils/test_util.py b/test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/plugins/module_utils/test_util.py
new file mode 100644
index 0000000..481c4b8
--- /dev/null
+++ b/test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/plugins/module_utils/test_util.py
@@ -0,0 +1,6 @@
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+def test_coverage():
+ pass
diff --git a/test/integration/targets/ansible-test-coverage/runme.sh b/test/integration/targets/ansible-test-coverage/runme.sh
new file mode 100755
index 0000000..de5a4eb
--- /dev/null
+++ b/test/integration/targets/ansible-test-coverage/runme.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+
+source ../collection/setup.sh
+
+set -x
+
+# common args for all tests
+common=(--venv --color --truncate 0 "${@}")
+
+# run a lightweight test that generates code coverge output
+ansible-test sanity --test import "${common[@]}" --coverage
+
+# report on code coverage in all supported formats
+ansible-test coverage report "${common[@]}"
+ansible-test coverage html "${common[@]}"
+ansible-test coverage xml "${common[@]}"
diff --git a/test/integration/targets/ansible-test-docker/aliases b/test/integration/targets/ansible-test-docker/aliases
new file mode 100644
index 0000000..c389df5
--- /dev/null
+++ b/test/integration/targets/ansible-test-docker/aliases
@@ -0,0 +1,3 @@
+shippable/generic/group1 # Runs in the default test container so access to tools like pwsh
+context/controller
+needs/target/collection
diff --git a/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/galaxy.yml b/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/galaxy.yml
new file mode 100644
index 0000000..08a32e8
--- /dev/null
+++ b/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/galaxy.yml
@@ -0,0 +1,6 @@
+namespace: ns
+name: col
+version: 1.0.0
+readme: README.rst
+authors:
+ - Ansible
diff --git a/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/doc_fragments/ps_util.py b/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/doc_fragments/ps_util.py
new file mode 100644
index 0000000..e69844b
--- /dev/null
+++ b/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/doc_fragments/ps_util.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+class ModuleDocFragment:
+
+ DOCUMENTATION = r'''
+options:
+ option1:
+ description:
+ - Test description
+ required: yes
+ aliases:
+ - alias1
+ type: str
+'''
diff --git a/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/module_utils/PSUtil.psm1 b/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/module_utils/PSUtil.psm1
new file mode 100644
index 0000000..f23b99e
--- /dev/null
+++ b/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/module_utils/PSUtil.psm1
@@ -0,0 +1,16 @@
+# Copyright (c) 2020 Ansible Project
+# # Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
+
+Function Get-PSUtilSpec {
+ <#
+ .SYNOPSIS
+ Shared util spec test
+ #>
+ @{
+ options = @{
+ option1 = @{ type = 'str'; required = $true; aliases = 'alias1' }
+ }
+ }
+}
+
+Export-ModuleMember -Function Get-PSUtilSpec
diff --git a/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/module_utils/my_util.py b/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/module_utils/my_util.py
new file mode 100644
index 0000000..b9c531c
--- /dev/null
+++ b/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/module_utils/my_util.py
@@ -0,0 +1,6 @@
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+def hello(name):
+ return 'Hello %s' % name
diff --git a/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/modules/hello.py b/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/modules/hello.py
new file mode 100644
index 0000000..c8a0cf7
--- /dev/null
+++ b/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/modules/hello.py
@@ -0,0 +1,46 @@
+#!/usr/bin/python
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+module: hello
+short_description: Hello test module
+description: Hello test module.
+options:
+ name:
+ description: Name to say hello to.
+ type: str
+author:
+ - Ansible Core Team
+'''
+
+EXAMPLES = '''
+- minimal:
+'''
+
+RETURN = ''''''
+
+from ansible.module_utils.basic import AnsibleModule
+from ..module_utils.my_util import hello
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ name=dict(type='str'),
+ ),
+ )
+
+ module.exit_json(**say_hello(module.params['name']))
+
+
+def say_hello(name):
+ return dict(
+ message=hello(name),
+ )
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/modules/win_util_args.ps1 b/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/modules/win_util_args.ps1
new file mode 100644
index 0000000..69922cd
--- /dev/null
+++ b/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/modules/win_util_args.ps1
@@ -0,0 +1,16 @@
+#!powershell
+
+# Copyright (c) 2020 Ansible Project
+# # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#AnsibleRequires -PowerShell ..module_utils.PSUtil
+
+$spec = @{
+ options = @{
+ my_opt = @{ type = "str"; required = $true }
+ }
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec, @(Get-PSUtilSpec))
+$module.ExitJson()
diff --git a/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/modules/win_util_args.py b/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/modules/win_util_args.py
new file mode 100644
index 0000000..ed49f4e
--- /dev/null
+++ b/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/modules/win_util_args.py
@@ -0,0 +1,39 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['stableinterface'],
+ 'supported_by': 'core'}
+
+DOCUMENTATION = r'''
+---
+module: win_util_args
+short_description: Short description
+description:
+- Some test description for the module
+options:
+ my_opt:
+ description:
+ - Test description
+ required: yes
+ type: str
+extends_documentation_fragment:
+- ns.col.ps_util
+
+author:
+- Ansible Test (@ansible)
+'''
+
+EXAMPLES = r'''
+- win_util_args:
+ option1: test
+ my_opt: test
+'''
+
+RETURN = r'''
+#
+'''
diff --git a/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/tests/integration/targets/minimal/aliases b/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/tests/integration/targets/minimal/aliases
new file mode 100644
index 0000000..1af1cf9
--- /dev/null
+++ b/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/tests/integration/targets/minimal/aliases
@@ -0,0 +1 @@
+context/controller
diff --git a/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/tests/integration/targets/minimal/tasks/main.yml b/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/tests/integration/targets/minimal/tasks/main.yml
new file mode 100644
index 0000000..c45c199
--- /dev/null
+++ b/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/tests/integration/targets/minimal/tasks/main.yml
@@ -0,0 +1,7 @@
+- hello:
+ name: Ansibull
+ register: hello
+
+- assert:
+ that:
+ - hello.message == 'Hello Ansibull'
diff --git a/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_my_util.py b/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_my_util.py
new file mode 100644
index 0000000..7df8710
--- /dev/null
+++ b/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_my_util.py
@@ -0,0 +1,8 @@
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from .....plugins.module_utils.my_util import hello
+
+
+def test_hello():
+ assert hello('Ansibull') == 'Hello Ansibull'
diff --git a/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/tests/unit/plugins/modules/test_hello.py b/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/tests/unit/plugins/modules/test_hello.py
new file mode 100644
index 0000000..95ee057
--- /dev/null
+++ b/test/integration/targets/ansible-test-docker/ansible_collections/ns/col/tests/unit/plugins/modules/test_hello.py
@@ -0,0 +1,8 @@
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from .....plugins.modules.hello import say_hello
+
+
+def test_say_hello():
+ assert say_hello('Ansibull') == dict(message='Hello Ansibull')
diff --git a/test/integration/targets/ansible-test-docker/runme.sh b/test/integration/targets/ansible-test-docker/runme.sh
new file mode 100755
index 0000000..014d363
--- /dev/null
+++ b/test/integration/targets/ansible-test-docker/runme.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+
+source ../collection/setup.sh
+
+set -x
+
+# common args for all tests
+# because we are running in shippable/generic/ we are already in the default docker container
+common=(--python "${ANSIBLE_TEST_PYTHON_VERSION}" --venv --venv-system-site-packages --color --truncate 0 "${@}")
+
+# tests
+ansible-test sanity "${common[@]}"
+ansible-test units "${common[@]}"
+ansible-test integration "${common[@]}"
diff --git a/test/integration/targets/ansible-test-git/aliases b/test/integration/targets/ansible-test-git/aliases
new file mode 100644
index 0000000..7741d44
--- /dev/null
+++ b/test/integration/targets/ansible-test-git/aliases
@@ -0,0 +1,4 @@
+shippable/posix/group3 # runs in the distro test containers
+shippable/generic/group1 # runs in the default test container
+context/controller
+needs/target/collection
diff --git a/test/integration/targets/ansible-test-git/ansible_collections/ns/col/tests/.keep b/test/integration/targets/ansible-test-git/ansible_collections/ns/col/tests/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/ansible-test-git/ansible_collections/ns/col/tests/.keep
diff --git a/test/integration/targets/ansible-test-git/collection-tests/git-at-collection-base.sh b/test/integration/targets/ansible-test-git/collection-tests/git-at-collection-base.sh
new file mode 100755
index 0000000..31ebfbb
--- /dev/null
+++ b/test/integration/targets/ansible-test-git/collection-tests/git-at-collection-base.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+set -eux -o pipefail
+
+export GIT_TOP_LEVEL SUBMODULE_DST
+
+GIT_TOP_LEVEL="${WORK_DIR}/super/ansible_collections/ns/col"
+SUBMODULE_DST="sub"
+
+source collection-tests/git-common.bash
diff --git a/test/integration/targets/ansible-test-git/collection-tests/git-at-collection-root.sh b/test/integration/targets/ansible-test-git/collection-tests/git-at-collection-root.sh
new file mode 100755
index 0000000..8af4387
--- /dev/null
+++ b/test/integration/targets/ansible-test-git/collection-tests/git-at-collection-root.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+set -eux -o pipefail
+
+export GIT_TOP_LEVEL SUBMODULE_DST
+
+GIT_TOP_LEVEL="${WORK_DIR}/super"
+SUBMODULE_DST="ansible_collections/ns/col/sub"
+
+source collection-tests/git-common.bash
diff --git a/test/integration/targets/ansible-test-git/collection-tests/git-common.bash b/test/integration/targets/ansible-test-git/collection-tests/git-common.bash
new file mode 100755
index 0000000..340b311
--- /dev/null
+++ b/test/integration/targets/ansible-test-git/collection-tests/git-common.bash
@@ -0,0 +1,65 @@
+#!/usr/bin/env bash
+
+set -eux -o pipefail
+
+# make sure git is installed
+set +e
+git --version
+gitres=$?
+set -e
+
+if [[ $gitres -ne 0 ]]; then
+ ansible-playbook collection-tests/install-git.yml -i ../../inventory "$@"
+fi
+
+dir="$(pwd)"
+
+uninstall_git() {
+ cd "$dir"
+ ansible-playbook collection-tests/uninstall-git.yml -i ../../inventory "$@"
+}
+
+# This is kind of a hack. The uninstall playbook has no way to know the result
+# of the install playbook to determine if it changed. So instead, we assume
+# that if git wasn't found to begin with, it was installed by the playbook and
+# and needs to be removed when we exit.
+if [[ $gitres -ne 0 ]]; then
+ trap uninstall_git EXIT
+fi
+
+# init sub project
+mkdir "${WORK_DIR}/sub"
+cd "${WORK_DIR}/sub"
+touch "README.md"
+git init
+git config user.name 'Ansible Test'
+git config user.email 'ansible-test@ansible.com'
+git add "README.md"
+git commit -m "Initial commit."
+
+# init super project
+rm -rf "${WORK_DIR}/super" # needed when re-creating in place
+mkdir "${WORK_DIR}/super"
+cp -a "${TEST_DIR}/ansible_collections" "${WORK_DIR}/super"
+cd "${GIT_TOP_LEVEL}"
+git init
+
+# add submodule
+git -c protocol.file.allow=always submodule add "${WORK_DIR}/sub" "${SUBMODULE_DST}"
+
+# prepare for tests
+expected="${WORK_DIR}/expected.txt"
+actual="${WORK_DIR}/actual.txt"
+cd "${WORK_DIR}/super/ansible_collections/ns/col"
+mkdir tests/.git
+touch tests/.git/keep.txt # make sure ansible-test correctly ignores version control within collection subdirectories
+find . -type f ! -path '*/.git/*' ! -name .git | sed 's|^\./||' | sort >"${expected}"
+set -x
+
+# test at the collection base
+ansible-test env --list-files | sort >"${actual}"
+diff --unified "${expected}" "${actual}"
+
+# test at the submodule base
+(cd sub && ansible-test env --list-files | sort >"${actual}")
+diff --unified "${expected}" "${actual}"
diff --git a/test/integration/targets/ansible-test-git/collection-tests/install-git.yml b/test/integration/targets/ansible-test-git/collection-tests/install-git.yml
new file mode 100644
index 0000000..29adead
--- /dev/null
+++ b/test/integration/targets/ansible-test-git/collection-tests/install-git.yml
@@ -0,0 +1,5 @@
+- hosts: localhost
+ tasks:
+ - name: Make sure git is installed
+ package:
+ name: git
diff --git a/test/integration/targets/ansible-test-git/collection-tests/uninstall-git.yml b/test/integration/targets/ansible-test-git/collection-tests/uninstall-git.yml
new file mode 100644
index 0000000..f94caea
--- /dev/null
+++ b/test/integration/targets/ansible-test-git/collection-tests/uninstall-git.yml
@@ -0,0 +1,18 @@
+- hosts: localhost
+ tasks:
+ - name: Make sure git is uninstalled
+ package:
+ name: git
+ state: absent
+ register: git_remove
+
+ # This gets dragged in as a dependency of git on FreeBSD.
+ # We need to remove it too when done.
+ - name: remove python37 if necessary
+ package:
+ name: python37
+ state: absent
+ when:
+ - git_remove is changed
+ - ansible_distribution == 'FreeBSD'
+ - ansible_python.version.major == 2
diff --git a/test/integration/targets/ansible-test-git/runme.sh b/test/integration/targets/ansible-test-git/runme.sh
new file mode 100755
index 0000000..04e8844
--- /dev/null
+++ b/test/integration/targets/ansible-test-git/runme.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+
+set -eux -o pipefail
+
+# tests must be executed outside of the ansible source tree
+# otherwise ansible-test will test the ansible source instead of the test collection
+# the temporary directory provided by ansible-test resides within the ansible source tree
+tmp_dir=$(mktemp -d)
+
+trap 'rm -rf "${tmp_dir}"' EXIT
+
+export TEST_DIR
+export WORK_DIR
+
+TEST_DIR="$PWD"
+
+for test in collection-tests/*.sh; do
+ WORK_DIR="${tmp_dir}/$(basename "${test}" ".sh")"
+ mkdir "${WORK_DIR}"
+ echo "**********************************************************************"
+ echo "TEST: ${test}: STARTING"
+ "${test}" "${@}" || (echo "TEST: ${test}: FAILED" && exit 1)
+ echo "TEST: ${test}: PASSED"
+done
diff --git a/test/integration/targets/ansible-test-integration-constraints/aliases b/test/integration/targets/ansible-test-integration-constraints/aliases
new file mode 100644
index 0000000..7741d44
--- /dev/null
+++ b/test/integration/targets/ansible-test-integration-constraints/aliases
@@ -0,0 +1,4 @@
+shippable/posix/group3 # runs in the distro test containers
+shippable/generic/group1 # runs in the default test container
+context/controller
+needs/target/collection
diff --git a/test/integration/targets/ansible-test-integration-constraints/ansible_collections/ns/col/tests/integration/constraints.txt b/test/integration/targets/ansible-test-integration-constraints/ansible_collections/ns/col/tests/integration/constraints.txt
new file mode 100644
index 0000000..01bb5cf
--- /dev/null
+++ b/test/integration/targets/ansible-test-integration-constraints/ansible_collections/ns/col/tests/integration/constraints.txt
@@ -0,0 +1 @@
+botocore == 1.13.49
diff --git a/test/integration/targets/ansible-test-integration-constraints/ansible_collections/ns/col/tests/integration/requirements.txt b/test/integration/targets/ansible-test-integration-constraints/ansible_collections/ns/col/tests/integration/requirements.txt
new file mode 100644
index 0000000..c5b9e12
--- /dev/null
+++ b/test/integration/targets/ansible-test-integration-constraints/ansible_collections/ns/col/tests/integration/requirements.txt
@@ -0,0 +1 @@
+botocore
diff --git a/test/integration/targets/ansible-test-integration-constraints/ansible_collections/ns/col/tests/integration/targets/constraints/aliases b/test/integration/targets/ansible-test-integration-constraints/ansible_collections/ns/col/tests/integration/targets/constraints/aliases
new file mode 100644
index 0000000..1af1cf9
--- /dev/null
+++ b/test/integration/targets/ansible-test-integration-constraints/ansible_collections/ns/col/tests/integration/targets/constraints/aliases
@@ -0,0 +1 @@
+context/controller
diff --git a/test/integration/targets/ansible-test-integration-constraints/ansible_collections/ns/col/tests/integration/targets/constraints/tasks/main.yml b/test/integration/targets/ansible-test-integration-constraints/ansible_collections/ns/col/tests/integration/targets/constraints/tasks/main.yml
new file mode 100644
index 0000000..c2c1f1a
--- /dev/null
+++ b/test/integration/targets/ansible-test-integration-constraints/ansible_collections/ns/col/tests/integration/targets/constraints/tasks/main.yml
@@ -0,0 +1,7 @@
+- name: get botocore version
+ command: python -c "import botocore; print(botocore.__version__)"
+ register: botocore_version
+- name: check botocore version
+ assert:
+ that:
+ - 'botocore_version.stdout == "1.13.49"'
diff --git a/test/integration/targets/ansible-test-integration-constraints/runme.sh b/test/integration/targets/ansible-test-integration-constraints/runme.sh
new file mode 100755
index 0000000..8467f25
--- /dev/null
+++ b/test/integration/targets/ansible-test-integration-constraints/runme.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+source ../collection/setup.sh
+
+set -x
+
+ansible-test integration --venv --color --truncate 0 "${@}"
diff --git a/test/integration/targets/ansible-test-integration-targets/aliases b/test/integration/targets/ansible-test-integration-targets/aliases
new file mode 100644
index 0000000..7741d44
--- /dev/null
+++ b/test/integration/targets/ansible-test-integration-targets/aliases
@@ -0,0 +1,4 @@
+shippable/posix/group3 # runs in the distro test containers
+shippable/generic/group1 # runs in the default test container
+context/controller
+needs/target/collection
diff --git a/test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/destructive_a/aliases b/test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/destructive_a/aliases
new file mode 100644
index 0000000..c9dc649
--- /dev/null
+++ b/test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/destructive_a/aliases
@@ -0,0 +1,2 @@
+context/controller
+destructive
diff --git a/test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/destructive_b/aliases b/test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/destructive_b/aliases
new file mode 100644
index 0000000..c9dc649
--- /dev/null
+++ b/test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/destructive_b/aliases
@@ -0,0 +1,2 @@
+context/controller
+destructive
diff --git a/test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/disabled_a/aliases b/test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/disabled_a/aliases
new file mode 100644
index 0000000..bd3e3ef
--- /dev/null
+++ b/test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/disabled_a/aliases
@@ -0,0 +1,2 @@
+context/controller
+disabled
diff --git a/test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/disabled_b/aliases b/test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/disabled_b/aliases
new file mode 100644
index 0000000..bd3e3ef
--- /dev/null
+++ b/test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/disabled_b/aliases
@@ -0,0 +1,2 @@
+context/controller
+disabled
diff --git a/test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/unstable_a/aliases b/test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/unstable_a/aliases
new file mode 100644
index 0000000..3497fae
--- /dev/null
+++ b/test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/unstable_a/aliases
@@ -0,0 +1,2 @@
+context/controller
+unstable
diff --git a/test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/unstable_b/aliases b/test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/unstable_b/aliases
new file mode 100644
index 0000000..3497fae
--- /dev/null
+++ b/test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/unstable_b/aliases
@@ -0,0 +1,2 @@
+context/controller
+unstable
diff --git a/test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/unsupported_a/aliases b/test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/unsupported_a/aliases
new file mode 100644
index 0000000..a899639
--- /dev/null
+++ b/test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/unsupported_a/aliases
@@ -0,0 +1,2 @@
+context/controller
+unsupported
diff --git a/test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/unsupported_b/aliases b/test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/unsupported_b/aliases
new file mode 100644
index 0000000..a899639
--- /dev/null
+++ b/test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/unsupported_b/aliases
@@ -0,0 +1,2 @@
+context/controller
+unsupported
diff --git a/test/integration/targets/ansible-test-integration-targets/runme.sh b/test/integration/targets/ansible-test-integration-targets/runme.sh
new file mode 100755
index 0000000..bd44702
--- /dev/null
+++ b/test/integration/targets/ansible-test-integration-targets/runme.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+test="$(pwd)/test.py"
+
+source ../collection/setup.sh
+
+set -x
+
+"${test}" -v
diff --git a/test/integration/targets/ansible-test-integration-targets/test.py b/test/integration/targets/ansible-test-integration-targets/test.py
new file mode 100755
index 0000000..443ed59
--- /dev/null
+++ b/test/integration/targets/ansible-test-integration-targets/test.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python
+
+import subprocess
+import unittest
+
+
+class OptionsTest(unittest.TestCase):
+ options = (
+ 'unsupported',
+ 'disabled',
+ 'unstable',
+ 'destructive',
+ )
+
+ def test_options(self):
+ for option in self.options:
+ with self.subTest(option=option):
+ try:
+ command = ['ansible-test', 'integration', '--list-targets']
+
+ skip_all = subprocess.run([*command, f'{option}_a', f'{option}_b'], text=True, capture_output=True, check=True)
+ allow_all = subprocess.run([*command, f'--allow-{option}', f'{option}_a', f'{option}_b'], text=True, capture_output=True, check=True)
+ allow_first = subprocess.run([*command, f'{option}/{option}_a', f'{option}_b'], text=True, capture_output=True, check=True)
+ allow_last = subprocess.run([*command, f'{option}_a', f'{option}/{option}_b'], text=True, capture_output=True, check=True)
+
+ self.assertEqual(skip_all.stdout.splitlines(), [])
+ self.assertEqual(allow_all.stdout.splitlines(), [f'{option}_a', f'{option}_b'])
+ self.assertEqual(allow_first.stdout.splitlines(), [f'{option}_a'])
+ self.assertEqual(allow_last.stdout.splitlines(), [f'{option}_b'])
+ except subprocess.CalledProcessError as ex:
+ raise Exception(f'{ex}:\n>>> Standard Output:\n{ex.stdout}\n>>> Standard Error:\n{ex.stderr}') from ex
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test/integration/targets/ansible-test-integration/aliases b/test/integration/targets/ansible-test-integration/aliases
new file mode 100644
index 0000000..7741d44
--- /dev/null
+++ b/test/integration/targets/ansible-test-integration/aliases
@@ -0,0 +1,4 @@
+shippable/posix/group3 # runs in the distro test containers
+shippable/generic/group1 # runs in the default test container
+context/controller
+needs/target/collection
diff --git a/test/integration/targets/ansible-test-integration/ansible_collections/ns/col/plugins/module_utils/my_util.py b/test/integration/targets/ansible-test-integration/ansible_collections/ns/col/plugins/module_utils/my_util.py
new file mode 100644
index 0000000..b9c531c
--- /dev/null
+++ b/test/integration/targets/ansible-test-integration/ansible_collections/ns/col/plugins/module_utils/my_util.py
@@ -0,0 +1,6 @@
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+def hello(name):
+ return 'Hello %s' % name
diff --git a/test/integration/targets/ansible-test-integration/ansible_collections/ns/col/plugins/modules/hello.py b/test/integration/targets/ansible-test-integration/ansible_collections/ns/col/plugins/modules/hello.py
new file mode 100644
index 0000000..033b6c9
--- /dev/null
+++ b/test/integration/targets/ansible-test-integration/ansible_collections/ns/col/plugins/modules/hello.py
@@ -0,0 +1,46 @@
+#!/usr/bin/python
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+module: hello
+short_description: Hello test module
+description: Hello test module.
+options:
+ name:
+ description: Name to say hello to.
+ type: str
+author:
+ - Ansible Core Team
+'''
+
+EXAMPLES = '''
+- hello:
+'''
+
+RETURN = ''''''
+
+from ansible.module_utils.basic import AnsibleModule
+from ..module_utils.my_util import hello
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ name=dict(type='str'),
+ ),
+ )
+
+ module.exit_json(**say_hello(module.params['name']))
+
+
+def say_hello(name):
+ return dict(
+ message=hello(name),
+ )
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-test-integration/ansible_collections/ns/col/tests/integration/targets/hello/aliases b/test/integration/targets/ansible-test-integration/ansible_collections/ns/col/tests/integration/targets/hello/aliases
new file mode 100644
index 0000000..1af1cf9
--- /dev/null
+++ b/test/integration/targets/ansible-test-integration/ansible_collections/ns/col/tests/integration/targets/hello/aliases
@@ -0,0 +1 @@
+context/controller
diff --git a/test/integration/targets/ansible-test-integration/ansible_collections/ns/col/tests/integration/targets/hello/tasks/main.yml b/test/integration/targets/ansible-test-integration/ansible_collections/ns/col/tests/integration/targets/hello/tasks/main.yml
new file mode 100644
index 0000000..c45c199
--- /dev/null
+++ b/test/integration/targets/ansible-test-integration/ansible_collections/ns/col/tests/integration/targets/hello/tasks/main.yml
@@ -0,0 +1,7 @@
+- hello:
+ name: Ansibull
+ register: hello
+
+- assert:
+ that:
+ - hello.message == 'Hello Ansibull'
diff --git a/test/integration/targets/ansible-test-integration/runme.sh b/test/integration/targets/ansible-test-integration/runme.sh
new file mode 100755
index 0000000..8467f25
--- /dev/null
+++ b/test/integration/targets/ansible-test-integration/runme.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+source ../collection/setup.sh
+
+set -x
+
+ansible-test integration --venv --color --truncate 0 "${@}"
diff --git a/test/integration/targets/ansible-test-no-tty/aliases b/test/integration/targets/ansible-test-no-tty/aliases
new file mode 100644
index 0000000..2d7f003
--- /dev/null
+++ b/test/integration/targets/ansible-test-no-tty/aliases
@@ -0,0 +1,4 @@
+context/controller
+shippable/posix/group3 # runs in the distro test containers
+shippable/generic/group1 # runs in the default test container
+needs/target/collection
diff --git a/test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/run-with-pty.py b/test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/run-with-pty.py
new file mode 100755
index 0000000..4639152
--- /dev/null
+++ b/test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/run-with-pty.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+"""Run a command using a PTY."""
+
+import sys
+
+if sys.version_info < (3, 10):
+ import vendored_pty as pty
+else:
+ import pty
+
+sys.exit(1 if pty.spawn(sys.argv[1:]) else 0)
diff --git a/test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/tests/integration/targets/no-tty/aliases b/test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/tests/integration/targets/no-tty/aliases
new file mode 100644
index 0000000..1af1cf9
--- /dev/null
+++ b/test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/tests/integration/targets/no-tty/aliases
@@ -0,0 +1 @@
+context/controller
diff --git a/test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/tests/integration/targets/no-tty/assert-no-tty.py b/test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/tests/integration/targets/no-tty/assert-no-tty.py
new file mode 100755
index 0000000..a2b094e
--- /dev/null
+++ b/test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/tests/integration/targets/no-tty/assert-no-tty.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python
+"""Assert no TTY is available."""
+
+import sys
+
+status = 0
+
+for handle in sys.stdin, sys.stdout, sys.stderr:
+ if handle.isatty():
+ print(f'{handle} is a TTY', file=sys.stderr)
+ status += 1
+
+sys.exit(status)
diff --git a/test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/tests/integration/targets/no-tty/runme.sh b/test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/tests/integration/targets/no-tty/runme.sh
new file mode 100755
index 0000000..ae712dd
--- /dev/null
+++ b/test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/tests/integration/targets/no-tty/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eux
+
+./assert-no-tty.py
diff --git a/test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/vendored_pty.py b/test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/vendored_pty.py
new file mode 100644
index 0000000..bc70803
--- /dev/null
+++ b/test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/vendored_pty.py
@@ -0,0 +1,189 @@
+# Vendored copy of https://github.com/python/cpython/blob/3680ebed7f3e529d01996dd0318601f9f0d02b4b/Lib/pty.py
+# PSF License (see licenses/PSF-license.txt or https://opensource.org/licenses/Python-2.0)
+"""Pseudo terminal utilities."""
+
+# Bugs: No signal handling. Doesn't set slave termios and window size.
+# Only tested on Linux, FreeBSD, and macOS.
+# See: W. Richard Stevens. 1992. Advanced Programming in the
+# UNIX Environment. Chapter 19.
+# Author: Steen Lumholt -- with additions by Guido.
+
+from select import select
+import os
+import sys
+import tty
+
+# names imported directly for test mocking purposes
+from os import close, waitpid
+from tty import setraw, tcgetattr, tcsetattr
+
+__all__ = ["openpty", "fork", "spawn"]
+
+STDIN_FILENO = 0
+STDOUT_FILENO = 1
+STDERR_FILENO = 2
+
+CHILD = 0
+
+def openpty():
+ """openpty() -> (master_fd, slave_fd)
+ Open a pty master/slave pair, using os.openpty() if possible."""
+
+ try:
+ return os.openpty()
+ except (AttributeError, OSError):
+ pass
+ master_fd, slave_name = _open_terminal()
+ slave_fd = slave_open(slave_name)
+ return master_fd, slave_fd
+
+def master_open():
+ """master_open() -> (master_fd, slave_name)
+ Open a pty master and return the fd, and the filename of the slave end.
+ Deprecated, use openpty() instead."""
+
+ try:
+ master_fd, slave_fd = os.openpty()
+ except (AttributeError, OSError):
+ pass
+ else:
+ slave_name = os.ttyname(slave_fd)
+ os.close(slave_fd)
+ return master_fd, slave_name
+
+ return _open_terminal()
+
+def _open_terminal():
+ """Open pty master and return (master_fd, tty_name)."""
+ for x in 'pqrstuvwxyzPQRST':
+ for y in '0123456789abcdef':
+ pty_name = '/dev/pty' + x + y
+ try:
+ fd = os.open(pty_name, os.O_RDWR)
+ except OSError:
+ continue
+ return (fd, '/dev/tty' + x + y)
+ raise OSError('out of pty devices')
+
+def slave_open(tty_name):
+ """slave_open(tty_name) -> slave_fd
+ Open the pty slave and acquire the controlling terminal, returning
+ opened filedescriptor.
+ Deprecated, use openpty() instead."""
+
+ result = os.open(tty_name, os.O_RDWR)
+ try:
+ from fcntl import ioctl, I_PUSH
+ except ImportError:
+ return result
+ try:
+ ioctl(result, I_PUSH, "ptem")
+ ioctl(result, I_PUSH, "ldterm")
+ except OSError:
+ pass
+ return result
+
+def fork():
+ """fork() -> (pid, master_fd)
+ Fork and make the child a session leader with a controlling terminal."""
+
+ try:
+ pid, fd = os.forkpty()
+ except (AttributeError, OSError):
+ pass
+ else:
+ if pid == CHILD:
+ try:
+ os.setsid()
+ except OSError:
+ # os.forkpty() already set us session leader
+ pass
+ return pid, fd
+
+ master_fd, slave_fd = openpty()
+ pid = os.fork()
+ if pid == CHILD:
+ # Establish a new session.
+ os.setsid()
+ os.close(master_fd)
+
+ # Slave becomes stdin/stdout/stderr of child.
+ os.dup2(slave_fd, STDIN_FILENO)
+ os.dup2(slave_fd, STDOUT_FILENO)
+ os.dup2(slave_fd, STDERR_FILENO)
+ if slave_fd > STDERR_FILENO:
+ os.close(slave_fd)
+
+ # Explicitly open the tty to make it become a controlling tty.
+ tmp_fd = os.open(os.ttyname(STDOUT_FILENO), os.O_RDWR)
+ os.close(tmp_fd)
+ else:
+ os.close(slave_fd)
+
+ # Parent and child process.
+ return pid, master_fd
+
+def _writen(fd, data):
+ """Write all the data to a descriptor."""
+ while data:
+ n = os.write(fd, data)
+ data = data[n:]
+
+def _read(fd):
+ """Default read function."""
+ return os.read(fd, 1024)
+
+def _copy(master_fd, master_read=_read, stdin_read=_read):
+ """Parent copy loop.
+ Copies
+ pty master -> standard output (master_read)
+ standard input -> pty master (stdin_read)"""
+ fds = [master_fd, STDIN_FILENO]
+ while fds:
+ rfds, _wfds, _xfds = select(fds, [], [])
+
+ if master_fd in rfds:
+ # Some OSes signal EOF by returning an empty byte string,
+ # some throw OSErrors.
+ try:
+ data = master_read(master_fd)
+ except OSError:
+ data = b""
+ if not data: # Reached EOF.
+ return # Assume the child process has exited and is
+ # unreachable, so we clean up.
+ else:
+ os.write(STDOUT_FILENO, data)
+
+ if STDIN_FILENO in rfds:
+ data = stdin_read(STDIN_FILENO)
+ if not data:
+ fds.remove(STDIN_FILENO)
+ else:
+ _writen(master_fd, data)
+
+def spawn(argv, master_read=_read, stdin_read=_read):
+ """Create a spawned process."""
+ if isinstance(argv, str):
+ argv = (argv,)
+ sys.audit('pty.spawn', argv)
+
+ pid, master_fd = fork()
+ if pid == CHILD:
+ os.execlp(argv[0], *argv)
+
+ try:
+ mode = tcgetattr(STDIN_FILENO)
+ setraw(STDIN_FILENO)
+ restore = True
+ except tty.error: # This is the same as termios.error
+ restore = False
+
+ try:
+ _copy(master_fd, master_read, stdin_read)
+ finally:
+ if restore:
+ tcsetattr(STDIN_FILENO, tty.TCSAFLUSH, mode)
+
+ close(master_fd)
+ return waitpid(pid, 0)[1]
diff --git a/test/integration/targets/ansible-test-no-tty/runme.sh b/test/integration/targets/ansible-test-no-tty/runme.sh
new file mode 100755
index 0000000..c02793a
--- /dev/null
+++ b/test/integration/targets/ansible-test-no-tty/runme.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+# Verify that ansible-test runs integration tests without a TTY.
+
+source ../collection/setup.sh
+
+set -x
+
+if ./run-with-pty.py tests/integration/targets/no-tty/assert-no-tty.py > /dev/null; then
+ echo "PTY assertion did not fail. Either PTY creation failed or PTY detection is broken."
+ exit 1
+fi
+
+./run-with-pty.py ansible-test integration --color "${@}"
diff --git a/test/integration/targets/ansible-test-sanity-ansible-doc/aliases b/test/integration/targets/ansible-test-sanity-ansible-doc/aliases
new file mode 100644
index 0000000..7741d44
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-ansible-doc/aliases
@@ -0,0 +1,4 @@
+shippable/posix/group3 # runs in the distro test containers
+shippable/generic/group1 # runs in the default test container
+context/controller
+needs/target/collection
diff --git a/test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/lookup/a/b/lookup2.py b/test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/lookup/a/b/lookup2.py
new file mode 100644
index 0000000..5cd2cf0
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/lookup/a/b/lookup2.py
@@ -0,0 +1,28 @@
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = """
+ name: lookup2
+ author: Ansible Core Team
+ short_description: hello test lookup
+ description:
+ - Hello test lookup.
+ options: {}
+"""
+
+EXAMPLES = """
+- minimal:
+"""
+
+RETURN = """
+"""
+
+from ansible.plugins.lookup import LookupBase
+
+
+class LookupModule(LookupBase):
+
+ def run(self, terms, variables=None, **kwargs):
+ return []
diff --git a/test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/lookup/lookup1.py b/test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/lookup/lookup1.py
new file mode 100644
index 0000000..e274f19
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/lookup/lookup1.py
@@ -0,0 +1,28 @@
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = """
+ name: lookup1
+ author: Ansible Core Team
+ short_description: hello test lookup
+ description:
+ - Hello test lookup.
+ options: {}
+"""
+
+EXAMPLES = """
+- minimal:
+"""
+
+RETURN = """
+"""
+
+from ansible.plugins.lookup import LookupBase
+
+
+class LookupModule(LookupBase):
+
+ def run(self, terms, variables=None, **kwargs):
+ return []
diff --git a/test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/modules/a/b/module2.py b/test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/modules/a/b/module2.py
new file mode 100644
index 0000000..6fafa19
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/modules/a/b/module2.py
@@ -0,0 +1,34 @@
+#!/usr/bin/python
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+module: module2
+short_description: Hello test module
+description: Hello test module.
+options: {}
+author:
+ - Ansible Core Team
+'''
+
+EXAMPLES = '''
+- minimal:
+'''
+
+RETURN = ''''''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec={},
+ )
+
+ module.exit_json()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/modules/module1.py b/test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/modules/module1.py
new file mode 100644
index 0000000..8847f5b
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/modules/module1.py
@@ -0,0 +1,34 @@
+#!/usr/bin/python
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+module: module1
+short_description: Hello test module
+description: Hello test module.
+options: {}
+author:
+ - Ansible Core Team
+'''
+
+EXAMPLES = '''
+- minimal:
+'''
+
+RETURN = ''''''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec={},
+ )
+
+ module.exit_json()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-test-sanity-ansible-doc/runme.sh b/test/integration/targets/ansible-test-sanity-ansible-doc/runme.sh
new file mode 100755
index 0000000..ee1a882
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-ansible-doc/runme.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+set -eu
+
+source ../collection/setup.sh
+
+set -x
+
+ansible-test sanity --test ansible-doc --color "${@}"
diff --git a/test/integration/targets/ansible-test-sanity-import/aliases b/test/integration/targets/ansible-test-sanity-import/aliases
new file mode 100644
index 0000000..09cbf4b
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-import/aliases
@@ -0,0 +1,5 @@
+shippable/posix/group3 # runs in the distro test containers
+shippable/generic/group1 # runs in the default test container
+context/controller
+needs/target/collection
+destructive # adds and then removes packages into lib/ansible/_vendor/
diff --git a/test/integration/targets/ansible-test-sanity-import/ansible_collections/ns/col/plugins/lookup/vendor1.py b/test/integration/targets/ansible-test-sanity-import/ansible_collections/ns/col/plugins/lookup/vendor1.py
new file mode 100644
index 0000000..f59b909
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-import/ansible_collections/ns/col/plugins/lookup/vendor1.py
@@ -0,0 +1,33 @@
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+name: vendor1
+short_description: lookup
+description: Lookup.
+author:
+ - Ansible Core Team
+'''
+
+EXAMPLES = '''#'''
+RETURN = '''#'''
+
+from ansible.plugins.lookup import LookupBase
+# noinspection PyUnresolvedReferences
+from ansible.plugins import loader # import the loader to verify it works when the collection loader has already been loaded
+
+try:
+ import demo
+except ImportError:
+ pass
+else:
+ raise Exception('demo import found when it should not be')
+
+
+class LookupModule(LookupBase):
+ def run(self, terms, variables, **kwargs):
+ self.set_options(var_options=variables, direct=kwargs)
+
+ return terms
diff --git a/test/integration/targets/ansible-test-sanity-import/ansible_collections/ns/col/plugins/lookup/vendor2.py b/test/integration/targets/ansible-test-sanity-import/ansible_collections/ns/col/plugins/lookup/vendor2.py
new file mode 100644
index 0000000..22b4236
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-import/ansible_collections/ns/col/plugins/lookup/vendor2.py
@@ -0,0 +1,33 @@
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+name: vendor2
+short_description: lookup
+description: Lookup.
+author:
+ - Ansible Core Team
+'''
+
+EXAMPLES = '''#'''
+RETURN = '''#'''
+
+from ansible.plugins.lookup import LookupBase
+# noinspection PyUnresolvedReferences
+from ansible.plugins import loader # import the loader to verify it works when the collection loader has already been loaded
+
+try:
+ import demo
+except ImportError:
+ pass
+else:
+ raise Exception('demo import found when it should not be')
+
+
+class LookupModule(LookupBase):
+ def run(self, terms, variables, **kwargs):
+ self.set_options(var_options=variables, direct=kwargs)
+
+ return terms
diff --git a/test/integration/targets/ansible-test-sanity-import/runme.sh b/test/integration/targets/ansible-test-sanity-import/runme.sh
new file mode 100755
index 0000000..a12e3e3
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-import/runme.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+
+source ../collection/setup.sh
+
+vendor_dir="$(python -c 'import pathlib, ansible._vendor; print(pathlib.Path(ansible._vendor.__file__).parent)')"
+
+cleanup() {
+ rm -rf "${vendor_dir}/demo/"
+}
+
+trap cleanup EXIT
+
+# Verify that packages installed in the vendor directory are not available to the import test.
+# If they are, the vendor logic will generate a warning which will be turned into an error.
+# Testing this requires at least two plugins (not modules) to be run through the import test.
+
+mkdir "${vendor_dir}/demo/"
+touch "${vendor_dir}/demo/__init__.py"
+
+ansible-test sanity --test import --color --truncate 0 plugins/lookup/vendor1.py plugins/lookup/vendor2.py "${@}"
diff --git a/test/integration/targets/ansible-test-sanity-lint/aliases b/test/integration/targets/ansible-test-sanity-lint/aliases
new file mode 100644
index 0000000..7741d44
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-lint/aliases
@@ -0,0 +1,4 @@
+shippable/posix/group3 # runs in the distro test containers
+shippable/generic/group1 # runs in the default test container
+context/controller
+needs/target/collection
diff --git a/test/integration/targets/ansible-test-sanity-lint/expected.txt b/test/integration/targets/ansible-test-sanity-lint/expected.txt
new file mode 100644
index 0000000..94238c8
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-lint/expected.txt
@@ -0,0 +1 @@
+plugins/modules/python-wrong-shebang.py:1:1: expected module shebang "b'#!/usr/bin/python'" but found: b'#!invalid'
diff --git a/test/integration/targets/ansible-test-sanity-lint/runme.sh b/test/integration/targets/ansible-test-sanity-lint/runme.sh
new file mode 100755
index 0000000..3e73cb4
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-lint/runme.sh
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+# Make sure that `ansible-test sanity --lint` outputs the correct format to stdout, even when delegation is used.
+
+set -eu
+
+# Create test scenarios at runtime that do not pass sanity tests.
+# This avoids the need to create ignore entries for the tests.
+
+mkdir -p ansible_collections/ns/col/plugins/modules
+
+(
+ cd ansible_collections/ns/col/plugins/modules
+
+ echo '#!invalid' > python-wrong-shebang.py # expected module shebang "b'#!/usr/bin/python'" but found: b'#!invalid'
+)
+
+source ../collection/setup.sh
+
+set -x
+
+###
+### Run the sanity test with the `--lint` option.
+###
+
+# Use the `--venv` option to verify that delegation preserves the output streams.
+ansible-test sanity --test shebang --color --failure-ok --lint --venv "${@}" 1> actual-stdout.txt 2> actual-stderr.txt
+diff -u "${TEST_DIR}/expected.txt" actual-stdout.txt
+grep -f "${TEST_DIR}/expected.txt" actual-stderr.txt
+
+# Run without delegation to verify direct output uses the correct streams.
+ansible-test sanity --test shebang --color --failure-ok --lint "${@}" 1> actual-stdout.txt 2> actual-stderr.txt
+diff -u "${TEST_DIR}/expected.txt" actual-stdout.txt
+grep -f "${TEST_DIR}/expected.txt" actual-stderr.txt
+
+###
+### Run the sanity test without the `--lint` option.
+###
+
+# Use the `--venv` option to verify that delegation preserves the output streams.
+ansible-test sanity --test shebang --color --failure-ok --venv "${@}" 1> actual-stdout.txt 2> actual-stderr.txt
+grep -f "${TEST_DIR}/expected.txt" actual-stdout.txt
+[ ! -s actual-stderr.txt ]
+
+# Run without delegation to verify direct output uses the correct streams.
+ansible-test sanity --test shebang --color --failure-ok "${@}" 1> actual-stdout.txt 2> actual-stderr.txt
+grep -f "${TEST_DIR}/expected.txt" actual-stdout.txt
+[ ! -s actual-stderr.txt ]
diff --git a/test/integration/targets/ansible-test-sanity-shebang/aliases b/test/integration/targets/ansible-test-sanity-shebang/aliases
new file mode 100644
index 0000000..7741d44
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-shebang/aliases
@@ -0,0 +1,4 @@
+shippable/posix/group3 # runs in the distro test containers
+shippable/generic/group1 # runs in the default test container
+context/controller
+needs/target/collection
diff --git a/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/plugins/modules/powershell.ps1 b/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/plugins/modules/powershell.ps1
new file mode 100644
index 0000000..9eb7192
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/plugins/modules/powershell.ps1
@@ -0,0 +1 @@
+#!powershell
diff --git a/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/plugins/modules/python-no-shebang.py b/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/plugins/modules/python-no-shebang.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/plugins/modules/python-no-shebang.py
diff --git a/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/plugins/modules/python.py b/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/plugins/modules/python.py
new file mode 100644
index 0000000..013e4b7
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/plugins/modules/python.py
@@ -0,0 +1 @@
+#!/usr/bin/python
diff --git a/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/scripts/env_bash.sh b/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/scripts/env_bash.sh
new file mode 100755
index 0000000..f1f641a
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/scripts/env_bash.sh
@@ -0,0 +1 @@
+#!/usr/bin/env bash
diff --git a/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/scripts/env_python.py b/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/scripts/env_python.py
new file mode 100755
index 0000000..4265cc3
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/scripts/env_python.py
@@ -0,0 +1 @@
+#!/usr/bin/env python
diff --git a/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/scripts/sh.sh b/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/scripts/sh.sh
new file mode 100755
index 0000000..1a24852
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/scripts/sh.sh
@@ -0,0 +1 @@
+#!/bin/sh
diff --git a/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/tests/integration/targets/valid/env_bash.sh b/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/tests/integration/targets/valid/env_bash.sh
new file mode 100755
index 0000000..f1f641a
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/tests/integration/targets/valid/env_bash.sh
@@ -0,0 +1 @@
+#!/usr/bin/env bash
diff --git a/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/tests/integration/targets/valid/env_python.py b/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/tests/integration/targets/valid/env_python.py
new file mode 100755
index 0000000..4265cc3
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/tests/integration/targets/valid/env_python.py
@@ -0,0 +1 @@
+#!/usr/bin/env python
diff --git a/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/tests/integration/targets/valid/sh.sh b/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/tests/integration/targets/valid/sh.sh
new file mode 100755
index 0000000..1a24852
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/tests/integration/targets/valid/sh.sh
@@ -0,0 +1 @@
+#!/bin/sh
diff --git a/test/integration/targets/ansible-test-sanity-shebang/expected.txt b/test/integration/targets/ansible-test-sanity-shebang/expected.txt
new file mode 100644
index 0000000..fbd7330
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-shebang/expected.txt
@@ -0,0 +1,9 @@
+plugins/modules/no-shebang-executable.py:0:0: file without shebang should not be executable
+plugins/modules/python-executable.py:0:0: module should not be executable
+plugins/modules/python-wrong-shebang.py:1:1: expected module shebang "b'#!/usr/bin/python'" but found: b'#!invalid'
+plugins/modules/utf-16-be-bom.py:0:0: file starts with a UTF-16 (BE) byte order mark
+plugins/modules/utf-16-le-bom.py:0:0: file starts with a UTF-16 (LE) byte order mark
+plugins/modules/utf-32-be-bom.py:0:0: file starts with a UTF-32 (BE) byte order mark
+plugins/modules/utf-32-le-bom.py:0:0: file starts with a UTF-32 (LE) byte order mark
+plugins/modules/utf-8-bom.py:0:0: file starts with a UTF-8 byte order mark
+scripts/unexpected-shebang:1:1: unexpected non-module shebang: b'#!/usr/bin/custom'
diff --git a/test/integration/targets/ansible-test-sanity-shebang/runme.sh b/test/integration/targets/ansible-test-sanity-shebang/runme.sh
new file mode 100755
index 0000000..f7fc68a
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-shebang/runme.sh
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+
+set -eu
+
+# Create test scenarios at runtime that do not pass sanity tests.
+# This avoids the need to create ignore entries for the tests.
+
+(
+ cd ansible_collections/ns/col/plugins/modules
+
+ touch no-shebang-executable.py && chmod +x no-shebang-executable.py # file without shebang should not be executable
+ python -c "open('utf-32-be-bom.py', 'wb').write(b'\x00\x00\xFE\xFF')" # file starts with a UTF-32 (BE) byte order mark
+ python -c "open('utf-32-le-bom.py', 'wb').write(b'\xFF\xFE\x00\x00')" # file starts with a UTF-32 (LE) byte order mark
+ python -c "open('utf-16-be-bom.py', 'wb').write(b'\xFE\xFF')" # file starts with a UTF-16 (BE) byte order mark
+ python -c "open('utf-16-le-bom.py', 'wb').write(b'\xFF\xFE')" # file starts with a UTF-16 (LE) byte order mark
+ python -c "open('utf-8-bom.py', 'wb').write(b'\xEF\xBB\xBF')" # file starts with a UTF-8 byte order mark
+ echo '#!/usr/bin/python' > python-executable.py && chmod +x python-executable.py # module should not be executable
+ echo '#!invalid' > python-wrong-shebang.py # expected module shebang "b'#!/usr/bin/python'" but found: b'#!invalid'
+)
+
+(
+ cd ansible_collections/ns/col/scripts
+
+ echo '#!/usr/bin/custom' > unexpected-shebang # unexpected non-module shebang: b'#!/usr/bin/custom'
+
+ echo '#!/usr/bin/make -f' > Makefile && chmod +x Makefile # pass
+ echo '#!/bin/bash -eu' > bash_eu.sh && chmod +x bash_eu.sh # pass
+ echo '#!/bin/bash -eux' > bash_eux.sh && chmod +x bash_eux.sh # pass
+ echo '#!/usr/bin/env fish' > env_fish.fish && chmod +x env_fish.fish # pass
+ echo '#!/usr/bin/env pwsh' > env_pwsh.ps1 && chmod +x env_pwsh.ps1 # pass
+)
+
+mkdir ansible_collections/ns/col/examples
+
+(
+ cd ansible_collections/ns/col/examples
+
+ echo '#!/usr/bin/custom' > unexpected-shebang # pass
+)
+
+source ../collection/setup.sh
+
+set -x
+
+ansible-test sanity --test shebang --color --lint --failure-ok "${@}" > actual.txt
+
+diff -u "${TEST_DIR}/expected.txt" actual.txt
diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/aliases b/test/integration/targets/ansible-test-sanity-validate-modules/aliases
new file mode 100644
index 0000000..7741d44
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-validate-modules/aliases
@@ -0,0 +1,4 @@
+shippable/posix/group3 # runs in the distro test containers
+shippable/generic/group1 # runs in the default test container
+context/controller
+needs/target/collection
diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/invalid_yaml_syntax.py b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/invalid_yaml_syntax.py
new file mode 100644
index 0000000..5dd753f
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/invalid_yaml_syntax.py
@@ -0,0 +1,27 @@
+#!/usr/bin/python
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+- key: "value"wrong
+'''
+
+EXAMPLES = '''
+- key: "value"wrong
+'''
+
+RETURN = '''
+- key: "value"wrong
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ AnsibleModule(argument_spec=dict())
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/no_callable.py b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/no_callable.py
new file mode 100644
index 0000000..176376a
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/no_callable.py
@@ -0,0 +1,23 @@
+#!/usr/bin/python
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+module: no_callable
+short_description: No callale test module
+description: No callable test module.
+author:
+ - Ansible Core Team
+'''
+
+EXAMPLES = '''#'''
+RETURN = ''''''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+if __name__ == '__main__':
+ module = AnsibleModule(argument_spec=dict())
+ module.exit_json()
diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/sidecar.py b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/sidecar.py
new file mode 100644
index 0000000..8377c40
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/sidecar.py
@@ -0,0 +1,11 @@
+#!/usr/bin/python
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+if __name__ == '__main__':
+ module = AnsibleModule(argument_spec=dict(
+ test=dict(type='str', choices=['foo', 'bar'], default='foo'),
+ ))
+ module.exit_json(test='foo')
diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/sidecar.yaml b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/sidecar.yaml
new file mode 100644
index 0000000..c257542
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/sidecar.yaml
@@ -0,0 +1,31 @@
+# Copyright (c) 2022 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION:
+ module: sidecar
+ short_description: Short description for sidecar module
+ description:
+ - Description for sidecar module
+ options:
+ test:
+ description:
+ - Description for test module option
+ type: str
+ choices:
+ - foo
+ - bar
+ default: foo
+ author:
+ - Ansible Core Team
+
+EXAMPLES: |
+ - name: example for sidecar
+ ns.col.sidecar:
+ test: bar
+
+RETURN:
+ test:
+ description: The test return value
+ returned: always
+ type: str
+ sample: abc
diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/README.rst b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/README.rst
new file mode 100644
index 0000000..bf1003f
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/README.rst
@@ -0,0 +1,3 @@
+README
+------
+This is a simple collection used to test failures with ``ansible-test sanity --test validate-modules``.
diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/galaxy.yml b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/galaxy.yml
new file mode 100644
index 0000000..3b11671
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/galaxy.yml
@@ -0,0 +1,6 @@
+namespace: ns
+name: failure
+version: 1.0.0
+readme: README.rst
+authors:
+ - Ansible
diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/meta/main.yml b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/meta/main.yml
new file mode 100644
index 0000000..1602a25
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/meta/main.yml
@@ -0,0 +1 @@
+requires_ansible: '>=2.9'
diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/plugins/modules/failure_ps.ps1 b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/plugins/modules/failure_ps.ps1
new file mode 100644
index 0000000..6ec0439
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/plugins/modules/failure_ps.ps1
@@ -0,0 +1,16 @@
+#!powershell
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+throw "test inner error message"
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, @{
+ options = @{
+ test = @{ type = 'str'; choices = @('foo', 'bar'); default = 'foo' }
+ }
+ })
+
+$module.Result.test = 'abc'
+
+$module.ExitJson()
diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/plugins/modules/failure_ps.yml b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/plugins/modules/failure_ps.yml
new file mode 100644
index 0000000..c657ec9
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/plugins/modules/failure_ps.yml
@@ -0,0 +1,31 @@
+# Copyright (c) 2022 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION:
+ module: failure_ps
+ short_description: Short description for failure_ps module
+ description:
+ - Description for failure_ps module
+ options:
+ test:
+ description:
+ - Description for test module option
+ type: str
+ choices:
+ - foo
+ - bar
+ default: foo
+ author:
+ - Ansible Core Team
+
+EXAMPLES: |
+ - name: example for failure_ps
+ ns.col.failure_ps:
+ test: bar
+
+RETURN:
+ test:
+ description: The test return value
+ returned: always
+ type: str
+ sample: abc
diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/README.rst b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/README.rst
new file mode 100644
index 0000000..bbdd513
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/README.rst
@@ -0,0 +1,3 @@
+README
+------
+This is a simple PowerShell-only collection used to verify that ``ansible-test`` works on a collection.
diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/galaxy.yml b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/galaxy.yml
new file mode 100644
index 0000000..0a78b9e
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/galaxy.yml
@@ -0,0 +1,6 @@
+namespace: ns
+name: ps_only
+version: 1.0.0
+readme: README.rst
+authors:
+ - Ansible
diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/meta/runtime.yml b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/meta/runtime.yml
new file mode 100644
index 0000000..1602a25
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/meta/runtime.yml
@@ -0,0 +1 @@
+requires_ansible: '>=2.9'
diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/module_utils/share_module.psm1 b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/module_utils/share_module.psm1
new file mode 100644
index 0000000..1e8ff90
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/module_utils/share_module.psm1
@@ -0,0 +1,19 @@
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+Function Invoke-AnsibleModule {
+ <#
+ .SYNOPSIS
+ validate
+ #>
+ [CmdletBinding()]
+ param ()
+
+ $module = [Ansible.Basic.AnsibleModule]::Create(@(), @{
+ options = @{
+ test = @{ type = 'str' }
+ }
+ })
+ $module.ExitJson()
+}
+
+Export-ModuleMember -Function Invoke-AnsibleModule
diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/module_utils/validate.psm1 b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/module_utils/validate.psm1
new file mode 100644
index 0000000..7072b31
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/module_utils/validate.psm1
@@ -0,0 +1,8 @@
+function Validate {
+ <#
+ .SYNOPSIS
+ validate
+ #>
+}
+
+Export-ModuleMember -Function "Validate"
diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/in_function.ps1 b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/in_function.ps1
new file mode 100644
index 0000000..8f74edc
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/in_function.ps1
@@ -0,0 +1,7 @@
+#!powershell
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#AnsibleRequires -PowerShell ..module_utils.share_module
+
+Invoke-AnsibleModule
diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/in_function.yml b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/in_function.yml
new file mode 100644
index 0000000..87d3ec7
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/in_function.yml
@@ -0,0 +1,25 @@
+# Copyright (c) 2022 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION:
+ module: in_function
+ short_description: Short description for in_function module
+ description:
+ - Description for in_function module
+ options:
+ test:
+ description: Description for test
+ type: str
+ author:
+ - Ansible Core Team
+
+EXAMPLES: |
+ - name: example for sidecar
+ ns.col.in_function:
+
+RETURN:
+ test:
+ description: The test return value
+ returned: always
+ type: str
+ sample: abc
diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/sidecar.ps1 b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/sidecar.ps1
new file mode 100644
index 0000000..1bfa066
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/sidecar.ps1
@@ -0,0 +1,14 @@
+#!powershell
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, @{
+ options = @{
+ test = @{ type = 'str'; choices = @('foo', 'bar'); default = 'foo' }
+ }
+ })
+
+$module.Result.test = 'abc'
+
+$module.ExitJson()
diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/sidecar.yml b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/sidecar.yml
new file mode 100644
index 0000000..c257542
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/sidecar.yml
@@ -0,0 +1,31 @@
+# Copyright (c) 2022 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION:
+ module: sidecar
+ short_description: Short description for sidecar module
+ description:
+ - Description for sidecar module
+ options:
+ test:
+ description:
+ - Description for test module option
+ type: str
+ choices:
+ - foo
+ - bar
+ default: foo
+ author:
+ - Ansible Core Team
+
+EXAMPLES: |
+ - name: example for sidecar
+ ns.col.sidecar:
+ test: bar
+
+RETURN:
+ test:
+ description: The test return value
+ returned: always
+ type: str
+ sample: abc
diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/validate.ps1 b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/validate.ps1
new file mode 100644
index 0000000..a587af8
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/validate.ps1
@@ -0,0 +1,8 @@
+#!powershell
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#AnsibleRequires -PowerShell ..module_utils.validate
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
+$module.ExitJson()
diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/validate.py b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/validate.py
new file mode 100644
index 0000000..ee1fb13
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/validate.py
@@ -0,0 +1,14 @@
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+module: validate
+short_description: validate
+description: validate
+author: "validate (@validate)"
+'''
+
+EXAMPLES = r'''
+'''
+
+RETURN = r'''
+'''
diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/expected.txt b/test/integration/targets/ansible-test-sanity-validate-modules/expected.txt
new file mode 100644
index 0000000..95f12f3
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-validate-modules/expected.txt
@@ -0,0 +1,5 @@
+plugins/modules/invalid_yaml_syntax.py:0:0: deprecation-mismatch: "meta/runtime.yml" and DOCUMENTATION.deprecation do not agree.
+plugins/modules/invalid_yaml_syntax.py:0:0: missing-documentation: No DOCUMENTATION provided
+plugins/modules/invalid_yaml_syntax.py:8:15: documentation-syntax-error: DOCUMENTATION is not valid YAML
+plugins/modules/invalid_yaml_syntax.py:12:15: invalid-examples: EXAMPLES is not valid YAML
+plugins/modules/invalid_yaml_syntax.py:16:15: return-syntax-error: RETURN is not valid YAML
diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/runme.sh b/test/integration/targets/ansible-test-sanity-validate-modules/runme.sh
new file mode 100755
index 0000000..e029996
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-validate-modules/runme.sh
@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+
+source ../collection/setup.sh
+
+set -eux
+
+ansible-test sanity --test validate-modules --color --truncate 0 --failure-ok --lint "${@}" 1> actual-stdout.txt 2> actual-stderr.txt
+diff -u "${TEST_DIR}/expected.txt" actual-stdout.txt
+grep -f "${TEST_DIR}/expected.txt" actual-stderr.txt
+
+cd ../ps_only
+
+if ! command -V pwsh; then
+ echo "skipping test since pwsh is not available"
+ exit 0
+fi
+
+# Use a PowerShell-only collection to verify that validate-modules does not load the collection loader multiple times.
+ansible-test sanity --test validate-modules --color --truncate 0 "${@}"
+
+cd ../failure
+
+if ansible-test sanity --test validate-modules --color --truncate 0 "${@}" 1> ansible-stdout.txt 2> ansible-stderr.txt; then
+ echo "ansible-test sanity for failure should cause failure"
+ exit 1
+fi
+
+cat ansible-stdout.txt
+grep -q "ERROR: plugins/modules/failure_ps.ps1:0:0: import-error: Exception attempting to import module for argument_spec introspection" < ansible-stdout.txt
+grep -q "test inner error message" < ansible-stdout.txt
+
+cat ansible-stderr.txt
+grep -q "FATAL: The 1 sanity test(s) listed below (out of 1) failed" < ansible-stderr.txt
+grep -q "validate-modules" < ansible-stderr.txt
diff --git a/test/integration/targets/ansible-test-sanity/aliases b/test/integration/targets/ansible-test-sanity/aliases
new file mode 100644
index 0000000..7741d44
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity/aliases
@@ -0,0 +1,4 @@
+shippable/posix/group3 # runs in the distro test containers
+shippable/generic/group1 # runs in the default test container
+context/controller
+needs/target/collection
diff --git a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/README.rst b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/README.rst
new file mode 100644
index 0000000..d8138d3
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/README.rst
@@ -0,0 +1,3 @@
+README
+------
+This is a simple collection used to verify that ``ansible-test`` works on a collection.
diff --git a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/galaxy.yml b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/galaxy.yml
new file mode 100644
index 0000000..08a32e8
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/galaxy.yml
@@ -0,0 +1,6 @@
+namespace: ns
+name: col
+version: 1.0.0
+readme: README.rst
+authors:
+ - Ansible
diff --git a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/meta/runtime.yml b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/meta/runtime.yml
new file mode 100644
index 0000000..fee22ad
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/meta/runtime.yml
@@ -0,0 +1,5 @@
+requires_ansible: '>=2.11' # force ansible-doc to check the Ansible version (requires packaging)
+plugin_routing:
+ modules:
+ hi:
+ redirect: hello
diff --git a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/filter/check_pylint.py b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/filter/check_pylint.py
new file mode 100644
index 0000000..f1be4f3
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/filter/check_pylint.py
@@ -0,0 +1,23 @@
+"""
+These test cases verify ansible-test version constraints for pylint and its dependencies across Python versions.
+The initial test cases were discovered while testing various Python versions against ansible/ansible.
+"""
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+# Python 3.8 fails with astroid 2.2.5 but works on 2.3.3
+# syntax-error: Cannot import 'string' due to syntax error 'invalid syntax (&lt;unknown&gt;, line 109)'
+# Python 3.9 fails with astroid 2.2.5 but works on 2.3.3
+# syntax-error: Cannot import 'string' due to syntax error 'invalid syntax (&lt;unknown&gt;, line 104)'
+import string
+
+# Python 3.9 fails with pylint 2.3.1 or 2.4.4 with astroid 2.3.3 but works with pylint 2.5.0 and astroid 2.4.0
+# 'Call' object has no attribute 'value'
+result = {None: None}[{}.get('something')]
+
+# pylint 2.3.1 and 2.4.4 report the following error but 2.5.0 and 2.6.0 do not
+# blacklisted-name: Black listed name "foo"
+# see: https://github.com/PyCQA/pylint/issues/3701
+# regression: documented as a known issue and removed from ignore.txt so pylint can be upgraded to 2.6.0
+# if future versions of pylint fix this issue then the ignore should be restored
+foo = {}.keys()
diff --git a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/lookup/bad.py b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/lookup/bad.py
new file mode 100644
index 0000000..580f9d8
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/lookup/bad.py
@@ -0,0 +1,31 @@
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+name: bad
+short_description: Bad lookup
+description: A bad lookup.
+author:
+ - Ansible Core Team
+'''
+
+EXAMPLES = '''
+- debug:
+ msg: "{{ lookup('ns.col.bad') }}"
+'''
+
+RETURN = ''' # '''
+
+from ansible.plugins.lookup import LookupBase
+from ansible import constants
+
+import lxml
+
+
+class LookupModule(LookupBase):
+ def run(self, terms, variables, **kwargs):
+ self.set_options(var_options=variables, direct=kwargs)
+
+ return terms
diff --git a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/lookup/world.py b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/lookup/world.py
new file mode 100644
index 0000000..dbb479a
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/lookup/world.py
@@ -0,0 +1,29 @@
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+name: world
+short_description: World lookup
+description: A world lookup.
+author:
+ - Ansible Core Team
+'''
+
+EXAMPLES = '''
+- debug:
+ msg: "{{ lookup('ns.col.world') }}"
+'''
+
+RETURN = ''' # '''
+
+from ansible.plugins.lookup import LookupBase
+from ansible import constants
+
+
+class LookupModule(LookupBase):
+ def run(self, terms, variables, **kwargs):
+ self.set_options(var_options=variables, direct=kwargs)
+
+ return terms
diff --git a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/module_utils/__init__.py b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/module_utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/module_utils/__init__.py
diff --git a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/modules/bad.py b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/modules/bad.py
new file mode 100644
index 0000000..e79613b
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/modules/bad.py
@@ -0,0 +1,34 @@
+#!/usr/bin/python
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+module: bad
+short_description: Bad test module
+description: Bad test module.
+author:
+ - Ansible Core Team
+'''
+
+EXAMPLES = '''
+- bad:
+'''
+
+RETURN = ''''''
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible import constants # intentionally trigger pylint ansible-bad-module-import error
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(),
+ )
+
+ module.exit_json()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/random_directory/bad.py b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/random_directory/bad.py
new file mode 100644
index 0000000..2e35cf8
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/random_directory/bad.py
@@ -0,0 +1,8 @@
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+# This is not an allowed import, but since this file is in a plugins/ subdirectory that is not checked,
+# the import sanity test will not complain.
+import lxml
diff --git a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/integration/targets/hello/files/bad.py b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/integration/targets/hello/files/bad.py
new file mode 100644
index 0000000..8221543
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/integration/targets/hello/files/bad.py
@@ -0,0 +1,16 @@
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import tempfile
+
+try:
+ import urllib2 # intentionally trigger pylint ansible-bad-import error
+except ImportError:
+ urllib2 = None
+
+try:
+ from urllib2 import Request # intentionally trigger pylint ansible-bad-import-from error
+except ImportError:
+ Request = None
+
+tempfile.mktemp() # intentionally trigger pylint ansible-bad-function error
diff --git a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/sanity/ignore.txt b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/sanity/ignore.txt
new file mode 100644
index 0000000..e1b3f4c
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/sanity/ignore.txt
@@ -0,0 +1,6 @@
+plugins/modules/bad.py import
+plugins/modules/bad.py pylint:ansible-bad-module-import
+plugins/lookup/bad.py import
+tests/integration/targets/hello/files/bad.py pylint:ansible-bad-function
+tests/integration/targets/hello/files/bad.py pylint:ansible-bad-import
+tests/integration/targets/hello/files/bad.py pylint:ansible-bad-import-from
diff --git a/test/integration/targets/ansible-test-sanity/runme.sh b/test/integration/targets/ansible-test-sanity/runme.sh
new file mode 100755
index 0000000..233db74
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity/runme.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+source ../collection/setup.sh
+
+set -x
+
+ansible-test sanity --color --truncate 0 "${@}"
diff --git a/test/integration/targets/ansible-test-shell/aliases b/test/integration/targets/ansible-test-shell/aliases
new file mode 100644
index 0000000..7741d44
--- /dev/null
+++ b/test/integration/targets/ansible-test-shell/aliases
@@ -0,0 +1,4 @@
+shippable/posix/group3 # runs in the distro test containers
+shippable/generic/group1 # runs in the default test container
+context/controller
+needs/target/collection
diff --git a/test/integration/targets/ansible-test-shell/ansible_collections/ns/col/.keep b/test/integration/targets/ansible-test-shell/ansible_collections/ns/col/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/ansible-test-shell/ansible_collections/ns/col/.keep
diff --git a/test/integration/targets/ansible-test-shell/expected-stderr.txt b/test/integration/targets/ansible-test-shell/expected-stderr.txt
new file mode 100644
index 0000000..af6415d
--- /dev/null
+++ b/test/integration/targets/ansible-test-shell/expected-stderr.txt
@@ -0,0 +1 @@
+stderr
diff --git a/test/integration/targets/ansible-test-shell/expected-stdout.txt b/test/integration/targets/ansible-test-shell/expected-stdout.txt
new file mode 100644
index 0000000..faa3a15
--- /dev/null
+++ b/test/integration/targets/ansible-test-shell/expected-stdout.txt
@@ -0,0 +1 @@
+stdout
diff --git a/test/integration/targets/ansible-test-shell/runme.sh b/test/integration/targets/ansible-test-shell/runme.sh
new file mode 100755
index 0000000..0e0d18a
--- /dev/null
+++ b/test/integration/targets/ansible-test-shell/runme.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+# Make sure that `ansible-test shell` outputs to the correct stream.
+
+set -eu
+
+source ../collection/setup.sh
+
+set -x
+
+# Try `shell` with delegation.
+
+ansible-test shell --venv -- \
+ python -c 'import sys; print("stdout"); print("stderr", file=sys.stderr)' 1> actual-stdout.txt 2> actual-stderr.txt
+
+cat actual-stdout.txt
+cat actual-stderr.txt
+
+diff -u "${TEST_DIR}/expected-stdout.txt" actual-stdout.txt
+grep -f "${TEST_DIR}/expected-stderr.txt" actual-stderr.txt
+
+# Try `shell` without delegation.
+
+ansible-test shell -- \
+ python -c 'import sys; print("stdout"); print("stderr", file=sys.stderr)' 1> actual-stdout.txt 2> actual-stderr.txt
+
+cat actual-stdout.txt
+cat actual-stderr.txt
+
+diff -u "${TEST_DIR}/expected-stdout.txt" actual-stdout.txt
+grep -f "${TEST_DIR}/expected-stderr.txt" actual-stderr.txt
diff --git a/test/integration/targets/ansible-test-units-constraints/aliases b/test/integration/targets/ansible-test-units-constraints/aliases
new file mode 100644
index 0000000..79d7dbd
--- /dev/null
+++ b/test/integration/targets/ansible-test-units-constraints/aliases
@@ -0,0 +1,5 @@
+shippable/posix/group3 # runs in the distro test containers
+shippable/generic/group1 # runs in the default test container
+context/controller
+needs/target/collection
+needs/target/ansible-test
diff --git a/test/integration/targets/ansible-test-units-constraints/ansible_collections/ns/col/tests/unit/constraints.txt b/test/integration/targets/ansible-test-units-constraints/ansible_collections/ns/col/tests/unit/constraints.txt
new file mode 100644
index 0000000..d098689
--- /dev/null
+++ b/test/integration/targets/ansible-test-units-constraints/ansible_collections/ns/col/tests/unit/constraints.txt
@@ -0,0 +1 @@
+botocore == 1.13.50
diff --git a/test/integration/targets/ansible-test-units-constraints/ansible_collections/ns/col/tests/unit/plugins/modules/test_constraints.py b/test/integration/targets/ansible-test-units-constraints/ansible_collections/ns/col/tests/unit/plugins/modules/test_constraints.py
new file mode 100644
index 0000000..857e8e5
--- /dev/null
+++ b/test/integration/targets/ansible-test-units-constraints/ansible_collections/ns/col/tests/unit/plugins/modules/test_constraints.py
@@ -0,0 +1,8 @@
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import botocore
+
+
+def test_constraints():
+ assert botocore.__version__ == '1.13.50'
diff --git a/test/integration/targets/ansible-test-units-constraints/ansible_collections/ns/col/tests/unit/requirements.txt b/test/integration/targets/ansible-test-units-constraints/ansible_collections/ns/col/tests/unit/requirements.txt
new file mode 100644
index 0000000..c5b9e12
--- /dev/null
+++ b/test/integration/targets/ansible-test-units-constraints/ansible_collections/ns/col/tests/unit/requirements.txt
@@ -0,0 +1 @@
+botocore
diff --git a/test/integration/targets/ansible-test-units-constraints/runme.sh b/test/integration/targets/ansible-test-units-constraints/runme.sh
new file mode 100755
index 0000000..3ad4ea6
--- /dev/null
+++ b/test/integration/targets/ansible-test-units-constraints/runme.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+source ../collection/setup.sh
+
+set -x
+
+options=$("${TEST_DIR}"/../ansible-test/venv-pythons.py)
+IFS=', ' read -r -a pythons <<< "${options}"
+
+ansible-test units --color --truncate 0 "${pythons[@]}" "${@}"
diff --git a/test/integration/targets/ansible-test-units/aliases b/test/integration/targets/ansible-test-units/aliases
new file mode 100644
index 0000000..79d7dbd
--- /dev/null
+++ b/test/integration/targets/ansible-test-units/aliases
@@ -0,0 +1,5 @@
+shippable/posix/group3 # runs in the distro test containers
+shippable/generic/group1 # runs in the default test container
+context/controller
+needs/target/collection
+needs/target/ansible-test
diff --git a/test/integration/targets/ansible-test-units/ansible_collections/ns/col/plugins/module_utils/my_util.py b/test/integration/targets/ansible-test-units/ansible_collections/ns/col/plugins/module_utils/my_util.py
new file mode 100644
index 0000000..b9c531c
--- /dev/null
+++ b/test/integration/targets/ansible-test-units/ansible_collections/ns/col/plugins/module_utils/my_util.py
@@ -0,0 +1,6 @@
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+def hello(name):
+ return 'Hello %s' % name
diff --git a/test/integration/targets/ansible-test-units/ansible_collections/ns/col/plugins/modules/hello.py b/test/integration/targets/ansible-test-units/ansible_collections/ns/col/plugins/modules/hello.py
new file mode 100644
index 0000000..033b6c9
--- /dev/null
+++ b/test/integration/targets/ansible-test-units/ansible_collections/ns/col/plugins/modules/hello.py
@@ -0,0 +1,46 @@
+#!/usr/bin/python
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+module: hello
+short_description: Hello test module
+description: Hello test module.
+options:
+ name:
+ description: Name to say hello to.
+ type: str
+author:
+ - Ansible Core Team
+'''
+
+EXAMPLES = '''
+- hello:
+'''
+
+RETURN = ''''''
+
+from ansible.module_utils.basic import AnsibleModule
+from ..module_utils.my_util import hello
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ name=dict(type='str'),
+ ),
+ )
+
+ module.exit_json(**say_hello(module.params['name']))
+
+
+def say_hello(name):
+ return dict(
+ message=hello(name),
+ )
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-test-units/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_my_util.py b/test/integration/targets/ansible-test-units/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_my_util.py
new file mode 100644
index 0000000..7df8710
--- /dev/null
+++ b/test/integration/targets/ansible-test-units/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_my_util.py
@@ -0,0 +1,8 @@
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from .....plugins.module_utils.my_util import hello
+
+
+def test_hello():
+ assert hello('Ansibull') == 'Hello Ansibull'
diff --git a/test/integration/targets/ansible-test-units/ansible_collections/ns/col/tests/unit/plugins/modules/test_hello.py b/test/integration/targets/ansible-test-units/ansible_collections/ns/col/tests/unit/plugins/modules/test_hello.py
new file mode 100644
index 0000000..95ee057
--- /dev/null
+++ b/test/integration/targets/ansible-test-units/ansible_collections/ns/col/tests/unit/plugins/modules/test_hello.py
@@ -0,0 +1,8 @@
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from .....plugins.modules.hello import say_hello
+
+
+def test_say_hello():
+ assert say_hello('Ansibull') == dict(message='Hello Ansibull')
diff --git a/test/integration/targets/ansible-test-units/runme.sh b/test/integration/targets/ansible-test-units/runme.sh
new file mode 100755
index 0000000..3ad4ea6
--- /dev/null
+++ b/test/integration/targets/ansible-test-units/runme.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+source ../collection/setup.sh
+
+set -x
+
+options=$("${TEST_DIR}"/../ansible-test/venv-pythons.py)
+IFS=', ' read -r -a pythons <<< "${options}"
+
+ansible-test units --color --truncate 0 "${pythons[@]}" "${@}"
diff --git a/test/integration/targets/ansible-test-unsupported-directory/aliases b/test/integration/targets/ansible-test-unsupported-directory/aliases
new file mode 100644
index 0000000..7741d44
--- /dev/null
+++ b/test/integration/targets/ansible-test-unsupported-directory/aliases
@@ -0,0 +1,4 @@
+shippable/posix/group3 # runs in the distro test containers
+shippable/generic/group1 # runs in the default test container
+context/controller
+needs/target/collection
diff --git a/test/integration/targets/ansible-test-unsupported-directory/ansible_collections/ns/col/.keep b/test/integration/targets/ansible-test-unsupported-directory/ansible_collections/ns/col/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/ansible-test-unsupported-directory/ansible_collections/ns/col/.keep
diff --git a/test/integration/targets/ansible-test-unsupported-directory/runme.sh b/test/integration/targets/ansible-test-unsupported-directory/runme.sh
new file mode 100755
index 0000000..087177c
--- /dev/null
+++ b/test/integration/targets/ansible-test-unsupported-directory/runme.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+
+source ../collection/setup.sh
+
+set -eux
+
+cd "${WORK_DIR}"
+
+# some options should succeed even in an unsupported directory
+ansible-test --help
+ansible-test --version
+
+# the --help option should show the current working directory when it is unsupported
+ansible-test --help 2>&1 | grep '^Current working directory: '
+
+# some shell commands also work without a supported directory
+ansible-test shell pwd
+
+if ansible-test sanity 1>stdout 2>stderr; then
+ echo "ansible-test did not fail"
+ exit 1
+fi
+
+grep '^Current working directory: ' stderr
+
+if grep raise stderr; then
+ echo "ansible-test failed with a traceback instead of an error message"
+ exit 2
+fi
diff --git a/test/integration/targets/ansible-test/aliases b/test/integration/targets/ansible-test/aliases
new file mode 100644
index 0000000..136c05e
--- /dev/null
+++ b/test/integration/targets/ansible-test/aliases
@@ -0,0 +1 @@
+hidden
diff --git a/test/integration/targets/ansible-test/venv-pythons.py b/test/integration/targets/ansible-test/venv-pythons.py
new file mode 100755
index 0000000..b380f14
--- /dev/null
+++ b/test/integration/targets/ansible-test/venv-pythons.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+"""Return target Python options for use with ansible-test."""
+
+import os
+import shutil
+import subprocess
+import sys
+
+from ansible import release
+
+
+def main():
+ ansible_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(release.__file__))))
+ source_root = os.path.join(ansible_root, 'test', 'lib')
+
+ sys.path.insert(0, source_root)
+
+ from ansible_test._internal import constants
+
+ args = []
+
+ for python_version in constants.SUPPORTED_PYTHON_VERSIONS:
+ executable = shutil.which(f'python{python_version}')
+
+ if executable:
+ if python_version.startswith('2.'):
+ cmd = [executable, '-m', 'virtualenv', '--version']
+ else:
+ cmd = [executable, '-m', 'venv', '--help']
+
+ process = subprocess.run(cmd, capture_output=True, check=False)
+
+ print(f'{executable} - {"fail" if process.returncode else "pass"}', file=sys.stderr)
+
+ if not process.returncode:
+ args.extend(['--target-python', f'venv/{python_version}'])
+
+ print(' '.join(args))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/ansible-vault/aliases b/test/integration/targets/ansible-vault/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/ansible-vault/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/ansible-vault/empty-password b/test/integration/targets/ansible-vault/empty-password
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/ansible-vault/empty-password
diff --git a/test/integration/targets/ansible-vault/encrypted-vault-password b/test/integration/targets/ansible-vault/encrypted-vault-password
new file mode 100644
index 0000000..7aa4e4b
--- /dev/null
+++ b/test/integration/targets/ansible-vault/encrypted-vault-password
@@ -0,0 +1,6 @@
+$ANSIBLE_VAULT;1.1;AES256
+34353166613539646338666531633061646161663836373965663032313466613135313130383133
+3634383331386336333436323832356264343033323166370a323737396234376132353731643863
+62386335616635363062613562666561643931626332623464306666636131356134386531363533
+3831323230353333620a616633376363373830346332663733316634663937336663633631326361
+62343638656532393932643530633133326233316134383036316333373962626164
diff --git a/test/integration/targets/ansible-vault/encrypted_file_encrypted_var_password b/test/integration/targets/ansible-vault/encrypted_file_encrypted_var_password
new file mode 100644
index 0000000..57bc06e
--- /dev/null
+++ b/test/integration/targets/ansible-vault/encrypted_file_encrypted_var_password
@@ -0,0 +1 @@
+test-encrypted-file-password
diff --git a/test/integration/targets/ansible-vault/example1_password b/test/integration/targets/ansible-vault/example1_password
new file mode 100644
index 0000000..e723c8f
--- /dev/null
+++ b/test/integration/targets/ansible-vault/example1_password
@@ -0,0 +1 @@
+example1
diff --git a/test/integration/targets/ansible-vault/example2_password b/test/integration/targets/ansible-vault/example2_password
new file mode 100644
index 0000000..7b010f8
--- /dev/null
+++ b/test/integration/targets/ansible-vault/example2_password
@@ -0,0 +1 @@
+example2
diff --git a/test/integration/targets/ansible-vault/example3_password b/test/integration/targets/ansible-vault/example3_password
new file mode 100644
index 0000000..f5bc5a8
--- /dev/null
+++ b/test/integration/targets/ansible-vault/example3_password
@@ -0,0 +1 @@
+example3
diff --git a/test/integration/targets/ansible-vault/faux-editor.py b/test/integration/targets/ansible-vault/faux-editor.py
new file mode 100755
index 0000000..b67c747
--- /dev/null
+++ b/test/integration/targets/ansible-vault/faux-editor.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+#
+# ansible-vault is a script that encrypts/decrypts YAML files. See
+# https://docs.ansible.com/ansible/latest/user_guide/vault.html for more details.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import sys
+import time
+import os
+
+
+def main(args):
+ path = os.path.abspath(args[1])
+
+ fo = open(path, 'r+')
+
+ content = fo.readlines()
+
+ content.append('faux editor added at %s\n' % time.time())
+
+ fo.seek(0)
+ fo.write(''.join(content))
+ fo.close()
+
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[:]))
diff --git a/test/integration/targets/ansible-vault/files/test_assemble/nonsecret.txt b/test/integration/targets/ansible-vault/files/test_assemble/nonsecret.txt
new file mode 100644
index 0000000..320b6b4
--- /dev/null
+++ b/test/integration/targets/ansible-vault/files/test_assemble/nonsecret.txt
@@ -0,0 +1 @@
+THIS IS OK
diff --git a/test/integration/targets/ansible-vault/files/test_assemble/secret.vault b/test/integration/targets/ansible-vault/files/test_assemble/secret.vault
new file mode 100644
index 0000000..fd27856
--- /dev/null
+++ b/test/integration/targets/ansible-vault/files/test_assemble/secret.vault
@@ -0,0 +1,7 @@
+$ANSIBLE_VAULT;1.1;AES256
+37626439373465656332623633333336353334326531333666363766303339336134313136616165
+6561333963343739386334653636393363396366396338660a663537666561643862343233393265
+33336436633864323935356337623861663631316530336532633932623635346364363338363437
+3365313831366365350a613934313862313538626130653539303834656634353132343065633162
+34316135313837623735653932663139353164643834303534346238386435373832366564646236
+3461333465343434666639373432366139363566303564643066
diff --git a/test/integration/targets/ansible-vault/format_1_1_AES256.yml b/test/integration/targets/ansible-vault/format_1_1_AES256.yml
new file mode 100644
index 0000000..5616605
--- /dev/null
+++ b/test/integration/targets/ansible-vault/format_1_1_AES256.yml
@@ -0,0 +1,6 @@
+$ANSIBLE_VAULT;1.1;AES256
+33613463343938323434396164663236376438313435633837336438366530666431643031333734
+6463646538393331333239393363333830613039376562360a396635393636636539346332336364
+35303039353164386461326439346165656463383137663932323930666632326263636266656461
+3232663537653637640a643166666232633936636664376435316664656631633166323237356163
+6138
diff --git a/test/integration/targets/ansible-vault/format_1_2_AES256.yml b/test/integration/targets/ansible-vault/format_1_2_AES256.yml
new file mode 100644
index 0000000..1e3795f
--- /dev/null
+++ b/test/integration/targets/ansible-vault/format_1_2_AES256.yml
@@ -0,0 +1,6 @@
+$ANSIBLE_VAULT;1.2;AES256;test_vault_id
+30383835613535356232333534303264656530633664616233386138396563623939626136366537
+3635323530646538626138383136636437616637616430610a386661346563346136326637656461
+64393364343964633364336666333630383164643662343930663432316333633537353938376437
+6134656262373731390a363166356461376663313532343733326438386632623930313366643038
+6133
diff --git a/test/integration/targets/ansible-vault/host_vars/myhost.yml b/test/integration/targets/ansible-vault/host_vars/myhost.yml
new file mode 100644
index 0000000..1434ec1
--- /dev/null
+++ b/test/integration/targets/ansible-vault/host_vars/myhost.yml
@@ -0,0 +1,7 @@
+myvar: !vault |
+ $ANSIBLE_VAULT;1.1;AES256
+ 31356335363836383937363933366135623233343830326234633633623734336636343630396464
+ 3234343638313166663237343536646336323862613739380a346266316336356230643838663031
+ 34623034383639323062373235356564393337346666393665313237313231306131356637346537
+ 3966393238666430310a363462326639323033653237373036643936613234623063643761663033
+ 3832
diff --git a/test/integration/targets/ansible-vault/host_vars/testhost.yml b/test/integration/targets/ansible-vault/host_vars/testhost.yml
new file mode 100644
index 0000000..b3e569a
--- /dev/null
+++ b/test/integration/targets/ansible-vault/host_vars/testhost.yml
@@ -0,0 +1,7 @@
+vaulted_utf8_value: !vault |
+ $ANSIBLE_VAULT;1.1;AES256
+ 39313961356631343234656136636231663539363963386364653436346133366366633031366364
+ 3332376636333837333036633662316135383365343335380a393331663434663238666537343163
+ 62363561336431623666633735313766613663333736653064373632666131356434336537383336
+ 3333343436613232330a643461363831633166333237653530353131316361643465353132616362
+ 3461
diff --git a/test/integration/targets/ansible-vault/invalid_format/README.md b/test/integration/targets/ansible-vault/invalid_format/README.md
new file mode 100644
index 0000000..cbbc07a
--- /dev/null
+++ b/test/integration/targets/ansible-vault/invalid_format/README.md
@@ -0,0 +1 @@
+Based on https://github.com/yves-vogl/ansible-inline-vault-issue
diff --git a/test/integration/targets/ansible-vault/invalid_format/broken-group-vars-tasks.yml b/test/integration/targets/ansible-vault/invalid_format/broken-group-vars-tasks.yml
new file mode 100644
index 0000000..71dbacc
--- /dev/null
+++ b/test/integration/targets/ansible-vault/invalid_format/broken-group-vars-tasks.yml
@@ -0,0 +1,23 @@
+---
+- hosts: broken-group-vars
+ gather_facts: false
+ tasks:
+ - name: EXPECTED FAILURE
+ debug:
+ msg: "some_var_that_fails: {{ some_var_that_fails }}"
+
+ - name: EXPECTED FAILURE Display hostvars
+ debug:
+ msg: "{{inventory_hostname}} hostvars: {{ hostvars[inventory_hostname] }}"
+
+
+# ansible-vault --vault-password-file=vault-secret encrypt_string test
+# !vault |
+# $ANSIBLE_VAULT;1.1;AES256
+# 64323332393930623633306662363165386332376638653035356132646165663632616263653366
+# 6233383362313531623238613461323861376137656265380a366464663835633065616361636231
+# 39653230653538366165623664326661653135306132313730393232343432333635326536373935
+# 3366323866663763660a323766383531396433663861656532373663373134376263383263316261
+# 3137
+
+# $ ansible-playbook -i inventory --vault-password-file=vault-secret tasks.yml
diff --git a/test/integration/targets/ansible-vault/invalid_format/broken-host-vars-tasks.yml b/test/integration/targets/ansible-vault/invalid_format/broken-host-vars-tasks.yml
new file mode 100644
index 0000000..9afbd58
--- /dev/null
+++ b/test/integration/targets/ansible-vault/invalid_format/broken-host-vars-tasks.yml
@@ -0,0 +1,7 @@
+---
+- hosts: broken-host-vars
+ gather_facts: false
+ tasks:
+ - name: EXPECTED FAILURE Display hostvars
+ debug:
+ msg: "{{inventory_hostname}} hostvars: {{ hostvars[inventory_hostname] }}"
diff --git a/test/integration/targets/ansible-vault/invalid_format/group_vars/broken-group-vars.yml b/test/integration/targets/ansible-vault/invalid_format/group_vars/broken-group-vars.yml
new file mode 100644
index 0000000..5f47743
--- /dev/null
+++ b/test/integration/targets/ansible-vault/invalid_format/group_vars/broken-group-vars.yml
@@ -0,0 +1,8 @@
+$ANSIBLE_VAULT;1.1;AES256
+64306566356165343030353932383461376334336665626135343932356431383134306338353664
+6435326361306561633165633536333234306665346437330a366265346466626464396264393262
+34616366626565336637653032336465363165363334356535353833393332313239353736623237
+6434373738633039650a353435303366323139356234616433613663626334643939303361303764
+3636363333333333333333333
+36313937643431303637353931366363643661396238303530323262326334343432383637633439
+6365373237336535353661356430313965656538363436333836
diff --git a/test/integration/targets/ansible-vault/invalid_format/host_vars/broken-host-vars.example.com/vars b/test/integration/targets/ansible-vault/invalid_format/host_vars/broken-host-vars.example.com/vars
new file mode 100644
index 0000000..2d309eb
--- /dev/null
+++ b/test/integration/targets/ansible-vault/invalid_format/host_vars/broken-host-vars.example.com/vars
@@ -0,0 +1,11 @@
+---
+example_vars:
+ some_key:
+ another_key: some_value
+ bad_vault_dict_key: !vault |
+ $ANSIBLE_VAULT;1.1;AES256
+ 64323332393930623633306662363165386332376638653035356132646165663632616263653366
+ 623338xyz2313531623238613461323861376137656265380a366464663835633065616361636231
+ 3366323866663763660a323766383531396433663861656532373663373134376263383263316261
+ 3137
+
diff --git a/test/integration/targets/ansible-vault/invalid_format/inventory b/test/integration/targets/ansible-vault/invalid_format/inventory
new file mode 100644
index 0000000..e6e259a
--- /dev/null
+++ b/test/integration/targets/ansible-vault/invalid_format/inventory
@@ -0,0 +1,5 @@
+[broken-group-vars]
+broken.example.com
+
+[broken-host-vars]
+broken-host-vars.example.com
diff --git a/test/integration/targets/ansible-vault/invalid_format/original-broken-host-vars b/test/integration/targets/ansible-vault/invalid_format/original-broken-host-vars
new file mode 100644
index 0000000..6be696b
--- /dev/null
+++ b/test/integration/targets/ansible-vault/invalid_format/original-broken-host-vars
@@ -0,0 +1,6 @@
+$ANSIBLE_VAULT;1.1;AES256
+64323332393930623633306662363165386332376638653035356132646165663632616263653366
+6233383362313531623238613461323861376137656265380a366464663835633065616361636231
+3366323866663763660a323766383531396433663861656532373663373134376263383263316261
+3137
+
diff --git a/test/integration/targets/ansible-vault/invalid_format/original-group-vars.yml b/test/integration/targets/ansible-vault/invalid_format/original-group-vars.yml
new file mode 100644
index 0000000..817557b
--- /dev/null
+++ b/test/integration/targets/ansible-vault/invalid_format/original-group-vars.yml
@@ -0,0 +1,2 @@
+---
+some_var_that_fails: blippy
diff --git a/test/integration/targets/ansible-vault/invalid_format/some-vars b/test/integration/targets/ansible-vault/invalid_format/some-vars
new file mode 100644
index 0000000..e841a26
--- /dev/null
+++ b/test/integration/targets/ansible-vault/invalid_format/some-vars
@@ -0,0 +1,6 @@
+$ANSIBLE_VAULT;1.1;AES256
+37303462633933386339386465613039363964643466663866356261313966663465646262636333
+3965643566363764356563363334363431656661636634380a333837343065326239336639373238
+64316236383836383434366662626339643561616630326137383262396331396538363136323063
+6236616130383264620a613863373631316234656236323332633166623738356664353531633239
+3533
diff --git a/test/integration/targets/ansible-vault/invalid_format/vault-secret b/test/integration/targets/ansible-vault/invalid_format/vault-secret
new file mode 100644
index 0000000..4406e35
--- /dev/null
+++ b/test/integration/targets/ansible-vault/invalid_format/vault-secret
@@ -0,0 +1 @@
+enemenemu \ No newline at end of file
diff --git a/test/integration/targets/ansible-vault/inventory.toml b/test/integration/targets/ansible-vault/inventory.toml
new file mode 100644
index 0000000..d97ed39
--- /dev/null
+++ b/test/integration/targets/ansible-vault/inventory.toml
@@ -0,0 +1,5 @@
+[vauled_group.hosts]
+vaulted_host_toml={ ansible_host="localhost", ansible_connection="local" }
+
+[vauled_group.vars]
+hello="world"
diff --git a/test/integration/targets/ansible-vault/password-script.py b/test/integration/targets/ansible-vault/password-script.py
new file mode 100755
index 0000000..1b7f02b
--- /dev/null
+++ b/test/integration/targets/ansible-vault/password-script.py
@@ -0,0 +1,33 @@
+#!/usr/bin/env python
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+#
+# ansible-vault is a script that encrypts/decrypts YAML files. See
+# https://docs.ansible.com/ansible/latest/user_guide/vault.html for more details.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import sys
+
+PASSWORD = 'test-vault-password'
+
+
+def main(args):
+ print(PASSWORD)
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[:]))
diff --git a/test/integration/targets/ansible-vault/realpath.yml b/test/integration/targets/ansible-vault/realpath.yml
new file mode 100644
index 0000000..6679635
--- /dev/null
+++ b/test/integration/targets/ansible-vault/realpath.yml
@@ -0,0 +1,10 @@
+- hosts: localhost
+ gather_facts: false
+ vars_files:
+ - vaulted.yml
+ tasks:
+ - name: see if we can decrypt
+ assert:
+ that:
+ - control is defined
+ - realpath == 'this is a secret'
diff --git a/test/integration/targets/ansible-vault/roles/test_vault/tasks/main.yml b/test/integration/targets/ansible-vault/roles/test_vault/tasks/main.yml
new file mode 100644
index 0000000..4e5551d
--- /dev/null
+++ b/test/integration/targets/ansible-vault/roles/test_vault/tasks/main.yml
@@ -0,0 +1,9 @@
+- assert:
+ that:
+ - 'secret_var == "secret"'
+
+
+- copy: src=vault-secret.txt dest={{output_dir}}/secret.txt
+
+- name: cleanup decrypted file
+ file: path={{ output_dir }}/secret.txt state=absent
diff --git a/test/integration/targets/ansible-vault/roles/test_vault/vars/main.yml b/test/integration/targets/ansible-vault/roles/test_vault/vars/main.yml
new file mode 100644
index 0000000..cfac107
--- /dev/null
+++ b/test/integration/targets/ansible-vault/roles/test_vault/vars/main.yml
@@ -0,0 +1,9 @@
+$ANSIBLE_VAULT;1.1;AES256
+31626536666232643662346539623662393436386162643439643434656231343435653936343235
+6139346364396166336636383734333430373763336434310a303137623539653939336132626234
+64613232396532313731313935333433353330666466646663303233323331636234326464643166
+6538653264636166370a613161313064653566323037393962643032353230396536313865326362
+34396262303130326632623162623230346238633932393938393766313036643835613936356233
+33323730373331386337353339613165373064323134343930333031623036326164353534646631
+31313963666234623731316238656233396638643331306231373539643039383434373035306233
+30386230363730643561
diff --git a/test/integration/targets/ansible-vault/roles/test_vault_embedded/tasks/main.yml b/test/integration/targets/ansible-vault/roles/test_vault_embedded/tasks/main.yml
new file mode 100644
index 0000000..eba9389
--- /dev/null
+++ b/test/integration/targets/ansible-vault/roles/test_vault_embedded/tasks/main.yml
@@ -0,0 +1,13 @@
+---
+- name: Assert that a embedded vault of a string with no newline works
+ assert:
+ that:
+ - '"{{ vault_encrypted_one_line_var }}" == "Setec Astronomy"'
+
+- name: Assert that a multi line embedded vault works, including new line
+ assert:
+ that:
+ - vault_encrypted_var == "Setec Astronomy\n"
+
+# TODO: add a expected fail here
+# - debug: var=vault_encrypted_one_line_var_with_embedded_template
diff --git a/test/integration/targets/ansible-vault/roles/test_vault_embedded/vars/main.yml b/test/integration/targets/ansible-vault/roles/test_vault_embedded/vars/main.yml
new file mode 100644
index 0000000..54e6004
--- /dev/null
+++ b/test/integration/targets/ansible-vault/roles/test_vault_embedded/vars/main.yml
@@ -0,0 +1,17 @@
+# If you use normal 'ansible-vault create' or edit, files always have at least one new line
+# so c&p from a vault encrypted that wasn't specifically created sans new line ends up with one.
+# (specifically created, as in 'echo -n "just one line" > my_secret.yml'
+vault_encrypted_var: !vault |
+ $ANSIBLE_VAULT;1.1;AES256
+ 66386439653236336462626566653063336164663966303231363934653561363964363833313662
+ 6431626536303530376336343832656537303632313433360a626438346336353331386135323734
+ 62656361653630373231613662633962316233633936396165386439616533353965373339616234
+ 3430613539666330390a313736323265656432366236633330313963326365653937323833366536
+ 34623731376664623134383463316265643436343438623266623965636363326136
+vault_encrypted_one_line_var: !vault |
+ $ANSIBLE_VAULT;1.1;AES256
+ 33363965326261303234626463623963633531343539616138316433353830356566396130353436
+ 3562643163366231316662386565383735653432386435610a306664636137376132643732393835
+ 63383038383730306639353234326630666539346233376330303938323639306661313032396437
+ 6233623062366136310a633866373936313238333730653739323461656662303864663666653563
+ 3138
diff --git a/test/integration/targets/ansible-vault/roles/test_vault_embedded_ids/tasks/main.yml b/test/integration/targets/ansible-vault/roles/test_vault_embedded_ids/tasks/main.yml
new file mode 100644
index 0000000..9aeaf24
--- /dev/null
+++ b/test/integration/targets/ansible-vault/roles/test_vault_embedded_ids/tasks/main.yml
@@ -0,0 +1,29 @@
+---
+- name: set a fact from vault_encrypted_example1_releases
+ set_fact:
+ example1_releases: "{{ vault_encrypted_example1_releases }}"
+
+- name: Assert that a embedded vault of a multiline string with a vault id works
+ assert:
+ that:
+ - "vault_encrypted_example1_releases is defined"
+ - "example1_releases is defined"
+ - "example1_releases.startswith('Ansible Releases')"
+ # - '"{{ vault_encrypted_example1_releases }}" == "Setec Astronomy"'
+
+- name: Assert that a embedded vault with a different vault id works
+ assert:
+ that:
+ - "vault_encrypted_example2_hello == 'Hello world'"
+
+- name: Assert that a embedded vault with no vault id and format 1.2 works
+ assert:
+ that:
+ - "vault_encrypted_example3_foobar == 'Foobar'"
+ #- name: Assert that a multi line embedded vault works, including new line
+ # assert:
+ # that:
+ # - vault_encrypted_var == "Setec Astronomy\n"
+
+# TODO: add a expected fail here
+# - debug: var=vault_encrypted_one_line_var_with_embedded_template
diff --git a/test/integration/targets/ansible-vault/roles/test_vault_embedded_ids/vars/main.yml b/test/integration/targets/ansible-vault/roles/test_vault_embedded_ids/vars/main.yml
new file mode 100644
index 0000000..9c8fa4b
--- /dev/null
+++ b/test/integration/targets/ansible-vault/roles/test_vault_embedded_ids/vars/main.yml
@@ -0,0 +1,194 @@
+vault_encrypted_example2_hello: !vault |
+ $ANSIBLE_VAULT;1.2;AES256;example2
+ 30383930326535616363383537613266376364323738313835353566633533353364363837383638
+ 3737633764613862343666346337353964613138653036610a313663393231386139343835626436
+ 66633336303866323335616661366363333463616530326635383836656432396665313338313737
+ 6539616630663262650a383762303362356438616261646564303230633930336563373566623235
+ 3566
+vault_encrypted_example1_releases: !vault |
+ $ANSIBLE_VAULT;1.2;AES256;example1
+ 63643833646565393535303862343135326261343362396234656137313731313864316539616462
+ 3333313439353638393963643535633835643035383331340a393639386166313838326336363032
+ 65396565616531663839316132646230316561613865333437653666323034396337626431663931
+ 3339363233356438350a363734616337306136376139346162376334343537613032633563666361
+ 36386437356463616563646336393064626131363963643434376439346331663836663961353533
+ 62343663623863663830663531663930636532653165636238636433373835623435313632313030
+ 33333734343566393739393661383430623063323132303132306361666433386166633564626434
+ 62666361653465616636646335353230373961393863373261633461303233313965346565643434
+ 63383633303131643730366233383264373865376562623962636562343732343266636535356362
+ 62396635613231336162393630343136663731366665623835303762636161393163373361383634
+ 65333739326264346136333337363666396336353065366161316130653738356133646364316130
+ 32346636386665633131376662356238386161373565336430623263353036323561633235303135
+ 35333031316366373636326665656230343934383334303863643364613364663436383030373237
+ 35323964376564313636643633303262633033363633663966393535613064343364313161383061
+ 66393733366463393936663033633038653465636539356266353936373162303661613962393662
+ 61313534643064366432333166666130663730653333613964316130363135646532303531376537
+ 63313339623337363464343637323431336438636337386264303961333139326666306365363937
+ 36386437343036346165366439636533666237393535316536333966376536623030643663343561
+ 64626362363736316234356639663039396634653766646237376636653062383530366562323138
+ 61343537616263373137613232393731363866653038633932643163633732326463656365346535
+ 63316337346636326631326134633339363133393337393035333730663133646332343536636337
+ 36626566633162333463613735656564393764356337346535646539373536363933326139626239
+ 35386434663636343366303830663531616530616563343737653761616232303865626634646537
+ 38383430366131396133636530383865356430343965633062373366383261383231663162323566
+ 30373061366533643938383363333266636463383134393264343662623465323164356464666364
+ 35636135316333636266313038613239616638343761326332663933356164323635653861346430
+ 65616661353162633765666633393139613830626535633462633166376563313236623465626339
+ 38663138633664613738656166356431343438653833623132383330656637343661616432623362
+ 66643466343663306434353237343737633535343233653765356134373739316234353836303034
+ 37336435376135363362323130316338316135633633303861303665393766616537356666653238
+ 63366461383334356666633134616436663731633666323261393761363264333430366234353732
+ 66333732373236303338333862626537326638393964363965303532353465613638393934313538
+ 66323366353064666334626461313933333961613637663332656131383038393264636537643730
+ 35626265346363393665663431663036633461613362343330643133333232326664623833626336
+ 65353363373962383561396163653361663736383235376661626132386131353137303764623231
+ 63326538623231396366356432663537333331343335633531326331616531313039393335313139
+ 65376461323434383065383834626535393063363432326233383930626437343961313538303135
+ 39386561623662333335313661636637656336353537313466386239613166396436626630376337
+ 36633739326336366530643733393962633737343035346536366336643266346162333931633235
+ 66643966626262343862393832663132356435343561646634373835306130623637633836633166
+ 30313732333963383565373261306232663365363033376431313437326366656264346532666561
+ 63386231636634613235333363326166616238613734643739343237303963663539633535356232
+ 66393365616165393130356561363733313735336132336166353839303230643437643165353338
+ 39663138313130366635386365663830336365646562666635323361373362626339306536313664
+ 32383934623533373361666536326131316630616661623839666137656330306433326637386134
+ 34393162343535633438643036613831303265646632383231306239646132393338663564653939
+ 63613232646230616338316434376663613266303362386631353733623335643034356631383139
+ 62613932396132636339393337383065613061306162633831386236323163633439303263393663
+ 38616237313761306533636361386161666264333839616463386631633233343132373732636639
+ 61326239383961656437646236656336303638656665316633643630393063373964323534643961
+ 39383538303234343438363736373136316464643165383361336262303231353937316432366639
+ 36613662393736386433356532626162643462313234316230643639333535653064303830373166
+ 31393332336539313362373136326639386566343637623633396134643533393839353934613064
+ 65396233353363393763363231633462663537626165646666633937343733653932633733313237
+ 31323633326463333938343062626361313761646133633865623130323665336634356364366566
+ 31626562373662313064306239356336376136306336643961323839313964393734343265306137
+ 62663563306665636463356465663432346331323832666163623530666265393164336466383936
+ 64653831316162313861373462643264373965623632653430373439656535636365383066643464
+ 61366436613631386161306631386331656632636337653864343261643433363438396361373831
+ 37363532346564343562356132306432303933643431636539303039306638356537353237323036
+ 63366334623438393838383561383937313330303832326330326366303264303437646666613638
+ 37653266633362636330656666303437323138346666373265663466616635326366313233323430
+ 62616165626239363833613565326264373063376232303837363062616663333461373062323266
+ 32626636316465666230626634396431323032323962313437323837336562313438346634656335
+ 33613566636461663334623966646465623531653631653565333836613261633534393439613738
+ 66356364383637666465336666333962393735643766633836383833396533626635633734326136
+ 65656562366337326161303466336232646533346135353332643030383433643662363465633931
+ 63323761623537383438333837333733363263663630336264376239336234663866633131376463
+ 66663438313439643565316138383439353839366365393238376439626537656535643739373237
+ 66666266366533393738363138613437666435366163643835383830643333323730303537313139
+ 32313436663932633933353265356431336138306437353936363638643539383236323232326630
+ 62323963626138633865376238666264666531613237636232373938303030393632643230336138
+ 38663237646637616232343664396136376534313533613364663062356535313766343331616431
+ 36616237336532333239386663643538643239613866393631393364306463303131643863363533
+ 31356436373062666266656431643038323766383632613939616539663637623164323161633464
+ 39666663353339383164363534616330323936333865663564646334373438303061656662656331
+ 37633530663666323834383333623136633164326632313938643234326235616461323734353638
+ 63393365313334646538373631643266383936333533383630623861343764373863346161316333
+ 38356466626234653336326433353234613430623135343739323433326435373663363237643531
+ 36626238613832633661343263383962373536353766653631323431393330623634656166333437
+ 66376537643836626264383961303465363035336666306165316631316661366637303361656332
+ 36616463626135653235393562343464353262616331326539316361393036623134623361383635
+ 39383565313433653139663963306362373233313738613933626563333230656239613462363164
+ 65396539333833633137313163396635373433303164633463383935663939343266396366666231
+ 30353434323837343563613662643632386662616363646630353530386466643939623866626331
+ 63613266366135646562653064333166356561626138343364373631376336393931313262323063
+ 32653938333837366231343865656239353433663537313763376132613366363333313137323065
+ 31666663656539333438343664323062323238353061663439326333366162303636626634313037
+ 38366631306438393333356138393730316161336233656239626565366134643535383536613034
+ 37343733663631663863643337373462633462666234393063336330306465366637653136393533
+ 63336535316438303564613366343565363831666233626466623161356635363464343634303136
+ 61616561393861393036353433356364376533656334326433323934643236346133363535613334
+ 32626332653362313731643035653335383164303534616537333132356535376233343566313736
+ 39353037636530376338383739366230346134643738313037386438613461323934663537666164
+ 66353330303730336435313735343333316364373432313030396361343061343632653765646336
+ 39666537366537343635396235373433363438393637663166666530356339316334313834363938
+ 33393837336265353265303635663363353439343062316363643637623564353261643637306434
+ 36393662363737316234323461373763663364356535313165656661613137396366386464663866
+ 63653562313539313839613436653137663262346233626464616237373737373736306231383265
+ 35323532373631613762616234386162643035613838376264343532396263626562623262363532
+ 36303530353137616134346262646464633462646662323262633366393736383834616665666466
+ 34393363353135616437346332386634396635363130623337653230666334303630653738633334
+ 33316162326335373838643261656561303736363331316134363736393362313734346236306638
+ 65343163646264643539643635633761393665623039653232623435383062363462346336613238
+ 38306138353832306263356265316236303065626566643134373836303933323130303634393931
+ 31633334373064353263353135656433623863636261633664646439336539343636656464306531
+ 36373364323637393634623666353730626532613534343638663966313332636437383233303864
+ 33356432613638303936653134373338626261353662653930333534643732656130653636316433
+ 33653364373636613739353439383066646530303565383432356134396436306134643030643034
+ 63323433396238636330383836396364613738616338356563633565613537313138346661636164
+ 34333566393738343661663062346433396532613032663331313566333161396230343336346264
+ 66333935316630653936346336366336303363376633623034346536643731313136363835303964
+ 37346537373236343832306637653563386435363435333537393733333966643461623064316639
+ 65323363343338326435633631303037623234303334353366303936373664383762316364663036
+ 61353638376335333663343066303961616234336664313732366630343331613537633336316534
+ 31656561626430383338353231376263383362333966666363316435373533613138323039363463
+ 33363031373035316431353930626632666165376538303638353631303931326262386363376330
+ 36333531303235306532363763313233616165646234343235306332383262663261366164623130
+ 66613232636264636336313230303261626639316465383265373762346434616362383562633533
+ 64346438653161306266663634623666646239383363313862383563386461626264383165373561
+ 64383431653061393132623833653337643266663462666462366339363233353335386264383936
+ 38396264373833343935653264373631626662653962353438313262633339316537306463663930
+ 31613634613535346364643930613739383035336164303064653736663031633135613966656463
+ 64333539643534376662666539653766666532333832333430346333613236356534643964383135
+ 38326235626164663364366163353434613530306531343735353761396563326536636335326336
+ 34613835333362346363623235316564363934333732646435373033613863346565353034306333
+ 33643763363838656339396435316162616539623764366163376438656266353137633262613464
+ 31393434646435623032383934373262666430616262353165343231666631666238653134396539
+ 32323137616639306262366638366536366665633331653363643234643238656338316133613166
+ 38343566623137353566306538616639363935303766633732633638356362373463616563663438
+ 66346133636562373031316363616662663132636263653037343962313630313535396563313230
+ 34613735663838613130346461343166663830623861393634353438376336363961326263333634
+ 34646465326238636630316164316339333961333939363139623262396531303665383230363562
+ 63626431333365663337323430653230613837396133636431303863366239303531653966653932
+ 65363139366637623531306333363465386636366334383734353330626566346532653263633238
+ 39383434346665323730366261316433303739313032653638636232666432323930653837643831
+ 63393565306538663365616364326334306333346463343330316161616362323063666666373035
+ 66383938383238353134386333343437623030363032303531643736353636643165373362363666
+ 31363037613064633164346638306231663161626265663535363634336665656163636637393161
+ 64313363373965396262386337613533393639353332316234643666613065343939393336366633
+ 64303637323531393936386365316366656432346230653066306334626431366335353130663233
+ 62303961663362623637303535333432313635303936363462336438663232333862303934383166
+ 31626438623963346262376135633434643533316162376633353661356463616538363733346464
+ 65646563626139356264363132616161303438653133353961636135333833376364333138353263
+ 36613437373365666665643664343666366234636164626437396139393864653031396331303938
+ 35323839646265393232326434616233323535396134346465363131366165373163353932363538
+ 39353764623463393732346134656539353966643366653765663038323631373432663839396239
+ 35623665623661326231643734346134623961663539363436323134333630306663653039653062
+ 36623730663538666166363436616131363233643739393966333437643637303737383733356138
+ 34343733623137326265343332326437316365346439316137663361373066333166383032396636
+ 35623561626139666264373363363965383633653633656464393932666634353962623637643262
+ 32323663303861376166656266653962643166326535363237316333663631323235333833636361
+ 31633038353265386439313766313966633536346230646566633333646632383938363761373363
+ 38353931343136633062303366643930323034616265653030643062333461616637366666336437
+ 36346330636666313833346534363461336366393533346338653061356333653839623364336266
+ 32373965346363613165383639366365396665353966393262393562353664623231326132363735
+ 38386238336135306464366332353035613938313262323739326638623733663030656533383438
+ 38316364393030376436313031613936363435633562633862323063643035383030313865396666
+ 66646338316262653734633431393862626633643163313732343638313066646163353264653531
+ 64346265656363323666656239333466313666373234626261633630653133316639313233303466
+ 62353735626634616661396238356138343064386332366361643530613364366365663764393037
+ 31613730313234393263653964376262373131383064393133636533656534343431613964663634
+ 65656365393439306433313333346234333332346230666462633132313863623765306665306461
+ 65633862656637646134353030393637353339646265613731646564333561313431346135626532
+ 66646363383932636562343731626164633138386463356634353062323965376235383130633231
+ 61623537333030383130623064356662356463646532613339303336666631366539613835646364
+ 37636634353430386632656331313936393261643638326162376238326139643939636333366364
+ 31626163376436336631
+vault_encrypted_example3_foobar: !vault |
+ $ANSIBLE_VAULT;1.1;AES256
+ 37336431373836376339373763306436396334623061366266353763363766313063363230636138
+ 3665663061366436306232323636376261303064616339620a333365323266643364396136626665
+ 62363862653134623665326635396563643832636234386266616436626334363839326434383431
+ 3330373333366233380a363431386334636164643936313430623661633265346632343331373866
+ 3732
+# We dont have a secret for this vaulttext, but nothing references it
+# so nothing should ever try to decrypt it. So this is testing that
+# we dont require all vaulted vars to be decrypted.
+vault_encrypted_example4_unknown_password: !vault |
+ $ANSIBLE_VAULT;1.1;AES256
+ 64316436303566666563393931613833316533346539373635663031376664366131353264366132
+ 3637623935356263643639313562366434383234633232660a353636666134353030646539643139
+ 65376235333932353531356666363434313066366161383532363166653762326533323233623431
+ 3934393962633637330a356337626634343736313339316365373239663031663938353063326665
+ 30643339386131663336366531663031383030313936356631613432336338313962
diff --git a/test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/README.md b/test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/README.md
new file mode 100644
index 0000000..4a75cec
--- /dev/null
+++ b/test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/README.md
@@ -0,0 +1 @@
+file is encrypted with password of 'test-encrypted-file-password'
diff --git a/test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml b/test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml
new file mode 100644
index 0000000..e09004a
--- /dev/null
+++ b/test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml
@@ -0,0 +1,13 @@
+---
+- name: Assert that a vault encrypted file with embedded vault of a string with no newline works
+ assert:
+ that:
+ - '"{{ vault_file_encrypted_with_encrypted_one_line_var }}" == "Setec Astronomy"'
+
+- name: Assert that a vault encrypted file with multi line embedded vault works, including new line
+ assert:
+ that:
+ - vault_file_encrypted_with_encrypted_var == "Setec Astronomy\n"
+
+# TODO: add a expected fail here
+# - debug: var=vault_encrypted_one_line_var_with_embedded_template
diff --git a/test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/vars/main.yml b/test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/vars/main.yml
new file mode 100644
index 0000000..89cc4a0
--- /dev/null
+++ b/test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/vars/main.yml
@@ -0,0 +1,76 @@
+$ANSIBLE_VAULT;1.1;AES256
+31613535653961393639346266636234373833316530373965356161373735666662613137386466
+3365303539306132613861646362396161323962373839640a653030376530316136643961623665
+65643665616338363432383264363730386538353635663339633932353933653132343430613332
+6136663837306333370a643139336230663465346637663032613231656364316533613235623532
+65643738663735636662363565313561646162343865393733663838393239646634633936336262
+39626235616537663934363932323831376539666331353334386636663738643932306239663265
+64646664616331643663326561386638393764313737303865326166373031336665663533373431
+35353736346264616135656164636337363966323935643032646138366166636537333565306230
+65646533623134393633623663336263393533613632663464653663313835306265333139646563
+35393061343266343138333936646364333735373930666262376137396562356231393330313731
+36363164623939393436363564353162373364626536376434626463343161646437316665613662
+38343534363965373735316339643061333931666264353566316235616433666536313065306132
+31623933633533366162323961343662323364353065316235303162306635663435663066393865
+64356634363761333838326331343865653633396665353638633730663134313565653166656131
+33366464396532313635326237363135316230663838393030303963616161393966393836633237
+30333338343031366235396438663838633136666563646161363332663533626662663531653439
+63643435383931663038613637346637383365336431646663366436626333313536396135636566
+31373133363661636338376166356664353366343730373164663361623338383636336464373038
+36306437363139346233623036636330333664323165636538666138306465653435666132623835
+30363266333666626363366465313165643761396562653761313764616562666439366437623766
+33343666623866653461376137353731356530363732386261383863666439333735666638653533
+38393430323961356333383464643036383739663064633461363937336538373539666662653764
+36376266333230666232396665616434303432653562353131383430643533623932363537346435
+33326335663561643564663936323832376634336363373531363666333732643363646130383464
+30656366633863643966656134653833343634383136363539366330336261313736343838663936
+39333835353035386664633331303264356339613933393162393037306565636563386436633532
+34376564343237303166613461383963353030383166326538643932323130643830376165366564
+30366432623761623366653966313865653262363064316130393339393366323539373338306265
+31626564393065303032383161343137636432353061333964613935363865356139313766303039
+32333863353465306265653237396232383330333438303866316362353161383266316633663364
+66353130326237376331656334633965633339303138656263616239323261663864666236323662
+33643463303965313264396463333963376464313838373765633463396534363836366132653437
+30303132633232623265303966316639373664656262636166653438323534326435363966616133
+33663463626536643930623034343237613933623462346635306565623834346532613539383838
+39356339303930663739333236316234666633623961323362323537313833383538363132636165
+31396433386664356532383432666464613137376561396534316665386134333665626430373064
+30626561363731326635393334633837303934653062616461303732316239663764633565353633
+33336161623332383064376538353531343534333836313139376439316564313436623462396134
+31643831656135653234396362653861643933346433646633383130323139353465616430383061
+34623164376436326466333765353037323630356662646364366265303534313764393862653238
+66376365323561643030343534636263386338333566613436383630613561646639616265313465
+66336239303432666361383038323038383663346561356664626634333037313838363732643463
+33373734663933373238363635623336323232313161353861306430323334353836616265623639
+65613436323939643932383537666530306134633435373331623963633436386162306565656433
+35383962633163643837343436383664313565656134646633393237353065666535316561613266
+64653234366462623764313438666466616664303138656565663036376230323763393135323330
+35383861306262356430656531343938643763306663323031636638383762626564616366393434
+33373035363633396230396161623433336530326432343666346332613262376338313731626462
+63616463363831333239643535383936646264336466616635353063383163306564373263656265
+65383466653162626132633463613037343865316639653931633965323637373733653131666233
+35643831646638383232616538656265663365306136343733633535323537653165636665383832
+65303162656238303665346232353136346639316263636264346533356263353066353438323535
+36303236326663303763653137656264336566646161663538383361306138323064336235616438
+32373731643331373239383339326365366337646237643836373238656339646362366239623533
+33306531353863653834666361393161366465626632643061363266353465653964363263613430
+32323132613866343733376437643239316661313330323661633234343630626132383434343461
+61663765383134666330316237633963323463363762383666323866386336316438373461306138
+38613266346532313134386236386131626262663534313935623635343533383831386332343534
+65333963353861656232383134396438613034663333633661346465636436373533346561306661
+33656535613963663938313233333736343036393734373363316236373765343736633635386336
+30323036393431363636316466393561626365366333623431353435633963613935346239666534
+33623037306334343464633932313430616666633631313366356532643938333835333231313039
+65363734336630303861626636613139663130616362333662616532313734393636353963643032
+39626162623933616561383736636466316331346135613063383261373865366232376562316237
+65393563633131653761646365313831646265316233343833653363626465363863363936316664
+63363863363761353264316662643338656432356336326339623961396538643838666330303934
+62343537653262353737316266366134623961323637613338303164383734613034383964623135
+35646130363038356530383638663431663238336337313034303631366538326361646530626138
+34653533383964353866653562666463333961313434373063333163346537636631393138316465
+62656361613365366137346337363830356263633162623466373564346437653036386136333333
+32323863393866373932353534343133306333303265336564383132616365363439393364336562
+62333130343664343436356338623336643735373164373962313762333763343137626238316536
+36376539666331376162376361646631396231306165316362343164616232393864656161393735
+63313439643865346231346363376137306464396637356539353139343932333438323964323035
+326532383066643037653036333166346238
diff --git a/test/integration/targets/ansible-vault/roles/test_vaulted_template/tasks/main.yml b/test/integration/targets/ansible-vault/roles/test_vaulted_template/tasks/main.yml
new file mode 100644
index 0000000..b4af5ef
--- /dev/null
+++ b/test/integration/targets/ansible-vault/roles/test_vaulted_template/tasks/main.yml
@@ -0,0 +1,19 @@
+---
+- name: Template from a vaulted template file
+ template:
+ src: vaulted_template.j2
+ dest: "{{ output_dir }}/vaulted_template.out"
+ vars:
+ vaulted_template_var: "here_i_am"
+
+- name: Get output template contents
+ slurp:
+ path: "{{ output_dir }}/vaulted_template.out"
+ register: vaulted_template_out
+
+- debug:
+ msg: "{{ vaulted_template_out.content|b64decode }}"
+
+- assert:
+ that:
+ - vaulted_template_out.content|b64decode == 'here_i_am\n'
diff --git a/test/integration/targets/ansible-vault/roles/test_vaulted_template/templates/vaulted_template.j2 b/test/integration/targets/ansible-vault/roles/test_vaulted_template/templates/vaulted_template.j2
new file mode 100644
index 0000000..af9c3eb
--- /dev/null
+++ b/test/integration/targets/ansible-vault/roles/test_vaulted_template/templates/vaulted_template.j2
@@ -0,0 +1,6 @@
+$ANSIBLE_VAULT;1.1;AES256
+65626437623461633630303033303939616334373263633438623938396564376435366534303865
+6363663439346464336437346263343235626463663130640a373233623733653830306262376430
+31666538323132343039613537323761343234613531353035373434666632333932623064316564
+3532363462643736380a303136353830636635313662663065343066323631633562356663633536
+31343265376433633234656432393066393865613235303165666338663930303035
diff --git a/test/integration/targets/ansible-vault/runme.sh b/test/integration/targets/ansible-vault/runme.sh
new file mode 100755
index 0000000..50720ea
--- /dev/null
+++ b/test/integration/targets/ansible-vault/runme.sh
@@ -0,0 +1,576 @@
+#!/usr/bin/env bash
+
+set -euvx
+source virtualenv.sh
+
+
+MYTMPDIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
+trap 'rm -rf "${MYTMPDIR}"' EXIT
+
+# create a test file
+TEST_FILE="${MYTMPDIR}/test_file"
+echo "This is a test file" > "${TEST_FILE}"
+
+TEST_FILE_1_2="${MYTMPDIR}/test_file_1_2"
+echo "This is a test file for format 1.2" > "${TEST_FILE_1_2}"
+
+TEST_FILE_ENC_PASSWORD="${MYTMPDIR}/test_file_enc_password"
+echo "This is a test file for encrypted with a vault password that is itself vault encrypted" > "${TEST_FILE_ENC_PASSWORD}"
+
+TEST_FILE_ENC_PASSWORD_DEFAULT="${MYTMPDIR}/test_file_enc_password_default"
+echo "This is a test file for encrypted with a vault password that is itself vault encrypted using --encrypted-vault-id default" > "${TEST_FILE_ENC_PASSWORD_DEFAULT}"
+
+TEST_FILE_OUTPUT="${MYTMPDIR}/test_file_output"
+
+TEST_FILE_EDIT="${MYTMPDIR}/test_file_edit"
+echo "This is a test file for edit" > "${TEST_FILE_EDIT}"
+
+TEST_FILE_EDIT2="${MYTMPDIR}/test_file_edit2"
+echo "This is a test file for edit2" > "${TEST_FILE_EDIT2}"
+
+# test case for https://github.com/ansible/ansible/issues/35834
+# (being prompted for new password on vault-edit with no configured passwords)
+
+TEST_FILE_EDIT3="${MYTMPDIR}/test_file_edit3"
+echo "This is a test file for edit3" > "${TEST_FILE_EDIT3}"
+
+# ansible-config view
+ansible-config view
+
+# ansible-config
+ansible-config dump --only-changed
+ansible-vault encrypt "$@" --vault-id vault-password "${TEST_FILE_EDIT3}"
+# EDITOR=./faux-editor.py ansible-vault edit "$@" "${TEST_FILE_EDIT3}"
+EDITOR=./faux-editor.py ansible-vault edit --vault-id vault-password -vvvvv "${TEST_FILE_EDIT3}"
+echo $?
+
+# view the vault encrypted password file
+ansible-vault view "$@" --vault-id vault-password encrypted-vault-password
+
+# encrypt with a password from a vault encrypted password file and multiple vault-ids
+# should fail because we dont know which vault id to use to encrypt with
+ansible-vault encrypt "$@" --vault-id vault-password --vault-id encrypted-vault-password "${TEST_FILE_ENC_PASSWORD}" && :
+WRONG_RC=$?
+echo "rc was $WRONG_RC (5 is expected)"
+[ $WRONG_RC -eq 5 ]
+
+# try to view the file encrypted with the vault-password we didnt specify
+# to verify we didnt choose the wrong vault-id
+ansible-vault view "$@" --vault-id vault-password encrypted-vault-password
+
+FORMAT_1_1_HEADER="\$ANSIBLE_VAULT;1.1;AES256"
+FORMAT_1_2_HEADER="\$ANSIBLE_VAULT;1.2;AES256"
+
+
+VAULT_PASSWORD_FILE=vault-password
+# new format, view, using password client script
+ansible-vault view "$@" --vault-id vault-password@test-vault-client.py format_1_1_AES256.yml
+
+# view, using password client script, unknown vault/keyname
+ansible-vault view "$@" --vault-id some_unknown_vault_id@test-vault-client.py format_1_1_AES256.yml && :
+
+# Use linux setsid to test without a tty. No setsid if osx/bsd though...
+if [ -x "$(command -v setsid)" ]; then
+ # tests related to https://github.com/ansible/ansible/issues/30993
+ CMD='ansible-playbook -i ../../inventory -vvvvv --ask-vault-pass test_vault.yml'
+ setsid sh -c "echo test-vault-password|${CMD}" < /dev/null > log 2>&1 && :
+ WRONG_RC=$?
+ cat log
+ echo "rc was $WRONG_RC (0 is expected)"
+ [ $WRONG_RC -eq 0 ]
+
+ setsid sh -c 'tty; ansible-vault view --ask-vault-pass -vvvvv test_vault.yml' < /dev/null > log 2>&1 && :
+ WRONG_RC=$?
+ echo "rc was $WRONG_RC (1 is expected)"
+ [ $WRONG_RC -eq 1 ]
+ cat log
+
+ setsid sh -c 'tty; echo passbhkjhword|ansible-playbook -i ../../inventory -vvvvv --ask-vault-pass test_vault.yml' < /dev/null > log 2>&1 && :
+ WRONG_RC=$?
+ echo "rc was $WRONG_RC (1 is expected)"
+ [ $WRONG_RC -eq 1 ]
+ cat log
+
+ setsid sh -c 'tty; echo test-vault-password |ansible-playbook -i ../../inventory -vvvvv --ask-vault-pass test_vault.yml' < /dev/null > log 2>&1
+ echo $?
+ cat log
+
+ setsid sh -c 'tty; echo test-vault-password|ansible-playbook -i ../../inventory -vvvvv --ask-vault-pass test_vault.yml' < /dev/null > log 2>&1
+ echo $?
+ cat log
+
+ setsid sh -c 'tty; echo test-vault-password |ansible-playbook -i ../../inventory -vvvvv --ask-vault-pass test_vault.yml' < /dev/null > log 2>&1
+ echo $?
+ cat log
+
+ setsid sh -c 'tty; echo test-vault-password|ansible-vault view --ask-vault-pass -vvvvv vaulted.inventory' < /dev/null > log 2>&1
+ echo $?
+ cat log
+
+ # test using --ask-vault-password option
+ CMD='ansible-playbook -i ../../inventory -vvvvv --ask-vault-password test_vault.yml'
+ setsid sh -c "echo test-vault-password|${CMD}" < /dev/null > log 2>&1 && :
+ WRONG_RC=$?
+ cat log
+ echo "rc was $WRONG_RC (0 is expected)"
+ [ $WRONG_RC -eq 0 ]
+fi
+
+ansible-vault view "$@" --vault-password-file vault-password-wrong format_1_1_AES256.yml && :
+WRONG_RC=$?
+echo "rc was $WRONG_RC (1 is expected)"
+[ $WRONG_RC -eq 1 ]
+
+set -eux
+
+
+# new format, view
+ansible-vault view "$@" --vault-password-file vault-password format_1_1_AES256.yml
+
+# new format, view with vault-id
+ansible-vault view "$@" --vault-id=vault-password format_1_1_AES256.yml
+
+# new format, view, using password script
+ansible-vault view "$@" --vault-password-file password-script.py format_1_1_AES256.yml
+
+# new format, view, using password script with vault-id
+ansible-vault view "$@" --vault-id password-script.py format_1_1_AES256.yml
+
+# new 1.2 format, view
+ansible-vault view "$@" --vault-password-file vault-password format_1_2_AES256.yml
+
+# new 1.2 format, view with vault-id
+ansible-vault view "$@" --vault-id=test_vault_id@vault-password format_1_2_AES256.yml
+
+# new 1,2 format, view, using password script
+ansible-vault view "$@" --vault-password-file password-script.py format_1_2_AES256.yml
+
+# new 1.2 format, view, using password script with vault-id
+ansible-vault view "$@" --vault-id password-script.py format_1_2_AES256.yml
+
+# newish 1.1 format, view, using a vault-id list from config env var
+ANSIBLE_VAULT_IDENTITY_LIST='wrong-password@vault-password-wrong,default@vault-password' ansible-vault view "$@" --vault-id password-script.py format_1_1_AES256.yml
+
+# new 1.2 format, view, ENFORCE_IDENTITY_MATCH=true, should fail, no 'test_vault_id' vault_id
+ANSIBLE_VAULT_ID_MATCH=1 ansible-vault view "$@" --vault-password-file vault-password format_1_2_AES256.yml && :
+WRONG_RC=$?
+echo "rc was $WRONG_RC (1 is expected)"
+[ $WRONG_RC -eq 1 ]
+
+# new 1.2 format, view with vault-id, ENFORCE_IDENTITY_MATCH=true, should work, 'test_vault_id' is provided
+ANSIBLE_VAULT_ID_MATCH=1 ansible-vault view "$@" --vault-id=test_vault_id@vault-password format_1_2_AES256.yml
+
+# new 1,2 format, view, using password script, ENFORCE_IDENTITY_MATCH=true, should fail, no 'test_vault_id'
+ANSIBLE_VAULT_ID_MATCH=1 ansible-vault view "$@" --vault-password-file password-script.py format_1_2_AES256.yml && :
+WRONG_RC=$?
+echo "rc was $WRONG_RC (1 is expected)"
+[ $WRONG_RC -eq 1 ]
+
+
+# new 1.2 format, view, using password script with vault-id, ENFORCE_IDENTITY_MATCH=true, should fail
+ANSIBLE_VAULT_ID_MATCH=1 ansible-vault view "$@" --vault-id password-script.py format_1_2_AES256.yml && :
+WRONG_RC=$?
+echo "rc was $WRONG_RC (1 is expected)"
+[ $WRONG_RC -eq 1 ]
+
+# new 1.2 format, view, using password script with vault-id, ENFORCE_IDENTITY_MATCH=true, 'test_vault_id' provided should work
+ANSIBLE_VAULT_ID_MATCH=1 ansible-vault view "$@" --vault-id=test_vault_id@password-script.py format_1_2_AES256.yml
+
+# test with a default vault password set via config/env, right password
+ANSIBLE_VAULT_PASSWORD_FILE=vault-password ansible-vault view "$@" format_1_1_AES256.yml
+
+# test with a default vault password set via config/env, wrong password
+ANSIBLE_VAULT_PASSWORD_FILE=vault-password-wrong ansible-vault view "$@" format_1_1_AES.yml && :
+WRONG_RC=$?
+echo "rc was $WRONG_RC (1 is expected)"
+[ $WRONG_RC -eq 1 ]
+
+# test with a default vault-id list set via config/env, right password
+ANSIBLE_VAULT_PASSWORD_FILE=wrong@vault-password-wrong,correct@vault-password ansible-vault view "$@" format_1_1_AES.yml && :
+
+# test with a default vault-id list set via config/env,wrong passwords
+ANSIBLE_VAULT_PASSWORD_FILE=wrong@vault-password-wrong,alsowrong@vault-password-wrong ansible-vault view "$@" format_1_1_AES.yml && :
+WRONG_RC=$?
+echo "rc was $WRONG_RC (1 is expected)"
+[ $WRONG_RC -eq 1 ]
+
+# try specifying a --encrypt-vault-id that doesnt exist, should exit with an error indicating
+# that --encrypt-vault-id and the known vault-ids
+ansible-vault encrypt "$@" --vault-password-file vault-password --encrypt-vault-id doesnt_exist "${TEST_FILE}" && :
+WRONG_RC=$?
+echo "rc was $WRONG_RC (1 is expected)"
+[ $WRONG_RC -eq 1 ]
+
+# encrypt it
+ansible-vault encrypt "$@" --vault-password-file vault-password "${TEST_FILE}"
+
+ansible-vault view "$@" --vault-password-file vault-password "${TEST_FILE}"
+
+# view with multiple vault-password files, including a wrong one
+ansible-vault view "$@" --vault-password-file vault-password --vault-password-file vault-password-wrong "${TEST_FILE}"
+
+# view with multiple vault-password files, including a wrong one, using vault-id
+ansible-vault view "$@" --vault-id vault-password --vault-id vault-password-wrong "${TEST_FILE}"
+
+# And with the password files specified in a different order
+ansible-vault view "$@" --vault-password-file vault-password-wrong --vault-password-file vault-password "${TEST_FILE}"
+
+# And with the password files specified in a different order, using vault-id
+ansible-vault view "$@" --vault-id vault-password-wrong --vault-id vault-password "${TEST_FILE}"
+
+# And with the password files specified in a different order, using --vault-id and non default vault_ids
+ansible-vault view "$@" --vault-id test_vault_id@vault-password-wrong --vault-id test_vault_id@vault-password "${TEST_FILE}"
+
+ansible-vault decrypt "$@" --vault-password-file vault-password "${TEST_FILE}"
+
+# encrypt it, using a vault_id so we write a 1.2 format file
+ansible-vault encrypt "$@" --vault-id test_vault_1_2@vault-password "${TEST_FILE_1_2}"
+
+ansible-vault view "$@" --vault-id vault-password "${TEST_FILE_1_2}"
+ansible-vault view "$@" --vault-id test_vault_1_2@vault-password "${TEST_FILE_1_2}"
+
+# view with multiple vault-password files, including a wrong one
+ansible-vault view "$@" --vault-id vault-password --vault-id wrong_password@vault-password-wrong "${TEST_FILE_1_2}"
+
+# And with the password files specified in a different order, using vault-id
+ansible-vault view "$@" --vault-id vault-password-wrong --vault-id vault-password "${TEST_FILE_1_2}"
+
+# And with the password files specified in a different order, using --vault-id and non default vault_ids
+ansible-vault view "$@" --vault-id test_vault_id@vault-password-wrong --vault-id test_vault_id@vault-password "${TEST_FILE_1_2}"
+
+ansible-vault decrypt "$@" --vault-id test_vault_1_2@vault-password "${TEST_FILE_1_2}"
+
+# multiple vault passwords
+ansible-vault view "$@" --vault-password-file vault-password --vault-password-file vault-password-wrong format_1_1_AES256.yml
+
+# multiple vault passwords, --vault-id
+ansible-vault view "$@" --vault-id test_vault_id@vault-password --vault-id test_vault_id@vault-password-wrong format_1_1_AES256.yml
+
+# encrypt it, with password from password script
+ansible-vault encrypt "$@" --vault-password-file password-script.py "${TEST_FILE}"
+
+ansible-vault view "$@" --vault-password-file password-script.py "${TEST_FILE}"
+
+ansible-vault decrypt "$@" --vault-password-file password-script.py "${TEST_FILE}"
+
+# encrypt it, with password from password script
+ansible-vault encrypt "$@" --vault-id test_vault_id@password-script.py "${TEST_FILE}"
+
+ansible-vault view "$@" --vault-id test_vault_id@password-script.py "${TEST_FILE}"
+
+ansible-vault decrypt "$@" --vault-id test_vault_id@password-script.py "${TEST_FILE}"
+
+# new password file for rekeyed file
+NEW_VAULT_PASSWORD="${MYTMPDIR}/new-vault-password"
+echo "newpassword" > "${NEW_VAULT_PASSWORD}"
+
+ansible-vault encrypt "$@" --vault-password-file vault-password "${TEST_FILE}"
+
+ansible-vault rekey "$@" --vault-password-file vault-password --new-vault-password-file "${NEW_VAULT_PASSWORD}" "${TEST_FILE}"
+
+# --new-vault-password-file and --new-vault-id should cause options error
+ansible-vault rekey "$@" --vault-password-file vault-password --new-vault-id=foobar --new-vault-password-file "${NEW_VAULT_PASSWORD}" "${TEST_FILE}" && :
+WRONG_RC=$?
+echo "rc was $WRONG_RC (2 is expected)"
+[ $WRONG_RC -eq 2 ]
+
+ansible-vault view "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" "${TEST_FILE}"
+
+# view file with unicode in filename
+ansible-vault view "$@" --vault-password-file vault-password vault-café.yml
+
+# view with old password file and new password file
+ansible-vault view "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" --vault-password-file vault-password "${TEST_FILE}"
+
+# view with old password file and new password file, different order
+ansible-vault view "$@" --vault-password-file vault-password --vault-password-file "${NEW_VAULT_PASSWORD}" "${TEST_FILE}"
+
+# view with old password file and new password file and another wrong
+ansible-vault view "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" --vault-password-file vault-password-wrong --vault-password-file vault-password "${TEST_FILE}"
+
+# view with old password file and new password file and another wrong, using --vault-id
+ansible-vault view "$@" --vault-id "tmp_new_password@${NEW_VAULT_PASSWORD}" --vault-id wrong_password@vault-password-wrong --vault-id myorg@vault-password "${TEST_FILE}"
+
+ansible-vault decrypt "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" "${TEST_FILE}"
+
+# reading/writing to/from stdin/stdin (See https://github.com/ansible/ansible/issues/23567)
+ansible-vault encrypt "$@" --vault-password-file "${VAULT_PASSWORD_FILE}" --output="${TEST_FILE_OUTPUT}" < "${TEST_FILE}"
+OUTPUT=$(ansible-vault decrypt "$@" --vault-password-file "${VAULT_PASSWORD_FILE}" --output=- < "${TEST_FILE_OUTPUT}")
+echo "${OUTPUT}" | grep 'This is a test file'
+
+OUTPUT_DASH=$(ansible-vault decrypt "$@" --vault-password-file "${VAULT_PASSWORD_FILE}" --output=- "${TEST_FILE_OUTPUT}")
+echo "${OUTPUT_DASH}" | grep 'This is a test file'
+
+OUTPUT_DASH_SPACE=$(ansible-vault decrypt "$@" --vault-password-file "${VAULT_PASSWORD_FILE}" --output - "${TEST_FILE_OUTPUT}")
+echo "${OUTPUT_DASH_SPACE}" | grep 'This is a test file'
+
+
+# test using an empty vault password file
+ansible-vault view "$@" --vault-password-file empty-password format_1_1_AES256.yml && :
+WRONG_RC=$?
+echo "rc was $WRONG_RC (1 is expected)"
+[ $WRONG_RC -eq 1 ]
+
+ansible-vault view "$@" --vault-id=empty@empty-password --vault-password-file empty-password format_1_1_AES256.yml && :
+WRONG_RC=$?
+echo "rc was $WRONG_RC (1 is expected)"
+[ $WRONG_RC -eq 1 ]
+
+echo 'foo' > some_file.txt
+ansible-vault encrypt "$@" --vault-password-file empty-password some_file.txt && :
+WRONG_RC=$?
+echo "rc was $WRONG_RC (1 is expected)"
+[ $WRONG_RC -eq 1 ]
+
+
+ansible-vault encrypt_string "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" "a test string"
+
+# Test with multiple vault password files
+# https://github.com/ansible/ansible/issues/57172
+env ANSIBLE_VAULT_PASSWORD_FILE=vault-password ansible-vault encrypt_string "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" --encrypt-vault-id default "a test string"
+
+ansible-vault encrypt_string "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" --name "blippy" "a test string names blippy"
+
+ansible-vault encrypt_string "$@" --vault-id "${NEW_VAULT_PASSWORD}" "a test string"
+
+ansible-vault encrypt_string "$@" --vault-id "${NEW_VAULT_PASSWORD}" --name "blippy" "a test string names blippy"
+
+
+# from stdin
+ansible-vault encrypt_string "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" < "${TEST_FILE}"
+
+ansible-vault encrypt_string "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" --stdin-name "the_var_from_stdin" < "${TEST_FILE}"
+
+# write to file
+ansible-vault encrypt_string "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" --name "blippy" "a test string names blippy" --output "${MYTMPDIR}/enc_string_test_file"
+
+[ -f "${MYTMPDIR}/enc_string_test_file" ];
+
+# test ansible-vault edit with a faux editor
+ansible-vault encrypt "$@" --vault-password-file vault-password "${TEST_FILE_EDIT}"
+
+# edit a 1.1 format with no vault-id, should stay 1.1
+EDITOR=./faux-editor.py ansible-vault edit "$@" --vault-password-file vault-password "${TEST_FILE_EDIT}"
+head -1 "${TEST_FILE_EDIT}" | grep "${FORMAT_1_1_HEADER}"
+
+# edit a 1.1 format with vault-id, should stay 1.1
+cat "${TEST_FILE_EDIT}"
+EDITOR=./faux-editor.py ansible-vault edit "$@" --vault-id vault_password@vault-password "${TEST_FILE_EDIT}"
+cat "${TEST_FILE_EDIT}"
+head -1 "${TEST_FILE_EDIT}" | grep "${FORMAT_1_1_HEADER}"
+
+ansible-vault encrypt "$@" --vault-id vault_password@vault-password "${TEST_FILE_EDIT2}"
+
+# verify that we aren't prompted for a new vault password on edit if we are running interactively (ie, with prompts)
+# have to use setsid nd --ask-vault-pass to force a prompt to simulate.
+# See https://github.com/ansible/ansible/issues/35834
+setsid sh -c 'tty; echo password |ansible-vault edit --ask-vault-pass vault_test.yml' < /dev/null > log 2>&1 && :
+grep 'New Vault password' log && :
+WRONG_RC=$?
+echo "The stdout log had 'New Vault password' in it and it is not supposed to. rc of grep was $WRONG_RC (1 is expected)"
+[ $WRONG_RC -eq 1 ]
+
+# edit a 1.2 format with vault id, should keep vault id and 1.2 format
+EDITOR=./faux-editor.py ansible-vault edit "$@" --vault-id vault_password@vault-password "${TEST_FILE_EDIT2}"
+head -1 "${TEST_FILE_EDIT2}" | grep "${FORMAT_1_2_HEADER};vault_password"
+
+# edit a 1.2 file with no vault-id, should keep vault id and 1.2 format
+EDITOR=./faux-editor.py ansible-vault edit "$@" --vault-password-file vault-password "${TEST_FILE_EDIT2}"
+head -1 "${TEST_FILE_EDIT2}" | grep "${FORMAT_1_2_HEADER};vault_password"
+
+# encrypt with a password from a vault encrypted password file and multiple vault-ids
+# should fail because we dont know which vault id to use to encrypt with
+ansible-vault encrypt "$@" --vault-id vault-password --vault-id encrypted-vault-password "${TEST_FILE_ENC_PASSWORD}" && :
+WRONG_RC=$?
+echo "rc was $WRONG_RC (5 is expected)"
+[ $WRONG_RC -eq 5 ]
+
+
+# encrypt with a password from a vault encrypted password file and multiple vault-ids
+# but this time specify with --encrypt-vault-id, but specifying vault-id names (instead of default)
+# ansible-vault encrypt "$@" --vault-id from_vault_password@vault-password --vault-id from_encrypted_vault_password@encrypted-vault-password --encrypt-vault-id from_encrypted_vault_password "${TEST_FILE(_ENC_PASSWORD}"
+
+# try to view the file encrypted with the vault-password we didnt specify
+# to verify we didnt choose the wrong vault-id
+# ansible-vault view "$@" --vault-id vault-password "${TEST_FILE_ENC_PASSWORD}" && :
+# WRONG_RC=$?
+# echo "rc was $WRONG_RC (1 is expected)"
+# [ $WRONG_RC -eq 1 ]
+
+ansible-vault encrypt "$@" --vault-id vault-password "${TEST_FILE_ENC_PASSWORD}"
+
+# view the file encrypted with a password from a vault encrypted password file
+ansible-vault view "$@" --vault-id vault-password --vault-id encrypted-vault-password "${TEST_FILE_ENC_PASSWORD}"
+
+# try to view the file encrypted with a password from a vault encrypted password file but without the password to the password file.
+# This should fail with an
+ansible-vault view "$@" --vault-id encrypted-vault-password "${TEST_FILE_ENC_PASSWORD}" && :
+WRONG_RC=$?
+echo "rc was $WRONG_RC (1 is expected)"
+[ $WRONG_RC -eq 1 ]
+
+
+# test playbooks using vaulted files
+ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-password-file vault-password --list-tasks
+ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-password-file vault-password --list-hosts
+ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-password-file vault-password --syntax-check
+ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-password-file vault-password
+ansible-playbook test_vault_embedded.yml -i ../../inventory -v "$@" --vault-password-file vault-password --syntax-check
+ansible-playbook test_vault_embedded.yml -i ../../inventory -v "$@" --vault-password-file vault-password
+ansible-playbook test_vaulted_inventory.yml -i vaulted.inventory -v "$@" --vault-password-file vault-password
+ansible-playbook test_vaulted_template.yml -i ../../inventory -v "$@" --vault-password-file vault-password
+
+# test using --vault-pass-file option
+ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-pass-file vault-password
+
+# install TOML for parse toml inventory
+# test playbooks using vaulted files(toml)
+pip install toml
+ansible-vault encrypt ./inventory.toml -v "$@" --vault-password-file=./vault-password
+ansible-playbook test_vaulted_inventory_toml.yml -i ./inventory.toml -v "$@" --vault-password-file vault-password
+ansible-vault decrypt ./inventory.toml -v "$@" --vault-password-file=./vault-password
+
+# test a playbook with a host_var whose value is non-ascii utf8 (see https://github.com/ansible/ansible/issues/37258)
+ansible-playbook -i ../../inventory -v "$@" --vault-id vault-password test_vaulted_utf8_value.yml
+
+# test with password from password script
+ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-password-file password-script.py
+ansible-playbook test_vault_embedded.yml -i ../../inventory -v "$@" --vault-password-file password-script.py
+
+# with multiple password files
+ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-password-file vault-password --vault-password-file vault-password-wrong
+ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-password-file vault-password-wrong --vault-password-file vault-password
+
+ansible-playbook test_vault_embedded.yml -i ../../inventory -v "$@" --vault-password-file vault-password --vault-password-file vault-password-wrong --syntax-check
+ansible-playbook test_vault_embedded.yml -i ../../inventory -v "$@" --vault-password-file vault-password-wrong --vault-password-file vault-password
+
+# test with a default vault password file set in config
+ANSIBLE_VAULT_PASSWORD_FILE=vault-password ansible-playbook test_vault_embedded.yml -i ../../inventory -v "$@" --vault-password-file vault-password-wrong
+
+# test using vault_identity_list config
+ANSIBLE_VAULT_IDENTITY_LIST='wrong-password@vault-password-wrong,default@vault-password' ansible-playbook test_vault.yml -i ../../inventory -v "$@"
+
+# test that we can have a vault encrypted yaml file that includes embedded vault vars
+# that were encrypted with a different vault secret
+ansible-playbook test_vault_file_encrypted_embedded.yml -i ../../inventory "$@" --vault-id encrypted_file_encrypted_var_password --vault-id vault-password
+
+# with multiple password files, --vault-id, ordering
+ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-id vault-password --vault-id vault-password-wrong
+ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-id vault-password-wrong --vault-id vault-password
+
+ansible-playbook test_vault_embedded.yml -i ../../inventory -v "$@" --vault-id vault-password --vault-id vault-password-wrong --syntax-check
+ansible-playbook test_vault_embedded.yml -i ../../inventory -v "$@" --vault-id vault-password-wrong --vault-id vault-password
+
+# test with multiple password files, including a script, and a wrong password
+ansible-playbook test_vault_embedded.yml -i ../../inventory -v "$@" --vault-password-file vault-password-wrong --vault-password-file password-script.py --vault-password-file vault-password
+
+# test with multiple password files, including a script, and a wrong password, and a mix of --vault-id and --vault-password-file
+ansible-playbook test_vault_embedded.yml -i ../../inventory -v "$@" --vault-password-file vault-password-wrong --vault-id password-script.py --vault-id vault-password
+
+# test with multiple password files, including a script, and a wrong password, and a mix of --vault-id and --vault-password-file
+ansible-playbook test_vault_embedded_ids.yml -i ../../inventory -v "$@" \
+ --vault-password-file vault-password-wrong \
+ --vault-id password-script.py --vault-id example1@example1_password \
+ --vault-id example2@example2_password --vault-password-file example3_password \
+ --vault-id vault-password
+
+# with wrong password
+ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-password-file vault-password-wrong && :
+WRONG_RC=$?
+echo "rc was $WRONG_RC (1 is expected)"
+[ $WRONG_RC -eq 1 ]
+
+# with multiple wrong passwords
+ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-password-file vault-password-wrong --vault-password-file vault-password-wrong && :
+WRONG_RC=$?
+echo "rc was $WRONG_RC (1 is expected)"
+[ $WRONG_RC -eq 1 ]
+
+# with wrong password, --vault-id
+ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-id vault-password-wrong && :
+WRONG_RC=$?
+echo "rc was $WRONG_RC (1 is expected)"
+[ $WRONG_RC -eq 1 ]
+
+# with multiple wrong passwords with --vault-id
+ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-id vault-password-wrong --vault-id vault-password-wrong && :
+WRONG_RC=$?
+echo "rc was $WRONG_RC (1 is expected)"
+[ $WRONG_RC -eq 1 ]
+
+# with multiple wrong passwords with --vault-id
+ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-id wrong1@vault-password-wrong --vault-id wrong2@vault-password-wrong && :
+WRONG_RC=$?
+echo "rc was $WRONG_RC (1 is expected)"
+[ $WRONG_RC -eq 1 ]
+
+# with empty password file
+ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-id empty@empty-password && :
+WRONG_RC=$?
+echo "rc was $WRONG_RC (1 is expected)"
+[ $WRONG_RC -eq 1 ]
+
+# test invalid format ala https://github.com/ansible/ansible/issues/28038
+EXPECTED_ERROR='Vault format unhexlify error: Non-hexadecimal digit found'
+ansible-playbook "$@" -i invalid_format/inventory --vault-id invalid_format/vault-secret invalid_format/broken-host-vars-tasks.yml 2>&1 | grep "${EXPECTED_ERROR}"
+
+EXPECTED_ERROR='Vault format unhexlify error: Odd-length string'
+ansible-playbook "$@" -i invalid_format/inventory --vault-id invalid_format/vault-secret invalid_format/broken-group-vars-tasks.yml 2>&1 | grep "${EXPECTED_ERROR}"
+
+# Run playbook with vault file with unicode in filename (https://github.com/ansible/ansible/issues/50316)
+ansible-playbook -i ../../inventory -v "$@" --vault-password-file vault-password test_utf8_value_in_filename.yml
+
+# Ensure we don't leave unencrypted temp files dangling
+ansible-playbook -v "$@" --vault-password-file vault-password test_dangling_temp.yml
+
+ansible-playbook "$@" --vault-password-file vault-password single_vault_as_string.yml
+
+# Test that only one accessible vault password is required
+export ANSIBLE_VAULT_IDENTITY_LIST="id1@./nonexistent, id2@${MYTMPDIR}/unreadable, id3@./vault-password"
+
+touch "${MYTMPDIR}/unreadable"
+sudo chmod 000 "${MYTMPDIR}/unreadable"
+
+ansible-vault encrypt_string content
+ansible-vault encrypt_string content --encrypt-vault-id id3
+
+set +e
+
+# Try to use a missing vault password file
+ansible-vault encrypt_string content --encrypt-vault-id id1 2>&1 | tee out.txt
+test $? -ne 0
+grep out.txt -e '[WARNING]: Error getting vault password file (id1)'
+grep out.txt -e "ERROR! Did not find a match for --encrypt-vault-id=id2 in the known vault-ids ['id3']"
+
+# Try to use an inaccessible vault password file
+ansible-vault encrypt_string content --encrypt-vault-id id2 2>&1 | tee out.txt
+test $? -ne 0
+grep out.txt -e "[WARNING]: Error in vault password file loading (id2)"
+grep out.txt -e "ERROR! Did not find a match for --encrypt-vault-id=id2 in the known vault-ids ['id3']"
+
+set -e
+unset ANSIBLE_VAULT_IDENTITY_LIST
+
+# 'real script'
+ansible-playbook realpath.yml "$@" --vault-password-file script/vault-secret.sh
+
+# using symlink
+ansible-playbook symlink.yml "$@" --vault-password-file symlink/get-password-symlink
+
+### NEGATIVE TESTS
+
+ER='Attempting to decrypt'
+#### no secrets
+# 'real script'
+ansible-playbook realpath.yml "$@" 2>&1 |grep "${ER}"
+
+# using symlink
+ansible-playbook symlink.yml "$@" 2>&1 |grep "${ER}"
+
+ER='Decryption failed'
+### wrong secrets
+# 'real script'
+ansible-playbook realpath.yml "$@" --vault-password-file symlink/get-password-symlink 2>&1 |grep "${ER}"
+
+# using symlink
+ansible-playbook symlink.yml "$@" --vault-password-file script/vault-secret.sh 2>&1 |grep "${ER}"
diff --git a/test/integration/targets/ansible-vault/script/vault-secret.sh b/test/integration/targets/ansible-vault/script/vault-secret.sh
new file mode 100755
index 0000000..3aa1c2e
--- /dev/null
+++ b/test/integration/targets/ansible-vault/script/vault-secret.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+
+set -eu
+
+# shellcheck disable=SC2086
+basename="$(basename $0)"
+# shellcheck disable=SC2046
+# shellcheck disable=SC2086
+dirname="$(basename $(dirname $0))"
+basename_prefix="get-password"
+default_password="foo-bar"
+
+case "${basename}" in
+ "${basename_prefix}"-*)
+ password="${default_password}-${basename#${basename_prefix}-}"
+ ;;
+ *)
+ password="${default_password}"
+ ;;
+esac
+
+# the password is different depending on the path used (direct or symlink)
+# it would be the same if symlink is 'resolved'.
+echo "${password}_${dirname}"
diff --git a/test/integration/targets/ansible-vault/single_vault_as_string.yml b/test/integration/targets/ansible-vault/single_vault_as_string.yml
new file mode 100644
index 0000000..2d523a0
--- /dev/null
+++ b/test/integration/targets/ansible-vault/single_vault_as_string.yml
@@ -0,0 +1,117 @@
+- hosts: localhost
+ vars:
+ vaulted_value: !vault |
+ $ANSIBLE_VAULT;1.1;AES256
+ 35323961353038346165643738646465376139363061353835303739663538343266303232326635
+ 3365353662646236356665323135633630656238316530640a663362363763633436373439663031
+ 33663433383037396438656464636433653837376361313638366362333037323961316364363363
+ 3835616438623261650a636164376534376661393134326662326362323131373964313961623365
+ 3833
+ tasks:
+ - debug:
+ msg: "{{ vaulted_value }}"
+
+ - debug:
+ msg: "{{ vaulted_value|type_debug }}"
+
+ - assert:
+ that:
+ - vaulted_value is vault_encrypted
+ - vaulted_value == 'foo bar'
+ - vaulted_value|string == 'foo bar'
+ - vaulted_value|quote == "'foo bar'"
+ - vaulted_value|capitalize == 'Foo bar'
+ - vaulted_value|center(width=9) == ' foo bar '
+ - vaulted_value|default('monkey') == 'foo bar'
+ - vaulted_value|escape == 'foo bar'
+ - vaulted_value|forceescape == 'foo bar'
+ - vaulted_value|first == 'f'
+ - "'%s'|format(vaulted_value) == 'foo bar'"
+ - vaulted_value|indent(first=True) == ' foo bar'
+ - vaulted_value.split() == ['foo', 'bar']
+ - vaulted_value|join('-') == 'f-o-o- -b-a-r'
+ - vaulted_value|last == 'r'
+ - vaulted_value|length == 7
+ - vaulted_value|list == ['f', 'o', 'o', ' ', 'b', 'a', 'r']
+ - vaulted_value|lower == 'foo bar'
+ - vaulted_value|replace('foo', 'baz') == 'baz bar'
+ - vaulted_value|reverse|string == 'rab oof'
+ - vaulted_value|safe == 'foo bar'
+ - vaulted_value|slice(2)|list == [['f', 'o', 'o', ' '], ['b', 'a', 'r']]
+ - vaulted_value|sort|list == [" ", "a", "b", "f", "o", "o", "r"]
+ - vaulted_value|trim == 'foo bar'
+ - vaulted_value|upper == 'FOO BAR'
+ # jinja2.filters.do_urlencode uses an isinstance against string_types
+ # - vaulted_value|urlencode == 'foo%20bar'
+ - vaulted_value|urlize == 'foo bar'
+ - vaulted_value is not callable
+ - vaulted_value is iterable
+ - vaulted_value is lower
+ - vaulted_value is not none
+ # This is not exactly a string, and UserString doesn't fulfill this
+ # - vaulted_value is string
+ - vaulted_value is not upper
+
+ - vaulted_value|b64encode == 'Zm9vIGJhcg=='
+ - vaulted_value|to_uuid == '0271fe51-bb26-560f-b118-5d6513850860'
+ - vaulted_value|string|to_json == '"foo bar"'
+ - vaulted_value|md5 == '327b6f07435811239bc47e1544353273'
+ - vaulted_value|sha1 == '3773dea65156909838fa6c22825cafe090ff8030'
+ - vaulted_value|hash == '3773dea65156909838fa6c22825cafe090ff8030'
+ - vaulted_value|regex_replace('foo', 'baz') == 'baz bar'
+ - vaulted_value|regex_escape == 'foo\ bar'
+ - vaulted_value|regex_search('foo') == 'foo'
+ - vaulted_value|regex_findall('foo') == ['foo']
+ - vaulted_value|comment == '#\n# foo bar\n#'
+
+ - assert:
+ that:
+ - vaulted_value|random(seed='foo') == ' '
+ - vaulted_value|shuffle(seed='foo') == ["o", "f", "r", "b", "o", "a", " "]
+ - vaulted_value|pprint == "'foo bar'"
+ when: ansible_python.version.major == 3
+
+ - assert:
+ that:
+ - vaulted_value|random(seed='foo') == 'r'
+ - vaulted_value|shuffle(seed='foo') == ["b", "o", "a", " ", "o", "f", "r"]
+ - vaulted_value|pprint == "u'foo bar'"
+ when: ansible_python.version.major == 2
+
+ - assert:
+ that:
+ - vaulted_value|map('upper')|list == ['F', 'O', 'O', ' ', 'B', 'A', 'R']
+
+ - assert:
+ that:
+ - vaulted_value.split()|first|int(base=36) == 20328
+ - vaulted_value|select('equalto', 'o')|list == ['o', 'o']
+ - vaulted_value|title == 'Foo Bar'
+ - vaulted_value is equalto('foo bar')
+
+ - assert:
+ that:
+ - vaulted_value|string|tojson == '"foo bar"'
+ - vaulted_value|truncate(4) == 'foo bar'
+
+ - assert:
+ that:
+ - vaulted_value|wordwrap(4) == 'foo\nbar'
+
+ - assert:
+ that:
+ - vaulted_value|wordcount == 2
+
+ - ping:
+ data: !vault |
+ $ANSIBLE_VAULT;1.1;AES256
+ 35323961353038346165643738646465376139363061353835303739663538343266303232326635
+ 3365353662646236356665323135633630656238316530640a663362363763633436373439663031
+ 33663433383037396438656464636433653837376361313638366362333037323961316364363363
+ 3835616438623261650a636164376534376661393134326662326362323131373964313961623365
+ 3833
+ register: ping_result
+
+ - assert:
+ that:
+ - ping_result.ping == 'foo bar'
diff --git a/test/integration/targets/ansible-vault/symlink.yml b/test/integration/targets/ansible-vault/symlink.yml
new file mode 100644
index 0000000..2dcf8a9
--- /dev/null
+++ b/test/integration/targets/ansible-vault/symlink.yml
@@ -0,0 +1,10 @@
+- hosts: localhost
+ gather_facts: false
+ vars_files:
+ - vaulted.yml
+ tasks:
+ - name: see if we can decrypt
+ assert:
+ that:
+ - control is defined
+ - symlink == 'this is a test'
diff --git a/test/integration/targets/ansible-vault/symlink/get-password-symlink b/test/integration/targets/ansible-vault/symlink/get-password-symlink
new file mode 100755
index 0000000..3aa1c2e
--- /dev/null
+++ b/test/integration/targets/ansible-vault/symlink/get-password-symlink
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+
+set -eu
+
+# shellcheck disable=SC2086
+basename="$(basename $0)"
+# shellcheck disable=SC2046
+# shellcheck disable=SC2086
+dirname="$(basename $(dirname $0))"
+basename_prefix="get-password"
+default_password="foo-bar"
+
+case "${basename}" in
+ "${basename_prefix}"-*)
+ password="${default_password}-${basename#${basename_prefix}-}"
+ ;;
+ *)
+ password="${default_password}"
+ ;;
+esac
+
+# the password is different depending on the path used (direct or symlink)
+# it would be the same if symlink is 'resolved'.
+echo "${password}_${dirname}"
diff --git a/test/integration/targets/ansible-vault/test-vault-client.py b/test/integration/targets/ansible-vault/test-vault-client.py
new file mode 100755
index 0000000..ee46188
--- /dev/null
+++ b/test/integration/targets/ansible-vault/test-vault-client.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+ANSIBLE_METADATA = {'status': ['preview'],
+ 'supported_by': 'community',
+ 'version': '1.0'}
+
+import argparse
+import sys
+
+# TODO: could read these from the files I suppose...
+secrets = {'vault-password': 'test-vault-password',
+ 'vault-password-wrong': 'hunter42',
+ 'vault-password-ansible': 'ansible',
+ 'password': 'password',
+ 'vault-client-password-1': 'password-1',
+ 'vault-client-password-2': 'password-2'}
+
+
+def build_arg_parser():
+ parser = argparse.ArgumentParser(description='Get a vault password from user keyring')
+
+ parser.add_argument('--vault-id', action='store', default=None,
+ dest='vault_id',
+ help='name of the vault secret to get from keyring')
+ parser.add_argument('--username', action='store', default=None,
+ help='the username whose keyring is queried')
+ parser.add_argument('--set', action='store_true', default=False,
+ dest='set_password',
+ help='set the password instead of getting it')
+ return parser
+
+
+def get_secret(keyname):
+ return secrets.get(keyname, None)
+
+
+def main():
+ rc = 0
+
+ arg_parser = build_arg_parser()
+ args = arg_parser.parse_args()
+ # print('args: %s' % args)
+
+ keyname = args.vault_id or 'ansible'
+
+ if args.set_password:
+ print('--set is not supported yet')
+ sys.exit(1)
+
+ secret = get_secret(keyname)
+ if secret is None:
+ sys.stderr.write('test-vault-client could not find key for vault-id="%s"\n' % keyname)
+ # key not found rc=2
+ return 2
+
+ sys.stdout.write('%s\n' % secret)
+
+ return rc
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/test/integration/targets/ansible-vault/test_dangling_temp.yml b/test/integration/targets/ansible-vault/test_dangling_temp.yml
new file mode 100644
index 0000000..71a9d73
--- /dev/null
+++ b/test/integration/targets/ansible-vault/test_dangling_temp.yml
@@ -0,0 +1,34 @@
+- hosts: localhost
+ gather_facts: False
+ vars:
+ od: "{{output_dir|default('/tmp')}}/test_vault_assemble"
+ tasks:
+ - name: create target directory
+ file:
+ path: "{{od}}"
+ state: directory
+
+ - name: assemble_file file with secret
+ assemble:
+ src: files/test_assemble
+ dest: "{{od}}/dest_file"
+ remote_src: no
+ mode: 0600
+
+ - name: remove assembled file with secret (so nothing should have unencrypted secret)
+ file: path="{{od}}/dest_file" state=absent
+
+ - name: find temp files with secrets
+ find:
+ paths: '{{temp_paths}}'
+ contains: 'VAULT TEST IN WHICH BAD THING HAPPENED'
+ recurse: yes
+ register: badthings
+ vars:
+ temp_paths: "{{[lookup('env', 'TMP'), lookup('env', 'TEMP'), hardcoded]|flatten(1)|unique|list}}"
+ hardcoded: ['/tmp', '/var/tmp']
+
+ - name: ensure we failed to find any
+ assert:
+ that:
+ - badthings['matched'] == 0
diff --git a/test/integration/targets/ansible-vault/test_utf8_value_in_filename.yml b/test/integration/targets/ansible-vault/test_utf8_value_in_filename.yml
new file mode 100644
index 0000000..9bd394d
--- /dev/null
+++ b/test/integration/targets/ansible-vault/test_utf8_value_in_filename.yml
@@ -0,0 +1,16 @@
+- name: "Test that the vaulted file with UTF-8 in filename decrypts correctly"
+ gather_facts: false
+ hosts: testhost
+ vars:
+ expected: "my_secret"
+ vars_files:
+ - vault-café.yml
+ tasks:
+ - name: decrypt vaulted file with utf8 in filename and show it in debug
+ debug:
+ var: vault_string
+
+ - name: assert decrypted value matches expected
+ assert:
+ that:
+ - "vault_string == expected"
diff --git a/test/integration/targets/ansible-vault/test_vault.yml b/test/integration/targets/ansible-vault/test_vault.yml
new file mode 100644
index 0000000..7f8ed11
--- /dev/null
+++ b/test/integration/targets/ansible-vault/test_vault.yml
@@ -0,0 +1,6 @@
+- hosts: testhost
+ gather_facts: False
+ vars:
+ - output_dir: .
+ roles:
+ - { role: test_vault, tags: test_vault}
diff --git a/test/integration/targets/ansible-vault/test_vault_embedded.yml b/test/integration/targets/ansible-vault/test_vault_embedded.yml
new file mode 100644
index 0000000..ee9739f
--- /dev/null
+++ b/test/integration/targets/ansible-vault/test_vault_embedded.yml
@@ -0,0 +1,4 @@
+- hosts: testhost
+ gather_facts: False
+ roles:
+ - { role: test_vault_embedded, tags: test_vault_embedded}
diff --git a/test/integration/targets/ansible-vault/test_vault_embedded_ids.yml b/test/integration/targets/ansible-vault/test_vault_embedded_ids.yml
new file mode 100644
index 0000000..23ebbb9
--- /dev/null
+++ b/test/integration/targets/ansible-vault/test_vault_embedded_ids.yml
@@ -0,0 +1,4 @@
+- hosts: testhost
+ gather_facts: False
+ roles:
+ - { role: test_vault_embedded_ids, tags: test_vault_embedded_ids}
diff --git a/test/integration/targets/ansible-vault/test_vault_file_encrypted_embedded.yml b/test/integration/targets/ansible-vault/test_vault_file_encrypted_embedded.yml
new file mode 100644
index 0000000..685d20e
--- /dev/null
+++ b/test/integration/targets/ansible-vault/test_vault_file_encrypted_embedded.yml
@@ -0,0 +1,4 @@
+- hosts: testhost
+ gather_facts: False
+ roles:
+ - { role: test_vault_file_encrypted_embedded, tags: test_vault_file_encrypted_embedded}
diff --git a/test/integration/targets/ansible-vault/test_vaulted_inventory.yml b/test/integration/targets/ansible-vault/test_vaulted_inventory.yml
new file mode 100644
index 0000000..06b6582
--- /dev/null
+++ b/test/integration/targets/ansible-vault/test_vaulted_inventory.yml
@@ -0,0 +1,5 @@
+- hosts: vaulted_host
+ gather_facts: no
+ tasks:
+ - name: See if we knew vaulted_host
+ debug: msg="Found vaulted_host from vaulted.inventory"
diff --git a/test/integration/targets/ansible-vault/test_vaulted_inventory_toml.yml b/test/integration/targets/ansible-vault/test_vaulted_inventory_toml.yml
new file mode 100644
index 0000000..f6e2c5d
--- /dev/null
+++ b/test/integration/targets/ansible-vault/test_vaulted_inventory_toml.yml
@@ -0,0 +1,9 @@
+- hosts: vaulted_host_toml
+ gather_facts: no
+ tasks:
+ - name: See if we knew vaulted_host_toml
+ debug: msg="Found vaulted_host from vaulted.inventory.toml"
+
+ - assert:
+ that:
+ - 'hello=="world"'
diff --git a/test/integration/targets/ansible-vault/test_vaulted_template.yml b/test/integration/targets/ansible-vault/test_vaulted_template.yml
new file mode 100644
index 0000000..b495211
--- /dev/null
+++ b/test/integration/targets/ansible-vault/test_vaulted_template.yml
@@ -0,0 +1,6 @@
+- hosts: testhost
+ gather_facts: False
+ vars:
+ - output_dir: .
+ roles:
+ - { role: test_vaulted_template, tags: test_vaulted_template}
diff --git a/test/integration/targets/ansible-vault/test_vaulted_utf8_value.yml b/test/integration/targets/ansible-vault/test_vaulted_utf8_value.yml
new file mode 100644
index 0000000..63b602b
--- /dev/null
+++ b/test/integration/targets/ansible-vault/test_vaulted_utf8_value.yml
@@ -0,0 +1,15 @@
+- name: "test that the vaulted_utf8_value decrypts correctly"
+ gather_facts: false
+ hosts: testhost
+ vars:
+ expected: "aöffü"
+ tasks:
+ - name: decrypt vaulted_utf8_value and show it in debug
+ debug:
+ var: vaulted_utf8_value
+
+ - name: assert decrypted vaulted_utf8_value matches expected
+ assert:
+ that:
+ - "vaulted_utf8_value == expected"
+ - "vaulted_utf8_value == 'aöffü'"
diff --git a/test/integration/targets/ansible-vault/vars/vaulted.yml b/test/integration/targets/ansible-vault/vars/vaulted.yml
new file mode 100644
index 0000000..40f5c54
--- /dev/null
+++ b/test/integration/targets/ansible-vault/vars/vaulted.yml
@@ -0,0 +1,15 @@
+control: 1
+realpath: !vault |
+ $ANSIBLE_VAULT;1.1;AES256
+ 64343436666664636436363065356463363630653766323230333931366661656262343030386366
+ 6536616433353864616132303033623835316430623762360a646234383932656637623439353333
+ 36336362616564333663353739313766363333376461353962643531366338633336613565636636
+ 3663663664653538620a646132623835666336393333623439363361313934666530646334333765
+ 39386364646262396234616666666438313233626336376330366539663765373566
+symlink: !vault |
+ $ANSIBLE_VAULT;1.1;AES256
+ 61656138353366306464386332353938623338336333303831353164633834353437643635343635
+ 3461646235303261613766383437623664323032623137350a663934653735316334363832383534
+ 33623733346164376430643535616433383331663238383363316634353339326235663461353166
+ 3064663735353766660a653963373432383432373365633239313033646466653664346236363635
+ 6637
diff --git a/test/integration/targets/ansible-vault/vault-café.yml b/test/integration/targets/ansible-vault/vault-café.yml
new file mode 100644
index 0000000..0d179ae
--- /dev/null
+++ b/test/integration/targets/ansible-vault/vault-café.yml
@@ -0,0 +1,6 @@
+$ANSIBLE_VAULT;1.1;AES256
+63363732353363646661643038636339343263303161346533393636336562336465396265373834
+6366313833613236356666646532613636303532366231340a316238666435306332656662613731
+31623433613434633539333564613564656439343661363831336364376266653462366161383038
+6530386533363933350a336631653833666663643166303932653261323431623333356539666265
+37316464303231366163333430346537353631376538393939646362313337363866
diff --git a/test/integration/targets/ansible-vault/vault-password b/test/integration/targets/ansible-vault/vault-password
new file mode 100644
index 0000000..9697392
--- /dev/null
+++ b/test/integration/targets/ansible-vault/vault-password
@@ -0,0 +1 @@
+test-vault-password
diff --git a/test/integration/targets/ansible-vault/vault-password-ansible b/test/integration/targets/ansible-vault/vault-password-ansible
new file mode 100644
index 0000000..90d4055
--- /dev/null
+++ b/test/integration/targets/ansible-vault/vault-password-ansible
@@ -0,0 +1 @@
+ansible
diff --git a/test/integration/targets/ansible-vault/vault-password-wrong b/test/integration/targets/ansible-vault/vault-password-wrong
new file mode 100644
index 0000000..50e2efa
--- /dev/null
+++ b/test/integration/targets/ansible-vault/vault-password-wrong
@@ -0,0 +1 @@
+hunter42
diff --git a/test/integration/targets/ansible-vault/vault-secret.txt b/test/integration/targets/ansible-vault/vault-secret.txt
new file mode 100644
index 0000000..b6bc9bf
--- /dev/null
+++ b/test/integration/targets/ansible-vault/vault-secret.txt
@@ -0,0 +1,6 @@
+$ANSIBLE_VAULT;1.1;AES256
+39303432393062643236616234306333383838333662386165616633303735336537613337396337
+6662666233356462326631653161663663363166323338320a653131656636666339633863346530
+32326238646631653133643936306666643065393038386234343736663239363665613963343661
+3230353633643361650a363034323631613864326438396665343237383566336339323837326464
+3930
diff --git a/test/integration/targets/ansible-vault/vaulted.inventory b/test/integration/targets/ansible-vault/vaulted.inventory
new file mode 100644
index 0000000..1ed258b
--- /dev/null
+++ b/test/integration/targets/ansible-vault/vaulted.inventory
@@ -0,0 +1,8 @@
+$ANSIBLE_VAULT;1.1;AES256
+62663838646564656432633932396339666332653932656230356332316530613665336461653731
+3839393466623734663861313636356530396434376462320a623966363661306334333639356263
+37366332626434326537353562636139333835613961333635633333313832666432396361393861
+3538626339636634360a396239383139646438323662383637663138646439306532613732306263
+64666237366334663931363462313131323861613237613337366562373532373537613531636334
+64653938333938313539653539303031393936306432623862363263663438653932643338373338
+633436626431656361633934363263303962
diff --git a/test/integration/targets/ansible/adhoc-callback.stdout b/test/integration/targets/ansible/adhoc-callback.stdout
new file mode 100644
index 0000000..05a93dd
--- /dev/null
+++ b/test/integration/targets/ansible/adhoc-callback.stdout
@@ -0,0 +1,12 @@
+v2_playbook_on_start
+v2_on_any
+v2_playbook_on_play_start
+v2_on_any
+v2_playbook_on_task_start
+v2_on_any
+v2_runner_on_start
+v2_on_any
+v2_runner_on_ok
+v2_on_any
+v2_playbook_on_stats
+v2_on_any
diff --git a/test/integration/targets/ansible/aliases b/test/integration/targets/ansible/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/ansible/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/ansible/ansible-testé.cfg b/test/integration/targets/ansible/ansible-testé.cfg
new file mode 100644
index 0000000..09af947
--- /dev/null
+++ b/test/integration/targets/ansible/ansible-testé.cfg
@@ -0,0 +1,3 @@
+[defaults]
+remote_user = admin
+collections_paths = /tmp/collections
diff --git a/test/integration/targets/ansible/callback_plugins/callback_debug.py b/test/integration/targets/ansible/callback_plugins/callback_debug.py
new file mode 100644
index 0000000..2462c1f
--- /dev/null
+++ b/test/integration/targets/ansible/callback_plugins/callback_debug.py
@@ -0,0 +1,24 @@
+# (c) 2020 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.callback import CallbackBase
+
+
+class CallbackModule(CallbackBase):
+ CALLBACK_VERSION = 2.0
+ CALLBACK_TYPE = 'stdout'
+ CALLBACK_NAME = 'callback_debug'
+
+ def __init__(self, *args, **kwargs):
+ super(CallbackModule, self).__init__(*args, **kwargs)
+ self._display.display('__init__')
+
+ for cb in [x for x in dir(CallbackBase) if x.startswith('v2_')]:
+ delattr(CallbackBase, cb)
+
+ def __getattr__(self, name):
+ if name.startswith('v2_'):
+ return lambda *args, **kwargs: self._display.display(name)
diff --git a/test/integration/targets/ansible/callback_plugins/callback_meta.py b/test/integration/targets/ansible/callback_plugins/callback_meta.py
new file mode 100644
index 0000000..e19c80f
--- /dev/null
+++ b/test/integration/targets/ansible/callback_plugins/callback_meta.py
@@ -0,0 +1,23 @@
+# (c) 2020 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.callback import CallbackBase
+import os
+
+
+class CallbackModule(CallbackBase):
+ CALLBACK_VERSION = 2.0
+ CALLBACK_TYPE = 'stdout'
+ CALLBACK_NAME = 'callback_meta'
+
+ def __init__(self, *args, **kwargs):
+ super(CallbackModule, self).__init__(*args, **kwargs)
+ self.wants_implicit_tasks = os.environ.get('CB_WANTS_IMPLICIT', False)
+
+ def v2_playbook_on_task_start(self, task, is_conditional):
+ if task.implicit:
+ self._display.display('saw implicit task')
+ self._display.display(task.get_name())
diff --git a/test/integration/targets/ansible/module_common_regex_regression.sh b/test/integration/targets/ansible/module_common_regex_regression.sh
new file mode 100755
index 0000000..4869f4f
--- /dev/null
+++ b/test/integration/targets/ansible/module_common_regex_regression.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+
+# #74270 -- ensure we escape directory names before passing to re.compile()
+# particularly in module_common.
+
+set -eux
+
+lib_path=$(python -c 'import os, ansible; print(os.path.dirname(os.path.dirname(ansible.__file__)))')
+bad_dir="${OUTPUT_DIR}/ansi[ble"
+
+mkdir "${bad_dir}"
+cp -a "${lib_path}" "${bad_dir}"
+
+PYTHONPATH="${bad_dir}/lib" ansible -m ping localhost -i ../../inventory "$@"
+rm -rf "${bad_dir}"
diff --git a/test/integration/targets/ansible/no-extension b/test/integration/targets/ansible/no-extension
new file mode 100644
index 0000000..61a99f4
--- /dev/null
+++ b/test/integration/targets/ansible/no-extension
@@ -0,0 +1,2 @@
+[defaults]
+remote_user = admin
diff --git a/test/integration/targets/ansible/playbook.yml b/test/integration/targets/ansible/playbook.yml
new file mode 100644
index 0000000..69c9b2b
--- /dev/null
+++ b/test/integration/targets/ansible/playbook.yml
@@ -0,0 +1,8 @@
+- hosts: all
+ gather_facts: false
+ tasks:
+ - debug:
+ msg: "{{ username }}"
+
+ - name: explicitly refresh inventory
+ meta: refresh_inventory
diff --git a/test/integration/targets/ansible/playbookdir_cfg.ini b/test/integration/targets/ansible/playbookdir_cfg.ini
new file mode 100644
index 0000000..16670c5
--- /dev/null
+++ b/test/integration/targets/ansible/playbookdir_cfg.ini
@@ -0,0 +1,2 @@
+[defaults]
+playbook_dir = /doesnotexist/tmp
diff --git a/test/integration/targets/ansible/runme.sh b/test/integration/targets/ansible/runme.sh
new file mode 100755
index 0000000..e9e72a9
--- /dev/null
+++ b/test/integration/targets/ansible/runme.sh
@@ -0,0 +1,86 @@
+#!/usr/bin/env bash
+
+set -eux -o pipefail
+
+ansible --version
+ansible --help
+
+ansible testhost -i ../../inventory -m ping "$@"
+ansible testhost -i ../../inventory -m setup "$@"
+
+ansible-config view -c ./ansible-testé.cfg | grep 'remote_user = admin'
+ansible-config dump -c ./ansible-testé.cfg | grep 'DEFAULT_REMOTE_USER([^)]*) = admin\>'
+ANSIBLE_REMOTE_USER=administrator ansible-config dump| grep 'DEFAULT_REMOTE_USER([^)]*) = administrator\>'
+ansible-config list | grep 'DEFAULT_REMOTE_USER'
+
+# Collection
+ansible-config view -c ./ansible-testé.cfg | grep 'collections_paths = /tmp/collections'
+ansible-config dump -c ./ansible-testé.cfg | grep 'COLLECTIONS_PATHS([^)]*) ='
+ANSIBLE_COLLECTIONS_PATHS=/tmp/collections ansible-config dump| grep 'COLLECTIONS_PATHS([^)]*) ='
+ansible-config list | grep 'COLLECTIONS_PATHS'
+
+# 'view' command must fail when config file is missing or has an invalid file extension
+ansible-config view -c ./ansible-non-existent.cfg 2> err1.txt || grep -Eq 'ERROR! The provided configuration file is missing or not accessible:' err1.txt || (cat err*.txt; rm -f err1.txt; exit 1)
+ansible-config view -c ./no-extension 2> err2.txt || grep -q 'Unsupported configuration file extension' err2.txt || (cat err2.txt; rm -f err*.txt; exit 1)
+rm -f err*.txt
+
+# test setting playbook_dir via envvar
+ANSIBLE_PLAYBOOK_DIR=/doesnotexist/tmp ansible localhost -m debug -a var=playbook_dir | grep '"playbook_dir": "/doesnotexist/tmp"'
+
+# test setting playbook_dir via cmdline
+ansible localhost -m debug -a var=playbook_dir --playbook-dir=/doesnotexist/tmp | grep '"playbook_dir": "/doesnotexist/tmp"'
+
+# test setting playbook dir via ansible.cfg
+env -u ANSIBLE_PLAYBOOK_DIR ANSIBLE_CONFIG=./playbookdir_cfg.ini ansible localhost -m debug -a var=playbook_dir | grep '"playbook_dir": "/doesnotexist/tmp"'
+
+# test adhoc callback triggers
+ANSIBLE_STDOUT_CALLBACK=callback_debug ANSIBLE_LOAD_CALLBACK_PLUGINS=1 ansible --playbook-dir . testhost -i ../../inventory -m ping | grep -E '^v2_' | diff -u adhoc-callback.stdout -
+
+# CB_WANTS_IMPLICIT isn't anything in Ansible itself.
+# Our test cb plugin just accepts it. It lets us avoid copypasting the whole
+# plugin just for two tests.
+CB_WANTS_IMPLICIT=1 ANSIBLE_STDOUT_CALLBACK=callback_meta ANSIBLE_LOAD_CALLBACK_PLUGINS=1 ansible-playbook -i ../../inventory --extra-vars @./vars.yml playbook.yml | grep 'saw implicit task'
+
+set +e
+if ANSIBLE_STDOUT_CALLBACK=callback_meta ANSIBLE_LOAD_CALLBACK_PLUGINS=1 ansible-playbook -i ../../inventory --extra-vars @./vars.yml playbook.yml | grep 'saw implicit task'; then
+ echo "Callback got implicit task and should not have"
+ exit 1
+fi
+set -e
+
+# Test that no tmp dirs are left behind when running ansible-config
+TMP_DIR=~/.ansible/tmptest
+if [[ -d "$TMP_DIR" ]]; then
+ rm -rf "$TMP_DIR"
+fi
+ANSIBLE_LOCAL_TEMP="$TMP_DIR" ansible-config list > /dev/null
+ANSIBLE_LOCAL_TEMP="$TMP_DIR" ansible-config dump > /dev/null
+ANSIBLE_LOCAL_TEMP="$TMP_DIR" ansible-config view > /dev/null
+
+# wc on macOS is dumb and returns leading spaces
+file_count=$(find "$TMP_DIR" -type d -maxdepth 1 | wc -l | sed 's/^ *//')
+if [[ $file_count -ne 1 ]]; then
+ echo "$file_count temporary files were left behind by ansible-config"
+ if [[ -d "$TMP_DIR" ]]; then
+ rm -rf "$TMP_DIR"
+ fi
+ exit 1
+fi
+
+# Ensure extra vars filename is prepended with '@' sign
+if ansible-playbook -i ../../inventory --extra-vars /tmp/non-existing-file playbook.yml; then
+ echo "extra_vars filename without '@' sign should cause failure"
+ exit 1
+fi
+
+# Ensure extra vars filename is prepended with '@' sign
+if ansible-playbook -i ../../inventory --extra-vars ./vars.yml playbook.yml; then
+ echo "extra_vars filename without '@' sign should cause failure"
+ exit 1
+fi
+
+ansible-playbook -i ../../inventory --extra-vars @./vars.yml playbook.yml
+
+# #74270 -- ensure we escape directory names before passing to re.compile()
+# particularly in module_common.
+bash module_common_regex_regression.sh
diff --git a/test/integration/targets/ansible/vars.yml b/test/integration/targets/ansible/vars.yml
new file mode 100644
index 0000000..a19e454
--- /dev/null
+++ b/test/integration/targets/ansible/vars.yml
@@ -0,0 +1 @@
+username: ansiboy
diff --git a/test/integration/targets/any_errors_fatal/50897.yml b/test/integration/targets/any_errors_fatal/50897.yml
new file mode 100644
index 0000000..1d09eb1
--- /dev/null
+++ b/test/integration/targets/any_errors_fatal/50897.yml
@@ -0,0 +1,19 @@
+- hosts: testhost,testhost2
+ gather_facts: no
+ any_errors_fatal: yes
+ tasks:
+ - name: EXPECTED FAILURE include_role that doesn't exist
+ include_role:
+ name: 'non-existant-role'
+ when:
+ - inventory_hostname == 'testhost2'
+ - test_name == 'test_include_role'
+
+ - name: EXPECTED FAILURE include_tasks that don't exist
+ include_tasks: non-existant.yml
+ when:
+ - inventory_hostname == 'testhost2'
+ - test_name == 'test_include_tasks'
+
+ - debug:
+ msg: 'any_errors_fatal_this_should_never_be_reached'
diff --git a/test/integration/targets/any_errors_fatal/aliases b/test/integration/targets/any_errors_fatal/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/any_errors_fatal/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/any_errors_fatal/always_block.yml b/test/integration/targets/any_errors_fatal/always_block.yml
new file mode 100644
index 0000000..8c6fbff
--- /dev/null
+++ b/test/integration/targets/any_errors_fatal/always_block.yml
@@ -0,0 +1,27 @@
+---
+- hosts: testhost
+ gather_facts: false
+ any_errors_fatal: true
+ tasks:
+ - block:
+ - name: initial block debug
+ debug: msg='any_errors_fatal_block, i execute normally'
+
+ - name: EXPECTED FAILURE any_errors_fatal, initial block, bin/false to simulate failure
+ command: /bin/false
+
+ - name: after a task that fails I should never execute
+ debug:
+ msg: 'any_errors_fatal_block_post_fail ... i never execute, cause ERROR!'
+ rescue:
+ - name: any_errors_fatal_rescue_block debug
+ debug: msg='any_errors_fatal_rescue_block_start ... I caught an error'
+
+ - name: EXPECTED FAILURE any_errors_fatal in rescue block, using bin/false to simulate error
+ command: /bin/false
+
+ - name: any_errors_fatal post debug
+ debug: msg='any_errors_fatal_rescue_block_post_fail ... I also never execute :-('
+ always:
+ - name: any errors fatal always block debug
+ debug: msg='any_errors_fatal_always_block_start'
diff --git a/test/integration/targets/any_errors_fatal/inventory b/test/integration/targets/any_errors_fatal/inventory
new file mode 100644
index 0000000..3ae8d9c
--- /dev/null
+++ b/test/integration/targets/any_errors_fatal/inventory
@@ -0,0 +1,6 @@
+[local]
+testhost ansible_connection=local host_var_role_name=role3
+testhost2 ansible_connection=local host_var_role_name=role2
+
+[local:vars]
+ansible_python_interpreter="{{ ansible_playbook_python }}"
diff --git a/test/integration/targets/any_errors_fatal/on_includes.yml b/test/integration/targets/any_errors_fatal/on_includes.yml
new file mode 100644
index 0000000..cbc51cb
--- /dev/null
+++ b/test/integration/targets/any_errors_fatal/on_includes.yml
@@ -0,0 +1,7 @@
+---
+# based on https://github.com/ansible/ansible/issues/22924
+- name: Test any errors fatal
+ hosts: testhost,testhost2
+ any_errors_fatal: True
+ tasks:
+ - import_tasks: test_fatal.yml
diff --git a/test/integration/targets/any_errors_fatal/play_level.yml b/test/integration/targets/any_errors_fatal/play_level.yml
new file mode 100644
index 0000000..d5a8920
--- /dev/null
+++ b/test/integration/targets/any_errors_fatal/play_level.yml
@@ -0,0 +1,15 @@
+- hosts: testhost
+ gather_facts: no
+ any_errors_fatal: true
+ tasks:
+ - name: EXPECTED FAILURE shell exe of /bin/false for testhost
+ shell: '{{ "/bin/false" if inventory_hostname == "testhost" else "/bin/true" }}'
+
+ - debug:
+ msg: "any_errors_fatal_play_level_post_fail"
+
+- hosts: testhost
+ any_errors_fatal: true
+ tasks:
+ - debug:
+ msg: "and in another play"
diff --git a/test/integration/targets/any_errors_fatal/runme.sh b/test/integration/targets/any_errors_fatal/runme.sh
new file mode 100755
index 0000000..c54ea8d
--- /dev/null
+++ b/test/integration/targets/any_errors_fatal/runme.sh
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+
+set -ux
+ansible-playbook -i inventory "$@" play_level.yml| tee out.txt | grep 'any_errors_fatal_play_level_post_fail'
+res=$?
+cat out.txt
+if [ "${res}" -eq 0 ] ; then
+ exit 1
+fi
+
+ansible-playbook -i inventory "$@" on_includes.yml | tee out.txt | grep 'any_errors_fatal_this_should_never_be_reached'
+res=$?
+cat out.txt
+if [ "${res}" -eq 0 ] ; then
+ exit 1
+fi
+
+set -ux
+
+ansible-playbook -i inventory "$@" always_block.yml | tee out.txt | grep 'any_errors_fatal_always_block_start'
+res=$?
+cat out.txt
+
+if [ "${res}" -ne 0 ] ; then
+ exit 1
+fi
+
+set -ux
+
+for test_name in test_include_role test_include_tasks; do
+ ansible-playbook -i inventory "$@" -e test_name=$test_name 50897.yml | tee out.txt | grep 'any_errors_fatal_this_should_never_be_reached'
+ res=$?
+ cat out.txt
+ if [ "${res}" -eq 0 ] ; then
+ exit 1
+ fi
+done
diff --git a/test/integration/targets/any_errors_fatal/test_fatal.yml b/test/integration/targets/any_errors_fatal/test_fatal.yml
new file mode 100644
index 0000000..a12d741
--- /dev/null
+++ b/test/integration/targets/any_errors_fatal/test_fatal.yml
@@ -0,0 +1,12 @@
+---
+- name: Setting the fact for 'test' to 'test value'
+ set_fact:
+ test: "test value"
+ when: inventory_hostname == 'testhost2'
+
+- name: EXPECTED FAILURE ejinja eval of a var that should not exist
+ debug: msg="{{ test }}"
+
+- name: testhost should never reach here as testhost2 failure above should end play
+ debug:
+ msg: "any_errors_fatal_this_should_never_be_reached"
diff --git a/test/integration/targets/apt/aliases b/test/integration/targets/apt/aliases
new file mode 100644
index 0000000..5f892f9
--- /dev/null
+++ b/test/integration/targets/apt/aliases
@@ -0,0 +1,6 @@
+shippable/posix/group2
+destructive
+skip/freebsd
+skip/osx
+skip/macos
+skip/rhel
diff --git a/test/integration/targets/apt/defaults/main.yml b/test/integration/targets/apt/defaults/main.yml
new file mode 100644
index 0000000..7ad2497
--- /dev/null
+++ b/test/integration/targets/apt/defaults/main.yml
@@ -0,0 +1,2 @@
+apt_foreign_arch: i386
+hello_old_version: 2.6-1
diff --git a/test/integration/targets/apt/handlers/main.yml b/test/integration/targets/apt/handlers/main.yml
new file mode 100644
index 0000000..0b6a98f
--- /dev/null
+++ b/test/integration/targets/apt/handlers/main.yml
@@ -0,0 +1,4 @@
+- name: remove package hello
+ apt:
+ name: hello
+ state: absent
diff --git a/test/integration/targets/apt/meta/main.yml b/test/integration/targets/apt/meta/main.yml
new file mode 100644
index 0000000..162d7fa
--- /dev/null
+++ b/test/integration/targets/apt/meta/main.yml
@@ -0,0 +1,3 @@
+dependencies:
+ - prepare_tests
+ - setup_deb_repo
diff --git a/test/integration/targets/apt/tasks/apt-builddep.yml b/test/integration/targets/apt/tasks/apt-builddep.yml
new file mode 100644
index 0000000..24ee1dc
--- /dev/null
+++ b/test/integration/targets/apt/tasks/apt-builddep.yml
@@ -0,0 +1,55 @@
+# test installing build-deps using netcat and quilt as test victims.
+#
+# Deps can be discovered like so (taken from ubuntu 12.04)
+# ====
+# root@localhost:~ # apt-rdepends --build-depends --follow=DEPENDS netcat
+# Reading package lists... Done
+# Building dependency tree
+# Reading state information... Done
+# netcat
+# Build-Depends: debhelper (>= 8.0.0)
+# Build-Depends: quilt
+# root@localhost:~ #
+# ====
+# Since many things depend on debhelper, let's just uninstall quilt, then
+# install build-dep for netcat to get it back. build-dep doesn't have an
+# uninstall, so we don't need to test for reverse actions (eg, uninstall
+# build-dep and ensure things are clean)
+
+# uninstall quilt
+- name: check quilt with dpkg
+ shell: dpkg -s quilt
+ register: dpkg_result
+ ignore_errors: true
+ tags: ['test_apt_builddep']
+
+- name: uninstall quilt with apt
+ apt: pkg=quilt state=absent purge=yes
+ register: apt_result
+ when: dpkg_result is successful
+ tags: ['test_apt_builddep']
+
+# install build-dep for rolldice
+- name: install rolldice build-dep with apt
+ apt: pkg=rolldice state=build-dep
+ register: apt_result
+ tags: ['test_apt_builddep']
+
+- name: verify build_dep of netcat
+ assert:
+ that:
+ - "'changed' in apt_result"
+ tags: ['test_apt_builddep']
+
+# ensure debhelper and qilt are installed
+- name: check build_deps with dpkg
+ shell: dpkg --get-selections | egrep '(debhelper|quilt)'
+ failed_when: False
+ register: dpkg_result
+ tags: ['test_apt_builddep']
+
+- name: verify build_deps are really there
+ assert:
+ that:
+ - "dpkg_result.rc == 0"
+ tags: ['test_apt_builddep']
diff --git a/test/integration/targets/apt/tasks/apt-multiarch.yml b/test/integration/targets/apt/tasks/apt-multiarch.yml
new file mode 100644
index 0000000..01f6766
--- /dev/null
+++ b/test/integration/targets/apt/tasks/apt-multiarch.yml
@@ -0,0 +1,44 @@
+# verify that apt is handling multi-arch systems properly
+
+- name: load version specific vars
+ include_vars: '{{ item }}'
+ with_first_found:
+ - files:
+ - '{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml'
+ - 'default.yml'
+ paths: '../vars'
+
+- name: add architecture {{ apt_foreign_arch }}
+ command: dpkg --add-architecture {{ apt_foreign_arch }}
+
+- name: install {{ multiarch_test_pkg }}:{{ apt_foreign_arch }} with apt
+ apt: pkg={{ multiarch_test_pkg }}:{{ apt_foreign_arch }} state=present update_cache=yes
+ register: apt_result
+ until: apt_result is success
+
+- name: check {{ multiarch_test_pkg }} version
+ shell: dpkg -s {{ multiarch_test_pkg }} | grep Version | awk '{print $2}'
+ register: pkg_version
+
+- name: uninstall {{ multiarch_test_pkg }}:{{ apt_foreign_arch }} with apt
+ apt: pkg={{ multiarch_test_pkg }}:{{ apt_foreign_arch }} state=absent purge=yes
+
+- name: install deb file
+ apt: deb="/var/cache/apt/archives/{{ multiarch_test_pkg }}_{{ pkg_version.stdout }}_{{ apt_foreign_arch }}.deb"
+ register: apt_multi_initial
+
+- name: install deb file again
+ apt: deb="/var/cache/apt/archives/{{ multiarch_test_pkg }}_{{ pkg_version.stdout }}_{{ apt_foreign_arch }}.deb"
+ register: apt_multi_secondary
+
+- name: verify installation of {{ multiarch_test_pkg }}:{{ apt_foreign_arch }}
+ assert:
+ that:
+ - "apt_multi_initial.changed"
+ - "not apt_multi_secondary.changed"
+
+- name: remove all {{ apt_foreign_arch }} packages
+ shell: "apt-get remove -y --allow-remove-essential '*:{{ apt_foreign_arch }}'"
+
+- name: remove {{ apt_foreign_arch }} architecture
+ command: dpkg --remove-architecture {{ apt_foreign_arch }}
diff --git a/test/integration/targets/apt/tasks/apt.yml b/test/integration/targets/apt/tasks/apt.yml
new file mode 100644
index 0000000..d273eda
--- /dev/null
+++ b/test/integration/targets/apt/tasks/apt.yml
@@ -0,0 +1,547 @@
+- name: use Debian mirror
+ set_fact:
+ distro_mirror: http://ftp.debian.org/debian
+ when: ansible_distribution == 'Debian'
+
+- name: use Ubuntu mirror
+ set_fact:
+ distro_mirror: http://archive.ubuntu.com/ubuntu
+ when: ansible_distribution == 'Ubuntu'
+
+# UNINSTALL 'python-apt'
+# The `apt` module has the smarts to auto-install `python-apt(3)`. To test, we
+# will first uninstall `python-apt`.
+- name: uninstall python-apt with apt
+ apt:
+ pkg: [python-apt, python3-apt]
+ state: absent
+ purge: yes
+ register: apt_result
+
+# In check mode, auto-install of `python-apt` must fail
+- name: test fail uninstall hello without required apt deps in check mode
+ apt:
+ pkg: hello
+ state: absent
+ purge: yes
+ register: apt_result
+ check_mode: yes
+ ignore_errors: yes
+
+- name: verify fail uninstall hello without required apt deps in check mode
+ assert:
+ that:
+ - apt_result is failed
+ - '"If run normally this module can auto-install it." in apt_result.msg'
+
+- name: check with dpkg
+ shell: dpkg -s python-apt python3-apt
+ register: dpkg_result
+ ignore_errors: true
+
+# UNINSTALL 'hello'
+# With 'python-apt' uninstalled, the first call to 'apt' should install
+# python-apt without updating the cache.
+- name: uninstall hello with apt and prevent updating the cache
+ apt:
+ pkg: hello
+ state: absent
+ purge: yes
+ update_cache: no
+ register: apt_result
+
+- name: check hello with dpkg
+ shell: dpkg-query -l hello
+ failed_when: False
+ register: dpkg_result
+
+- name: verify uninstall hello with apt and prevent updating the cache
+ assert:
+ that:
+ - "'changed' in apt_result"
+ - apt_result is not changed
+ - "dpkg_result.rc == 1"
+
+- name: Test installing fnmatch package
+ apt:
+ name:
+ - hel?o
+ - he?lo
+ register: apt_install_fnmatch
+
+- name: Test uninstalling fnmatch package
+ apt:
+ name:
+ - hel?o
+ - he?lo
+ state: absent
+ register: apt_uninstall_fnmatch
+
+- name: verify fnmatch
+ assert:
+ that:
+ - apt_install_fnmatch is changed
+ - apt_uninstall_fnmatch is changed
+
+- name: Test update_cache 1 (check mode)
+ apt:
+ update_cache: true
+ cache_valid_time: 10
+ register: apt_update_cache_1_check_mode
+ check_mode: true
+
+- name: Test update_cache 1
+ apt:
+ update_cache: true
+ cache_valid_time: 10
+ register: apt_update_cache_1
+
+- name: Test update_cache 2 (check mode)
+ apt:
+ update_cache: true
+ cache_valid_time: 10
+ register: apt_update_cache_2_check_mode
+ check_mode: true
+
+- name: Test update_cache 2
+ apt:
+ update_cache: true
+ cache_valid_time: 10
+ register: apt_update_cache_2
+
+- name: verify update_cache
+ assert:
+ that:
+ - apt_update_cache_1_check_mode is changed
+ - apt_update_cache_1 is changed
+ - apt_update_cache_2_check_mode is not changed
+ - apt_update_cache_2 is not changed
+
+- name: uninstall apt bindings with apt again
+ apt:
+ pkg: [python-apt, python3-apt]
+ state: absent
+ purge: yes
+
+# UNINSTALL 'hello'
+# With 'python-apt' uninstalled, the first call to 'apt' should install
+# python-apt.
+- name: uninstall hello with apt
+ apt: pkg=hello state=absent purge=yes
+ register: apt_result
+ until: apt_result is success
+
+- name: check hello with dpkg
+ shell: dpkg-query -l hello
+ failed_when: False
+ register: dpkg_result
+
+- name: verify uninstallation of hello
+ assert:
+ that:
+ - "'changed' in apt_result"
+ - apt_result is not changed
+ - "dpkg_result.rc == 1"
+
+# UNINSTALL AGAIN
+- name: uninstall hello with apt
+ apt: pkg=hello state=absent purge=yes
+ register: apt_result
+
+- name: verify no change on re-uninstall
+ assert:
+ that:
+ - "not apt_result.changed"
+
+# INSTALL
+- name: install hello with apt
+ apt: name=hello state=present
+ register: apt_result
+
+- name: check hello with dpkg
+ shell: dpkg-query -l hello
+ failed_when: False
+ register: dpkg_result
+
+- name: verify installation of hello
+ assert:
+ that:
+ - "apt_result.changed"
+ - "dpkg_result.rc == 0"
+
+- name: verify apt module outputs
+ assert:
+ that:
+ - "'changed' in apt_result"
+ - "'stderr' in apt_result"
+ - "'stdout' in apt_result"
+ - "'stdout_lines' in apt_result"
+
+# INSTALL AGAIN
+- name: install hello with apt
+ apt: name=hello state=present
+ register: apt_result
+
+- name: verify no change on re-install
+ assert:
+ that:
+ - "not apt_result.changed"
+
+# UNINSTALL AGAIN
+- name: uninstall hello with apt
+ apt: pkg=hello state=absent purge=yes
+ register: apt_result
+
+# INSTALL WITH VERSION WILDCARD
+- name: install hello with apt
+ apt: name=hello=2.* state=present
+ register: apt_result
+
+- name: check hello with wildcard with dpkg
+ shell: dpkg-query -l hello
+ failed_when: False
+ register: dpkg_result
+
+- name: verify installation of hello
+ assert:
+ that:
+ - "apt_result.changed"
+ - "dpkg_result.rc == 0"
+
+- name: check hello version
+ shell: dpkg -s hello | grep Version | awk '{print $2}'
+ register: hello_version
+
+- name: check hello architecture
+ shell: dpkg -s hello | grep Architecture | awk '{print $2}'
+ register: hello_architecture
+
+- name: uninstall hello with apt
+ apt: pkg=hello state=absent purge=yes
+
+# INSTALL WITHOUT REMOVALS
+- name: Install hello, that conflicts with hello-traditional
+ apt:
+ pkg: hello
+ state: present
+ update_cache: no
+
+- name: check hello
+ shell: dpkg-query -l hello
+ register: dpkg_result
+
+- name: verify installation of hello
+ assert:
+ that:
+ - "apt_result.changed"
+ - "dpkg_result.rc == 0"
+
+- name: Try installing hello-traditional, that conflicts with hello
+ apt:
+ pkg: hello-traditional
+ state: present
+ fail_on_autoremove: yes
+ ignore_errors: yes
+ register: apt_result
+
+- name: verify failure of installing hello-traditional, because it is required to remove hello to install.
+ assert:
+ that:
+ - apt_result is failed
+ - '"Packages need to be removed but remove is disabled." in apt_result.msg'
+
+- name: uninstall hello with apt
+ apt:
+ pkg: hello
+ state: absent
+ purge: yes
+ update_cache: no
+
+- name: install deb file
+ apt: deb="/var/cache/apt/archives/hello_{{ hello_version.stdout }}_{{ hello_architecture.stdout }}.deb"
+ register: apt_initial
+
+- name: install deb file again
+ apt: deb="/var/cache/apt/archives/hello_{{ hello_version.stdout }}_{{ hello_architecture.stdout }}.deb"
+ register: apt_secondary
+
+- name: verify installation of hello
+ assert:
+ that:
+ - "apt_initial.changed"
+ - "not apt_secondary.changed"
+
+- name: uninstall hello with apt
+ apt: pkg=hello state=absent purge=yes
+
+- name: install deb file from URL
+ apt: deb="{{ distro_mirror }}/pool/main/h/hello/hello_{{ hello_version.stdout }}_{{ hello_architecture.stdout }}.deb"
+ register: apt_url
+
+- name: verify installation of hello
+ assert:
+ that:
+ - "apt_url.changed"
+
+- name: uninstall hello with apt
+ apt: pkg=hello state=absent purge=yes
+
+- name: force install of deb
+ apt: deb="/var/cache/apt/archives/hello_{{ hello_version.stdout }}_{{ hello_architecture.stdout }}.deb" force=true
+ register: dpkg_force
+
+- name: verify installation of hello
+ assert:
+ that:
+ - "dpkg_force.changed"
+
+# NEGATIVE: upgrade all packages while providing additional packages to install
+- name: provide additional packages to install while upgrading all installed packages
+ apt: pkg=*,test state=latest
+ ignore_errors: True
+ register: apt_result
+
+- name: verify failure of upgrade packages and install
+ assert:
+ that:
+ - "not apt_result.changed"
+ - "apt_result.failed"
+
+- name: autoclean during install
+ apt: pkg=hello state=present autoclean=yes
+
+- name: undo previous install
+ apt: pkg=hello state=absent
+
+# https://github.com/ansible/ansible/issues/23155
+- name: create a repo file
+ copy:
+ dest: /etc/apt/sources.list.d/non-existing.list
+ content: deb http://ppa.launchpad.net/non-existing trusty main
+
+- name: test for sane error message
+ apt:
+ update_cache: yes
+ register: apt_result
+ ignore_errors: yes
+
+- name: verify sane error message
+ assert:
+ that:
+ - "'Failed to fetch' in apt_result['msg']"
+ - "'403' in apt_result['msg']"
+
+- name: Clean up
+ file:
+ name: /etc/apt/sources.list.d/non-existing.list
+ state: absent
+
+# https://github.com/ansible/ansible/issues/28907
+- name: Install parent package
+ apt:
+ name: libcaca-dev
+
+- name: Install child package
+ apt:
+ name: libslang2-dev
+
+- shell: apt-mark showmanual | grep libcaca-dev
+ ignore_errors: yes
+ register: parent_output
+
+- name: Check that parent package is marked as installed manually
+ assert:
+ that:
+ - "'libcaca-dev' in parent_output.stdout"
+
+- shell: apt-mark showmanual | grep libslang2-dev
+ ignore_errors: yes
+ register: child_output
+
+- name: Check that child package is marked as installed manually
+ assert:
+ that:
+ - "'libslang2-dev' in child_output.stdout"
+
+- name: Clean up
+ apt:
+ name: "{{ pkgs }}"
+ state: absent
+ vars:
+ pkgs:
+ - libcaca-dev
+ - libslang2-dev
+
+# https://github.com/ansible/ansible/issues/38995
+- name: build-dep for a package
+ apt:
+ name: tree
+ state: build-dep
+ register: apt_result
+
+- name: Check the result
+ assert:
+ that:
+ - apt_result is changed
+
+- name: build-dep for a package (idempotency)
+ apt:
+ name: tree
+ state: build-dep
+ register: apt_result
+
+- name: Check the result
+ assert:
+ that:
+ - apt_result is not changed
+
+# check policy_rc_d parameter
+
+- name: Install unscd but forbid service start
+ apt:
+ name: unscd
+ policy_rc_d: 101
+
+- name: Stop unscd service
+ service:
+ name: unscd
+ state: stopped
+ register: service_unscd_stop
+
+- name: unscd service shouldn't have been stopped by previous task
+ assert:
+ that: service_unscd_stop is not changed
+
+- name: Uninstall unscd
+ apt:
+ name: unscd
+ policy_rc_d: 101
+
+- name: Create incorrect /usr/sbin/policy-rc.d
+ copy:
+ dest: /usr/sbin/policy-rc.d
+ content: apt integration test
+ mode: 0755
+
+- name: Install unscd but forbid service start
+ apt:
+ name: unscd
+ policy_rc_d: 101
+
+- name: Stop unscd service
+ service:
+ name: unscd
+ state: stopped
+ register: service_unscd_stop
+
+- name: unscd service shouldn't have been stopped by previous task
+ assert:
+ that: service_unscd_stop is not changed
+
+- name: Create incorrect /usr/sbin/policy-rc.d
+ copy:
+ dest: /usr/sbin/policy-rc.d
+ content: apt integration test
+ mode: 0755
+ register: policy_rc_d
+
+- name: Check if /usr/sbin/policy-rc.d was correctly backed-up during unscd install
+ assert:
+ that: policy_rc_d is not changed
+
+- name: Delete /usr/sbin/policy-rc.d
+ file:
+ path: /usr/sbin/policy-rc.d
+ state: absent
+
+# https://github.com/ansible/ansible/issues/65325
+- name: Download and install old version of hello (to test allow_change_held_packages option)
+ apt: "deb=https://ci-files.testing.ansible.com/test/integration/targets/dpkg_selections/hello_{{ hello_old_version }}_amd64.deb"
+ notify:
+ - remove package hello
+
+- name: Put hello on hold
+ shell: apt-mark hold hello
+
+- name: Get hold list
+ shell: apt-mark showhold
+ register: allow_change_held_packages_hold
+
+- name: Check that the package hello is on the hold list
+ assert:
+ that:
+ - "'hello' in allow_change_held_packages_hold.stdout"
+
+- name: Try updating package to the latest version (allow_change_held_packages=no)
+ apt:
+ name: hello
+ state: latest
+ ignore_errors: True
+ register: allow_change_held_packages_failed_update
+
+- name: Get the version of the package
+ shell: dpkg -s hello | grep Version | awk '{print $2}'
+ register: allow_change_held_packages_hello_version
+
+- name: Verify that the package was not updated (apt returns with an error)
+ assert:
+ that:
+ - "allow_change_held_packages_failed_update is failed"
+ - "'--allow-change-held-packages' in allow_change_held_packages_failed_update.stderr"
+ - "allow_change_held_packages_hello_version.stdout == hello_old_version"
+
+- name: Try updating package to the latest version (allow_change_held_packages=yes)
+ apt:
+ name: hello
+ state: latest
+ allow_change_held_packages: yes
+ register: allow_change_held_packages_successful_update
+
+- name: Get the version of the package
+ shell: dpkg -s hello | grep Version | awk '{print $2}'
+ register: allow_change_held_packages_hello_version
+
+- name: Verify that the package was updated
+ assert:
+ that:
+ - "allow_change_held_packages_successful_update is changed"
+ - "allow_change_held_packages_hello_version.stdout != hello_old_version"
+
+- name: Try updating package to the latest version again
+ apt:
+ name: hello
+ state: latest
+ allow_change_held_packages: yes
+ register: allow_change_held_packages_no_update
+
+- name: Get the version of the package
+ shell: dpkg -s hello | grep Version | awk '{print $2}'
+ register: allow_change_held_packages_hello_version_again
+
+- name: Verify that the package was not updated
+ assert:
+ that:
+ - "allow_change_held_packages_no_update is not changed"
+ - "allow_change_held_packages_hello_version.stdout == allow_change_held_packages_hello_version_again.stdout"
+
+# Virtual package
+- name: Install a virtual package
+ apt:
+ package:
+ - emacs-nox
+ - yaml-mode # <- the virtual package
+ state: latest
+ register: install_virtual_package_result
+
+- name: Check the virtual package install result
+ assert:
+ that:
+ - install_virtual_package_result is changed
+
+- name: Clean up virtual-package install
+ apt:
+ package:
+ - emacs-nox
+ - elpa-yaml-mode
+ state: absent
+ purge: yes
diff --git a/test/integration/targets/apt/tasks/downgrade.yml b/test/integration/targets/apt/tasks/downgrade.yml
new file mode 100644
index 0000000..896b644
--- /dev/null
+++ b/test/integration/targets/apt/tasks/downgrade.yml
@@ -0,0 +1,77 @@
+- block:
+ - name: Disable ubuntu repos so system packages are not upgraded and do not change testing env
+ command: mv /etc/apt/sources.list /etc/apt/sources.list.backup
+
+ - name: install latest foo
+ apt:
+ name: foo
+ state: latest
+ allow_unauthenticated: yes
+
+ - name: check foo version
+ shell: dpkg -s foo | grep Version | awk '{print $2}'
+ register: apt_downgrade_foo_version
+
+ - name: ensure the correct version of foo has been installed
+ assert:
+ that:
+ - "'1.0.1' in apt_downgrade_foo_version.stdout"
+
+ - name: try to downgrade foo
+ apt:
+ name: foo=1.0.0
+ state: present
+ allow_unauthenticated: yes
+ ignore_errors: yes
+ register: apt_downgrade_foo_fail
+
+ - name: verify failure of downgrading without allow downgrade flag
+ assert:
+ that:
+ - apt_downgrade_foo_fail is failed
+
+ - name: try to downgrade foo with flag
+ apt:
+ name: foo=1.0.0
+ state: present
+ allow_downgrade: yes
+ allow_unauthenticated: yes
+ register: apt_downgrade_foo_succeed
+
+ - name: verify success of downgrading with allow downgrade flag
+ assert:
+ that:
+ - apt_downgrade_foo_succeed is success
+
+ - name: check foo version
+ shell: dpkg -s foo | grep Version | awk '{print $2}'
+ register: apt_downgrade_foo_version
+
+ - name: check that version downgraded correctly
+ assert:
+ that:
+ - "'1.0.0' in apt_downgrade_foo_version.stdout"
+ - "{{ apt_downgrade_foo_version.changed }}"
+
+ - name: downgrade foo with flag again
+ apt:
+ name: foo=1.0.0
+ state: present
+ allow_downgrade: yes
+ allow_unauthenticated: yes
+ register: apt_downgrade_second_downgrade
+
+ - name: check that nothing has changed (idempotent)
+ assert:
+ that:
+ - "apt_downgrade_second_downgrade.changed == false"
+
+ always:
+ - name: Clean up
+ apt:
+ pkg: foo,foobar
+ state: absent
+ autoclean: yes
+
+ - name: Restore ubuntu repos
+ command: mv /etc/apt/sources.list.backup /etc/apt/sources.list
diff --git a/test/integration/targets/apt/tasks/main.yml b/test/integration/targets/apt/tasks/main.yml
new file mode 100644
index 0000000..13d3e4f
--- /dev/null
+++ b/test/integration/targets/apt/tasks/main.yml
@@ -0,0 +1,40 @@
+# (c) 2014, James Tanner <tanner.jc@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- block:
+ - import_tasks: 'apt.yml'
+
+ - import_tasks: 'url-with-deps.yml'
+
+ - import_tasks: 'apt-multiarch.yml'
+ when:
+ - ansible_userspace_architecture != apt_foreign_arch
+
+ - import_tasks: 'apt-builddep.yml'
+
+ - block:
+ - import_tasks: 'repo.yml'
+ always:
+ - file:
+ path: /etc/apt/sources.list.d/file_tmp_repo.list
+ state: absent
+ - file:
+ name: "{{ repodir }}"
+ state: absent
+
+ when:
+ - ansible_distribution in ('Ubuntu', 'Debian')
diff --git a/test/integration/targets/apt/tasks/repo.yml b/test/integration/targets/apt/tasks/repo.yml
new file mode 100644
index 0000000..d4cce78
--- /dev/null
+++ b/test/integration/targets/apt/tasks/repo.yml
@@ -0,0 +1,452 @@
+- block:
+ - name: Install foo package version 1.0.0
+ apt:
+ name: foo=1.0.0
+ allow_unauthenticated: yes
+ register: apt_result
+
+ - name: Check install with dpkg
+ shell: dpkg-query -l foo
+ register: dpkg_result
+
+ - name: Check if install was successful
+ assert:
+ that:
+ - "apt_result is success"
+ - "dpkg_result is success"
+ - "'1.0.0' in dpkg_result.stdout"
+
+ - name: Update to foo version 1.0.1
+ apt:
+ name: foo
+ state: latest
+ allow_unauthenticated: yes
+ register: apt_result
+
+ - name: Check install with dpkg
+ shell: dpkg-query -l foo
+ register: dpkg_result
+
+ - name: Check if install was successful
+ assert:
+ that:
+ - "apt_result is success"
+ - "dpkg_result is success"
+ - "'1.0.1' in dpkg_result.stdout"
+ always:
+ - name: Clean up
+ apt:
+ name: foo
+ state: absent
+ allow_unauthenticated: yes
+
+- name: Try to install non-existent version
+ apt:
+ name: foo=99
+ state: present
+ ignore_errors: true
+ register: apt_result
+
+- name: Check if install failed
+ assert:
+ that:
+ - apt_result is failed
+
+# https://github.com/ansible/ansible/issues/30638
+- block:
+ - name: Don't install foo=1.0.1 since foo is not installed and only_upgrade is set
+ apt:
+ name: foo=1.0.1
+ state: present
+ only_upgrade: yes
+ allow_unauthenticated: yes
+ ignore_errors: yes
+ register: apt_result
+
+ - name: Check that foo was not upgraded
+ assert:
+ that:
+ - "apt_result is not changed"
+ - "apt_result is success"
+
+ - apt:
+ name: foo=1.0.0
+ allow_unauthenticated: yes
+
+ - name: Upgrade foo to 1.0.1 but don't upgrade foobar since it is not installed
+ apt:
+ name: foobar=1.0.1,foo=1.0.1
+ state: present
+ only_upgrade: yes
+ allow_unauthenticated: yes
+ register: apt_result
+
+ - name: Check install with dpkg
+ shell: "dpkg-query -l {{ item }}"
+ register: dpkg_result
+ ignore_errors: yes
+ loop:
+ - foobar
+ - foo
+
+ - name: Check if install was successful
+ assert:
+ that:
+ - "apt_result is success"
+ - "dpkg_result.results[0] is failure"
+ - "'1.0.1' not in dpkg_result.results[0].stdout"
+ - "dpkg_result.results[1] is success"
+ - "'1.0.1' in dpkg_result.results[1].stdout"
+ always:
+ - name: Clean up
+ apt:
+ name: foo
+ state: absent
+ allow_unauthenticated: yes
+
+- block:
+ - name: Install foo=1.0.0
+ apt:
+ name: foo=1.0.0
+
+ - name: Run version test matrix
+ apt:
+ name: foo{{ item.0 }}
+ default_release: '{{ item.1 }}'
+ state: '{{ item.2 | ternary("latest","present") }}'
+ check_mode: true
+ register: apt_result
+ loop:
+ # [filter, release, state_latest, expected]
+ - ["", null, false, null]
+ - ["", null, true, "1.0.1"]
+ - ["=1.0.0", null, false, null]
+ - ["=1.0.0", null, true, null]
+ - ["=1.0.1", null, false, "1.0.1"]
+ #- ["=1.0.*", null, false, null] # legacy behavior. should not upgrade without state=latest
+ - ["=1.0.*", null, true, "1.0.1"]
+ - [">=1.0.0", null, false, null]
+ - [">=1.0.0", null, true, "1.0.1"]
+ - [">=1.0.1", null, false, "1.0.1"]
+ - ["", "testing", false, null]
+ - ["", "testing", true, "2.0.1"]
+ - ["=2.0.0", null, false, "2.0.0"]
+ - [">=2.0.0", "testing", false, "2.0.1"]
+
+ - name: Validate version test matrix
+ assert:
+ that:
+ - (item.item.3 is not none) == (item.stdout is defined)
+ - item.item.3 is none or "Inst foo [1.0.0] (" + item.item.3 + " localhost [all])" in item.stdout_lines
+ loop: '{{ apt_result.results }}'
+
+ - name: Pin foo=1.0.0
+ copy:
+ content: |-
+ Package: foo
+ Pin: version 1.0.0
+ Pin-Priority: 1000
+ dest: /etc/apt/preferences.d/foo
+
+ - name: Run pinning version test matrix
+ apt:
+ name: foo{{ item.0 }}
+ default_release: '{{ item.1 }}'
+ state: '{{ item.2 | ternary("latest","present") }}'
+ check_mode: true
+ ignore_errors: true
+ register: apt_result
+ loop:
+ # [filter, release, state_latest, expected] # expected=null for no change. expected=False to assert an error
+ - ["", null, false, null]
+ - ["", null, true, null]
+ - ["=1.0.0", null, false, null]
+ - ["=1.0.0", null, true, null]
+ - ["=1.0.1", null, false, "1.0.1"]
+ #- ["=1.0.*", null, false, null] # legacy behavior. should not upgrade without state=latest
+ - ["=1.0.*", null, true, "1.0.1"]
+ - [">=1.0.0", null, false, null]
+ - [">=1.0.0", null, true, null]
+ - [">=1.0.1", null, false, False]
+ - ["", "testing", false, null]
+ - ["", "testing", true, null]
+ - ["=2.0.0", null, false, "2.0.0"]
+ - [">=2.0.0", "testing", false, False]
+
+ - name: Validate pinning version test matrix
+ assert:
+ that:
+ - (item.item.3 != False) or (item.item.3 == False and item is failed)
+ - (item.item.3 is string) == (item.stdout is defined)
+ - item.item.3 is not string or "Inst foo [1.0.0] (" + item.item.3 + " localhost [all])" in item.stdout_lines
+ loop: '{{ apt_result.results }}'
+
+ always:
+ - name: Uninstall foo
+ apt:
+ name: foo
+ state: absent
+
+ - name: Unpin foo
+ file:
+ path: /etc/apt/preferences.d/foo
+ state: absent
+
+# https://github.com/ansible/ansible/issues/35900
+- block:
+ - name: Disable ubuntu repos so system packages are not upgraded and do not change testing env
+ command: mv /etc/apt/sources.list /etc/apt/sources.list.backup
+
+ - name: Install foobar, installs foo as a dependency
+ apt:
+ name: foobar=1.0.0
+ allow_unauthenticated: yes
+
+ - name: mark foobar as auto for next test
+ shell: apt-mark auto foobar
+
+ - name: Install foobar (marked as manual) (check mode)
+ apt:
+ name: foobar=1.0.1
+ allow_unauthenticated: yes
+ check_mode: yes
+ register: manual_foobar_install_check_mode
+
+ - name: check foobar was not marked as manually installed by check mode
+ shell: apt-mark showmanual | grep foobar
+ ignore_errors: yes
+ register: showmanual
+
+ - assert:
+ that:
+ - manual_foobar_install_check_mode.changed
+ - "'foobar' not in showmanual.stdout"
+
+ - name: Install foobar (marked as manual)
+ apt:
+ name: foobar=1.0.1
+ allow_unauthenticated: yes
+ register: manual_foobar_install
+
+ - name: check foobar was marked as manually installed
+ shell: apt-mark showmanual | grep foobar
+ ignore_errors: yes
+ register: showmanual
+
+ - assert:
+ that:
+ - manual_foobar_install.changed
+ - "'foobar' in showmanual.stdout"
+
+ - name: Upgrade foobar to a version which does not depend on foo, autoremove should remove foo
+ apt:
+ upgrade: dist
+ autoremove: yes
+ allow_unauthenticated: yes
+
+ - name: Check foo with dpkg
+ shell: dpkg-query -l foo
+ register: dpkg_result
+ ignore_errors: yes
+
+ - name: Check that foo was removed by autoremove
+ assert:
+ that:
+ - "dpkg_result is failed"
+
+ always:
+ - name: Clean up
+ apt:
+ pkg: foo,foobar
+ state: absent
+ autoclean: yes
+
+ - name: Restore ubuntu repos
+ command: mv /etc/apt/sources.list.backup /etc/apt/sources.list
+
+
+# https://github.com/ansible/ansible/issues/26298
+- block:
+ - name: Disable ubuntu repos so system packages are not upgraded and do not change testing env
+ command: mv /etc/apt/sources.list /etc/apt/sources.list.backup
+
+ - name: Install foobar, installs foo as a dependency
+ apt:
+ name: foobar=1.0.0
+ allow_unauthenticated: yes
+
+ - name: Upgrade foobar to a version which does not depend on foo
+ apt:
+ upgrade: dist
+ force: yes # workaround for --allow-unauthenticated used along with upgrade
+
+ - name: autoremove should remove foo
+ apt:
+ autoremove: yes
+ register: autoremove_result
+
+ - name: Check that autoremove correctly reports changed=True
+ assert:
+ that:
+ - "autoremove_result is changed"
+
+ - name: Check foo with dpkg
+ shell: dpkg-query -l foo
+ register: dpkg_result
+ ignore_errors: yes
+
+ - name: Check that foo was removed by autoremove
+ assert:
+ that:
+ - "dpkg_result is failed"
+
+ - name: Nothing to autoremove
+ apt:
+ autoremove: yes
+ register: autoremove_result
+
+ - name: Check that autoremove correctly reports changed=False
+ assert:
+ that:
+ - "autoremove_result is not changed"
+
+ - name: Create a fake .deb file for autoclean to remove
+ file:
+ name: /var/cache/apt/archives/python3-q_2.4-1_all.deb
+ state: touch
+
+ - name: autoclean fake .deb file
+ apt:
+ autoclean: yes
+ register: autoclean_result
+
+ - name: Check if the .deb file exists
+ stat:
+ path: /var/cache/apt/archives/python3-q_2.4-1_all.deb
+ register: stat_result
+
+ - name: Check that autoclean correctly reports changed=True and file was removed
+ assert:
+ that:
+ - "autoclean_result is changed"
+ - "not stat_result.stat.exists"
+
+ - name: Nothing to autoclean
+ apt:
+ autoclean: yes
+ register: autoclean_result
+
+ - name: Check that autoclean correctly reports changed=False
+ assert:
+ that:
+ - "autoclean_result is not changed"
+
+ always:
+ - name: Clean up
+ apt:
+ pkg: foo,foobar
+ state: absent
+ autoclean: yes
+
+ - name: Restore ubuntu repos
+ command: mv /etc/apt/sources.list.backup /etc/apt/sources.list
+
+- name: Downgrades
+ import_tasks: "downgrade.yml"
+
+- name: Upgrades
+ block:
+ - import_tasks: "upgrade.yml"
+ vars:
+ aptitude_present: "{{ True | bool }}"
+ upgrade_type: "dist"
+ force_apt_get: "{{ False | bool }}"
+
+ - name: Check if aptitude is installed
+ command: dpkg-query --show --showformat='${db:Status-Abbrev}' aptitude
+ register: aptitude_status
+
+ - name: Remove aptitude, if installed, to test fall-back to apt-get
+ apt:
+ pkg: aptitude
+ state: absent
+ when:
+ - aptitude_status.stdout.find('ii') != -1
+
+ - include_tasks: "upgrade.yml"
+ vars:
+ aptitude_present: "{{ False | bool }}"
+ upgrade_type: "{{ item.upgrade_type }}"
+ force_apt_get: "{{ item.force_apt_get }}"
+ with_items:
+ - { upgrade_type: safe, force_apt_get: False }
+ - { upgrade_type: full, force_apt_get: False }
+ - { upgrade_type: safe, force_apt_get: True }
+ - { upgrade_type: full, force_apt_get: True }
+
+ - name: (Re-)Install aptitude, run same tests again
+ apt:
+ pkg: aptitude
+ state: present
+
+ - include_tasks: "upgrade.yml"
+ vars:
+ aptitude_present: "{{ True | bool }}"
+ upgrade_type: "{{ item.upgrade_type }}"
+ force_apt_get: "{{ item.force_apt_get }}"
+ with_items:
+ - { upgrade_type: safe, force_apt_get: False }
+ - { upgrade_type: full, force_apt_get: False }
+ - { upgrade_type: safe, force_apt_get: True }
+ - { upgrade_type: full, force_apt_get: True }
+
+ - name: Remove aptitude if not originally present
+ apt:
+ pkg: aptitude
+ state: absent
+ when:
+ - aptitude_status.stdout.find('ii') == -1
+
+- block:
+ - name: Install the foo package with diff=yes
+ apt:
+ name: foo
+ allow_unauthenticated: yes
+ diff: yes
+ register: apt_result
+
+ - name: Check the content of diff.prepared
+ assert:
+ that:
+ - apt_result is success
+ - "'The following NEW packages will be installed:\n foo' in apt_result.diff.prepared"
+ always:
+ - name: Clean up
+ apt:
+ name: foo
+ state: absent
+ allow_unauthenticated: yes
+
+- block:
+ - name: Install foo package version 1.0.0 with force=yes, implies allow_unauthenticated=yes
+ apt:
+ name: foo=1.0.0
+ force: yes
+ register: apt_result
+
+ - name: Check install with dpkg
+ shell: dpkg-query -l foo
+ register: dpkg_result
+
+ - name: Check if install was successful
+ assert:
+ that:
+ - "apt_result is success"
+ - "dpkg_result is success"
+ - "'1.0.0' in dpkg_result.stdout"
+ always:
+ - name: Clean up
+ apt:
+ name: foo
+ state: absent
+ allow_unauthenticated: yes
diff --git a/test/integration/targets/apt/tasks/upgrade.yml b/test/integration/targets/apt/tasks/upgrade.yml
new file mode 100644
index 0000000..cf747c8
--- /dev/null
+++ b/test/integration/targets/apt/tasks/upgrade.yml
@@ -0,0 +1,64 @@
+- block:
+ - name: Disable ubuntu repos so system packages are not upgraded and do not change testing env
+ command: mv /etc/apt/sources.list /etc/apt/sources.list.backup
+
+ - name: install foo-1.0.0
+ apt:
+ name: foo=1.0.0
+ state: present
+ allow_unauthenticated: yes
+
+ - name: check foo version
+ shell: dpkg -s foo | grep Version | awk '{print $2}'
+ register: foo_version
+
+ - name: ensure the correct version of foo has been installed
+ assert:
+ that:
+ - "'1.0.0' in foo_version.stdout"
+
+ - name: "(upgrade type: {{upgrade_type}}) upgrade packages to latest version, force_apt_get: {{force_apt_get}}"
+ apt:
+ upgrade: "{{ upgrade_type }}"
+ force_apt_get: "{{ force_apt_get }}"
+ force: yes
+ register: upgrade_result
+
+ - name: check foo version
+ shell: dpkg -s foo | grep Version | awk '{print $2}'
+ register: foo_version
+
+ - name: check that warning is not given when force_apt_get set
+ assert:
+ that:
+ - "'warnings' not in upgrade_result"
+ when:
+ - force_apt_get
+
+ - name: check that old version upgraded correctly
+ assert:
+ that:
+ - "'1.0.0' not in foo_version.stdout"
+ - "{{ foo_version.changed }}"
+
+ - name: "(upgrade type: {{upgrade_type}}) upgrade packages to latest version (Idempotant)"
+ apt:
+ upgrade: "{{ upgrade_type }}"
+ force_apt_get: "{{ force_apt_get }}"
+ force: yes
+ register: second_upgrade_result
+
+ - name: check that nothing has changed (Idempotant)
+ assert:
+ that:
+ - "second_upgrade_result.changed == false"
+
+ always:
+ - name: Clean up
+ apt:
+ pkg: foo,foobar
+ state: absent
+ autoclean: yes
+
+ - name: Restore ubuntu repos
+ command: mv /etc/apt/sources.list.backup /etc/apt/sources.list
diff --git a/test/integration/targets/apt/tasks/url-with-deps.yml b/test/integration/targets/apt/tasks/url-with-deps.yml
new file mode 100644
index 0000000..7c70eb9
--- /dev/null
+++ b/test/integration/targets/apt/tasks/url-with-deps.yml
@@ -0,0 +1,56 @@
+- block:
+ - name: Install https transport for apt
+ apt:
+ name: apt-transport-https
+
+ - name: Ensure echo-hello is not installed
+ apt:
+ name: echo-hello
+ state: absent
+ purge: yes
+
+ # Note that this .deb is just a stupidly tiny one that has a dependency
+ # on vim-tiny. Really any .deb will work here so long as it has
+ # dependencies that exist in a repo and get brought in.
+ # The source and files for building this .deb can be found here:
+ # https://ci-files.testing.ansible.com/test/integration/targets/apt/echo-hello-source.tar.gz
+ - name: Install deb file with dependencies from URL (check_mode)
+ apt:
+ deb: https://ci-files.testing.ansible.com/test/integration/targets/apt/echo-hello_1.0_all.deb
+ check_mode: true
+ register: apt_url_deps_check_mode
+
+ - name: check to make sure we didn't install the package due to check_mode
+ shell: dpkg-query -l echo-hello
+ failed_when: false
+ register: dpkg_result_check_mode
+
+ - name: verify check_mode installation of echo-hello
+ assert:
+ that:
+ - apt_url_deps_check_mode is changed
+ - dpkg_result_check_mode.rc != 0
+
+ - name: Install deb file with dependencies from URL (for real this time)
+ apt:
+ deb: https://ci-files.testing.ansible.com/test/integration/targets/apt/echo-hello_1.0_all.deb
+ register: apt_url_deps
+
+ - name: check to make sure we installed the package
+ shell: dpkg-query -l echo-hello
+ failed_when: False
+ register: dpkg_result
+
+ - name: verify real installation of echo-hello
+ assert:
+ that:
+ - apt_url_deps is changed
+ - dpkg_result is successful
+ - dpkg_result.rc == 0
+
+ always:
+ - name: uninstall echo-hello with apt
+ apt:
+ pkg: echo-hello
+ state: absent
+ purge: yes
diff --git a/test/integration/targets/apt/vars/Ubuntu-20.yml b/test/integration/targets/apt/vars/Ubuntu-20.yml
new file mode 100644
index 0000000..7b32755
--- /dev/null
+++ b/test/integration/targets/apt/vars/Ubuntu-20.yml
@@ -0,0 +1 @@
+multiarch_test_pkg: libunistring2
diff --git a/test/integration/targets/apt/vars/Ubuntu-22.yml b/test/integration/targets/apt/vars/Ubuntu-22.yml
new file mode 100644
index 0000000..7b32755
--- /dev/null
+++ b/test/integration/targets/apt/vars/Ubuntu-22.yml
@@ -0,0 +1 @@
+multiarch_test_pkg: libunistring2
diff --git a/test/integration/targets/apt/vars/default.yml b/test/integration/targets/apt/vars/default.yml
new file mode 100644
index 0000000..bed3a96
--- /dev/null
+++ b/test/integration/targets/apt/vars/default.yml
@@ -0,0 +1 @@
+multiarch_test_pkg: hello
diff --git a/test/integration/targets/apt_key/aliases b/test/integration/targets/apt_key/aliases
new file mode 100644
index 0000000..a820ec9
--- /dev/null
+++ b/test/integration/targets/apt_key/aliases
@@ -0,0 +1,5 @@
+shippable/posix/group1
+skip/freebsd
+skip/osx
+skip/macos
+skip/rhel
diff --git a/test/integration/targets/apt_key/meta/main.yml b/test/integration/targets/apt_key/meta/main.yml
new file mode 100644
index 0000000..07faa21
--- /dev/null
+++ b/test/integration/targets/apt_key/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - prepare_tests
diff --git a/test/integration/targets/apt_key/tasks/apt_key.yml b/test/integration/targets/apt_key/tasks/apt_key.yml
new file mode 100644
index 0000000..0e01723
--- /dev/null
+++ b/test/integration/targets/apt_key/tasks/apt_key.yml
@@ -0,0 +1,25 @@
+- name: Ensure key is not there to start with.
+ apt_key:
+ id: 36A1D7869245C8950F966E92D8576A8BA88D21E9
+ state: absent
+
+- name: run first docs example
+ apt_key:
+ keyserver: keyserver.ubuntu.com
+ id: 36A1D7869245C8950F966E92D8576A8BA88D21E9
+ register: apt_key_test0
+
+- debug: var=apt_key_test0
+
+- name: re-run first docs example
+ apt_key:
+ keyserver: keyserver.ubuntu.com
+ id: 36A1D7869245C8950F966E92D8576A8BA88D21E9
+ register: apt_key_test1
+
+- name: validate results
+ assert:
+ that:
+ - 'apt_key_test0.changed is defined'
+ - 'apt_key_test0.changed'
+ - 'not apt_key_test1.changed'
diff --git a/test/integration/targets/apt_key/tasks/apt_key_binary.yml b/test/integration/targets/apt_key/tasks/apt_key_binary.yml
new file mode 100644
index 0000000..b120bd5
--- /dev/null
+++ b/test/integration/targets/apt_key/tasks/apt_key_binary.yml
@@ -0,0 +1,12 @@
+---
+
+- name: Ensure import of binary key downloaded using URLs works
+ apt_key:
+ url: https://ci-files.testing.ansible.com/test/integration/targets/apt_key/apt-key-example-binary.gpg
+ register: apt_key_binary_test
+
+- name: Validate the results
+ assert:
+ that:
+ - 'apt_key_binary_test.changed is defined'
+ - 'apt_key_binary_test.changed'
diff --git a/test/integration/targets/apt_key/tasks/apt_key_inline_data.yml b/test/integration/targets/apt_key/tasks/apt_key_inline_data.yml
new file mode 100644
index 0000000..916fa5a
--- /dev/null
+++ b/test/integration/targets/apt_key/tasks/apt_key_inline_data.yml
@@ -0,0 +1,5 @@
+- name: "Ensure import of a deliberately corrupted downloaded GnuPG binary key results in an 'inline data' occurence in the message"
+ apt_key:
+ url: https://ci-files.testing.ansible.com/test/integration/targets/apt_key/apt-key-corrupt-zeros-2k.gpg
+ register: gpg_inline_result
+ failed_when: "not ('inline data' in gpg_inline_result.msg)"
diff --git a/test/integration/targets/apt_key/tasks/file.yml b/test/integration/targets/apt_key/tasks/file.yml
new file mode 100644
index 0000000..c22f3a4
--- /dev/null
+++ b/test/integration/targets/apt_key/tasks/file.yml
@@ -0,0 +1,52 @@
+- name: Get Fedora GPG Key
+ get_url:
+ url: https://ci-files.testing.ansible.com/test/integration/targets/apt_key/fedora.gpg
+ dest: /tmp/fedora.gpg
+
+- name: Ensure clean slate
+ apt_key:
+ id: 1161AE6945719A39
+ state: absent
+
+- name: Run apt_key with both file and keyserver
+ apt_key:
+ file: /tmp/fedora.gpg
+ keyserver: keys.gnupg.net
+ id: 97A1AE57C3A2372CCA3A4ABA6C13026D12C944D0
+ register: both_file_keyserver
+ ignore_errors: true
+
+- name: Run apt_key with file only
+ apt_key:
+ file: /tmp/fedora.gpg
+ register: only_file
+
+- name: Run apt_key with keyserver only
+ apt_key:
+ keyserver: keys.gnupg.net
+ id: 97A1AE57C3A2372CCA3A4ABA6C13026D12C944D0
+ register: only_keyserver
+
+- name: validate results
+ assert:
+ that:
+ - 'both_file_keyserver is failed'
+ - 'only_file.changed'
+ - 'not only_keyserver.changed'
+
+- name: remove fedora.gpg
+ apt_key:
+ id: 1161AE6945719A39
+ state: absent
+ register: remove_fedora
+
+- name: add key from url
+ apt_key:
+ url: https://ci-files.testing.ansible.com/test/integration/targets/apt_key/fedora.gpg
+ register: apt_key_url
+
+- name: verify key from url
+ assert:
+ that:
+ - remove_fedora is changed
+ - apt_key_url is changed
diff --git a/test/integration/targets/apt_key/tasks/main.yml b/test/integration/targets/apt_key/tasks/main.yml
new file mode 100644
index 0000000..ffb89b2
--- /dev/null
+++ b/test/integration/targets/apt_key/tasks/main.yml
@@ -0,0 +1,29 @@
+# Test code for the apt_key module.
+# (c) 2017, James Tanner <tanner.jc@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- import_tasks: 'apt_key.yml'
+ when: ansible_distribution in ('Ubuntu', 'Debian')
+
+- import_tasks: 'apt_key_inline_data.yml'
+ when: ansible_distribution in ('Ubuntu', 'Debian')
+
+- import_tasks: 'file.yml'
+ when: ansible_distribution in ('Ubuntu', 'Debian')
+
+- import_tasks: 'apt_key_binary.yml'
+ when: ansible_distribution in ('Ubuntu', 'Debian')
diff --git a/test/integration/targets/apt_repository/aliases b/test/integration/targets/apt_repository/aliases
new file mode 100644
index 0000000..34e2b54
--- /dev/null
+++ b/test/integration/targets/apt_repository/aliases
@@ -0,0 +1,6 @@
+destructive
+shippable/posix/group1
+skip/freebsd
+skip/osx
+skip/macos
+skip/rhel
diff --git a/test/integration/targets/apt_repository/meta/main.yml b/test/integration/targets/apt_repository/meta/main.yml
new file mode 100644
index 0000000..07faa21
--- /dev/null
+++ b/test/integration/targets/apt_repository/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - prepare_tests
diff --git a/test/integration/targets/apt_repository/tasks/apt.yml b/test/integration/targets/apt_repository/tasks/apt.yml
new file mode 100644
index 0000000..0dc25af
--- /dev/null
+++ b/test/integration/targets/apt_repository/tasks/apt.yml
@@ -0,0 +1,243 @@
+---
+
+- set_fact:
+ test_ppa_name: 'ppa:git-core/ppa'
+ test_ppa_filename: 'git-core'
+ test_ppa_spec: 'deb http://ppa.launchpad.net/git-core/ppa/ubuntu {{ansible_distribution_release}} main'
+ test_ppa_key: 'E1DF1F24' # http://keyserver.ubuntu.com:11371/pks/lookup?search=0xD06AAF4C11DAB86DF421421EFE6B20ECA7AD98A1&op=index
+
+- name: show python version
+ debug: var=ansible_python_version
+
+- name: use python-apt
+ set_fact:
+ python_apt: python-apt
+ when: ansible_python_version is version('3', '<')
+
+- name: use python3-apt
+ set_fact:
+ python_apt: python3-apt
+ when: ansible_python_version is version('3', '>=')
+
+# UNINSTALL 'python-apt'
+# The `apt_repository` module has the smarts to auto-install `python-apt`. To
+# test, we will first uninstall `python-apt`.
+- name: check {{ python_apt }} with dpkg
+ shell: dpkg -s {{ python_apt }}
+ register: dpkg_result
+ ignore_errors: true
+
+- name: uninstall {{ python_apt }} with apt
+ apt: pkg={{ python_apt }} state=absent purge=yes
+ register: apt_result
+ when: dpkg_result is successful
+
+#
+# TEST: apt_repository: repo=<name>
+#
+- import_tasks: 'cleanup.yml'
+
+- name: 'record apt cache mtime'
+ stat: path='/var/cache/apt/pkgcache.bin'
+ register: cache_before
+
+- name: 'name=<name> (expect: pass)'
+ apt_repository: repo='{{test_ppa_name}}' state=present
+ register: result
+
+- name: 'assert the apt cache did *NOT* change'
+ assert:
+ that:
+ - 'result.changed'
+ - 'result.state == "present"'
+ - 'result.repo == "{{test_ppa_name}}"'
+
+- name: 'examine apt cache mtime'
+ stat: path='/var/cache/apt/pkgcache.bin'
+ register: cache_after
+
+- name: 'assert the apt cache did change'
+ assert:
+ that:
+ - 'cache_before.stat.mtime != cache_after.stat.mtime'
+
+- name: 'ensure ppa key is installed (expect: pass)'
+ apt_key: id='{{test_ppa_key}}' state=present
+
+#
+# TEST: apt_repository: repo=<name> update_cache=no
+#
+- import_tasks: 'cleanup.yml'
+
+- name: 'record apt cache mtime'
+ stat: path='/var/cache/apt/pkgcache.bin'
+ register: cache_before
+
+- name: 'name=<name> update_cache=no (expect: pass)'
+ apt_repository: repo='{{test_ppa_name}}' state=present update_cache=no
+ register: result
+
+- assert:
+ that:
+ - 'result.changed'
+ - 'result.state == "present"'
+ - 'result.repo == "{{test_ppa_name}}"'
+
+- name: 'examine apt cache mtime'
+ stat: path='/var/cache/apt/pkgcache.bin'
+ register: cache_after
+
+- name: 'assert the apt cache did *NOT* change'
+ assert:
+ that:
+ - 'cache_before.stat.mtime == cache_after.stat.mtime'
+
+- name: 'ensure ppa key is installed (expect: pass)'
+ apt_key: id='{{test_ppa_key}}' state=present
+
+#
+# TEST: apt_repository: repo=<name> update_cache=yes
+#
+- import_tasks: 'cleanup.yml'
+
+- name: 'record apt cache mtime'
+ stat: path='/var/cache/apt/pkgcache.bin'
+ register: cache_before
+
+- name: 'name=<name> update_cache=yes (expect: pass)'
+ apt_repository: repo='{{test_ppa_name}}' state=present update_cache=yes
+ register: result
+
+- assert:
+ that:
+ - 'result.changed'
+ - 'result.state == "present"'
+ - 'result.repo == "{{test_ppa_name}}"'
+
+- name: 'examine apt cache mtime'
+ stat: path='/var/cache/apt/pkgcache.bin'
+ register: cache_after
+
+- name: 'assert the apt cache did change'
+ assert:
+ that:
+ - 'cache_before.stat.mtime != cache_after.stat.mtime'
+
+- name: 'ensure ppa key is installed (expect: pass)'
+ apt_key: id='{{test_ppa_key}}' state=present
+
+#
+# TEST: apt_repository: repo=<spec>
+#
+- import_tasks: 'cleanup.yml'
+
+- name: 'record apt cache mtime'
+ stat: path='/var/cache/apt/pkgcache.bin'
+ register: cache_before
+
+- name: ensure ppa key is present before adding repo that requires authentication
+ apt_key: keyserver=keyserver.ubuntu.com id='{{test_ppa_key}}' state=present
+
+- name: 'name=<spec> (expect: pass)'
+ apt_repository: repo='{{test_ppa_spec}}' state=present
+ register: result
+
+- name: update the cache
+ apt:
+ update_cache: true
+ register: result_cache
+
+- assert:
+ that:
+ - 'result.changed'
+ - 'result.state == "present"'
+ - 'result.repo == "{{test_ppa_spec}}"'
+ - result_cache is not changed
+
+- name: 'examine apt cache mtime'
+ stat: path='/var/cache/apt/pkgcache.bin'
+ register: cache_after
+
+- name: 'assert the apt cache did change'
+ assert:
+ that:
+ - 'cache_before.stat.mtime != cache_after.stat.mtime'
+
+- name: remove repo by spec
+ apt_repository: repo='{{test_ppa_spec}}' state=absent
+ register: result
+
+# When installing a repo with the spec, the key is *NOT* added
+- name: 'ensure ppa key is absent (expect: pass)'
+ apt_key: id='{{test_ppa_key}}' state=absent
+
+#
+# TEST: apt_repository: repo=<spec> filename=<filename>
+#
+- import_tasks: 'cleanup.yml'
+
+- name: 'record apt cache mtime'
+ stat: path='/var/cache/apt/pkgcache.bin'
+ register: cache_before
+
+- name: ensure ppa key is present before adding repo that requires authentication
+ apt_key: keyserver=keyserver.ubuntu.com id='{{test_ppa_key}}' state=present
+
+- name: 'name=<spec> filename=<filename> (expect: pass)'
+ apt_repository: repo='{{test_ppa_spec}}' filename='{{test_ppa_filename}}' state=present
+ register: result
+
+- assert:
+ that:
+ - 'result.changed'
+ - 'result.state == "present"'
+ - 'result.repo == "{{test_ppa_spec}}"'
+
+- name: 'examine source file'
+ stat: path='/etc/apt/sources.list.d/{{test_ppa_filename}}.list'
+ register: source_file
+
+- name: 'assert source file exists'
+ assert:
+ that:
+ - 'source_file.stat.exists == True'
+
+- name: 'examine apt cache mtime'
+ stat: path='/var/cache/apt/pkgcache.bin'
+ register: cache_after
+
+- name: 'assert the apt cache did change'
+ assert:
+ that:
+ - 'cache_before.stat.mtime != cache_after.stat.mtime'
+
+# When installing a repo with the spec, the key is *NOT* added
+- name: 'ensure ppa key is absent (expect: pass)'
+ apt_key: id='{{test_ppa_key}}' state=absent
+
+- name: Test apt_repository with a null value for repo
+ apt_repository:
+ repo:
+ register: result
+ ignore_errors: yes
+
+- assert:
+ that:
+ - result is failed
+ - result.msg == 'Please set argument \'repo\' to a non-empty value'
+
+- name: Test apt_repository with an empty value for repo
+ apt_repository:
+ repo: ""
+ register: result
+ ignore_errors: yes
+
+- assert:
+ that:
+ - result is failed
+ - result.msg == 'Please set argument \'repo\' to a non-empty value'
+
+#
+# TEARDOWN
+#
+- import_tasks: 'cleanup.yml'
diff --git a/test/integration/targets/apt_repository/tasks/cleanup.yml b/test/integration/targets/apt_repository/tasks/cleanup.yml
new file mode 100644
index 0000000..92280ce
--- /dev/null
+++ b/test/integration/targets/apt_repository/tasks/cleanup.yml
@@ -0,0 +1,17 @@
+---
+# tasks to cleanup a repo and assert it is gone
+
+- name: remove existing ppa
+ apt_repository: repo={{test_ppa_name}} state=absent
+ ignore_errors: true
+
+- name: test that ppa does not exist (expect pass)
+ shell: cat /etc/apt/sources.list /etc/apt/sources.list.d/* | grep "{{test_ppa_spec}}"
+ register: command
+ failed_when: command.rc == 0
+ changed_when: false
+
+# Should this use apt-key, maybe?
+- name: remove ppa key
+ apt_key: id={{test_ppa_key}} state=absent
+ ignore_errors: true
diff --git a/test/integration/targets/apt_repository/tasks/main.yml b/test/integration/targets/apt_repository/tasks/main.yml
new file mode 100644
index 0000000..5d72f6f
--- /dev/null
+++ b/test/integration/targets/apt_repository/tasks/main.yml
@@ -0,0 +1,25 @@
+# test code for the apt_repository module
+# (c) 2014, James Laska <jlaska@ansible.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- import_tasks: 'apt.yml'
+ when: ansible_distribution in ('Ubuntu')
+
+- import_tasks: mode.yaml
+ when: ansible_distribution in ('Ubuntu')
+ tags:
+ - test_apt_repository_mode
diff --git a/test/integration/targets/apt_repository/tasks/mode.yaml b/test/integration/targets/apt_repository/tasks/mode.yaml
new file mode 100644
index 0000000..4b4fabf
--- /dev/null
+++ b/test/integration/targets/apt_repository/tasks/mode.yaml
@@ -0,0 +1,135 @@
+---
+
+# These tests are likely slower than they should be, since each
+# invocation of apt_repository seems to end up querying for
+# lots (all?) configured repos.
+
+- set_fact:
+ test_repo_spec: "deb http://apt.postgresql.org/pub/repos/apt/ {{ ansible_distribution_release }}-pgdg main"
+ test_repo_path: /etc/apt/sources.list.d/apt_postgresql_org_pub_repos_apt.list
+
+- import_tasks: mode_cleanup.yaml
+
+- name: Add GPG key to verify signatures
+ apt_key:
+ id: 7FCC7D46ACCC4CF8
+ keyserver: keyserver.ubuntu.com
+
+- name: Mode specified as yaml literal 0600
+ apt_repository:
+ repo: "{{ test_repo_spec }}"
+ state: present
+ mode: 0600
+ update_cache: false
+ register: mode_given_results
+
+- name: Gather mode_given_as_literal_yaml stat
+ stat:
+ path: "{{ test_repo_path }}"
+ register: mode_given_yaml_literal_0600
+
+- name: Show mode_given_yaml_literal_0600
+ debug:
+ var: mode_given_yaml_literal_0600
+
+- import_tasks: mode_cleanup.yaml
+
+- name: Assert mode_given_yaml_literal_0600 is correct
+ assert:
+ that: "mode_given_yaml_literal_0600.stat.mode == '0600'"
+
+- name: No mode specified
+ apt_repository:
+ repo: "{{ test_repo_spec }}"
+ state: present
+ update_cache: false
+ register: no_mode_results
+
+- name: Gather no mode stat
+ stat:
+ path: "{{ test_repo_path }}"
+ register: no_mode_stat
+
+- name: Show no mode stat
+ debug:
+ var: no_mode_stat
+
+- import_tasks: mode_cleanup.yaml
+
+- name: Assert no_mode_stat is correct
+ assert:
+ that: "no_mode_stat.stat.mode == '0644'"
+
+- name: Mode specified as string 0600
+ apt_repository:
+ repo: "{{ test_repo_spec }}"
+ state: present
+ mode: "0600"
+ update_cache: false
+ register: mode_given_string_results
+
+- name: Gather mode_given_string stat
+ stat:
+ path: "{{ test_repo_path }}"
+ register: mode_given_string_stat
+
+- name: Show mode_given_string_stat
+ debug:
+ var: mode_given_string_stat
+
+- import_tasks: mode_cleanup.yaml
+
+- name: Mode specified as string 600
+ apt_repository:
+ repo: "{{ test_repo_spec }}"
+ state: present
+ mode: "600"
+ update_cache: false
+ register: mode_given_string_600_results
+
+- name: Gather mode_given_600_string stat
+ stat:
+ path: "{{ test_repo_path }}"
+ register: mode_given_string_600_stat
+
+- name: Show mode_given_string_stat
+ debug:
+ var: mode_given_string_600_stat
+
+- import_tasks: mode_cleanup.yaml
+
+- name: Assert mode is correct
+ assert:
+ that: "mode_given_string_600_stat.stat.mode == '0600'"
+
+- name: Mode specified as yaml literal 600
+ apt_repository:
+ repo: "{{ test_repo_spec }}"
+ state: present
+ mode: 600
+ update_cache: false
+ register: mode_given_short_results
+
+- name: Gather mode_given_yaml_literal_600 stat
+ stat:
+ path: "{{ test_repo_path }}"
+ register: mode_given_yaml_literal_600
+
+- name: Show mode_given_yaml_literal_600
+ debug:
+ var: mode_given_yaml_literal_600
+
+- import_tasks: mode_cleanup.yaml
+
+# a literal 600 as the mode will fail currently, in the sense that it
+# doesn't guess and consider 600 and 0600 to be the same, and will instead
+# intepret literal 600 as the decimal 600 (and thereby octal 1130).
+# The literal 0600 can be interpreted as octal correctly. Note that
+# a decimal 644 is octal 420. The default perm is 0644 so a mis intrpretation
+# of 644 was previously resulting in a default file mode of 0420.
+# 'mode: 600' is likely not what a user meant but there isnt enough info
+# to determine that. Note that a string arg of '600' will be intrepeted as 0600.
+# See https://github.com/ansible/ansible/issues/16370
+- name: Assert mode_given_yaml_literal_600 is correct
+ assert:
+ that: "mode_given_yaml_literal_600.stat.mode == '1130'"
diff --git a/test/integration/targets/apt_repository/tasks/mode_cleanup.yaml b/test/integration/targets/apt_repository/tasks/mode_cleanup.yaml
new file mode 100644
index 0000000..726de11
--- /dev/null
+++ b/test/integration/targets/apt_repository/tasks/mode_cleanup.yaml
@@ -0,0 +1,7 @@
+---
+# tasks to cleanup after creating a repo file, specifically for testing the 'mode' arg
+
+- name: Delete existing repo
+ file:
+ path: "{{ test_repo_path }}"
+ state: absent \ No newline at end of file
diff --git a/test/integration/targets/args/aliases b/test/integration/targets/args/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/args/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/args/runme.sh b/test/integration/targets/args/runme.sh
new file mode 100755
index 0000000..af1c31d
--- /dev/null
+++ b/test/integration/targets/args/runme.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+set -eu
+
+echo "arg[#]: $#"
+echo "arg[0]: $0"
+
+i=0
+for arg in "$@"; do
+ i=$((i+1))
+ echo "arg[$i]: ${arg}"
+done
diff --git a/test/integration/targets/argspec/aliases b/test/integration/targets/argspec/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/argspec/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/argspec/library/argspec.py b/test/integration/targets/argspec/library/argspec.py
new file mode 100644
index 0000000..b6d6d11
--- /dev/null
+++ b/test/integration/targets/argspec/library/argspec.py
@@ -0,0 +1,268 @@
+#!/usr/bin/python
+# Copyright: (c) 2020, Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ {
+ 'required': {
+ 'required': True,
+ },
+ 'required_one_of_one': {},
+ 'required_one_of_two': {},
+ 'required_by_one': {},
+ 'required_by_two': {},
+ 'required_by_three': {},
+ 'state': {
+ 'type': 'str',
+ 'choices': ['absent', 'present'],
+ },
+ 'path': {},
+ 'content': {},
+ 'mapping': {
+ 'type': 'dict',
+ },
+ 'required_one_of': {
+ 'required_one_of': [['thing', 'other']],
+ 'type': 'list',
+ 'elements': 'dict',
+ 'options': {
+ 'thing': {},
+ 'other': {'aliases': ['other_alias']},
+ },
+ },
+ 'required_by': {
+ 'required_by': {'thing': 'other'},
+ 'type': 'list',
+ 'elements': 'dict',
+ 'options': {
+ 'thing': {},
+ 'other': {},
+ },
+ },
+ 'required_together': {
+ 'required_together': [['thing', 'other']],
+ 'type': 'list',
+ 'elements': 'dict',
+ 'options': {
+ 'thing': {},
+ 'other': {},
+ 'another': {},
+ },
+ },
+ 'required_if': {
+ 'required_if': (
+ ('thing', 'foo', ('other',), True),
+ ),
+ 'type': 'list',
+ 'elements': 'dict',
+ 'options': {
+ 'thing': {},
+ 'other': {},
+ 'another': {},
+ },
+ },
+ 'json': {
+ 'type': 'json',
+ },
+ 'fail_on_missing_params': {
+ 'type': 'list',
+ 'default': [],
+ },
+ 'needed_param': {},
+ 'required_together_one': {},
+ 'required_together_two': {},
+ 'suboptions_list_no_elements': {
+ 'type': 'list',
+ 'options': {
+ 'thing': {},
+ },
+ },
+ 'choices_with_strings_like_bools': {
+ 'type': 'str',
+ 'choices': [
+ 'on',
+ 'off',
+ ],
+ },
+ 'choices': {
+ 'type': 'str',
+ 'choices': [
+ 'foo',
+ 'bar',
+ ],
+ },
+ 'list_choices': {
+ 'type': 'list',
+ 'choices': [
+ 'foo',
+ 'bar',
+ 'baz',
+ ],
+ },
+ 'primary': {
+ 'type': 'str',
+ 'aliases': [
+ 'alias',
+ ],
+ },
+ 'password': {
+ 'type': 'str',
+ 'no_log': True,
+ },
+ 'not_a_password': {
+ 'type': 'str',
+ 'no_log': False,
+ },
+ 'maybe_password': {
+ 'type': 'str',
+ },
+ 'int': {
+ 'type': 'int',
+ },
+ 'apply_defaults': {
+ 'type': 'dict',
+ 'apply_defaults': True,
+ 'options': {
+ 'foo': {
+ 'type': 'str',
+ },
+ 'bar': {
+ 'type': 'str',
+ 'default': 'baz',
+ 'aliases': ['bar_alias1', 'bar_alias2'],
+ },
+ },
+ },
+ 'deprecation_aliases': {
+ 'type': 'str',
+ 'aliases': [
+ 'deprecation_aliases_version',
+ 'deprecation_aliases_date',
+ ],
+ 'deprecated_aliases': [
+ {
+ 'name': 'deprecation_aliases_version',
+ 'version': '2.0.0',
+ 'collection_name': 'foo.bar',
+ },
+ {
+ 'name': 'deprecation_aliases_date',
+ 'date': '2023-01-01',
+ 'collection_name': 'foo.bar',
+ },
+ ],
+ },
+ 'deprecation_param_version': {
+ 'type': 'str',
+ 'removed_in_version': '2.0.0',
+ 'removed_from_collection': 'foo.bar',
+ },
+ 'deprecation_param_date': {
+ 'type': 'str',
+ 'removed_at_date': '2023-01-01',
+ 'removed_from_collection': 'foo.bar',
+ },
+ 'subdeprecation': {
+ 'aliases': [
+ 'subdeprecation_alias',
+ ],
+ 'type': 'dict',
+ 'options': {
+ 'deprecation_aliases': {
+ 'type': 'str',
+ 'aliases': [
+ 'deprecation_aliases_version',
+ 'deprecation_aliases_date',
+ ],
+ 'deprecated_aliases': [
+ {
+ 'name': 'deprecation_aliases_version',
+ 'version': '2.0.0',
+ 'collection_name': 'foo.bar',
+ },
+ {
+ 'name': 'deprecation_aliases_date',
+ 'date': '2023-01-01',
+ 'collection_name': 'foo.bar',
+ },
+ ],
+ },
+ 'deprecation_param_version': {
+ 'type': 'str',
+ 'removed_in_version': '2.0.0',
+ 'removed_from_collection': 'foo.bar',
+ },
+ 'deprecation_param_date': {
+ 'type': 'str',
+ 'removed_at_date': '2023-01-01',
+ 'removed_from_collection': 'foo.bar',
+ },
+ },
+ },
+ 'subdeprecation_list': {
+ 'type': 'list',
+ 'elements': 'dict',
+ 'options': {
+ 'deprecation_aliases': {
+ 'type': 'str',
+ 'aliases': [
+ 'deprecation_aliases_version',
+ 'deprecation_aliases_date',
+ ],
+ 'deprecated_aliases': [
+ {
+ 'name': 'deprecation_aliases_version',
+ 'version': '2.0.0',
+ 'collection_name': 'foo.bar',
+ },
+ {
+ 'name': 'deprecation_aliases_date',
+ 'date': '2023-01-01',
+ 'collection_name': 'foo.bar',
+ },
+ ],
+ },
+ 'deprecation_param_version': {
+ 'type': 'str',
+ 'removed_in_version': '2.0.0',
+ 'removed_from_collection': 'foo.bar',
+ },
+ 'deprecation_param_date': {
+ 'type': 'str',
+ 'removed_at_date': '2023-01-01',
+ 'removed_from_collection': 'foo.bar',
+ },
+ },
+ }
+ },
+ required_if=(
+ ('state', 'present', ('path', 'content'), True),
+ ),
+ mutually_exclusive=(
+ ('path', 'content'),
+ ),
+ required_one_of=(
+ ('required_one_of_one', 'required_one_of_two'),
+ ),
+ required_by={
+ 'required_by_one': ('required_by_two', 'required_by_three'),
+ },
+ required_together=(
+ ('required_together_one', 'required_together_two'),
+ ),
+ )
+
+ module.fail_on_missing_params(module.params['fail_on_missing_params'])
+
+ module.exit_json(**module.params)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/argspec/tasks/main.yml b/test/integration/targets/argspec/tasks/main.yml
new file mode 100644
index 0000000..6e8ec05
--- /dev/null
+++ b/test/integration/targets/argspec/tasks/main.yml
@@ -0,0 +1,659 @@
+- argspec:
+ required: value
+ required_one_of_one: value
+
+- argspec:
+ required_one_of_one: value
+ register: argspec_required_fail
+ ignore_errors: true
+
+- argspec:
+ required: value
+ required_one_of_two: value
+
+- argspec:
+ required: value
+ register: argspec_required_one_of_fail
+ ignore_errors: true
+
+- argspec:
+ required: value
+ required_one_of_two: value
+ required_by_one: value
+ required_by_two: value
+ required_by_three: value
+
+- argspec:
+ required: value
+ required_one_of_two: value
+ required_by_one: value
+ required_by_two: value
+ register: argspec_required_by_fail
+ ignore_errors: true
+
+- argspec:
+ state: absent
+ required: value
+ required_one_of_one: value
+
+- argspec:
+ state: present
+ required: value
+ required_one_of_one: value
+ register: argspec_required_if_fail
+ ignore_errors: true
+
+- argspec:
+ state: present
+ path: foo
+ required: value
+ required_one_of_one: value
+
+- argspec:
+ state: present
+ content: foo
+ required: value
+ required_one_of_one: value
+
+- argspec:
+ state: present
+ content: foo
+ path: foo
+ required: value
+ required_one_of_one: value
+ register: argspec_mutually_exclusive_fail
+ ignore_errors: true
+
+- argspec:
+ mapping:
+ foo: bar
+ required: value
+ required_one_of_one: value
+ register: argspec_good_mapping
+
+- argspec:
+ mapping: foo=bar
+ required: value
+ required_one_of_one: value
+ register: argspec_good_mapping_kv
+
+- argspec:
+ mapping: !!str '{"foo": "bar"}'
+ required: value
+ required_one_of_one: value
+ register: argspec_good_mapping_json
+
+- argspec:
+ mapping: !!str '{"foo": False}'
+ required: value
+ required_one_of_one: value
+ register: argspec_good_mapping_dict_repr
+
+- argspec:
+ mapping: foo
+ required: value
+ required_one_of_one: value
+ register: argspec_bad_mapping_string
+ ignore_errors: true
+
+- argspec:
+ mapping: 1
+ required: value
+ required_one_of_one: value
+ register: argspec_bad_mapping_int
+ ignore_errors: true
+
+- argspec:
+ mapping:
+ - foo
+ - bar
+ required: value
+ required_one_of_one: value
+ register: argspec_bad_mapping_list
+ ignore_errors: true
+
+- argspec:
+ required_together:
+ - thing: foo
+ other: bar
+ another: baz
+ required: value
+ required_one_of_one: value
+
+- argspec:
+ required_together:
+ - another: baz
+ required: value
+ required_one_of_one: value
+
+- argspec:
+ required_together:
+ - thing: foo
+ required: value
+ required_one_of_one: value
+ register: argspec_required_together_fail
+ ignore_errors: true
+
+- argspec:
+ required_together:
+ - thing: foo
+ other: bar
+ required: value
+ required_one_of_one: value
+
+- argspec:
+ required_if:
+ - thing: bar
+ required: value
+ required_one_of_one: value
+
+- argspec:
+ required_if:
+ - thing: foo
+ other: bar
+ required: value
+ required_one_of_one: value
+
+- argspec:
+ required_if:
+ - thing: foo
+ required: value
+ required_one_of_one: value
+ register: argspec_required_if_fail_2
+ ignore_errors: true
+
+- argspec:
+ required_one_of:
+ - thing: foo
+ other: bar
+ required: value
+ required_one_of_one: value
+
+- argspec:
+ required_one_of:
+ - {}
+ required: value
+ required_one_of_one: value
+ register: argspec_required_one_of_fail_2
+ ignore_errors: true
+
+- argspec:
+ required_by:
+ - thing: foo
+ other: bar
+ required: value
+ required_one_of_one: value
+
+- argspec:
+ required_by:
+ - thing: foo
+ required: value
+ required_one_of_one: value
+ register: argspec_required_by_fail_2
+ ignore_errors: true
+
+- argspec:
+ json: !!str '{"foo": "bar"}'
+ required: value
+ required_one_of_one: value
+ register: argspec_good_json_string
+
+- argspec:
+ json:
+ foo: bar
+ required: value
+ required_one_of_one: value
+ register: argspec_good_json_dict
+
+- argspec:
+ json: 1
+ required: value
+ required_one_of_one: value
+ register: argspec_bad_json
+ ignore_errors: true
+
+- argspec:
+ fail_on_missing_params:
+ - needed_param
+ needed_param: whatever
+ required: value
+ required_one_of_one: value
+
+- argspec:
+ fail_on_missing_params:
+ - needed_param
+ required: value
+ required_one_of_one: value
+ register: argspec_fail_on_missing_params_bad
+ ignore_errors: true
+
+- argspec:
+ required_together_one: foo
+ required_together_two: bar
+ required: value
+ required_one_of_one: value
+
+- argspec:
+ required_together_one: foo
+ required: value
+ required_one_of_one: value
+ register: argspec_fail_required_together_2
+ ignore_errors: true
+
+- argspec:
+ suboptions_list_no_elements:
+ - thing: foo
+ required: value
+ required_one_of_one: value
+ register: argspec_suboptions_list_no_elements
+
+- argspec:
+ choices_with_strings_like_bools: on
+ required: value
+ required_one_of_one: value
+ register: argspec_choices_with_strings_like_bools_true
+
+- argspec:
+ choices_with_strings_like_bools: 'on'
+ required: value
+ required_one_of_one: value
+ register: argspec_choices_with_strings_like_bools_true_bool
+
+- argspec:
+ choices_with_strings_like_bools: off
+ required: value
+ required_one_of_one: value
+ register: argspec_choices_with_strings_like_bools_false
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ choices: foo
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ choices: baz
+ register: argspec_choices_bad_choice
+ ignore_errors: true
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ list_choices:
+ - bar
+ - baz
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ list_choices:
+ - bar
+ - baz
+ - qux
+ register: argspec_list_choices_bad_choice
+ ignore_errors: true
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ primary: foo
+ register: argspec_aliases_primary
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ alias: foo
+ register: argspec_aliases_alias
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ primary: foo
+ alias: foo
+ register: argspec_aliases_both
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ primary: foo
+ alias: bar
+ register: argspec_aliases_both_different
+
+- command: >-
+ ansible localhost -m argspec
+ -a 'required=value required_one_of_one=value primary=foo alias=bar'
+ environment:
+ ANSIBLE_LIBRARY: '{{ role_path }}/library'
+ register: argspec_aliases_both_warning
+
+- command: ansible localhost -m import_role -a 'role=argspec tasks_from=password_no_log.yml'
+ register: argspec_password_no_log
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ int: 1
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ int: foo
+ register: argspec_int_invalid
+ ignore_errors: true
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ register: argspec_apply_defaults_not_specified
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ apply_defaults: ~
+ register: argspec_apply_defaults_none
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ apply_defaults: {}
+ register: argspec_apply_defaults_empty
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ apply_defaults:
+ foo: bar
+ register: argspec_apply_defaults_one
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ deprecation_aliases_version: value
+ register: deprecation_alias_version
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ deprecation_aliases_date: value
+ register: deprecation_alias_date
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ deprecation_param_version: value
+ register: deprecation_param_version
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ deprecation_param_date: value
+ register: deprecation_param_date
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ subdeprecation:
+ deprecation_aliases_version: value
+ register: sub_deprecation_alias_version
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ subdeprecation:
+ deprecation_aliases_date: value
+ register: sub_deprecation_alias_date
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ subdeprecation:
+ deprecation_param_version: value
+ register: sub_deprecation_param_version
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ subdeprecation:
+ deprecation_param_date: value
+ register: sub_deprecation_param_date
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ subdeprecation_alias:
+ deprecation_aliases_version: value
+ register: subalias_deprecation_alias_version
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ subdeprecation_alias:
+ deprecation_aliases_date: value
+ register: subalias_deprecation_alias_date
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ subdeprecation_alias:
+ deprecation_param_version: value
+ register: subalias_deprecation_param_version
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ subdeprecation_alias:
+ deprecation_param_date: value
+ register: subalias_deprecation_param_date
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ subdeprecation_list:
+ - deprecation_aliases_version: value
+ register: sublist_deprecation_alias_version
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ subdeprecation_list:
+ - deprecation_aliases_date: value
+ register: sublist_deprecation_alias_date
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ subdeprecation_list:
+ - deprecation_param_version: value
+ register: sublist_deprecation_param_version
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ subdeprecation_list:
+ - deprecation_param_date: value
+ register: sublist_deprecation_param_date
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ apply_defaults:
+ bar_alias1: foo
+ bar_alias2: baz
+ register: alias_warning_dict
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ required_one_of:
+ - other: foo
+ other_alias: bar
+ register: alias_warning_listdict
+
+- assert:
+ that:
+ - argspec_required_fail is failed
+
+ - argspec_required_one_of_fail is failed
+
+ - argspec_required_by_fail is failed
+
+ - argspec_required_if_fail is failed
+
+ - argspec_mutually_exclusive_fail is failed
+
+ - argspec_good_mapping is successful
+ - >-
+ argspec_good_mapping.mapping == {'foo': 'bar'}
+ - argspec_good_mapping_json is successful
+ - >-
+ argspec_good_mapping_json.mapping == {'foo': 'bar'}
+ - argspec_good_mapping_dict_repr is successful
+ - >-
+ argspec_good_mapping_dict_repr.mapping == {'foo': False}
+ - argspec_good_mapping_kv is successful
+ - >-
+ argspec_good_mapping_kv.mapping == {'foo': 'bar'}
+ - argspec_bad_mapping_string is failed
+ - argspec_bad_mapping_int is failed
+ - argspec_bad_mapping_list is failed
+
+ - argspec_required_together_fail is failed
+
+ - argspec_required_if_fail_2 is failed
+
+ - argspec_required_one_of_fail_2 is failed
+
+ - argspec_required_by_fail_2 is failed
+
+ - argspec_good_json_string is successful
+ - >-
+ argspec_good_json_string.json == '{"foo": "bar"}'
+ - argspec_good_json_dict is successful
+ - >-
+ argspec_good_json_dict.json == '{"foo": "bar"}'
+ - argspec_bad_json is failed
+
+ - argspec_fail_on_missing_params_bad is failed
+
+ - argspec_fail_required_together_2 is failed
+
+ - >-
+ argspec_suboptions_list_no_elements.suboptions_list_no_elements.0 == {'thing': 'foo'}
+
+ - argspec_choices_with_strings_like_bools_true.choices_with_strings_like_bools == 'on'
+ - argspec_choices_with_strings_like_bools_true_bool.choices_with_strings_like_bools == 'on'
+ - argspec_choices_with_strings_like_bools_false.choices_with_strings_like_bools == 'off'
+
+ - argspec_choices_bad_choice is failed
+
+ - argspec_list_choices_bad_choice is failed
+
+ - argspec_aliases_primary.primary == 'foo'
+ - argspec_aliases_primary.alias is undefined
+ - argspec_aliases_alias.primary == 'foo'
+ - argspec_aliases_alias.alias == 'foo'
+ - argspec_aliases_both.primary == 'foo'
+ - argspec_aliases_both.alias == 'foo'
+ - argspec_aliases_both_different.primary == 'bar'
+ - argspec_aliases_both_different.alias == 'bar'
+ - '"[WARNING]: Both option primary and its alias alias are set." in argspec_aliases_both_warning.stderr'
+
+ - '"Module did not set no_log for maybe_password" in argspec_password_no_log.stderr'
+ - '"Module did not set no_log for password" not in argspec_password_no_log.stderr'
+ - '"Module did not set no_log for not_a_password" not in argspec_password_no_log.stderr'
+ - argspec_password_no_log.stdout|regex_findall('VALUE_SPECIFIED_IN_NO_LOG_PARAMETER')|length == 1
+
+ - argspec_int_invalid is failed
+
+ - "argspec_apply_defaults_not_specified.apply_defaults == {'foo': none, 'bar': 'baz'}"
+ - "argspec_apply_defaults_none.apply_defaults == {'foo': none, 'bar': 'baz'}"
+ - "argspec_apply_defaults_empty.apply_defaults == {'foo': none, 'bar': 'baz'}"
+ - "argspec_apply_defaults_one.apply_defaults == {'foo': 'bar', 'bar': 'baz'}"
+
+ - deprecation_alias_version.deprecations | length == 1
+ - deprecation_alias_version.deprecations[0].msg == "Alias 'deprecation_aliases_version' is deprecated. See the module docs for more information"
+ - deprecation_alias_version.deprecations[0].collection_name == 'foo.bar'
+ - deprecation_alias_version.deprecations[0].version == '2.0.0'
+ - "'date' not in deprecation_alias_version.deprecations[0]"
+ - deprecation_alias_date.deprecations | length == 1
+ - deprecation_alias_date.deprecations[0].msg == "Alias 'deprecation_aliases_date' is deprecated. See the module docs for more information"
+ - deprecation_alias_date.deprecations[0].collection_name == 'foo.bar'
+ - deprecation_alias_date.deprecations[0].date == '2023-01-01'
+ - "'version' not in deprecation_alias_date.deprecations[0]"
+ - deprecation_param_version.deprecations | length == 1
+ - deprecation_param_version.deprecations[0].msg == "Param 'deprecation_param_version' is deprecated. See the module docs for more information"
+ - deprecation_param_version.deprecations[0].collection_name == 'foo.bar'
+ - deprecation_param_version.deprecations[0].version == '2.0.0'
+ - "'date' not in deprecation_param_version.deprecations[0]"
+ - deprecation_param_date.deprecations | length == 1
+ - deprecation_param_date.deprecations[0].msg == "Param 'deprecation_param_date' is deprecated. See the module docs for more information"
+ - deprecation_param_date.deprecations[0].collection_name == 'foo.bar'
+ - deprecation_param_date.deprecations[0].date == '2023-01-01'
+ - "'version' not in deprecation_param_date.deprecations[0]"
+
+ - sub_deprecation_alias_version.deprecations | length == 1
+ - sub_deprecation_alias_version.deprecations[0].msg == "Alias 'subdeprecation.deprecation_aliases_version' is deprecated. See the module docs for more information"
+ - sub_deprecation_alias_version.deprecations[0].collection_name == 'foo.bar'
+ - sub_deprecation_alias_version.deprecations[0].version == '2.0.0'
+ - "'date' not in sub_deprecation_alias_version.deprecations[0]"
+ - sub_deprecation_alias_date.deprecations | length == 1
+ - sub_deprecation_alias_date.deprecations[0].msg == "Alias 'subdeprecation.deprecation_aliases_date' is deprecated. See the module docs for more information"
+ - sub_deprecation_alias_date.deprecations[0].collection_name == 'foo.bar'
+ - sub_deprecation_alias_date.deprecations[0].date == '2023-01-01'
+ - "'version' not in sub_deprecation_alias_date.deprecations[0]"
+ - sub_deprecation_param_version.deprecations | length == 1
+ - sub_deprecation_param_version.deprecations[0].msg == "Param 'subdeprecation[\"deprecation_param_version\"]' is deprecated. See the module docs for more information"
+ - sub_deprecation_param_version.deprecations[0].collection_name == 'foo.bar'
+ - sub_deprecation_param_version.deprecations[0].version == '2.0.0'
+ - "'date' not in sub_deprecation_param_version.deprecations[0]"
+ - sub_deprecation_param_date.deprecations | length == 1
+ - sub_deprecation_param_date.deprecations[0].msg == "Param 'subdeprecation[\"deprecation_param_date\"]' is deprecated. See the module docs for more information"
+ - sub_deprecation_param_date.deprecations[0].collection_name == 'foo.bar'
+ - sub_deprecation_param_date.deprecations[0].date == '2023-01-01'
+ - "'version' not in sub_deprecation_param_date.deprecations[0]"
+
+ - subalias_deprecation_alias_version.deprecations | length == 1
+ - subalias_deprecation_alias_version.deprecations[0].msg == "Alias 'subdeprecation.deprecation_aliases_version' is deprecated. See the module docs for more information"
+ - subalias_deprecation_alias_version.deprecations[0].collection_name == 'foo.bar'
+ - subalias_deprecation_alias_version.deprecations[0].version == '2.0.0'
+ - "'date' not in subalias_deprecation_alias_version.deprecations[0]"
+ - subalias_deprecation_alias_date.deprecations | length == 1
+ - subalias_deprecation_alias_date.deprecations[0].msg == "Alias 'subdeprecation.deprecation_aliases_date' is deprecated. See the module docs for more information"
+ - subalias_deprecation_alias_date.deprecations[0].collection_name == 'foo.bar'
+ - subalias_deprecation_alias_date.deprecations[0].date == '2023-01-01'
+ - "'version' not in subalias_deprecation_alias_date.deprecations[0]"
+ - subalias_deprecation_param_version.deprecations | length == 1
+ - subalias_deprecation_param_version.deprecations[0].msg == "Param 'subdeprecation[\"deprecation_param_version\"]' is deprecated. See the module docs for more information"
+ - subalias_deprecation_param_version.deprecations[0].collection_name == 'foo.bar'
+ - subalias_deprecation_param_version.deprecations[0].version == '2.0.0'
+ - "'date' not in subalias_deprecation_param_version.deprecations[0]"
+ - subalias_deprecation_param_date.deprecations | length == 1
+ - subalias_deprecation_param_date.deprecations[0].msg == "Param 'subdeprecation[\"deprecation_param_date\"]' is deprecated. See the module docs for more information"
+ - subalias_deprecation_param_date.deprecations[0].collection_name == 'foo.bar'
+ - subalias_deprecation_param_date.deprecations[0].date == '2023-01-01'
+ - "'version' not in subalias_deprecation_param_date.deprecations[0]"
+
+ - sublist_deprecation_alias_version.deprecations | length == 1
+ - sublist_deprecation_alias_version.deprecations[0].msg == "Alias 'subdeprecation_list[0].deprecation_aliases_version' is deprecated. See the module docs for more information"
+ - sublist_deprecation_alias_version.deprecations[0].collection_name == 'foo.bar'
+ - sublist_deprecation_alias_version.deprecations[0].version == '2.0.0'
+ - "'date' not in sublist_deprecation_alias_version.deprecations[0]"
+ - sublist_deprecation_alias_date.deprecations | length == 1
+ - sublist_deprecation_alias_date.deprecations[0].msg == "Alias 'subdeprecation_list[0].deprecation_aliases_date' is deprecated. See the module docs for more information"
+ - sublist_deprecation_alias_date.deprecations[0].collection_name == 'foo.bar'
+ - sublist_deprecation_alias_date.deprecations[0].date == '2023-01-01'
+ - "'version' not in sublist_deprecation_alias_date.deprecations[0]"
+ - sublist_deprecation_param_version.deprecations | length == 1
+ - sublist_deprecation_param_version.deprecations[0].msg == "Param 'subdeprecation_list[\"deprecation_param_version\"]' is deprecated. See the module docs for more information"
+ - sublist_deprecation_param_version.deprecations[0].collection_name == 'foo.bar'
+ - sublist_deprecation_param_version.deprecations[0].version == '2.0.0'
+ - "'date' not in sublist_deprecation_param_version.deprecations[0]"
+ - sublist_deprecation_param_date.deprecations | length == 1
+ - sublist_deprecation_param_date.deprecations[0].msg == "Param 'subdeprecation_list[\"deprecation_param_date\"]' is deprecated. See the module docs for more information"
+ - sublist_deprecation_param_date.deprecations[0].collection_name == 'foo.bar'
+ - sublist_deprecation_param_date.deprecations[0].date == '2023-01-01'
+ - "'version' not in sublist_deprecation_param_date.deprecations[0]"
+
+ - "'Both option apply_defaults.bar and its alias apply_defaults.bar_alias2 are set.' in alias_warning_dict.warnings"
+ - "'Both option required_one_of[0].other and its alias required_one_of[0].other_alias are set.' in alias_warning_listdict.warnings"
diff --git a/test/integration/targets/argspec/tasks/password_no_log.yml b/test/integration/targets/argspec/tasks/password_no_log.yml
new file mode 100644
index 0000000..99c3307
--- /dev/null
+++ b/test/integration/targets/argspec/tasks/password_no_log.yml
@@ -0,0 +1,14 @@
+- argspec:
+ required: value
+ required_one_of_one: value
+ password: foo
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ not_a_password: foo
+
+- argspec:
+ required: value
+ required_one_of_one: value
+ maybe_password: foo
diff --git a/test/integration/targets/assemble/aliases b/test/integration/targets/assemble/aliases
new file mode 100644
index 0000000..765b70d
--- /dev/null
+++ b/test/integration/targets/assemble/aliases
@@ -0,0 +1 @@
+shippable/posix/group2
diff --git a/test/integration/targets/assemble/files/fragment1 b/test/integration/targets/assemble/files/fragment1
new file mode 100644
index 0000000..a00d3ea
--- /dev/null
+++ b/test/integration/targets/assemble/files/fragment1
@@ -0,0 +1 @@
+this is fragment 1
diff --git a/test/integration/targets/assemble/files/fragment2 b/test/integration/targets/assemble/files/fragment2
new file mode 100644
index 0000000..860f760
--- /dev/null
+++ b/test/integration/targets/assemble/files/fragment2
@@ -0,0 +1 @@
+this is fragment 2
diff --git a/test/integration/targets/assemble/files/fragment3 b/test/integration/targets/assemble/files/fragment3
new file mode 100644
index 0000000..df95b24
--- /dev/null
+++ b/test/integration/targets/assemble/files/fragment3
@@ -0,0 +1 @@
+this is fragment 3
diff --git a/test/integration/targets/assemble/files/fragment4 b/test/integration/targets/assemble/files/fragment4
new file mode 100644
index 0000000..c83252b
--- /dev/null
+++ b/test/integration/targets/assemble/files/fragment4
@@ -0,0 +1 @@
+this is fragment 4
diff --git a/test/integration/targets/assemble/files/fragment5 b/test/integration/targets/assemble/files/fragment5
new file mode 100644
index 0000000..8a527d1
--- /dev/null
+++ b/test/integration/targets/assemble/files/fragment5
@@ -0,0 +1 @@
+this is fragment 5
diff --git a/test/integration/targets/assemble/meta/main.yml b/test/integration/targets/assemble/meta/main.yml
new file mode 100644
index 0000000..d057311
--- /dev/null
+++ b/test/integration/targets/assemble/meta/main.yml
@@ -0,0 +1,21 @@
+# test code for the assemble module
+# (c) 2014, James Cammarata <jcammarata@ansible.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+dependencies:
+ - prepare_tests
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/assemble/tasks/main.yml b/test/integration/targets/assemble/tasks/main.yml
new file mode 100644
index 0000000..14eea3f
--- /dev/null
+++ b/test/integration/targets/assemble/tasks/main.yml
@@ -0,0 +1,154 @@
+# test code for the assemble module
+# (c) 2014, James Cammarata <jcammarata@ansible.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: copy the files to a new directory
+ copy: src="./" dest="{{remote_tmp_dir}}/src"
+ register: result
+
+- name: create unicode file for test
+ shell: echo "π" > {{ remote_tmp_dir }}/src/ßΩ.txt
+ register: result
+
+- name: assert that the new file was created
+ assert:
+ that:
+ - "result.changed == true"
+
+- name: test assemble with all fragments
+ assemble: src="{{remote_tmp_dir}}/src" dest="{{remote_tmp_dir}}/assembled1"
+ register: result
+
+- name: assert the fragments were assembled
+ assert:
+ that:
+ - "result.state == 'file'"
+ - "result.changed == True"
+ - "result.checksum == '74152e9224f774191bc0bedf460d35de86ad90e6'"
+
+- name: test assemble with all fragments
+ assemble: src="{{remote_tmp_dir}}/src" dest="{{remote_tmp_dir}}/assembled1"
+ register: result
+
+- name: assert that the same assemble made no changes
+ assert:
+ that:
+ - "result.state == 'file'"
+ - "result.changed == False"
+ - "result.checksum == '74152e9224f774191bc0bedf460d35de86ad90e6'"
+
+- name: test assemble with all fragments and decrypt=True
+ assemble: src="{{remote_tmp_dir}}/src" dest="{{remote_tmp_dir}}/assembled2" decrypt=yes
+ register: result
+
+- name: assert the fragments were assembled with decrypt=True
+ assert:
+ that:
+ - "result.state == 'file'"
+ - "result.changed == True"
+ - "result.checksum == '74152e9224f774191bc0bedf460d35de86ad90e6'"
+
+- name: test assemble with all fragments and decrypt=True
+ assemble: src="{{remote_tmp_dir}}/src" dest="{{remote_tmp_dir}}/assembled2" decrypt=yes
+ register: result
+
+- name: assert that the same assemble made no changes with decrypt=True
+ assert:
+ that:
+ - "result.state == 'file'"
+ - "result.changed == False"
+ - "result.checksum == '74152e9224f774191bc0bedf460d35de86ad90e6'"
+
+- name: test assemble with fragments matching a regex
+ assemble: src="{{remote_tmp_dir}}/src" dest="{{remote_tmp_dir}}/assembled3" regexp="^fragment[1-3]$"
+ register: result
+
+- name: assert the fragments were assembled with a regex
+ assert:
+ that:
+ - "result.state == 'file'"
+ - "result.checksum == 'edfe2d7487ef8f5ebc0f1c4dc57ba7b70a7b8e2b'"
+
+- name: test assemble with fragments matching a regex and decrypt=True
+ assemble: src="{{remote_tmp_dir}}/src" dest="{{remote_tmp_dir}}/assembled4" regexp="^fragment[1-3]$" decrypt=yes
+ register: result
+
+- name: assert the fragments were assembled with a regex and decrypt=True
+ assert:
+ that:
+ - "result.state == 'file'"
+ - "result.checksum == 'edfe2d7487ef8f5ebc0f1c4dc57ba7b70a7b8e2b'"
+
+- name: test assemble with a delimiter
+ assemble: src="{{remote_tmp_dir}}/src" dest="{{remote_tmp_dir}}/assembled5" delimiter="#--- delimiter ---#"
+ register: result
+
+- name: assert the fragments were assembled with a delimiter
+ assert:
+ that:
+ - "result.state == 'file'"
+ - "result.checksum == 'd986cefb82e34e4cf14d33a3cda132ff45aa2980'"
+
+- name: test assemble with a delimiter and decrypt=True
+ assemble: src="{{remote_tmp_dir}}/src" dest="{{remote_tmp_dir}}/assembled6" delimiter="#--- delimiter ---#" decrypt=yes
+ register: result
+
+- name: assert the fragments were assembled with a delimiter and decrypt=True
+ assert:
+ that:
+ - "result.state == 'file'"
+ - "result.checksum == 'd986cefb82e34e4cf14d33a3cda132ff45aa2980'"
+
+- name: test assemble with remote_src=False
+ assemble: src="./" dest="{{remote_tmp_dir}}/assembled7" remote_src=no
+ register: result
+
+- name: assert the fragments were assembled without remote
+ assert:
+ that:
+ - "result.state == 'file'"
+ - "result.checksum == '048a1bd1951aa5ccc427eeb4ca19aee45e9c68b3'"
+
+- name: test assemble with remote_src=False and decrypt=True
+ assemble: src="./" dest="{{remote_tmp_dir}}/assembled8" remote_src=no decrypt=yes
+ register: result
+
+- name: assert the fragments were assembled without remote and decrypt=True
+ assert:
+ that:
+ - "result.state == 'file'"
+ - "result.checksum == '048a1bd1951aa5ccc427eeb4ca19aee45e9c68b3'"
+
+- name: test assemble with remote_src=False and a delimiter
+ assemble: src="./" dest="{{remote_tmp_dir}}/assembled9" remote_src=no delimiter="#--- delimiter ---#"
+ register: result
+
+- name: assert the fragments were assembled without remote
+ assert:
+ that:
+ - "result.state == 'file'"
+ - "result.checksum == '505359f48c65b3904127cf62b912991d4da7ed6d'"
+
+- name: test assemble with remote_src=False and a delimiter and decrypt=True
+ assemble: src="./" dest="{{remote_tmp_dir}}/assembled10" remote_src=no delimiter="#--- delimiter ---#" decrypt=yes
+ register: result
+
+- name: assert the fragments were assembled without remote
+ assert:
+ that:
+ - "result.state == 'file'"
+ - "result.checksum == '505359f48c65b3904127cf62b912991d4da7ed6d'"
diff --git a/test/integration/targets/assert/aliases b/test/integration/targets/assert/aliases
new file mode 100644
index 0000000..a1b27a8
--- /dev/null
+++ b/test/integration/targets/assert/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller # this is a controller-only action, the module is just for documentation
diff --git a/test/integration/targets/assert/assert_quiet.out.quiet.stderr b/test/integration/targets/assert/assert_quiet.out.quiet.stderr
new file mode 100644
index 0000000..bd973b0
--- /dev/null
+++ b/test/integration/targets/assert/assert_quiet.out.quiet.stderr
@@ -0,0 +1,2 @@
++ ansible-playbook -i localhost, -c local quiet.yml
+++ set +x
diff --git a/test/integration/targets/assert/assert_quiet.out.quiet.stdout b/test/integration/targets/assert/assert_quiet.out.quiet.stdout
new file mode 100644
index 0000000..b62aac6
--- /dev/null
+++ b/test/integration/targets/assert/assert_quiet.out.quiet.stdout
@@ -0,0 +1,17 @@
+
+PLAY [localhost] ***************************************************************
+
+TASK [assert] ******************************************************************
+ok: [localhost] => (item=item_A)
+
+TASK [assert] ******************************************************************
+ok: [localhost] => (item=item_A) => {
+ "ansible_loop_var": "item",
+ "changed": false,
+ "item": "item_A",
+ "msg": "All assertions passed"
+}
+
+PLAY RECAP *********************************************************************
+localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
+
diff --git a/test/integration/targets/assert/inventory b/test/integration/targets/assert/inventory
new file mode 100644
index 0000000..1618200
--- /dev/null
+++ b/test/integration/targets/assert/inventory
@@ -0,0 +1,3 @@
+[all]
+localhost
+
diff --git a/test/integration/targets/assert/quiet.yml b/test/integration/targets/assert/quiet.yml
new file mode 100644
index 0000000..6834712
--- /dev/null
+++ b/test/integration/targets/assert/quiet.yml
@@ -0,0 +1,16 @@
+---
+- hosts: localhost
+ gather_facts: False
+ vars:
+ item_A: yes
+ tasks:
+ - assert:
+ that: "{{ item }} is defined"
+ quiet: True
+ with_items:
+ - item_A
+ - assert:
+ that: "{{ item }} is defined"
+ quiet: False
+ with_items:
+ - item_A
diff --git a/test/integration/targets/assert/runme.sh b/test/integration/targets/assert/runme.sh
new file mode 100755
index 0000000..ca0a858
--- /dev/null
+++ b/test/integration/targets/assert/runme.sh
@@ -0,0 +1,71 @@
+#!/usr/bin/env bash
+
+# This test compares "known good" output with various settings against output
+# with the current code. It's brittle by nature, but this is probably the
+# "best" approach possible.
+#
+# Notes:
+# * options passed to this script (such as -v) are ignored, as they would change
+# the output and break the test
+# * the number of asterisks after a "banner" differs is forced to 79 by
+# redirecting stdin from /dev/null
+
+set -eux
+
+run_test() {
+ # testname is playbook name
+ local testname=$1
+
+ # The shenanigans with redirection and 'tee' are to capture STDOUT and
+ # STDERR separately while still displaying both to the console
+ { ansible-playbook -i 'localhost,' -c local "${testname}.yml" \
+ > >(set +x; tee "${OUTFILE}.${testname}.stdout"); } \
+ 2> >(set +x; tee "${OUTFILE}.${testname}.stderr" >&2) 0</dev/null
+
+ sed -i -e 's/ *$//' "${OUTFILE}.${testname}.stdout"
+ sed -i -e 's/ *$//' "${OUTFILE}.${testname}.stderr"
+
+ # Scrub deprication warning that shows up in Python 2.6 on CentOS 6
+ sed -i -e '/RandomPool_DeprecationWarning/d' "${OUTFILE}.${testname}.stderr"
+
+ diff -u "${ORIGFILE}.${testname}.stdout" "${OUTFILE}.${testname}.stdout" || diff_failure
+ diff -u "${ORIGFILE}.${testname}.stderr" "${OUTFILE}.${testname}.stderr" || diff_failure
+}
+
+diff_failure() {
+ if [[ $INIT = 0 ]]; then
+ echo "FAILURE...diff mismatch!"
+ exit 1
+ fi
+}
+
+cleanup() {
+ if [[ $INIT = 0 ]]; then
+ rm -f "${OUTFILE}."*
+ fi
+}
+
+BASEFILE=assert_quiet.out
+
+ORIGFILE="${BASEFILE}"
+OUTFILE="${BASEFILE}.new"
+
+trap 'cleanup' EXIT
+
+# The --init flag will (re)generate the "good" output files used by the tests
+INIT=0
+if [[ ${1:-} == "--init" ]]; then
+ shift
+ OUTFILE=$ORIGFILE
+ INIT=1
+fi
+
+# Force the 'default' callback plugin
+export ANSIBLE_STDOUT_CALLBACK=default
+# Disable color in output for consistency
+export ANSIBLE_FORCE_COLOR=0
+export ANSIBLE_NOCOLOR=1
+# Disable retry files
+export ANSIBLE_RETRY_FILES_ENABLED=0
+
+run_test quiet
diff --git a/test/integration/targets/async/aliases b/test/integration/targets/async/aliases
new file mode 100644
index 0000000..c989cd7
--- /dev/null
+++ b/test/integration/targets/async/aliases
@@ -0,0 +1,3 @@
+async_status
+async_wrapper
+shippable/posix/group2
diff --git a/test/integration/targets/async/callback_test.yml b/test/integration/targets/async/callback_test.yml
new file mode 100644
index 0000000..01afd59
--- /dev/null
+++ b/test/integration/targets/async/callback_test.yml
@@ -0,0 +1,7 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: Async poll callback test
+ command: sleep 5
+ async: 6
+ poll: 1
diff --git a/test/integration/targets/async/library/async_test.py b/test/integration/targets/async/library/async_test.py
new file mode 100644
index 0000000..f89bd10
--- /dev/null
+++ b/test/integration/targets/async/library/async_test.py
@@ -0,0 +1,49 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+import sys
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ if "--interactive" in sys.argv:
+ import ansible.module_utils.basic
+ ansible.module_utils.basic._ANSIBLE_ARGS = json.dumps(dict(
+ ANSIBLE_MODULE_ARGS=dict(
+ fail_mode="graceful"
+ )
+ ))
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ fail_mode=dict(type='list', default=['success'])
+ )
+ )
+
+ result = dict(changed=True)
+
+ fail_mode = module.params['fail_mode']
+
+ try:
+ if 'leading_junk' in fail_mode:
+ print("leading junk before module output")
+
+ if 'graceful' in fail_mode:
+ module.fail_json(msg="failed gracefully")
+
+ if 'exception' in fail_mode:
+ raise Exception('failing via exception')
+
+ if 'stderr' in fail_mode:
+ print('printed to stderr', file=sys.stderr)
+
+ module.exit_json(**result)
+
+ finally:
+ if 'trailing_junk' in fail_mode:
+ print("trailing junk after module output")
+
+
+main()
diff --git a/test/integration/targets/async/meta/main.yml b/test/integration/targets/async/meta/main.yml
new file mode 100644
index 0000000..07faa21
--- /dev/null
+++ b/test/integration/targets/async/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - prepare_tests
diff --git a/test/integration/targets/async/tasks/main.yml b/test/integration/targets/async/tasks/main.yml
new file mode 100644
index 0000000..05c789e
--- /dev/null
+++ b/test/integration/targets/async/tasks/main.yml
@@ -0,0 +1,300 @@
+# test code for the async keyword
+# (c) 2014, James Tanner <tanner.jc@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: run a 2 second loop
+ shell: for i in $(seq 1 2); do echo $i ; sleep 1; done;
+ async: 10
+ poll: 1
+ register: async_result
+
+
+- debug: var=async_result
+
+- name: validate async returns
+ assert:
+ that:
+ - "'ansible_job_id' in async_result"
+ - "'changed' in async_result"
+ - "'cmd' in async_result"
+ - "'delta' in async_result"
+ - "'end' in async_result"
+ - "'rc' in async_result"
+ - "'start' in async_result"
+ - "'stderr' in async_result"
+ - "'stdout' in async_result"
+ - "'stdout_lines' in async_result"
+ - async_result.rc == 0
+ - async_result.finished == 1
+ - async_result is finished
+
+- name: assert temp async directory exists
+ stat:
+ path: "~/.ansible_async"
+ register: dir_st
+
+- assert:
+ that:
+ - dir_st.stat.isdir is defined and dir_st.stat.isdir
+
+- name: stat temp async status file
+ stat:
+ path: "~/.ansible_async/{{ async_result.ansible_job_id }}"
+ register: tmp_async_file_st
+
+- name: validate automatic cleanup of temp async status file on completed run
+ assert:
+ that:
+ - not tmp_async_file_st.stat.exists
+
+- name: test async without polling
+ command: sleep 5
+ async: 30
+ poll: 0
+ register: async_result
+
+- debug: var=async_result
+
+- name: validate async without polling returns
+ assert:
+ that:
+ - "'ansible_job_id' in async_result"
+ - "'started' in async_result"
+ - async_result.finished == 0
+ - async_result is not finished
+
+- name: test skipped task handling
+ command: /bin/true
+ async: 15
+ poll: 0
+ when: False
+
+# test async "fire and forget, but check later"
+
+- name: 'start a task with "fire-and-forget"'
+ command: sleep 3
+ async: 30
+ poll: 0
+ register: fnf_task
+
+- name: assert task was successfully started
+ assert:
+ that:
+ - fnf_task.started == 1
+ - fnf_task is started
+ - "'ansible_job_id' in fnf_task"
+
+- name: 'check on task started as a "fire-and-forget"'
+ async_status: jid={{ fnf_task.ansible_job_id }}
+ register: fnf_result
+ until: fnf_result is finished
+ retries: 10
+ delay: 1
+
+- name: assert task was successfully checked
+ assert:
+ that:
+ - fnf_result.finished
+ - fnf_result is finished
+
+- name: test graceful module failure
+ async_test:
+ fail_mode: graceful
+ async: 30
+ poll: 1
+ register: async_result
+ ignore_errors: true
+
+- name: assert task failed correctly
+ assert:
+ that:
+ - async_result.ansible_job_id is match('\d+\.\d+')
+ - async_result.finished == 1
+ - async_result is finished
+ - async_result is not changed
+ - async_result is failed
+ - async_result.msg == 'failed gracefully'
+
+- name: test exception module failure
+ async_test:
+ fail_mode: exception
+ async: 5
+ poll: 1
+ register: async_result
+ ignore_errors: true
+
+- name: validate response
+ assert:
+ that:
+ - async_result.ansible_job_id is match('\d+\.\d+')
+ - async_result.finished == 1
+ - async_result is finished
+ - async_result.changed == false
+ - async_result is not changed
+ - async_result.failed == true
+ - async_result is failed
+ - async_result.stderr is search('failing via exception', multiline=True)
+
+- name: test leading junk before JSON
+ async_test:
+ fail_mode: leading_junk
+ async: 5
+ poll: 1
+ register: async_result
+
+- name: validate response
+ assert:
+ that:
+ - async_result.ansible_job_id is match('\d+\.\d+')
+ - async_result.finished == 1
+ - async_result is finished
+ - async_result.changed == true
+ - async_result is changed
+ - async_result is successful
+
+- name: test trailing junk after JSON
+ async_test:
+ fail_mode: trailing_junk
+ async: 5
+ poll: 1
+ register: async_result
+
+- name: validate response
+ assert:
+ that:
+ - async_result.ansible_job_id is match('\d+\.\d+')
+ - async_result.finished == 1
+ - async_result is finished
+ - async_result.changed == true
+ - async_result is changed
+ - async_result is successful
+ - async_result.warnings[0] is search('trailing junk after module output')
+
+- name: test stderr handling
+ async_test:
+ fail_mode: stderr
+ async: 30
+ poll: 1
+ register: async_result
+ ignore_errors: true
+
+- assert:
+ that:
+ - async_result.stderr == "printed to stderr\n"
+
+# NOTE: This should report a warning that cannot be tested
+- name: test async properties on non-async task
+ command: sleep 1
+ register: non_async_result
+
+- name: validate response
+ assert:
+ that:
+ - non_async_result is successful
+ - non_async_result is changed
+ - non_async_result is finished
+ - "'ansible_job_id' not in non_async_result"
+
+- name: set fact of custom tmp dir
+ set_fact:
+ custom_async_tmp: ~/.ansible_async_test
+
+- name: ensure custom async tmp dir is absent
+ file:
+ path: '{{ custom_async_tmp }}'
+ state: absent
+
+- block:
+ - name: run async task with custom dir
+ command: sleep 1
+ register: async_custom_dir
+ async: 5
+ poll: 1
+ vars:
+ ansible_async_dir: '{{ custom_async_tmp }}'
+
+ - name: check if the async temp dir is created
+ stat:
+ path: '{{ custom_async_tmp }}'
+ register: async_custom_dir_result
+
+ - name: assert run async task with custom dir
+ assert:
+ that:
+ - async_custom_dir is successful
+ - async_custom_dir is finished
+ - async_custom_dir_result.stat.exists
+
+ - name: remove custom async dir again
+ file:
+ path: '{{ custom_async_tmp }}'
+ state: absent
+
+ - name: remove custom async dir after deprecation test
+ file:
+ path: '{{ custom_async_tmp }}'
+ state: absent
+
+ - name: run fire and forget async task with custom dir
+ command: echo moo
+ register: async_fandf_custom_dir
+ async: 5
+ poll: 0
+ vars:
+ ansible_async_dir: '{{ custom_async_tmp }}'
+
+ - name: fail to get async status with custom dir with defaults
+ async_status:
+ jid: '{{ async_fandf_custom_dir.ansible_job_id }}'
+ register: async_fandf_custom_dir_fail
+ ignore_errors: yes
+
+ - name: get async status with custom dir using newer format
+ async_status:
+ jid: '{{ async_fandf_custom_dir.ansible_job_id }}'
+ register: async_fandf_custom_dir_result
+ vars:
+ ansible_async_dir: '{{ custom_async_tmp }}'
+
+ - name: assert run fire and forget async task with custom dir
+ assert:
+ that:
+ - async_fandf_custom_dir is successful
+ - async_fandf_custom_dir_fail is failed
+ - async_fandf_custom_dir_fail.msg == "could not find job"
+ - async_fandf_custom_dir_result is successful
+
+ always:
+ - name: remove custom tmp dir after test
+ file:
+ path: '{{ custom_async_tmp }}'
+ state: absent
+
+- name: Test that async has stdin
+ command: >
+ {{ ansible_python_interpreter|default('/usr/bin/python') }} -c 'import os; os.fdopen(os.dup(0), "r")'
+ async: 1
+ poll: 1
+
+- name: run async poll callback test playbook
+ command: ansible-playbook {{ role_path }}/callback_test.yml
+ delegate_to: localhost
+ register: callback_output
+
+- assert:
+ that:
+ - '"ASYNC POLL on localhost" in callback_output.stdout'
diff --git a/test/integration/targets/async_extra_data/aliases b/test/integration/targets/async_extra_data/aliases
new file mode 100644
index 0000000..6452e6d
--- /dev/null
+++ b/test/integration/targets/async_extra_data/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group2
+context/target
diff --git a/test/integration/targets/async_extra_data/library/junkping.py b/test/integration/targets/async_extra_data/library/junkping.py
new file mode 100644
index 0000000..b61d965
--- /dev/null
+++ b/test/integration/targets/async_extra_data/library/junkping.py
@@ -0,0 +1,15 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+
+def main():
+ print("junk_before_module_output")
+ print(json.dumps(dict(changed=False, source='user')))
+ print("junk_after_module_output")
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/async_extra_data/runme.sh b/test/integration/targets/async_extra_data/runme.sh
new file mode 100755
index 0000000..4613273
--- /dev/null
+++ b/test/integration/targets/async_extra_data/runme.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# Verify that extra data before module JSON output during async call is ignored, and that the warning exists.
+ANSIBLE_DEBUG=0 ansible-playbook -i ../../inventory test_async.yml -v "$@" \
+ | grep 'junk after the JSON data: junk_after_module_output'
diff --git a/test/integration/targets/async_extra_data/test_async.yml b/test/integration/targets/async_extra_data/test_async.yml
new file mode 100644
index 0000000..480a2a6
--- /dev/null
+++ b/test/integration/targets/async_extra_data/test_async.yml
@@ -0,0 +1,10 @@
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ # make sure non-JSON data before module output is ignored
+ - name: async ping wrapped in extra junk
+ junkping:
+ async: 10
+ poll: 1
+ register: result
+ - debug: var=result
diff --git a/test/integration/targets/async_fail/action_plugins/normal.py b/test/integration/targets/async_fail/action_plugins/normal.py
new file mode 100644
index 0000000..297cbd9
--- /dev/null
+++ b/test/integration/targets/async_fail/action_plugins/normal.py
@@ -0,0 +1,62 @@
+# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.errors import AnsibleError
+from ansible.plugins.action import ActionBase
+from ansible.utils.vars import merge_hash
+
+
+class ActionModule(ActionBase):
+
+ def run(self, tmp=None, task_vars=None):
+
+ # individual modules might disagree but as the generic the action plugin, pass at this point.
+ self._supports_check_mode = True
+ self._supports_async = True
+
+ result = super(ActionModule, self).run(tmp, task_vars)
+ del tmp # tmp no longer has any effect
+
+ if not result.get('skipped'):
+
+ if result.get('invocation', {}).get('module_args'):
+ # avoid passing to modules in case of no_log
+ # should not be set anymore but here for backwards compatibility
+ del result['invocation']['module_args']
+
+ # FUTURE: better to let _execute_module calculate this internally?
+ wrap_async = self._task.async_val and not self._connection.has_native_async
+
+ # do work!
+ result = merge_hash(result, self._execute_module(task_vars=task_vars, wrap_async=wrap_async))
+
+ # hack to keep --verbose from showing all the setup module result
+ # moved from setup module as now we filter out all _ansible_ from result
+ if self._task.action == 'setup':
+ result['_ansible_verbose_override'] = True
+
+ # Simulate a transient network failure
+ if self._task.action == 'async_status' and 'finished' in result and result['finished'] != 1:
+ raise AnsibleError('Pretend to fail somewher ein executing async_status')
+
+ if not wrap_async:
+ # remove a temporary path we created
+ self._remove_tmp_path(self._connection._shell.tmpdir)
+
+ return result
diff --git a/test/integration/targets/async_fail/aliases b/test/integration/targets/async_fail/aliases
new file mode 100644
index 0000000..c989cd7
--- /dev/null
+++ b/test/integration/targets/async_fail/aliases
@@ -0,0 +1,3 @@
+async_status
+async_wrapper
+shippable/posix/group2
diff --git a/test/integration/targets/async_fail/library/async_test.py b/test/integration/targets/async_fail/library/async_test.py
new file mode 100644
index 0000000..e0cbd6f
--- /dev/null
+++ b/test/integration/targets/async_fail/library/async_test.py
@@ -0,0 +1,53 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+import sys
+import time
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ if "--interactive" in sys.argv:
+ import ansible.module_utils.basic
+ ansible.module_utils.basic._ANSIBLE_ARGS = json.dumps(dict(
+ ANSIBLE_MODULE_ARGS=dict(
+ fail_mode="graceful"
+ )
+ ))
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ fail_mode=dict(type='list', default=['success'])
+ )
+ )
+
+ result = dict(changed=True)
+
+ fail_mode = module.params['fail_mode']
+
+ try:
+ if 'leading_junk' in fail_mode:
+ print("leading junk before module output")
+
+ if 'graceful' in fail_mode:
+ module.fail_json(msg="failed gracefully")
+
+ if 'exception' in fail_mode:
+ raise Exception('failing via exception')
+
+ if 'recovered_fail' in fail_mode:
+ result = {"msg": "succeeded", "failed": False, "changed": True}
+ # Wait in the middle to setup a race where the controller reads incomplete data from our
+ # special async_status the first poll
+ time.sleep(5)
+
+ module.exit_json(**result)
+
+ finally:
+ if 'trailing_junk' in fail_mode:
+ print("trailing junk after module output")
+
+
+main()
diff --git a/test/integration/targets/async_fail/meta/main.yml b/test/integration/targets/async_fail/meta/main.yml
new file mode 100644
index 0000000..07faa21
--- /dev/null
+++ b/test/integration/targets/async_fail/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - prepare_tests
diff --git a/test/integration/targets/async_fail/tasks/main.yml b/test/integration/targets/async_fail/tasks/main.yml
new file mode 100644
index 0000000..40f72e1
--- /dev/null
+++ b/test/integration/targets/async_fail/tasks/main.yml
@@ -0,0 +1,36 @@
+# test code for the async keyword failing in the middle of output
+# (c) 2018, Ansible Project
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# This uses a special copy of the normal action plugin which simulates
+# a transient failure in the module
+- name: test that we can recover from initial failures to read
+ async_test:
+ fail_mode: recovered_fail
+ async: 10
+ poll: 1
+ register: async_result
+
+- name: validate that by the end of the retry interval, we succeeded
+ assert:
+ that:
+ - async_result.ansible_job_id is match('\d+\.\d+')
+ - async_result.finished == 1
+ - async_result is finished
+ - async_result is changed
+ - async_result is successful
+ - async_result.msg is search('succeeded')
diff --git a/test/integration/targets/become/aliases b/test/integration/targets/become/aliases
new file mode 100644
index 0000000..0c490f1
--- /dev/null
+++ b/test/integration/targets/become/aliases
@@ -0,0 +1,4 @@
+destructive
+shippable/posix/group1
+context/target
+gather_facts/no
diff --git a/test/integration/targets/become/files/copy.txt b/test/integration/targets/become/files/copy.txt
new file mode 100644
index 0000000..b8d834d
--- /dev/null
+++ b/test/integration/targets/become/files/copy.txt
@@ -0,0 +1 @@
+testing tilde expansion with become
diff --git a/test/integration/targets/become/meta/main.yml b/test/integration/targets/become/meta/main.yml
new file mode 100644
index 0000000..0cef72c
--- /dev/null
+++ b/test/integration/targets/become/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - setup_test_user
diff --git a/test/integration/targets/become/tasks/become.yml b/test/integration/targets/become/tasks/become.yml
new file mode 100644
index 0000000..d31634f
--- /dev/null
+++ b/test/integration/targets/become/tasks/become.yml
@@ -0,0 +1,49 @@
+- name: test becoming user ({{ become_test }})
+ raw: whoami
+ register: whoami
+
+- name: implicit tilde expansion reflects become user ({{ become_test }})
+ stat:
+ path: "~"
+ register: stat_home_implicit
+
+- name: explicit tilde expansion reflects become user ({{ become_test }})
+ stat:
+ path: "~{{ ansible_become_user }}"
+ register: stat_home_explicit
+
+- name: put a file ({{ become_test }})
+ copy:
+ src: copy.txt
+ dest: "~{{ ansible_become_user }}/{{ ansible_become_method }}-{{ ansible_become_user }}-copy.txt"
+ register: put_file
+
+- name: fetch a file ({{ become_test }})
+ fetch:
+ src: "~{{ ansible_become_user }}/{{ ansible_become_method }}-{{ ansible_become_user }}-copy.txt"
+ dest: "{{ output_dir }}"
+ register: fetch_file
+
+- name: explicit tilde expansion reflects become user ({{ become_test }})
+ stat:
+ path: "~{{ ansible_become_user }}/{{ ansible_become_method }}-{{ ansible_become_user }}-copy.txt"
+ register: stat_file
+
+- name: verify results from previous tasks ({{ become_test }})
+ assert:
+ that:
+ - "whoami.stdout|trim == ansible_become_user"
+
+ - "stat_home_implicit.stat.exists == True"
+ - "stat_home_implicit.stat.path|basename == ansible_become_user"
+
+ - "stat_home_explicit.stat.exists == True"
+ - "stat_home_explicit.stat.path|basename == ansible_become_user"
+
+ - "put_file.uid == test_user.uid"
+ - "put_file.gid == test_user.group"
+
+ - "fetch_file.remote_checksum == put_file.checksum"
+
+ - "stat_file.stat.exists == True"
+ - "stat_file.stat.path|dirname|basename == ansible_become_user"
diff --git a/test/integration/targets/become/tasks/main.yml b/test/integration/targets/become/tasks/main.yml
new file mode 100644
index 0000000..4a2ce64
--- /dev/null
+++ b/test/integration/targets/become/tasks/main.yml
@@ -0,0 +1,20 @@
+- name: determine connection user
+ command: whoami
+ register: connection_user
+ vars:
+ ansible_become: no
+
+- name: include become tests
+ include_tasks: become.yml
+ vars:
+ ansible_become: yes
+ ansible_become_user: "{{ become_test_config.user }}"
+ ansible_become_method: "{{ become_test_config.method }}"
+ ansible_become_password: "{{ become_test_config.password | default(None) }}"
+ loop: "{{
+ (become_methods | selectattr('skip', 'undefined') | list)+
+ (become_methods | selectattr('skip', 'defined') | rejectattr('skip') | list)
+ }}"
+ loop_control:
+ loop_var: become_test_config
+ label: "{{ become_test }}"
diff --git a/test/integration/targets/become/vars/main.yml b/test/integration/targets/become/vars/main.yml
new file mode 100644
index 0000000..d9c1cd0
--- /dev/null
+++ b/test/integration/targets/become/vars/main.yml
@@ -0,0 +1,14 @@
+become_test: >-
+ {{ become_test_config.method }} from {{ connection_user.stdout }} to {{ become_test_config.user }}
+ {{ 'with' if become_test_config.password else 'without' }} password
+
+become_methods:
+ - method: sudo
+ user: "{{ test_user_name }}"
+ password: "{{ test_user_plaintext_password if connection_user.stdout != 'root' else None }}"
+ # Some systems are not configured to allow sudo for non-root users.
+ # The tests could be updated in the future to temporarily enable sudo for the connection user.
+ skip: "{{ connection_user.stdout != 'root' and ansible_distribution == 'FreeBSD' }}"
+ - method: su
+ user: "{{ test_user_name }}"
+ password: "{{ test_user_plaintext_password if connection_user.stdout != 'root' else None }}"
diff --git a/test/integration/targets/become_su/aliases b/test/integration/targets/become_su/aliases
new file mode 100644
index 0000000..04089be
--- /dev/null
+++ b/test/integration/targets/become_su/aliases
@@ -0,0 +1,3 @@
+destructive
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/become_su/runme.sh b/test/integration/targets/become_su/runme.sh
new file mode 100755
index 0000000..87a3511
--- /dev/null
+++ b/test/integration/targets/become_su/runme.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# ensure we execute su with a pseudo terminal
+[ "$(ansible -a whoami --become-method=su localhost --become)" != "su: requires a terminal to execute" ]
diff --git a/test/integration/targets/become_unprivileged/action_plugins/tmpdir.py b/test/integration/targets/become_unprivileged/action_plugins/tmpdir.py
new file mode 100644
index 0000000..b7cbb7a
--- /dev/null
+++ b/test/integration/targets/become_unprivileged/action_plugins/tmpdir.py
@@ -0,0 +1,14 @@
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.action import ActionBase
+
+
+class ActionModule(ActionBase):
+
+ def run(self, tmp=None, task_vars=None):
+ result = super(ActionModule, self).run(tmp, task_vars)
+ result.update(self._execute_module('ping', task_vars=task_vars))
+ result['tmpdir'] = self._connection._shell.tmpdir
+ return result
diff --git a/test/integration/targets/become_unprivileged/aliases b/test/integration/targets/become_unprivileged/aliases
new file mode 100644
index 0000000..70cf577
--- /dev/null
+++ b/test/integration/targets/become_unprivileged/aliases
@@ -0,0 +1,5 @@
+destructive
+shippable/posix/group3
+needs/ssh
+needs/root
+context/controller
diff --git a/test/integration/targets/become_unprivileged/chmod_acl_macos/test.yml b/test/integration/targets/become_unprivileged/chmod_acl_macos/test.yml
new file mode 100644
index 0000000..eaa5f5f
--- /dev/null
+++ b/test/integration/targets/become_unprivileged/chmod_acl_macos/test.yml
@@ -0,0 +1,26 @@
+- name: Tests for chmod +a ACL functionality on macOS
+ hosts: ssh
+ gather_facts: yes
+ remote_user: unpriv1
+ become: yes
+ become_user: unpriv2
+
+ tasks:
+ - name: Get AnsiballZ temp directory
+ action: tmpdir
+ register: tmpdir
+ become_user: unpriv2
+ become: yes
+
+ - name: run whoami
+ command: whoami
+ register: whoami
+
+ - name: Ensure we used the right fallback
+ shell: ls -le /var/tmp/ansible*/*_command.py
+ register: ls
+
+ - assert:
+ that:
+ - whoami.stdout == "unpriv2"
+ - "'user:unpriv2 allow read' in ls.stdout"
diff --git a/test/integration/targets/become_unprivileged/cleanup_unpriv_users.yml b/test/integration/targets/become_unprivileged/cleanup_unpriv_users.yml
new file mode 100644
index 0000000..8be2fe6
--- /dev/null
+++ b/test/integration/targets/become_unprivileged/cleanup_unpriv_users.yml
@@ -0,0 +1,53 @@
+- name: Clean up host and remove unprivileged users
+ hosts: ssh
+ gather_facts: yes
+ remote_user: root
+ tasks:
+ # Do this first so we can use tilde notation while the user still exists
+ - name: Delete homedirs
+ file:
+ path: '~{{ item }}'
+ state: absent
+ with_items:
+ - unpriv1
+ - unpriv2
+
+ - name: Delete users
+ user:
+ name: "{{ item }}"
+ state: absent
+ force: yes # I think this is needed in case pipelining is used and the session remains open
+ with_items:
+ - unpriv1
+ - unpriv2
+
+ - name: Delete groups
+ group:
+ name: "{{ item }}"
+ state: absent
+ with_items:
+ - acommongroup
+ - unpriv1
+ - unpriv2
+
+ - name: Fix sudoers.d path for FreeBSD
+ set_fact:
+ sudoers_etc: /usr/local/etc
+ when: ansible_distribution == 'FreeBSD'
+
+ - name: Fix sudoers.d path for everything else
+ set_fact:
+ sudoers_etc: /etc
+ when: ansible_distribution != 'FreeBSD'
+
+ - name: Undo OpenSUSE
+ lineinfile:
+ path: "{{ sudoers_etc }}/sudoers"
+ regexp: '^### Defaults targetpw'
+ line: 'Defaults targetpw'
+ backrefs: yes
+
+ - name: Nuke custom sudoers file
+ file:
+ path: "{{ sudoers_etc }}/sudoers.d/unpriv1"
+ state: absent
diff --git a/test/integration/targets/become_unprivileged/common_remote_group/cleanup.yml b/test/integration/targets/become_unprivileged/common_remote_group/cleanup.yml
new file mode 100644
index 0000000..41784fc
--- /dev/null
+++ b/test/integration/targets/become_unprivileged/common_remote_group/cleanup.yml
@@ -0,0 +1,35 @@
+- name: Cleanup (as root)
+ hosts: ssh
+ gather_facts: yes
+ remote_user: root
+ tasks:
+ - name: Remove group for unprivileged users
+ group:
+ name: commongroup
+ state: absent
+
+ - name: Check if /usr/bin/setfacl exists
+ stat:
+ path: /usr/bin/setfacl
+ register: usr_bin_setfacl
+
+ - name: Check if /bin/setfacl exists
+ stat:
+ path: /bin/setfacl
+ register: bin_setfacl
+
+ - name: Set path to setfacl
+ set_fact:
+ setfacl_path: /usr/bin/setfacl
+ when: usr_bin_setfacl.stat.exists
+
+ - name: Set path to setfacl
+ set_fact:
+ setfacl_path: /bin/setfacl
+ when: bin_setfacl.stat.exists
+
+ - name: chmod +x setfacl
+ file:
+ path: "{{ setfacl_path }}"
+ mode: a+x
+ when: setfacl_path is defined
diff --git a/test/integration/targets/become_unprivileged/common_remote_group/setup.yml b/test/integration/targets/become_unprivileged/common_remote_group/setup.yml
new file mode 100644
index 0000000..1e799c4
--- /dev/null
+++ b/test/integration/targets/become_unprivileged/common_remote_group/setup.yml
@@ -0,0 +1,43 @@
+- name: Prep (as root)
+ hosts: ssh
+ gather_facts: yes
+ remote_user: root
+ tasks:
+ - name: Create group for unprivileged users
+ group:
+ name: commongroup
+
+ - name: Add them to the group
+ user:
+ name: "{{ item }}"
+ groups: commongroup
+ append: yes
+ with_items:
+ - unpriv1
+ - unpriv2
+
+ - name: Check if /usr/bin/setfacl exists
+ stat:
+ path: /usr/bin/setfacl
+ register: usr_bin_setfacl
+
+ - name: Check if /bin/setfacl exists
+ stat:
+ path: /bin/setfacl
+ register: bin_setfacl
+
+ - name: Set path to setfacl
+ set_fact:
+ setfacl_path: /usr/bin/setfacl
+ when: usr_bin_setfacl.stat.exists
+
+ - name: Set path to setfacl
+ set_fact:
+ setfacl_path: /bin/setfacl
+ when: bin_setfacl.stat.exists
+
+ - name: chmod -x setfacl to disable it
+ file:
+ path: "{{ setfacl_path }}"
+ mode: a-x
+ when: setfacl_path is defined
diff --git a/test/integration/targets/become_unprivileged/common_remote_group/test.yml b/test/integration/targets/become_unprivileged/common_remote_group/test.yml
new file mode 100644
index 0000000..4bc51f8
--- /dev/null
+++ b/test/integration/targets/become_unprivileged/common_remote_group/test.yml
@@ -0,0 +1,36 @@
+- name: Tests for ANSIBLE_COMMON_REMOTE_GROUP functionality
+ hosts: ssh
+ gather_facts: yes
+ remote_user: unpriv1
+
+ tasks:
+ - name: foo
+ action: tmpdir
+ register: tmpdir
+ become_user: unpriv2
+ become: yes
+
+ - name: run whoami with become
+ command: whoami
+ register: whoami
+ become_user: unpriv2
+ become: yes
+
+ - set_fact:
+ stat_cmd: stat -c '%U %G' {{ tmpdir.tmpdir }}/*
+ when: ansible_distribution not in ['MacOSX', 'FreeBSD']
+
+ - set_fact:
+ stat_cmd: stat -f '%Su %Sg' {{ tmpdir.tmpdir }}/*
+ when: ansible_distribution in ['MacOSX', 'FreeBSD']
+
+ - name: Ensure we tested the right fallback
+ shell: "{{ stat_cmd }}"
+ register: stat
+ become_user: unpriv2
+ become: yes
+
+ - assert:
+ that:
+ - whoami.stdout == "unpriv2"
+ - stat.stdout == 'unpriv1 commongroup'
diff --git a/test/integration/targets/become_unprivileged/inventory b/test/integration/targets/become_unprivileged/inventory
new file mode 100644
index 0000000..025d8cf
--- /dev/null
+++ b/test/integration/targets/become_unprivileged/inventory
@@ -0,0 +1,10 @@
+[ssh]
+#ssh-pipelining ansible_ssh_pipelining=true
+ssh-no-pipelining ansible_ssh_pipelining=false
+[ssh:vars]
+ansible_host=localhost
+ansible_connection=ssh
+ansible_python_interpreter="{{ ansible_playbook_python }}"
+
+[all:vars]
+ansible_python_interpreter="{{ ansible_playbook_python }}" \ No newline at end of file
diff --git a/test/integration/targets/become_unprivileged/runme.sh b/test/integration/targets/become_unprivileged/runme.sh
new file mode 100755
index 0000000..7a3f7b8
--- /dev/null
+++ b/test/integration/targets/become_unprivileged/runme.sh
@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+
+set -eux
+
+export ANSIBLE_KEEP_REMOTE_FILES=True
+ANSIBLE_ACTION_PLUGINS="$(pwd)/action_plugins"
+export ANSIBLE_ACTION_PLUGINS
+export ANSIBLE_BECOME_PASS='iWishIWereCoolEnoughForRoot!'
+
+begin_sandwich() {
+ ansible-playbook setup_unpriv_users.yml -i inventory -v "$@"
+}
+
+end_sandwich() {
+ unset ANSIBLE_KEEP_REMOTE_FILES
+ unset ANSIBLE_COMMON_REMOTE_GROUP
+ unset ANSIBLE_BECOME_PASS
+
+ # Do a few cleanup tasks (nuke users, groups, and homedirs, undo config changes)
+ ansible-playbook cleanup_unpriv_users.yml -i inventory -v "$@"
+
+ # We do these last since they do things like remove groups and will error
+ # if there are still users in them.
+ for pb in */cleanup.yml; do
+ ansible-playbook "$pb" -i inventory -v "$@"
+ done
+}
+
+trap "end_sandwich \"\$@\"" EXIT
+
+# Common group tests
+# Skip on macOS, chmod fallback will take over.
+# 1) chmod is stupidly hard to disable, so hitting this test case on macOS would
+# be a suuuuuuper edge case scenario
+# 2) even if we can trick it so chmod doesn't exist, then other things break.
+# Ansible wants a `chmod` around, even if it's not the final thing that gets
+# us enough permission to run the task.
+if [[ "$OSTYPE" != darwin* ]]; then
+ begin_sandwich "$@"
+ ansible-playbook common_remote_group/setup.yml -i inventory -v "$@"
+ export ANSIBLE_COMMON_REMOTE_GROUP=commongroup
+ ansible-playbook common_remote_group/test.yml -i inventory -v "$@"
+ end_sandwich "$@"
+fi
+
+if [[ "$OSTYPE" == darwin* ]]; then
+ begin_sandwich "$@"
+ # In the default case this should happen on macOS, so no need for a setup
+ # It should just work.
+ ansible-playbook chmod_acl_macos/test.yml -i inventory -v "$@"
+ end_sandwich "$@"
+fi
diff --git a/test/integration/targets/become_unprivileged/setup_unpriv_users.yml b/test/integration/targets/become_unprivileged/setup_unpriv_users.yml
new file mode 100644
index 0000000..4f67741
--- /dev/null
+++ b/test/integration/targets/become_unprivileged/setup_unpriv_users.yml
@@ -0,0 +1,109 @@
+####################################################################
+# NOTE! Any destructive changes you make here... Undo them in
+# cleanup_become_unprivileged so that they don't affect other tests.
+####################################################################
+- name: Set up host and create unprivileged users
+ hosts: ssh
+ gather_facts: yes
+ remote_user: root
+ tasks:
+ - name: Create groups for unprivileged users
+ group:
+ name: "{{ item }}"
+ with_items:
+ - unpriv1
+ - unpriv2
+
+ # MacOS requires unencrypted password
+ - name: Set password for unpriv1 (MacOSX)
+ set_fact:
+ password: 'iWishIWereCoolEnoughForRoot!'
+ when: ansible_distribution == 'MacOSX'
+
+ - name: Set password for unpriv1 (everything else)
+ set_fact:
+ password: $6$CRuKRUfAoVwibjUI$1IEOISMFAE/a0VG73K9QsD0uruXNPLNkZ6xWg4Sk3kZIXwv6.YJLECzfNjn6pu8ay6XlVcj2dUvycLetL5Lgx1
+ when: ansible_distribution != 'MacOSX'
+
+ # This user is special. It gets a password so we can sudo as it
+ # (we set the sudo password in runme.sh) and it gets wheel so it can
+ # `become` unpriv2 without an overly complex sudoers file.
+ - name: Create first unprivileged user
+ user:
+ name: unpriv1
+ group: unpriv1
+ password: "{{ password }}"
+
+ - name: Create second unprivileged user
+ user:
+ name: unpriv2
+ group: unpriv2
+
+ - name: Special case group add for macOS
+ user:
+ name: unpriv1
+ groups: com.apple.access_ssh
+ append: yes
+ when: ansible_distribution == 'MacOSX'
+
+ - name: Create .ssh for unpriv1
+ file:
+ path: ~unpriv1/.ssh
+ state: directory
+ owner: unpriv1
+ group: unpriv1
+ mode: 0700
+
+ - name: Set authorized key for unpriv1
+ copy:
+ src: ~root/.ssh/authorized_keys
+ dest: ~unpriv1/.ssh/authorized_keys
+ remote_src: yes
+ owner: unpriv1
+ group: unpriv1
+ mode: 0600
+
+ # Without this we get:
+ # "Failed to connect to the host via ssh: "System is booting up. Unprivileged
+ # users are not permitted to log in yet. Please come back later."
+ - name: Nuke /run/nologin
+ file:
+ path: /run/nologin
+ state: absent
+
+ - name: Fix sudoers.d path for FreeBSD
+ set_fact:
+ sudoers_etc: /usr/local/etc
+ when: ansible_distribution == 'FreeBSD'
+
+ - name: Fix sudoers.d path for everything else
+ set_fact:
+ sudoers_etc: /etc
+ when: sudoers_etc is not defined
+
+ - name: Set chown group for bsd and osx
+ set_fact:
+ chowngroup: wheel
+ when: ansible_distribution in ('FreeBSD', 'MacOSX')
+
+ - name: Chown group for everything else
+ set_fact:
+ chowngroup: root
+ when: chowngroup is not defined
+
+ - name: Make it so unpriv1 can sudo (Chapter 1)
+ copy:
+ dest: "{{ sudoers_etc }}/sudoers.d/unpriv1"
+ content: unpriv1 ALL=(ALL) ALL
+ owner: root
+ group: "{{ chowngroup }}"
+ mode: 0644
+
+ # OpenSUSE has a weird sudo default here and requires the root pw
+ # instead of the user pw. Undo that setting, we can clean it up later.
+ - name: Make it so unpriv1 can sudo (Chapter 2 - The Return Of the OpenSUSE)
+ lineinfile:
+ dest: "{{ sudoers_etc }}/sudoers"
+ regexp: '^Defaults targetpw'
+ line: '### Defaults targetpw'
+ backrefs: yes
diff --git a/test/integration/targets/binary/aliases b/test/integration/targets/binary/aliases
new file mode 100644
index 0000000..6452e6d
--- /dev/null
+++ b/test/integration/targets/binary/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group2
+context/target
diff --git a/test/integration/targets/binary/files/b64_latin1 b/test/integration/targets/binary/files/b64_latin1
new file mode 100644
index 0000000..c7fbdeb
--- /dev/null
+++ b/test/integration/targets/binary/files/b64_latin1
@@ -0,0 +1 @@
+Café Eñe
diff --git a/test/integration/targets/binary/files/b64_utf8 b/test/integration/targets/binary/files/b64_utf8
new file mode 100644
index 0000000..c7fbdeb
--- /dev/null
+++ b/test/integration/targets/binary/files/b64_utf8
@@ -0,0 +1 @@
+Café Eñe
diff --git a/test/integration/targets/binary/files/from_playbook b/test/integration/targets/binary/files/from_playbook
new file mode 100644
index 0000000..c7fbdeb
--- /dev/null
+++ b/test/integration/targets/binary/files/from_playbook
@@ -0,0 +1 @@
+Café Eñe
diff --git a/test/integration/targets/binary/meta/main.yml b/test/integration/targets/binary/meta/main.yml
new file mode 100644
index 0000000..cb6005d
--- /dev/null
+++ b/test/integration/targets/binary/meta/main.yml
@@ -0,0 +1,3 @@
+dependencies:
+ - prepare_tests
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/binary/tasks/main.yml b/test/integration/targets/binary/tasks/main.yml
new file mode 100644
index 0000000..2d417b5
--- /dev/null
+++ b/test/integration/targets/binary/tasks/main.yml
@@ -0,0 +1,131 @@
+---
+# Various ways users want to use binary data
+# Could integrate into individual modules but currently these don't all work.
+# Probably easier to see them all in a single block to know what we're testing.
+# When we can start testing v2 we should test that all of these work.
+
+# In v1: The following line will traceback if it's the first task in the role.
+# Does not traceback if it's the second or third etc task.
+- debug: msg="{{ utf8_simple_accents|b64decode}}"
+
+# Expected values of the written files
+- name: get checksums that we expect later files to have
+ copy:
+ src: from_playbook
+ dest: "{{ remote_tmp_dir }}"
+
+- copy:
+ src: b64_utf8
+ dest: "{{ remote_tmp_dir }}"
+
+- copy:
+ src: b64_latin1
+ dest: "{{ remote_tmp_dir }}"
+
+- stat:
+ path: "{{ remote_tmp_dir }}/from_playbook"
+ register: from_playbook
+
+- stat:
+ path: "{{ remote_tmp_dir }}/b64_utf8"
+ register: b64_utf8
+
+- stat:
+ path: "{{ remote_tmp_dir }}/b64_latin1"
+ register: b64_latin1
+
+# Tests themselves
+- name: copy with utf-8 content in a playbook
+ copy:
+ content: "{{ simple_accents }}\n"
+ dest: "{{ remote_tmp_dir }}/from_playbook.txt"
+
+- name: Check that copying utf-8 content matches
+ stat:
+ path: "{{ remote_tmp_dir }}/from_playbook.txt"
+ register: results
+
+- assert:
+ that:
+ - 'results.stat.checksum == from_playbook.stat.checksum'
+
+- name: copy with utf8 in a base64 encoded string
+ copy:
+ content: "{{ utf8_simple_accents|b64decode }}\n"
+ dest: "{{ remote_tmp_dir }}/b64_utf8.txt"
+
+- name: Check that utf8 in a base64 string matches
+ stat:
+ path: "{{ remote_tmp_dir }}/b64_utf8.txt"
+ register: results
+
+- assert:
+ that:
+ - 'results.stat.checksum == b64_utf8.stat.checksum'
+
+- name: copy with latin1 in a base64 encoded string
+ copy:
+ content: "{{ latin1_simple_accents|b64decode }}\n"
+ dest: "{{ remote_tmp_dir }}/b64_latin1.txt"
+
+- name: Check that latin1 in a base64 string matches
+ stat:
+ path: "{{ remote_tmp_dir }}/b64_latin1.txt"
+ register: results
+
+- assert:
+ that:
+ - 'results.stat.checksum == b64_latin1.stat.checksum'
+ # This one depends on being able to pass binary data through
+ # Might be a while before we find a solution for this
+ ignore_errors: True
+
+- name: Template with a unicode string from the playbook
+ template:
+ src: "from_playbook_template.j2"
+ dest: "{{ remote_tmp_dir }}/from_playbook_template.txt"
+
+- name: Check that writing a template from a playbook var matches
+ stat:
+ path: "{{ remote_tmp_dir }}/from_playbook_template.txt"
+ register: results
+
+- assert:
+ that:
+ - 'results.stat.checksum == from_playbook.stat.checksum'
+
+- name: Template with utf8 in a base64 encoded string
+ template:
+ src: "b64_utf8_template.j2"
+ dest: "{{ remote_tmp_dir }}/b64_utf8_template.txt"
+
+- name: Check that writing a template from a base64 encoded utf8 string matches
+ stat:
+ path: "{{ remote_tmp_dir }}/b64_utf8_template.txt"
+ register: results
+
+- assert:
+ that:
+ - 'results.stat.checksum == b64_utf8.stat.checksum'
+
+- name: Template with latin1 in a base64 encoded string
+ template:
+ src: "b64_latin1_template.j2"
+ dest: "{{ remote_tmp_dir }}/b64_latin1_template.txt"
+
+- name: Check that writing a template from a base64 encoded latin1 string matches
+ stat:
+ path: "{{ remote_tmp_dir }}/b64_latin1_template.txt"
+ register: results
+
+- assert:
+ that:
+ - 'results.stat.checksum == b64_latin1.stat.checksum'
+ # This one depends on being able to pass binary data through
+ # Might be a while before we find a solution for this
+ ignore_errors: True
+
+# These might give garbled output but none of them should traceback
+- debug: var=simple_accents
+- debug: msg="{{ utf8_simple_accents|b64decode}}"
+- debug: msg="{{ latin1_simple_accents|b64decode}}"
diff --git a/test/integration/targets/binary/templates/b64_latin1_template.j2 b/test/integration/targets/binary/templates/b64_latin1_template.j2
new file mode 100644
index 0000000..ee2fc1b
--- /dev/null
+++ b/test/integration/targets/binary/templates/b64_latin1_template.j2
@@ -0,0 +1 @@
+{{ latin1_simple_accents|b64decode }}
diff --git a/test/integration/targets/binary/templates/b64_utf8_template.j2 b/test/integration/targets/binary/templates/b64_utf8_template.j2
new file mode 100644
index 0000000..9fd3ed4
--- /dev/null
+++ b/test/integration/targets/binary/templates/b64_utf8_template.j2
@@ -0,0 +1 @@
+{{ utf8_simple_accents|b64decode }}
diff --git a/test/integration/targets/binary/templates/from_playbook_template.j2 b/test/integration/targets/binary/templates/from_playbook_template.j2
new file mode 100644
index 0000000..3be6dd4
--- /dev/null
+++ b/test/integration/targets/binary/templates/from_playbook_template.j2
@@ -0,0 +1 @@
+{{ simple_accents }}
diff --git a/test/integration/targets/binary/vars/main.yml b/test/integration/targets/binary/vars/main.yml
new file mode 100644
index 0000000..f6d4023
--- /dev/null
+++ b/test/integration/targets/binary/vars/main.yml
@@ -0,0 +1,3 @@
+simple_accents: 'Café Eñe'
+utf8_simple_accents: 'Q2Fmw6kgRcOxZQ=='
+latin1_simple_accents: 'Q2Fm6SBF8WU='
diff --git a/test/integration/targets/binary_modules/Makefile b/test/integration/targets/binary_modules/Makefile
new file mode 100644
index 0000000..398866f
--- /dev/null
+++ b/test/integration/targets/binary_modules/Makefile
@@ -0,0 +1,15 @@
+.PHONY: all clean
+
+all:
+ # Compiled versions of these binary modules are available at the url below.
+ # This avoids a dependency on go and keeps the binaries out of our git repository.
+ # https://ci-files.testing.ansible.com/test/integration/roles/test_binary_modules/
+ cd library; \
+ GOOS=linux GOARCH=amd64 go build -o helloworld_linux_x86_64 helloworld.go; \
+ GOOS=linux GOARCH=ppc64le go build -o helloworld_linux_ppc64le helloworld.go; \
+ GOOS=windows GOARCH=amd64 go build -o helloworld_win32nt_64-bit.exe helloworld.go; \
+ GOOS=darwin GOARCH=amd64 go build -o helloworld_darwin_x86_64 helloworld.go; \
+ GOOS=freebsd GOARCH=amd64 go build -o helloworld_freebsd_amd64 helloworld.go
+
+clean:
+ rm -f library/helloworld_*
diff --git a/test/integration/targets/binary_modules/aliases b/test/integration/targets/binary_modules/aliases
new file mode 100644
index 0000000..136c05e
--- /dev/null
+++ b/test/integration/targets/binary_modules/aliases
@@ -0,0 +1 @@
+hidden
diff --git a/test/integration/targets/binary_modules/download_binary_modules.yml b/test/integration/targets/binary_modules/download_binary_modules.yml
new file mode 100644
index 0000000..80b9145
--- /dev/null
+++ b/test/integration/targets/binary_modules/download_binary_modules.yml
@@ -0,0 +1,9 @@
+- hosts: testhost
+ tasks:
+ - name: download binary module
+ tags: test_binary_modules
+ get_url:
+ url: "https://ci-files.testing.ansible.com/test/integration/roles/test_binary_modules/{{ filename }}"
+ dest: "{{ playbook_dir }}/library/{{ filename }}"
+ mode: 0755
+ delegate_to: localhost
diff --git a/test/integration/targets/binary_modules/group_vars/all b/test/integration/targets/binary_modules/group_vars/all
new file mode 100644
index 0000000..1d3ff5e
--- /dev/null
+++ b/test/integration/targets/binary_modules/group_vars/all
@@ -0,0 +1,3 @@
+system: "{{ ansible_system|lower }}"
+suffix: "{{ '.exe' if system == 'win32nt' else '' }}"
+filename: "helloworld_{{ system }}_{{ ansible_architecture }}{{ suffix }}"
diff --git a/test/integration/targets/binary_modules/library/.gitignore b/test/integration/targets/binary_modules/library/.gitignore
new file mode 100644
index 0000000..d034a06
--- /dev/null
+++ b/test/integration/targets/binary_modules/library/.gitignore
@@ -0,0 +1 @@
+helloworld_*
diff --git a/test/integration/targets/binary_modules/library/helloworld.go b/test/integration/targets/binary_modules/library/helloworld.go
new file mode 100644
index 0000000..a4c16b2
--- /dev/null
+++ b/test/integration/targets/binary_modules/library/helloworld.go
@@ -0,0 +1,89 @@
+// This file is part of Ansible
+//
+// Ansible is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Ansible is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "os"
+)
+
+type ModuleArgs struct {
+ Name string
+}
+
+type Response struct {
+ Msg string `json:"msg"`
+ Changed bool `json:"changed"`
+ Failed bool `json:"failed"`
+}
+
+func ExitJson(responseBody Response) {
+ returnResponse(responseBody)
+}
+
+func FailJson(responseBody Response) {
+ responseBody.Failed = true
+ returnResponse(responseBody)
+}
+
+func returnResponse(responseBody Response) {
+ var response []byte
+ var err error
+ response, err = json.Marshal(responseBody)
+ if err != nil {
+ response, _ = json.Marshal(Response{Msg: "Invalid response object"})
+ }
+ fmt.Println(string(response))
+ if responseBody.Failed {
+ os.Exit(1)
+ } else {
+ os.Exit(0)
+ }
+}
+
+func main() {
+ var response Response
+
+ if len(os.Args) != 2 {
+ response.Msg = "No argument file provided"
+ FailJson(response)
+ }
+
+ argsFile := os.Args[1]
+
+ text, err := ioutil.ReadFile(argsFile)
+ if err != nil {
+ response.Msg = "Could not read configuration file: " + argsFile
+ FailJson(response)
+ }
+
+ var moduleArgs ModuleArgs
+ err = json.Unmarshal(text, &moduleArgs)
+ if err != nil {
+ response.Msg = "Configuration file not valid JSON: " + argsFile
+ FailJson(response)
+ }
+
+ var name string = "World"
+ if moduleArgs.Name != "" {
+ name = moduleArgs.Name
+ }
+
+ response.Msg = "Hello, " + name + "!"
+ ExitJson(response)
+}
diff --git a/test/integration/targets/binary_modules/roles/test_binary_modules/tasks/main.yml b/test/integration/targets/binary_modules/roles/test_binary_modules/tasks/main.yml
new file mode 100644
index 0000000..35a58dc
--- /dev/null
+++ b/test/integration/targets/binary_modules/roles/test_binary_modules/tasks/main.yml
@@ -0,0 +1,53 @@
+- debug: var=ansible_system
+
+- name: ping
+ ping:
+ when: ansible_system != 'Win32NT'
+
+- name: win_ping
+ action: win_ping
+ when: ansible_system == 'Win32NT'
+
+- name: Hello, World!
+ action: "{{ filename }}"
+ register: hello_world
+
+- assert:
+ that:
+ - 'hello_world.msg == "Hello, World!"'
+
+- name: Hello, Ansible!
+ action: "{{ filename }}"
+ args:
+ name: Ansible
+ register: hello_ansible
+
+- assert:
+ that:
+ - 'hello_ansible.msg == "Hello, Ansible!"'
+
+- name: Async Hello, World!
+ action: "{{ filename }}"
+ async: 10
+ poll: 1
+ when: ansible_system != 'Win32NT'
+ register: async_hello_world
+
+- assert:
+ that:
+ - 'async_hello_world.msg == "Hello, World!"'
+ when: async_hello_world is not skipped
+
+- name: Async Hello, Ansible!
+ action: "{{ filename }}"
+ args:
+ name: Ansible
+ async: 10
+ poll: 1
+ when: ansible_system != 'Win32NT'
+ register: async_hello_ansible
+
+- assert:
+ that:
+ - 'async_hello_ansible.msg == "Hello, Ansible!"'
+ when: async_hello_ansible is not skipped
diff --git a/test/integration/targets/binary_modules/test.sh b/test/integration/targets/binary_modules/test.sh
new file mode 100755
index 0000000..7f04667
--- /dev/null
+++ b/test/integration/targets/binary_modules/test.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+set -eux
+
+[ -f "${INVENTORY}" ]
+
+ANSIBLE_HOST_KEY_CHECKING=false ansible-playbook download_binary_modules.yml -i "${INVENTORY}" -v "$@"
+ANSIBLE_HOST_KEY_CHECKING=false ansible-playbook test_binary_modules.yml -i "${INVENTORY}" -v "$@"
diff --git a/test/integration/targets/binary_modules/test_binary_modules.yml b/test/integration/targets/binary_modules/test_binary_modules.yml
new file mode 100644
index 0000000..bdf2a06
--- /dev/null
+++ b/test/integration/targets/binary_modules/test_binary_modules.yml
@@ -0,0 +1,5 @@
+- hosts: testhost
+ roles:
+ - role: test_binary_modules
+ tags:
+ - test_binary_modules
diff --git a/test/integration/targets/binary_modules_posix/aliases b/test/integration/targets/binary_modules_posix/aliases
new file mode 100644
index 0000000..2400dc6
--- /dev/null
+++ b/test/integration/targets/binary_modules_posix/aliases
@@ -0,0 +1,3 @@
+shippable/posix/group2
+needs/target/binary_modules
+context/target
diff --git a/test/integration/targets/binary_modules_posix/runme.sh b/test/integration/targets/binary_modules_posix/runme.sh
new file mode 100755
index 0000000..670477d
--- /dev/null
+++ b/test/integration/targets/binary_modules_posix/runme.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+set -eux
+
+cd ../binary_modules
+INVENTORY=../../inventory ./test.sh "$@"
diff --git a/test/integration/targets/binary_modules_winrm/aliases b/test/integration/targets/binary_modules_winrm/aliases
new file mode 100644
index 0000000..ba3d200
--- /dev/null
+++ b/test/integration/targets/binary_modules_winrm/aliases
@@ -0,0 +1,4 @@
+shippable/windows/group1
+shippable/windows/smoketest
+windows
+needs/target/binary_modules
diff --git a/test/integration/targets/binary_modules_winrm/runme.sh b/test/integration/targets/binary_modules_winrm/runme.sh
new file mode 100755
index 0000000..f182c2d
--- /dev/null
+++ b/test/integration/targets/binary_modules_winrm/runme.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+set -eux
+
+cd ../binary_modules
+INVENTORY=../../inventory.winrm ./test.sh "$@"
diff --git a/test/integration/targets/blockinfile/aliases b/test/integration/targets/blockinfile/aliases
new file mode 100644
index 0000000..a6dafcf
--- /dev/null
+++ b/test/integration/targets/blockinfile/aliases
@@ -0,0 +1 @@
+shippable/posix/group1
diff --git a/test/integration/targets/blockinfile/files/sshd_config b/test/integration/targets/blockinfile/files/sshd_config
new file mode 100644
index 0000000..41fea19
--- /dev/null
+++ b/test/integration/targets/blockinfile/files/sshd_config
@@ -0,0 +1,135 @@
+# $OpenBSD: sshd_config,v 1.100 2016/08/15 12:32:04 naddy Exp $
+
+# This is the sshd server system-wide configuration file. See
+# sshd_config(5) for more information.
+
+# This sshd was compiled with PATH=/usr/local/bin:/usr/bin
+
+# The strategy used for options in the default sshd_config shipped with
+# OpenSSH is to specify options with their default value where
+# possible, but leave them commented. Uncommented options override the
+# default value.
+
+# If you want to change the port on a SELinux system, you have to tell
+# SELinux about this change.
+# semanage port -a -t ssh_port_t -p tcp #PORTNUMBER
+#
+#Port 22
+#AddressFamily any
+#ListenAddress 0.0.0.0
+#ListenAddress ::
+
+HostKey /etc/ssh/ssh_host_rsa_key
+#HostKey /etc/ssh/ssh_host_dsa_key
+HostKey /etc/ssh/ssh_host_ecdsa_key
+HostKey /etc/ssh/ssh_host_ed25519_key
+
+# Ciphers and keying
+#RekeyLimit default none
+
+# Logging
+#SyslogFacility AUTH
+SyslogFacility AUTHPRIV
+#LogLevel INFO
+
+# Authentication:
+
+#LoginGraceTime 2m
+PermitRootLogin yes
+#StrictModes yes
+#MaxAuthTries 6
+#MaxSessions 10
+
+#PubkeyAuthentication yes
+
+# The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2
+# but this is overridden so installations will only check .ssh/authorized_keys
+AuthorizedKeysFile .ssh/authorized_keys
+
+#AuthorizedPrincipalsFile none
+
+#AuthorizedKeysCommand none
+#AuthorizedKeysCommandUser nobody
+
+# For this to work you will also need host keys in /etc/ssh/ssh_known_hosts
+#HostbasedAuthentication no
+# Change to yes if you don't trust ~/.ssh/known_hosts for
+# HostbasedAuthentication
+#IgnoreUserKnownHosts no
+# Don't read the user's ~/.rhosts and ~/.shosts files
+#IgnoreRhosts yes
+
+# To disable tunneled clear text passwords, change to no here!
+#PermitEmptyPasswords no
+
+# Change to no to disable s/key passwords
+#ChallengeResponseAuthentication yes
+ChallengeResponseAuthentication no
+
+# Kerberos options
+#KerberosAuthentication no
+#KerberosOrLocalPasswd yes
+#KerberosTicketCleanup yes
+#KerberosGetAFSToken no
+#KerberosUseKuserok yes
+
+# GSSAPI options
+GSSAPIAuthentication yes
+GSSAPICleanupCredentials no
+#GSSAPIStrictAcceptorCheck yes
+#GSSAPIKeyExchange no
+#GSSAPIEnablek5users no
+
+# Set this to 'yes' to enable PAM authentication, account processing,
+# and session processing. If this is enabled, PAM authentication will
+# be allowed through the ChallengeResponseAuthentication and
+# PAM authentication via ChallengeResponseAuthentication may bypass
+# the setting of "PermitRootLogin without-password".
+# If you just want the PAM account and session checks to run without
+# and ChallengeResponseAuthentication to 'no'.
+# WARNING: 'UsePAM no' is not supported in Fedora and may cause several
+# problems.
+UsePAM yes
+
+#AllowAgentForwarding yes
+#AllowTcpForwarding yes
+#GatewayPorts no
+X11Forwarding yes
+#X11DisplayOffset 10
+#X11UseLocalhost yes
+#PermitTTY yes
+#PrintMotd yes
+#PrintLastLog yes
+#TCPKeepAlive yes
+#UseLogin no
+#UsePrivilegeSeparation sandbox
+#PermitUserEnvironment no
+#Compression delayed
+#ClientAliveInterval 0
+#ClientAliveCountMax 3
+#ShowPatchLevel no
+#UseDNS no
+#PidFile /var/run/sshd.pid
+#MaxStartups 10:30:100
+#PermitTunnel no
+#ChrootDirectory none
+#VersionAddendum none
+
+# no default banner path
+#Banner none
+
+# Accept locale-related environment variables
+AcceptEnv LANG LC_CTYPE LC_NUMERIC LC_TIME LC_COLLATE LC_MONETARY LC_MESSAGES
+AcceptEnv LC_PAPER LC_NAME LC_ADDRESS LC_TELEPHONE LC_MEASUREMENT
+AcceptEnv LC_IDENTIFICATION LC_ALL LANGUAGE
+AcceptEnv XMODIFIERS
+
+# override default of no subsystems
+Subsystem sftp /usr/libexec/openssh/sftp-server
+
+# Example of overriding settings on a per-user basis
+#Match User anoncvs
+# X11Forwarding no
+# AllowTcpForwarding no
+# PermitTTY no
+# ForceCommand cvs server
diff --git a/test/integration/targets/blockinfile/meta/main.yml b/test/integration/targets/blockinfile/meta/main.yml
new file mode 100644
index 0000000..cb6005d
--- /dev/null
+++ b/test/integration/targets/blockinfile/meta/main.yml
@@ -0,0 +1,3 @@
+dependencies:
+ - prepare_tests
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/blockinfile/tasks/add_block_to_existing_file.yml b/test/integration/targets/blockinfile/tasks/add_block_to_existing_file.yml
new file mode 100644
index 0000000..c610905
--- /dev/null
+++ b/test/integration/targets/blockinfile/tasks/add_block_to_existing_file.yml
@@ -0,0 +1,52 @@
+- name: copy the sshd_config to the test dir
+ copy:
+ src: sshd_config
+ dest: "{{ remote_tmp_dir_test }}"
+
+- name: insert/update "Match User" configuration block in sshd_config
+ blockinfile:
+ path: "{{ remote_tmp_dir_test }}/sshd_config"
+ block: |
+ Match User ansible-agent
+ PasswordAuthentication no
+ backup: yes
+ register: blockinfile_test0
+
+- name: ensure we have a bcackup file
+ assert:
+ that:
+ - "'backup_file' in blockinfile_test0"
+
+- name: check content
+ shell: 'grep -c -e "Match User ansible-agent" -e "PasswordAuthentication no" {{ remote_tmp_dir_test }}/sshd_config'
+ register: blockinfile_test0_grep
+
+- debug:
+ var: blockinfile_test0
+ verbosity: 1
+
+- debug:
+ var: blockinfile_test0_grep
+ verbosity: 1
+
+- name: validate first example results
+ assert:
+ that:
+ - 'blockinfile_test0.changed is defined'
+ - 'blockinfile_test0.msg is defined'
+ - 'blockinfile_test0.changed'
+ - 'blockinfile_test0.msg == "Block inserted"'
+ - 'blockinfile_test0_grep.stdout == "2"'
+
+- name: check idemptotence
+ blockinfile:
+ path: "{{ remote_tmp_dir_test }}/sshd_config"
+ block: |
+ Match User ansible-agent
+ PasswordAuthentication no
+ register: blockinfile_test1
+
+- name: validate idempotence results
+ assert:
+ that:
+ - 'not blockinfile_test1.changed'
diff --git a/test/integration/targets/blockinfile/tasks/block_without_trailing_newline.yml b/test/integration/targets/blockinfile/tasks/block_without_trailing_newline.yml
new file mode 100644
index 0000000..466e86c
--- /dev/null
+++ b/test/integration/targets/blockinfile/tasks/block_without_trailing_newline.yml
@@ -0,0 +1,30 @@
+- name: Add block without trailing line separator
+ blockinfile:
+ path: "{{ remote_tmp_dir_test }}/chomped_block_test.txt"
+ create: yes
+ content: |-
+ one
+ two
+ three
+ register: chomptest1
+
+- name: Add block without trailing line separator again
+ blockinfile:
+ path: "{{ remote_tmp_dir_test }}/chomped_block_test.txt"
+ content: |-
+ one
+ two
+ three
+ register: chomptest2
+
+- name: Check output file
+ stat:
+ path: "{{ remote_tmp_dir_test }}/chomped_block_test.txt"
+ register: chomptest_file
+
+- name: Ensure chomptest results are correct
+ assert:
+ that:
+ - chomptest1 is changed
+ - chomptest2 is not changed
+ - chomptest_file.stat.checksum == '50d49f528a5f7147c7029ed6220c326b1ee2c4ae'
diff --git a/test/integration/targets/blockinfile/tasks/create_file.yml b/test/integration/targets/blockinfile/tasks/create_file.yml
new file mode 100644
index 0000000..c8ded30
--- /dev/null
+++ b/test/integration/targets/blockinfile/tasks/create_file.yml
@@ -0,0 +1,32 @@
+- name: Create a file with blockinfile
+ blockinfile:
+ path: "{{ remote_tmp_dir_test }}/empty.txt"
+ block: |
+ Hey
+ there
+ state: present
+ create: yes
+ register: empty_test_1
+
+- name: Run a task that results in an empty file
+ blockinfile:
+ path: "{{ remote_tmp_dir_test }}/empty.txt"
+ block: |
+ Hey
+ there
+ state: absent
+ create: yes
+ register: empty_test_2
+
+- stat:
+ path: "{{ remote_tmp_dir_test }}/empty.txt"
+ register: empty_test_stat
+
+- name: Ensure empty file was created
+ assert:
+ that:
+ - empty_test_1 is changed
+ - "'File created' in empty_test_1.msg"
+ - empty_test_2 is changed
+ - "'Block removed' in empty_test_2.msg"
+ - empty_test_stat.stat.size == 0
diff --git a/test/integration/targets/blockinfile/tasks/diff.yml b/test/integration/targets/blockinfile/tasks/diff.yml
new file mode 100644
index 0000000..56ef08d
--- /dev/null
+++ b/test/integration/targets/blockinfile/tasks/diff.yml
@@ -0,0 +1,18 @@
+- name: Create a test file
+ copy:
+ content: diff test
+ dest: "{{ remote_tmp_dir_test }}/diff.txt"
+
+- name: Add block to file with diff
+ blockinfile:
+ path: "{{ remote_tmp_dir_test }}/diff.txt"
+ block: |
+ line 1
+ line 2
+ register: difftest
+ diff: yes
+
+- name: Ensure diff was shown
+ assert:
+ that:
+ - difftest.diff | length > 0
diff --git a/test/integration/targets/blockinfile/tasks/file_without_trailing_newline.yml b/test/integration/targets/blockinfile/tasks/file_without_trailing_newline.yml
new file mode 100644
index 0000000..797ffc5
--- /dev/null
+++ b/test/integration/targets/blockinfile/tasks/file_without_trailing_newline.yml
@@ -0,0 +1,36 @@
+- name: Create file without trailing newline
+ copy:
+ content: '# File with no newline'
+ dest: "{{ remote_tmp_dir_test }}/no_newline_at_end.txt"
+ register: no_newline
+
+
+- name: Add block to file that does not have a newline at the end
+ blockinfile:
+ path: "{{ remote_tmp_dir_test }}/no_newline_at_end.txt"
+ content: |
+ one
+ two
+ three
+ register: no_newline_test1
+
+- name: Add block to file that does not have a newline at the end again
+ blockinfile:
+ path: "{{ remote_tmp_dir_test }}/no_newline_at_end.txt"
+ content: |
+ one
+ two
+ three
+ register: no_newline_test2
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir_test }}/no_newline_at_end.txt"
+ register: no_newline_file
+
+- name: Ensure block was correctly written to file with no newline at end
+ assert:
+ that:
+ - no_newline_test1 is changed
+ - no_newline_test2 is not changed
+ - no_newline_file.stat.checksum == 'dab16f864025e59125e74d1498ffb2bb048224e6'
diff --git a/test/integration/targets/blockinfile/tasks/insertafter.yml b/test/integration/targets/blockinfile/tasks/insertafter.yml
new file mode 100644
index 0000000..a4cdd5f
--- /dev/null
+++ b/test/integration/targets/blockinfile/tasks/insertafter.yml
@@ -0,0 +1,37 @@
+- name: Create insertafter test file
+ copy:
+ dest: "{{ remote_tmp_dir }}/after.txt"
+ content: |
+ line1
+ line2
+ line3
+
+- name: Add block using insertafter
+ blockinfile:
+ path: "{{ remote_tmp_dir }}/after.txt"
+ insertafter: line2
+ block: |
+ block1
+ block2
+ register: after1
+
+- name: Add block using insertafter again
+ blockinfile:
+ path: "{{ remote_tmp_dir }}/after.txt"
+ insertafter: line2
+ block: |
+ block1
+ block2
+ register: after2
+
+- name: Stat the after.txt file
+ stat:
+ path: "{{ remote_tmp_dir }}/after.txt"
+ register: after_file
+
+- name: Ensure insertafter worked correctly
+ assert:
+ that:
+ - after1 is changed
+ - after2 is not changed
+ - after_file.stat.checksum == 'a8adeb971358230a28ce554f3b8fdd1ef65fdf1c'
diff --git a/test/integration/targets/blockinfile/tasks/insertbefore.yml b/test/integration/targets/blockinfile/tasks/insertbefore.yml
new file mode 100644
index 0000000..03e51c9
--- /dev/null
+++ b/test/integration/targets/blockinfile/tasks/insertbefore.yml
@@ -0,0 +1,39 @@
+- name: Create insertbefore test file
+ copy:
+ dest: "{{ remote_tmp_dir }}/before.txt"
+ content: |
+ line1
+ line2
+ line3
+
+- name: Add block using insertbefore
+ blockinfile:
+ path: "{{ remote_tmp_dir }}/before.txt"
+ insertbefore: line2
+ block: |
+ block1
+ block2
+ register: after1
+
+- name: Add block using insertbefore again
+ blockinfile:
+ path: "{{ remote_tmp_dir }}/before.txt"
+ insertbefore: line2
+ block: |
+ block1
+ block2
+ register: after2
+
+- name: Stat the before.txt file
+ stat:
+ path: "{{ remote_tmp_dir }}/before.txt"
+ register: after_file
+
+- command: cat {{ remote_tmp_dir }}/before.txt
+
+- name: Ensure insertbefore worked correctly
+ assert:
+ that:
+ - after1 is changed
+ - after2 is not changed
+ - after_file.stat.checksum == '16681d1d7f29d173243bb951d6afb9c0824d7bf4'
diff --git a/test/integration/targets/blockinfile/tasks/main.yml b/test/integration/targets/blockinfile/tasks/main.yml
new file mode 100644
index 0000000..054e554
--- /dev/null
+++ b/test/integration/targets/blockinfile/tasks/main.yml
@@ -0,0 +1,41 @@
+# Test code for the blockinfile module.
+# (c) 2017, James Tanner <tanner.jc@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- set_fact:
+ remote_tmp_dir_test: "{{ remote_tmp_dir }}/test_blockinfile"
+
+- name: make sure our testing sub-directory does not exist
+ file:
+ path: "{{ remote_tmp_dir_test }}"
+ state: absent
+
+- name: create our testing sub-directory
+ file:
+ path: "{{ remote_tmp_dir_test }}"
+ state: directory
+
+- import_tasks: add_block_to_existing_file.yml
+- import_tasks: create_file.yml
+- import_tasks: preserve_line_endings.yml
+- import_tasks: block_without_trailing_newline.yml
+- import_tasks: file_without_trailing_newline.yml
+- import_tasks: diff.yml
+- import_tasks: validate.yml
+- import_tasks: insertafter.yml
+- import_tasks: insertbefore.yml
+- import_tasks: multiline_search.yml
diff --git a/test/integration/targets/blockinfile/tasks/multiline_search.yml b/test/integration/targets/blockinfile/tasks/multiline_search.yml
new file mode 100644
index 0000000..8eb94f5
--- /dev/null
+++ b/test/integration/targets/blockinfile/tasks/multiline_search.yml
@@ -0,0 +1,70 @@
+- name: Create multiline_search test file
+ copy:
+ dest: "{{ remote_tmp_dir }}/listener.ora"
+ content: |
+ LISTENER=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=IPC)(KEY=LISTENER)))) # line added by Agent
+ ENABLE_GLOBAL_DYNAMIC_ENDPOINT_LISTENER=ON # line added by Agent
+
+ SID_LIST_LISTENER_DG =
+ (SID_LIST =
+ (SID_DESC =
+ (GLOBAL_DBNAME = DB01_DG)
+ (ORACLE_HOME = /u01/app/oracle/product/12.1.0.1/db_1)
+ (SID_NAME = DB011)
+ )
+ )
+
+ SID_LIST_LISTENER =
+ (SID_LIST =
+ (SID_DESC =
+ (GLOBAL_DBNAME = DB02)
+ (ORACLE_HOME = /u01/app/oracle/product/12.1.0.1/db_1)
+ (SID_NAME = DB021)
+ )
+ )
+
+- name: Set fact listener_line
+ set_fact:
+ listener_line: |
+ (SID_DESC =
+ (GLOBAL_DBNAME = DB03
+ (ORACLE_HOME = /u01/app/oracle/product/12.1.0.1/db_1)
+ (SID_NAME = DB031)
+ )
+
+- name: Add block using multiline_search enabled
+ blockinfile:
+ path: "{{ remote_tmp_dir }}/listener.ora"
+ block: "{{ listener_line }}"
+ insertafter: '(?m)SID_LIST_LISTENER_DG =\n.*\(SID_LIST ='
+ marker: " <!-- {mark} ANSIBLE MANAGED BLOCK 1-->"
+ register: multiline_search1
+
+- name: Add block using multiline_search enabled again
+ blockinfile:
+ path: "{{ remote_tmp_dir }}/listener.ora"
+ block: "{{ listener_line }}"
+ insertafter: '(?m)SID_LIST_LISTENER_DG =\n.*\(SID_LIST ='
+ marker: " <!-- {mark} ANSIBLE MANAGED BLOCK 1-->"
+ register: multiline_search2
+
+- name: Try to add block using without multiline flag in regex should add block add end of file
+ blockinfile:
+ path: "{{ remote_tmp_dir }}/listener.ora"
+ block: "{{ listener_line }}"
+ insertafter: 'SID_LIST_LISTENER_DG =\n.*\(SID_LIST ='
+ marker: " <!-- {mark} ANSIBLE MANAGED BLOCK 2-->"
+ register: multiline_search3
+
+- name: Stat the listener.ora file
+ stat:
+ path: "{{ remote_tmp_dir }}/listener.ora"
+ register: listener_ora_file
+
+- name: Ensure insertafter worked correctly
+ assert:
+ that:
+ - multiline_search1 is changed
+ - multiline_search2 is not changed
+ - multiline_search3 is changed
+ - listener_ora_file.stat.checksum == '5a8010ac4a2fad7c822e6aeb276931657cee75c0'
diff --git a/test/integration/targets/blockinfile/tasks/preserve_line_endings.yml b/test/integration/targets/blockinfile/tasks/preserve_line_endings.yml
new file mode 100644
index 0000000..0528c3b
--- /dev/null
+++ b/test/integration/targets/blockinfile/tasks/preserve_line_endings.yml
@@ -0,0 +1,24 @@
+- name: create line_endings_test.txt in the test dir
+ copy:
+ dest: "{{ remote_tmp_dir_test }}/line_endings_test.txt"
+ # generating the content like this instead of copying a fixture file
+ # prevents sanity checks from warning about mixed line endings
+ content: "unix\nunix\nunix\n\ndos\r\ndos\r\ndos\r\n\nunix\nunix\n# BEGIN ANSIBLE MANAGED BLOCK\ndos\r\n# END ANSIBLE MANAGED BLOCK\nunix\nunix\nunix\nunix\n"
+
+- name: insert/update "dos" configuration block in line_endings_test.txt
+ blockinfile:
+ path: "{{ remote_tmp_dir_test }}/line_endings_test.txt"
+ block: "dos\r\ndos\r\ndos\r\n"
+ register: blockinfile_test2
+
+- name: check content
+ # using the more precise `grep -Pc "^dos\\r$" ...` fails on BSD/macOS
+ shell: 'grep -c "^dos.$" {{ remote_tmp_dir_test }}/line_endings_test.txt'
+ register: blockinfile_test2_grep
+
+- name: validate line_endings_test.txt results
+ assert:
+ that:
+ - 'blockinfile_test2 is changed'
+ - 'blockinfile_test2.msg == "Block inserted"'
+ - 'blockinfile_test2_grep.stdout == "6"'
diff --git a/test/integration/targets/blockinfile/tasks/validate.yml b/test/integration/targets/blockinfile/tasks/validate.yml
new file mode 100644
index 0000000..aa7aa63
--- /dev/null
+++ b/test/integration/targets/blockinfile/tasks/validate.yml
@@ -0,0 +1,28 @@
+- name: EXPECTED FAILURE test improper validate
+ blockinfile:
+ path: "{{ remote_tmp_dir }}/validate.txt"
+ block: |
+ line1
+ line2
+ create: yes
+ validate: grep
+ ignore_errors: yes
+
+- name: EXPECTED FAILURE test failure to validate
+ blockinfile:
+ path: "{{ remote_tmp_dir }}/validate.txt"
+ block: |
+ line1
+ line2
+ create: yes
+ validate: grep line47 %s
+ ignore_errors: yes
+
+- name: Test proper validate
+ blockinfile:
+ path: "{{ remote_tmp_dir }}/validate.txt"
+ block: |
+ line1
+ line2
+ create: yes
+ validate: grep line1 %s
diff --git a/test/integration/targets/blocks/43191-2.yml b/test/integration/targets/blocks/43191-2.yml
new file mode 100644
index 0000000..4beda4e
--- /dev/null
+++ b/test/integration/targets/blocks/43191-2.yml
@@ -0,0 +1,17 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - block:
+ - block:
+ - name: EXPECTED FAILURE
+ fail:
+ always:
+ - block:
+ - debug:
+ always:
+ - debug:
+ rescue:
+ - assert:
+ that:
+ - ansible_failed_task is defined
+ - ansible_failed_result is defined
diff --git a/test/integration/targets/blocks/43191.yml b/test/integration/targets/blocks/43191.yml
new file mode 100644
index 0000000..d69e438
--- /dev/null
+++ b/test/integration/targets/blocks/43191.yml
@@ -0,0 +1,18 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - block:
+ - block:
+ - name: EXPECTED FAILURE
+ fail:
+ always:
+ - block:
+ - block:
+ - debug:
+ rescue:
+ - block:
+ - block:
+ - assert:
+ that:
+ - ansible_failed_task is defined
+ - ansible_failed_result is defined
diff --git a/test/integration/targets/blocks/69848.yml b/test/integration/targets/blocks/69848.yml
new file mode 100644
index 0000000..3b43eeb
--- /dev/null
+++ b/test/integration/targets/blocks/69848.yml
@@ -0,0 +1,5 @@
+- hosts: host1,host2
+ gather_facts: no
+ roles:
+ - role-69848-1
+ - role-69848-2
diff --git a/test/integration/targets/blocks/72725.yml b/test/integration/targets/blocks/72725.yml
new file mode 100644
index 0000000..54a70c6
--- /dev/null
+++ b/test/integration/targets/blocks/72725.yml
@@ -0,0 +1,24 @@
+- hosts: host1,host2
+ gather_facts: no
+ tasks:
+ - block:
+ - block:
+ - name: EXPECTED FAILURE host1 fails
+ fail:
+ when: inventory_hostname == 'host1'
+
+ - set_fact:
+ only_host2_fact: yes
+
+ - name: should not fail
+ fail:
+ when: only_host2_fact is not defined
+ always:
+ - block:
+ - meta: clear_host_errors
+
+ - assert:
+ that:
+ - only_host2_fact is defined
+ when:
+ - inventory_hostname == 'host2'
diff --git a/test/integration/targets/blocks/72781.yml b/test/integration/targets/blocks/72781.yml
new file mode 100644
index 0000000..f124cce
--- /dev/null
+++ b/test/integration/targets/blocks/72781.yml
@@ -0,0 +1,13 @@
+- hosts: all
+ gather_facts: no
+ any_errors_fatal: true
+ tasks:
+ - block:
+ - block:
+ - fail:
+ when: inventory_hostname == 'host1'
+ rescue:
+ - fail:
+ - block:
+ - debug:
+ msg: "SHOULD NOT HAPPEN"
diff --git a/test/integration/targets/blocks/78612.yml b/test/integration/targets/blocks/78612.yml
new file mode 100644
index 0000000..38efc6b
--- /dev/null
+++ b/test/integration/targets/blocks/78612.yml
@@ -0,0 +1,16 @@
+- hosts: all
+ gather_facts: false
+ tasks:
+ - block:
+ - fail:
+ when: inventory_hostname == 'host1'
+ rescue:
+ - block:
+ - fail:
+ when: inventory_hostname == 'host1'
+
+ - assert:
+ that:
+ - "'host1' not in ansible_play_hosts"
+ - "'host1' not in ansible_play_batch"
+ success_msg: PASSED
diff --git a/test/integration/targets/blocks/79711.yml b/test/integration/targets/blocks/79711.yml
new file mode 100644
index 0000000..ca9bfbb
--- /dev/null
+++ b/test/integration/targets/blocks/79711.yml
@@ -0,0 +1,17 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - block:
+ - block:
+ - debug:
+ - name: EXPECTED FAILURE
+ fail:
+ rescue:
+ - debug:
+ - debug:
+ - name: EXPECTED FAILURE
+ fail:
+ always:
+ - debug:
+ always:
+ - debug:
diff --git a/test/integration/targets/blocks/aliases b/test/integration/targets/blocks/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/blocks/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/blocks/always_failure_no_rescue_rc.yml b/test/integration/targets/blocks/always_failure_no_rescue_rc.yml
new file mode 100644
index 0000000..924643c
--- /dev/null
+++ b/test/integration/targets/blocks/always_failure_no_rescue_rc.yml
@@ -0,0 +1,13 @@
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - block:
+ - name: EXPECTED FAILURE
+ fail:
+ msg: Failure in block
+ always:
+ - name: EXPECTED FAILURE
+ fail:
+ msg: Failure in always
+ - debug:
+ msg: DID NOT RUN
diff --git a/test/integration/targets/blocks/always_failure_with_rescue_rc.yml b/test/integration/targets/blocks/always_failure_with_rescue_rc.yml
new file mode 100644
index 0000000..f3029cb
--- /dev/null
+++ b/test/integration/targets/blocks/always_failure_with_rescue_rc.yml
@@ -0,0 +1,16 @@
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - block:
+ - name: EXPECTED FAILURE
+ fail:
+ msg: Failure in block
+ rescue:
+ - debug:
+ msg: Rescue
+ always:
+ - name: EXPECTED FAILURE
+ fail:
+ msg: Failure in always
+ - debug:
+ msg: DID NOT RUN
diff --git a/test/integration/targets/blocks/always_no_rescue_rc.yml b/test/integration/targets/blocks/always_no_rescue_rc.yml
new file mode 100644
index 0000000..a4e8641
--- /dev/null
+++ b/test/integration/targets/blocks/always_no_rescue_rc.yml
@@ -0,0 +1,12 @@
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - block:
+ - name: EXPECTED FAILURE
+ fail:
+ msg: Failure in block
+ always:
+ - debug:
+ msg: Always
+ - debug:
+ msg: DID NOT RUN
diff --git a/test/integration/targets/blocks/block_fail.yml b/test/integration/targets/blocks/block_fail.yml
new file mode 100644
index 0000000..6b84d05
--- /dev/null
+++ b/test/integration/targets/blocks/block_fail.yml
@@ -0,0 +1,5 @@
+---
+- name: Include tasks that have a failure in a block
+ hosts: localhost
+ tasks:
+ - include_tasks: block_fail_tasks.yml
diff --git a/test/integration/targets/blocks/block_fail_tasks.yml b/test/integration/targets/blocks/block_fail_tasks.yml
new file mode 100644
index 0000000..6e70dc2
--- /dev/null
+++ b/test/integration/targets/blocks/block_fail_tasks.yml
@@ -0,0 +1,9 @@
+- block:
+ - name: EXPECTED FAILURE
+ fail:
+ msg: failure
+
+ always:
+ - name: run always task
+ debug:
+ msg: TEST COMPLETE
diff --git a/test/integration/targets/blocks/block_in_rescue.yml b/test/integration/targets/blocks/block_in_rescue.yml
new file mode 100644
index 0000000..1536030
--- /dev/null
+++ b/test/integration/targets/blocks/block_in_rescue.yml
@@ -0,0 +1,33 @@
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - block:
+ - name: "EXPECTED FAILURE"
+ fail:
+ msg: "fail to test single level block in rescue"
+ rescue:
+ - block:
+ - debug:
+ msg: Rescued!
+
+ - block:
+ - name: "EXPECTED FAILURE"
+ fail:
+ msg: "fail to test multi-level block in rescue"
+ rescue:
+ - block:
+ - block:
+ - debug:
+ msg: Rescued!
+
+ - name: "Outer block"
+ block:
+ - name: "Inner block"
+ block:
+ - name: "EXPECTED FAILURE"
+ fail:
+ msg: "fail to test multi-level block"
+ rescue:
+ - name: "Rescue block"
+ block:
+ - debug: msg="Inner block rescue"
diff --git a/test/integration/targets/blocks/block_rescue_vars.yml b/test/integration/targets/blocks/block_rescue_vars.yml
new file mode 100644
index 0000000..404f7a3
--- /dev/null
+++ b/test/integration/targets/blocks/block_rescue_vars.yml
@@ -0,0 +1,16 @@
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - block:
+ - name: EXPECTED FAILURE
+ fail:
+ rescue:
+ - name: Assert that ansible_failed_task is defined
+ assert:
+ that:
+ - ansible_failed_task is defined
+
+ - name: Assert that ansible_failed_result is defined
+ assert:
+ that:
+ - ansible_failed_result is defined
diff --git a/test/integration/targets/blocks/fail.yml b/test/integration/targets/blocks/fail.yml
new file mode 100644
index 0000000..ae94655
--- /dev/null
+++ b/test/integration/targets/blocks/fail.yml
@@ -0,0 +1,2 @@
+- name: EXPECTED FAILURE
+ fail: msg="{{msg}}"
diff --git a/test/integration/targets/blocks/finalized_task.yml b/test/integration/targets/blocks/finalized_task.yml
new file mode 100644
index 0000000..300401b
--- /dev/null
+++ b/test/integration/targets/blocks/finalized_task.yml
@@ -0,0 +1,17 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - block:
+ - include_role:
+ name: '{{ item }}'
+ loop:
+ - fail
+ rescue:
+ - debug:
+ msg: "{{ ansible_failed_task.name }}"
+
+ - assert:
+ that:
+ - ansible_failed_task.name == "Fail"
+ - ansible_failed_task.action == "fail"
+ - ansible_failed_task.parent is not defined
diff --git a/test/integration/targets/blocks/inherit_notify.yml b/test/integration/targets/blocks/inherit_notify.yml
new file mode 100644
index 0000000..d8e8742
--- /dev/null
+++ b/test/integration/targets/blocks/inherit_notify.yml
@@ -0,0 +1,19 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: test notify inheritance in block
+ notify: hello
+ block:
+ - debug: msg='trigger it'
+ changed_when: true
+
+ handlers:
+ - name: hello
+ set_fact: hello=world
+
+ post_tasks:
+ - name: ensure handler ran
+ assert:
+ that:
+ - hello is defined
+ - "hello == 'world'"
diff --git a/test/integration/targets/blocks/issue29047.yml b/test/integration/targets/blocks/issue29047.yml
new file mode 100644
index 0000000..9743773
--- /dev/null
+++ b/test/integration/targets/blocks/issue29047.yml
@@ -0,0 +1,4 @@
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - include_tasks: issue29047_tasks.yml
diff --git a/test/integration/targets/blocks/issue29047_tasks.yml b/test/integration/targets/blocks/issue29047_tasks.yml
new file mode 100644
index 0000000..3470d86
--- /dev/null
+++ b/test/integration/targets/blocks/issue29047_tasks.yml
@@ -0,0 +1,13 @@
+---
+- name: "EXPECTED FAILURE"
+ block:
+ - fail:
+ msg: "EXPECTED FAILURE"
+ rescue:
+ - name: Assert that ansible_failed_task is defined
+ assert:
+ that: ansible_failed_task is defined
+
+ - name: Assert that ansible_failed_result is defined
+ assert:
+ that: ansible_failed_result is defined
diff --git a/test/integration/targets/blocks/issue71306.yml b/test/integration/targets/blocks/issue71306.yml
new file mode 100644
index 0000000..9762f6e
--- /dev/null
+++ b/test/integration/targets/blocks/issue71306.yml
@@ -0,0 +1,16 @@
+- hosts: all
+ gather_facts: no
+ tasks:
+ - block:
+ - block:
+ - block:
+ - name: EXPECTED FAILURE
+ fail:
+ when: ansible_host == "host1"
+
+ - debug:
+ msg: "I am successful!"
+ run_once: true
+ rescue:
+ - debug:
+ msg: "Attemp 1 failed!"
diff --git a/test/integration/targets/blocks/main.yml b/test/integration/targets/blocks/main.yml
new file mode 100644
index 0000000..efe358a
--- /dev/null
+++ b/test/integration/targets/blocks/main.yml
@@ -0,0 +1,128 @@
+- name: simple block test
+ hosts: testhost
+ gather_facts: yes
+ strategy: "{{test_strategy|default('linear')}}"
+ vars:
+ block_tasks_run: false
+ block_rescue_run: false
+ block_always_run: false
+ nested_block_always_run: false
+ tasks_run_after_failure: false
+ rescue_run_after_failure: false
+ always_run_after_failure: false
+ nested_block_fail_always: false
+ tasks:
+ - block:
+ - name: set block tasks run flag
+ set_fact:
+ block_tasks_run: true
+ - name: EXPECTED FAILURE fail in tasks
+ fail:
+ - name: tasks flag should not be set after failure
+ set_fact:
+ tasks_run_after_failure: true
+ rescue:
+ - name: set block rescue run flag
+ set_fact:
+ block_rescue_run: true
+ - name: EXPECTED FAILURE fail in rescue
+ fail:
+ - name: tasks flag should not be set after failure in rescue
+ set_fact:
+ rescue_run_after_failure: true
+ always:
+ - name: set block always run flag
+ set_fact:
+ block_always_run: true
+ #- block:
+ # - meta: noop
+ # always:
+ # - name: set nested block always run flag
+ # set_fact:
+ # nested_block_always_run: true
+ # - name: fail in always
+ # fail:
+ # - name: tasks flag should not be set after failure in always
+ # set_fact:
+ # always_run_after_failure: true
+ - meta: clear_host_errors
+
+ # https://github.com/ansible/ansible/issues/35148
+ - block:
+ - block:
+ - name: EXPECTED FAILURE test triggering always by failing in nested block with run_once set
+ fail:
+ run_once: true
+ always:
+ - name: set block fail always run flag
+ set_fact:
+ nested_block_fail_always: true
+ - meta: clear_host_errors
+
+ - block:
+ - block:
+ - name: EXPECTED FAILURE test triggering always by failing in nested block with any_errors_fatal set
+ fail:
+ any_errors_fatal: true
+ always:
+ - name: set block fail always run flag
+ set_fact:
+ nested_block_fail_always: true
+ - meta: clear_host_errors
+
+ post_tasks:
+ - assert:
+ that:
+ - block_tasks_run
+ - block_rescue_run
+ - block_always_run
+ #- nested_block_always_run
+ - not tasks_run_after_failure
+ - not rescue_run_after_failure
+ - not always_run_after_failure
+ - nested_block_fail_always
+ - debug: msg="TEST COMPLETE"
+
+- name: block with includes
+ hosts: testhost
+ gather_facts: yes
+ strategy: "{{test_strategy|default('linear')}}"
+ vars:
+ rescue_run_after_include_fail: false
+ always_run_after_include_fail_in_rescue: false
+ tasks_run_after_failure: false
+ rescue_run_after_failure: false
+ always_run_after_failure: false
+ tasks:
+ - block:
+ - name: include fail.yml in tasks
+ import_tasks: fail.yml
+ vars:
+ msg: "failed from tasks"
+ - name: tasks flag should not be set after failure
+ set_fact:
+ tasks_run_after_failure: true
+ rescue:
+ - set_fact:
+ rescue_run_after_include_fail: true
+ - name: include fail.yml in rescue
+ import_tasks: fail.yml
+ vars:
+ msg: "failed from rescue"
+ - name: flag should not be set after failure in rescue
+ set_fact:
+ rescue_run_after_failure: true
+ always:
+ - set_fact:
+ always_run_after_include_fail_in_rescue: true
+ - meta: clear_host_errors
+
+ post_tasks:
+ - assert:
+ that:
+ - rescue_run_after_include_fail
+ - always_run_after_include_fail_in_rescue
+ - not tasks_run_after_failure
+ - not rescue_run_after_failure
+ - not always_run_after_failure
+ - debug: msg="TEST COMPLETE"
diff --git a/test/integration/targets/blocks/nested_fail.yml b/test/integration/targets/blocks/nested_fail.yml
new file mode 100644
index 0000000..12e33cb
--- /dev/null
+++ b/test/integration/targets/blocks/nested_fail.yml
@@ -0,0 +1,3 @@
+- import_tasks: fail.yml
+ vars:
+ msg: "nested {{msg}}"
diff --git a/test/integration/targets/blocks/nested_nested_fail.yml b/test/integration/targets/blocks/nested_nested_fail.yml
new file mode 100644
index 0000000..f63fa5c
--- /dev/null
+++ b/test/integration/targets/blocks/nested_nested_fail.yml
@@ -0,0 +1,3 @@
+- import_tasks: nested_fail.yml
+ vars:
+ msg: "nested {{msg}}"
diff --git a/test/integration/targets/blocks/roles/fail/tasks/main.yml b/test/integration/targets/blocks/roles/fail/tasks/main.yml
new file mode 100644
index 0000000..176fe54
--- /dev/null
+++ b/test/integration/targets/blocks/roles/fail/tasks/main.yml
@@ -0,0 +1,3 @@
+- name: Fail
+ fail:
+ msg: fail
diff --git a/test/integration/targets/blocks/roles/role-69848-1/meta/main.yml b/test/integration/targets/blocks/roles/role-69848-1/meta/main.yml
new file mode 100644
index 0000000..d34d662
--- /dev/null
+++ b/test/integration/targets/blocks/roles/role-69848-1/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - role: role-69848-3
diff --git a/test/integration/targets/blocks/roles/role-69848-2/meta/main.yml b/test/integration/targets/blocks/roles/role-69848-2/meta/main.yml
new file mode 100644
index 0000000..d34d662
--- /dev/null
+++ b/test/integration/targets/blocks/roles/role-69848-2/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - role: role-69848-3
diff --git a/test/integration/targets/blocks/roles/role-69848-3/tasks/main.yml b/test/integration/targets/blocks/roles/role-69848-3/tasks/main.yml
new file mode 100644
index 0000000..0d01b74
--- /dev/null
+++ b/test/integration/targets/blocks/roles/role-69848-3/tasks/main.yml
@@ -0,0 +1,8 @@
+- block:
+ - debug:
+ msg: Tagged task
+ tags:
+ - foo
+
+- debug:
+ msg: Not tagged task
diff --git a/test/integration/targets/blocks/runme.sh b/test/integration/targets/blocks/runme.sh
new file mode 100755
index 0000000..820107b
--- /dev/null
+++ b/test/integration/targets/blocks/runme.sh
@@ -0,0 +1,138 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# This test does not use "$@" to avoid further increasing the verbosity beyond what is required for the test.
+# Increasing verbosity from -vv to -vvv can increase the line count from ~400 to ~9K on our centos6 test container.
+
+# remove old output log
+rm -f block_test.out
+# run the test and check to make sure the right number of completions was logged
+ansible-playbook -vv main.yml -i ../../inventory | tee block_test.out
+env python -c \
+ 'import sys, re; sys.stdout.write(re.sub("\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]", "", sys.stdin.read()))' \
+ <block_test.out >block_test_wo_colors.out
+[ "$(grep -c 'TEST COMPLETE' block_test.out)" = "$(grep -E '^[0-9]+ plays in' block_test_wo_colors.out | cut -f1 -d' ')" ]
+# cleanup the output log again, to make sure the test is clean
+rm -f block_test.out block_test_wo_colors.out
+# run test with free strategy and again count the completions
+ansible-playbook -vv main.yml -i ../../inventory -e test_strategy=free | tee block_test.out
+env python -c \
+ 'import sys, re; sys.stdout.write(re.sub("\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]", "", sys.stdin.read()))' \
+ <block_test.out >block_test_wo_colors.out
+[ "$(grep -c 'TEST COMPLETE' block_test.out)" = "$(grep -E '^[0-9]+ plays in' block_test_wo_colors.out | cut -f1 -d' ')" ]
+# cleanup the output log again, to make sure the test is clean
+rm -f block_test.out block_test_wo_colors.out
+# run test with host_pinned strategy and again count the completions
+ansible-playbook -vv main.yml -i ../../inventory -e test_strategy=host_pinned | tee block_test.out
+env python -c \
+ 'import sys, re; sys.stdout.write(re.sub("\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]", "", sys.stdin.read()))' \
+ <block_test.out >block_test_wo_colors.out
+[ "$(grep -c 'TEST COMPLETE' block_test.out)" = "$(grep -E '^[0-9]+ plays in' block_test_wo_colors.out | cut -f1 -d' ')" ]
+
+# run test that includes tasks that fail inside a block with always
+rm -f block_test.out block_test_wo_colors.out
+ansible-playbook -vv block_fail.yml -i ../../inventory | tee block_test.out
+env python -c \
+ 'import sys, re; sys.stdout.write(re.sub("\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]", "", sys.stdin.read()))' \
+ <block_test.out >block_test_wo_colors.out
+[ "$(grep -c 'TEST COMPLETE' block_test.out)" = "$(grep -E '^[0-9]+ plays in' block_test_wo_colors.out | cut -f1 -d' ')" ]
+
+ansible-playbook -vv block_rescue_vars.yml
+
+# https://github.com/ansible/ansible/issues/70000
+set +e
+exit_code=0
+ansible-playbook -vv always_failure_with_rescue_rc.yml > rc_test.out || exit_code=$?
+set -e
+cat rc_test.out
+[ $exit_code -eq 2 ]
+[ "$(grep -c 'Failure in block' rc_test.out )" -eq 1 ]
+[ "$(grep -c 'Rescue' rc_test.out )" -eq 1 ]
+[ "$(grep -c 'Failure in always' rc_test.out )" -eq 1 ]
+[ "$(grep -c 'DID NOT RUN' rc_test.out )" -eq 0 ]
+rm -f rc_test_out
+
+set +e
+exit_code=0
+ansible-playbook -vv always_no_rescue_rc.yml > rc_test.out || exit_code=$?
+set -e
+cat rc_test.out
+[ $exit_code -eq 2 ]
+[ "$(grep -c 'Failure in block' rc_test.out )" -eq 1 ]
+[ "$(grep -c 'Always' rc_test.out )" -eq 1 ]
+[ "$(grep -c 'DID NOT RUN' rc_test.out )" -eq 0 ]
+rm -f rc_test.out
+
+set +e
+exit_code=0
+ansible-playbook -vv always_failure_no_rescue_rc.yml > rc_test.out || exit_code=$?
+set -e
+cat rc_test.out
+[ $exit_code -eq 2 ]
+[ "$(grep -c 'Failure in block' rc_test.out )" -eq 1 ]
+[ "$(grep -c 'Failure in always' rc_test.out )" -eq 1 ]
+[ "$(grep -c 'DID NOT RUN' rc_test.out )" -eq 0 ]
+rm -f rc_test.out
+
+# https://github.com/ansible/ansible/issues/29047
+ansible-playbook -vv issue29047.yml -i ../../inventory
+
+# https://github.com/ansible/ansible/issues/61253
+ansible-playbook -vv block_in_rescue.yml -i ../../inventory > rc_test.out
+cat rc_test.out
+[ "$(grep -c 'rescued=3' rc_test.out)" -eq 1 ]
+[ "$(grep -c 'failed=0' rc_test.out)" -eq 1 ]
+rm -f rc_test.out
+
+# https://github.com/ansible/ansible/issues/71306
+set +e
+exit_code=0
+ansible-playbook -i host1,host2 -vv issue71306.yml > rc_test.out || exit_code=$?
+set -e
+cat rc_test.out
+[ $exit_code -eq 0 ]
+rm -f rc_test_out
+
+# https://github.com/ansible/ansible/issues/69848
+ansible-playbook -i host1,host2 --tags foo -vv 69848.yml > role_complete_test.out
+cat role_complete_test.out
+[ "$(grep -c 'Tagged task' role_complete_test.out)" -eq 2 ]
+[ "$(grep -c 'Not tagged task' role_complete_test.out)" -eq 0 ]
+rm -f role_complete_test.out
+
+# test notify inheritance
+ansible-playbook inherit_notify.yml "$@"
+
+ansible-playbook unsafe_failed_task.yml "$@"
+
+ansible-playbook finalized_task.yml "$@"
+
+# https://github.com/ansible/ansible/issues/72725
+ansible-playbook -i host1,host2 -vv 72725.yml
+
+# https://github.com/ansible/ansible/issues/72781
+set +e
+ansible-playbook -i host1,host2 -vv 72781.yml > 72781.out
+set -e
+cat 72781.out
+[ "$(grep -c 'SHOULD NOT HAPPEN' 72781.out)" -eq 0 ]
+rm -f 72781.out
+
+set +e
+ansible-playbook -i host1,host2 -vv 78612.yml | tee 78612.out
+set -e
+[ "$(grep -c 'PASSED' 78612.out)" -eq 1 ]
+rm -f 78612.out
+
+ansible-playbook -vv 43191.yml
+ansible-playbook -vv 43191-2.yml
+
+# https://github.com/ansible/ansible/issues/79711
+set +e
+ANSIBLE_FORCE_HANDLERS=0 ansible-playbook -vv 79711.yml | tee 79711.out
+set -e
+[ "$(grep -c 'ok=5' 79711.out)" -eq 1 ]
+[ "$(grep -c 'failed=1' 79711.out)" -eq 1 ]
+[ "$(grep -c 'rescued=1' 79711.out)" -eq 1 ]
+rm -f 79711.out
diff --git a/test/integration/targets/blocks/unsafe_failed_task.yml b/test/integration/targets/blocks/unsafe_failed_task.yml
new file mode 100644
index 0000000..adfa492
--- /dev/null
+++ b/test/integration/targets/blocks/unsafe_failed_task.yml
@@ -0,0 +1,17 @@
+- hosts: localhost
+ gather_facts: false
+ vars:
+ - data: {}
+ tasks:
+ - block:
+ - name: template error
+ debug:
+ msg: "{{ data.value }}"
+ rescue:
+ - debug:
+ msg: "{{ ansible_failed_task.action }}"
+
+ - assert:
+ that:
+ - ansible_failed_task.name == "template error"
+ - ansible_failed_task.action == "debug"
diff --git a/test/integration/targets/builtin_vars_prompt/aliases b/test/integration/targets/builtin_vars_prompt/aliases
new file mode 100644
index 0000000..a4c82f5
--- /dev/null
+++ b/test/integration/targets/builtin_vars_prompt/aliases
@@ -0,0 +1,4 @@
+setup/always/setup_passlib
+setup/always/setup_pexpect
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/builtin_vars_prompt/runme.sh b/test/integration/targets/builtin_vars_prompt/runme.sh
new file mode 100755
index 0000000..af55579
--- /dev/null
+++ b/test/integration/targets/builtin_vars_prompt/runme.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# Interactively test vars_prompt
+python test-vars_prompt.py -i ../../inventory "$@"
diff --git a/test/integration/targets/builtin_vars_prompt/test-vars_prompt.py b/test/integration/targets/builtin_vars_prompt/test-vars_prompt.py
new file mode 100644
index 0000000..93958fc
--- /dev/null
+++ b/test/integration/targets/builtin_vars_prompt/test-vars_prompt.py
@@ -0,0 +1,130 @@
+#!/usr/bin/env python
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+import pexpect
+import sys
+
+from ansible.module_utils.six import PY2
+
+if PY2:
+ log_buffer = sys.stdout
+else:
+ log_buffer = sys.stdout.buffer
+
+env_vars = {
+ 'ANSIBLE_ROLES_PATH': './roles',
+ 'ANSIBLE_NOCOLOR': 'True',
+ 'ANSIBLE_RETRY_FILES_ENABLED': 'False',
+}
+
+
+def run_test(playbook, test_spec, args=None, timeout=10, env=None):
+
+ if not env:
+ env = os.environ.copy()
+ env.update(env_vars)
+
+ if not args:
+ args = sys.argv[1:]
+
+ vars_prompt_test = pexpect.spawn(
+ 'ansible-playbook',
+ args=[playbook] + args,
+ timeout=timeout,
+ env=env,
+ )
+
+ vars_prompt_test.logfile = log_buffer
+ for item in test_spec[0]:
+ vars_prompt_test.expect(item[0])
+ if item[1]:
+ vars_prompt_test.send(item[1])
+ vars_prompt_test.expect(test_spec[1])
+ vars_prompt_test.expect(pexpect.EOF)
+ vars_prompt_test.close()
+
+
+# These are the tests to run. Each test is a playbook and a test_spec.
+#
+# The test_spec is a list with two elements.
+#
+# The first element is a list of two element tuples. The first is the regexp to look
+# for in the output, the second is the line to send.
+#
+# The last element is the last string of text to look for in the output.
+#
+tests = [
+ # Basic vars_prompt
+ {'playbook': 'vars_prompt-1.yml',
+ 'test_spec': [
+ [('input:', 'some input\r')],
+ '"input": "some input"']},
+
+ # Custom prompt
+ {'playbook': 'vars_prompt-2.yml',
+ 'test_spec': [
+ [('Enter some input:', 'some more input\r')],
+ '"input": "some more input"']},
+
+ # Test confirm, both correct and incorrect
+ {'playbook': 'vars_prompt-3.yml',
+ 'test_spec': [
+ [('input:', 'confirm me\r'),
+ ('confirm input:', 'confirm me\r')],
+ '"input": "confirm me"']},
+
+ {'playbook': 'vars_prompt-3.yml',
+ 'test_spec': [
+ [('input:', 'confirm me\r'),
+ ('confirm input:', 'incorrect\r'),
+ (r'\*\*\*\*\* VALUES ENTERED DO NOT MATCH \*\*\*\*', ''),
+ ('input:', 'confirm me\r'),
+ ('confirm input:', 'confirm me\r')],
+ '"input": "confirm me"']},
+
+ # Test private
+ {'playbook': 'vars_prompt-4.yml',
+ 'test_spec': [
+ [('not_secret', 'this is displayed\r'),
+ ('this is displayed', '')],
+ '"not_secret": "this is displayed"']},
+
+ # Test hashing
+ {'playbook': 'vars_prompt-5.yml',
+ 'test_spec': [
+ [('password', 'Scenic-Improving-Payphone\r'),
+ ('confirm password', 'Scenic-Improving-Payphone\r')],
+ r'"password": "\$6\$']},
+
+ # Test variables in prompt field
+ # https://github.com/ansible/ansible/issues/32723
+ {'playbook': 'vars_prompt-6.yml',
+ 'test_spec': [
+ [('prompt from variable:', 'input\r')],
+ '']},
+
+ # Test play vars coming from vars_prompt
+ # https://github.com/ansible/ansible/issues/37984
+ {'playbook': 'vars_prompt-7.yml',
+ 'test_spec': [
+ [('prompting for host:', 'testhost\r')],
+ r'testhost.*ok=1']},
+
+ # Test play unsafe toggle
+ {'playbook': 'unsafe.yml',
+ 'test_spec': [
+ [('prompting for variable:', '{{whole}}\r')],
+ r'testhost.*ok=2']},
+
+ # Test unsupported keys
+ {'playbook': 'unsupported.yml',
+ 'test_spec': [
+ [],
+ "Invalid vars_prompt data structure, found unsupported key 'when'"]},
+]
+
+for t in tests:
+ run_test(playbook=t['playbook'], test_spec=t['test_spec'])
diff --git a/test/integration/targets/builtin_vars_prompt/unsafe.yml b/test/integration/targets/builtin_vars_prompt/unsafe.yml
new file mode 100644
index 0000000..348ce15
--- /dev/null
+++ b/test/integration/targets/builtin_vars_prompt/unsafe.yml
@@ -0,0 +1,20 @@
+- name: Test vars_prompt unsafe
+ hosts: testhost
+ become: no
+ gather_facts: no
+ vars:
+ whole: INVALID
+ vars_prompt:
+ - name: input
+ prompt: prompting for variable
+ unsafe: true
+
+ tasks:
+ - name:
+ assert:
+ that:
+ - input != whole
+ - input != 'INVALID'
+
+ - debug:
+ var: input
diff --git a/test/integration/targets/builtin_vars_prompt/unsupported.yml b/test/integration/targets/builtin_vars_prompt/unsupported.yml
new file mode 100644
index 0000000..eab02fd
--- /dev/null
+++ b/test/integration/targets/builtin_vars_prompt/unsupported.yml
@@ -0,0 +1,18 @@
+- name: Test vars_prompt unsupported key
+ hosts: testhost
+ become: no
+ gather_facts: no
+ vars_prompt:
+ - name: input
+ prompt: prompting for variable
+ # Unsupported key for vars_prompt
+ when: foo is defined
+
+ tasks:
+ - name:
+ assert:
+ that:
+ - input is not defined
+
+ - debug:
+ var: input
diff --git a/test/integration/targets/builtin_vars_prompt/vars_prompt-1.yml b/test/integration/targets/builtin_vars_prompt/vars_prompt-1.yml
new file mode 100644
index 0000000..727c60e
--- /dev/null
+++ b/test/integration/targets/builtin_vars_prompt/vars_prompt-1.yml
@@ -0,0 +1,15 @@
+- name: Basic vars_prompt test
+ hosts: testhost
+ become: no
+ gather_facts: no
+
+ vars_prompt:
+ - name: input
+
+ tasks:
+ - assert:
+ that:
+ - input == 'some input'
+
+ - debug:
+ var: input
diff --git a/test/integration/targets/builtin_vars_prompt/vars_prompt-2.yml b/test/integration/targets/builtin_vars_prompt/vars_prompt-2.yml
new file mode 100644
index 0000000..d8f20db
--- /dev/null
+++ b/test/integration/targets/builtin_vars_prompt/vars_prompt-2.yml
@@ -0,0 +1,16 @@
+- name: Test vars_prompt custom prompt
+ hosts: testhost
+ become: no
+ gather_facts: no
+
+ vars_prompt:
+ - name: input
+ prompt: "Enter some input"
+
+ tasks:
+ - assert:
+ that:
+ - input == 'some more input'
+
+ - debug:
+ var: input
diff --git a/test/integration/targets/builtin_vars_prompt/vars_prompt-3.yml b/test/integration/targets/builtin_vars_prompt/vars_prompt-3.yml
new file mode 100644
index 0000000..f814818
--- /dev/null
+++ b/test/integration/targets/builtin_vars_prompt/vars_prompt-3.yml
@@ -0,0 +1,17 @@
+- name: Test vars_prompt confirm
+ hosts: testhost
+ become: no
+ gather_facts: no
+
+ vars_prompt:
+ - name: input
+ confirm: yes
+
+ tasks:
+ - name:
+ assert:
+ that:
+ - input == 'confirm me'
+
+ - debug:
+ var: input
diff --git a/test/integration/targets/builtin_vars_prompt/vars_prompt-4.yml b/test/integration/targets/builtin_vars_prompt/vars_prompt-4.yml
new file mode 100644
index 0000000..d33cc90
--- /dev/null
+++ b/test/integration/targets/builtin_vars_prompt/vars_prompt-4.yml
@@ -0,0 +1,16 @@
+- name: Test vars_prompt not private
+ hosts: testhost
+ become: no
+ gather_facts: no
+
+ vars_prompt:
+ - name: not_secret
+ private: no
+
+ tasks:
+ - assert:
+ that:
+ - not_secret == 'this is displayed'
+
+ - debug:
+ var: not_secret
diff --git a/test/integration/targets/builtin_vars_prompt/vars_prompt-5.yml b/test/integration/targets/builtin_vars_prompt/vars_prompt-5.yml
new file mode 100644
index 0000000..62c8ad8
--- /dev/null
+++ b/test/integration/targets/builtin_vars_prompt/vars_prompt-5.yml
@@ -0,0 +1,14 @@
+- name: Test vars_prompt hashing
+ hosts: testhost
+ become: no
+ gather_facts: no
+
+ vars_prompt:
+ - name: password
+ confirm: yes
+ encrypt: sha512_crypt
+ salt: 'jESIyad4F08hP3Ta'
+
+ tasks:
+ - debug:
+ var: password
diff --git a/test/integration/targets/builtin_vars_prompt/vars_prompt-6.yml b/test/integration/targets/builtin_vars_prompt/vars_prompt-6.yml
new file mode 100644
index 0000000..ea3fe62
--- /dev/null
+++ b/test/integration/targets/builtin_vars_prompt/vars_prompt-6.yml
@@ -0,0 +1,20 @@
+- name: Test vars_prompt custom variables in prompt
+ hosts: testhost
+ become: no
+ gather_facts: no
+
+ vars:
+ prompt_var: prompt from variable
+
+ vars_prompt:
+ - name: input
+ prompt: "{{ prompt_var }}"
+
+ tasks:
+ - name:
+ assert:
+ that:
+ - input == 'input'
+
+ - debug:
+ var: input
diff --git a/test/integration/targets/builtin_vars_prompt/vars_prompt-7.yml b/test/integration/targets/builtin_vars_prompt/vars_prompt-7.yml
new file mode 100644
index 0000000..a6b086d
--- /dev/null
+++ b/test/integration/targets/builtin_vars_prompt/vars_prompt-7.yml
@@ -0,0 +1,12 @@
+- name: Test vars_prompt play vars
+ hosts: "{{ target_hosts }}"
+ become: no
+ gather_facts: no
+
+ vars_prompt:
+ - name: target_hosts
+ prompt: prompting for host
+ private: no
+
+ tasks:
+ - ping:
diff --git a/test/integration/targets/callback_default/aliases b/test/integration/targets/callback_default/aliases
new file mode 100644
index 0000000..b598321
--- /dev/null
+++ b/test/integration/targets/callback_default/aliases
@@ -0,0 +1 @@
+shippable/posix/group3
diff --git a/test/integration/targets/callback_default/callback_default.out.check_markers_dry.stderr b/test/integration/targets/callback_default/callback_default.out.check_markers_dry.stderr
new file mode 100644
index 0000000..431a020
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.check_markers_dry.stderr
@@ -0,0 +1,2 @@
++ ansible-playbook -i inventory --check test_dryrun.yml
+++ set +x
diff --git a/test/integration/targets/callback_default/callback_default.out.check_markers_dry.stdout b/test/integration/targets/callback_default/callback_default.out.check_markers_dry.stdout
new file mode 100644
index 0000000..8a34909
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.check_markers_dry.stdout
@@ -0,0 +1,78 @@
+
+DRY RUN ************************************************************************
+
+PLAY [A common play] [CHECK MODE] **********************************************
+
+TASK [debug] [CHECK MODE] ******************************************************
+ok: [testhost] => {
+ "msg": "ansible_check_mode: True"
+}
+
+TASK [Command] [CHECK MODE] ****************************************************
+skipping: [testhost]
+
+TASK [Command with check_mode: false] ******************************************
+changed: [testhost]
+
+TASK [Command with check_mode: true] [CHECK MODE] ******************************
+skipping: [testhost]
+
+PLAY [Play with check_mode: true (runs always in check_mode)] [CHECK MODE] *****
+
+TASK [debug] [CHECK MODE] ******************************************************
+ok: [testhost] => {
+ "msg": "ansible_check_mode: True"
+}
+
+TASK [Command] [CHECK MODE] ****************************************************
+skipping: [testhost]
+
+TASK [Command with check_mode: false] ******************************************
+changed: [testhost]
+
+TASK [Command with check_mode: true] [CHECK MODE] ******************************
+skipping: [testhost]
+
+PLAY [Play with check_mode: false (runs always in wet mode)] *******************
+
+TASK [debug] *******************************************************************
+ok: [testhost] => {
+ "msg": "ansible_check_mode: True"
+}
+
+TASK [Command] *****************************************************************
+changed: [testhost]
+
+TASK [Command with check_mode: false] ******************************************
+changed: [testhost]
+
+TASK [Command with check_mode: true] [CHECK MODE] ******************************
+skipping: [testhost]
+
+PLAY [Play with a block with check_mode: true] [CHECK MODE] ********************
+
+TASK [Command] [CHECK MODE] ****************************************************
+skipping: [testhost]
+
+TASK [Command with check_mode: false] ******************************************
+changed: [testhost]
+
+TASK [Command with check_mode: true] [CHECK MODE] ******************************
+skipping: [testhost]
+
+PLAY [Play with a block with check_mode: false] [CHECK MODE] *******************
+
+TASK [Command] *****************************************************************
+changed: [testhost]
+
+TASK [Command with check_mode: false] ******************************************
+changed: [testhost]
+
+TASK [Command with check_mode: true] [CHECK MODE] ******************************
+skipping: [testhost]
+
+PLAY RECAP *********************************************************************
+testhost : ok=10 changed=7 unreachable=0 failed=0 skipped=8 rescued=0 ignored=0
+
+
+DRY RUN ************************************************************************
diff --git a/test/integration/targets/callback_default/callback_default.out.check_markers_wet.stderr b/test/integration/targets/callback_default/callback_default.out.check_markers_wet.stderr
new file mode 100644
index 0000000..e430942
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.check_markers_wet.stderr
@@ -0,0 +1,2 @@
++ ansible-playbook -i inventory test_dryrun.yml
+++ set +x
diff --git a/test/integration/targets/callback_default/callback_default.out.check_markers_wet.stdout b/test/integration/targets/callback_default/callback_default.out.check_markers_wet.stdout
new file mode 100644
index 0000000..f5f4510
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.check_markers_wet.stdout
@@ -0,0 +1,74 @@
+
+PLAY [A common play] ***********************************************************
+
+TASK [debug] *******************************************************************
+ok: [testhost] => {
+ "msg": "ansible_check_mode: False"
+}
+
+TASK [Command] *****************************************************************
+changed: [testhost]
+
+TASK [Command with check_mode: false] ******************************************
+changed: [testhost]
+
+TASK [Command with check_mode: true] [CHECK MODE] ******************************
+skipping: [testhost]
+
+PLAY [Play with check_mode: true (runs always in check_mode)] [CHECK MODE] *****
+
+TASK [debug] [CHECK MODE] ******************************************************
+ok: [testhost] => {
+ "msg": "ansible_check_mode: False"
+}
+
+TASK [Command] [CHECK MODE] ****************************************************
+skipping: [testhost]
+
+TASK [Command with check_mode: false] ******************************************
+changed: [testhost]
+
+TASK [Command with check_mode: true] [CHECK MODE] ******************************
+skipping: [testhost]
+
+PLAY [Play with check_mode: false (runs always in wet mode)] *******************
+
+TASK [debug] *******************************************************************
+ok: [testhost] => {
+ "msg": "ansible_check_mode: False"
+}
+
+TASK [Command] *****************************************************************
+changed: [testhost]
+
+TASK [Command with check_mode: false] ******************************************
+changed: [testhost]
+
+TASK [Command with check_mode: true] [CHECK MODE] ******************************
+skipping: [testhost]
+
+PLAY [Play with a block with check_mode: true] *********************************
+
+TASK [Command] [CHECK MODE] ****************************************************
+skipping: [testhost]
+
+TASK [Command with check_mode: false] ******************************************
+changed: [testhost]
+
+TASK [Command with check_mode: true] [CHECK MODE] ******************************
+skipping: [testhost]
+
+PLAY [Play with a block with check_mode: false] ********************************
+
+TASK [Command] *****************************************************************
+changed: [testhost]
+
+TASK [Command with check_mode: false] ******************************************
+changed: [testhost]
+
+TASK [Command with check_mode: true] [CHECK MODE] ******************************
+skipping: [testhost]
+
+PLAY RECAP *********************************************************************
+testhost : ok=11 changed=8 unreachable=0 failed=0 skipped=7 rescued=0 ignored=0
+
diff --git a/test/integration/targets/callback_default/callback_default.out.check_nomarkers_dry.stderr b/test/integration/targets/callback_default/callback_default.out.check_nomarkers_dry.stderr
new file mode 100644
index 0000000..431a020
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.check_nomarkers_dry.stderr
@@ -0,0 +1,2 @@
++ ansible-playbook -i inventory --check test_dryrun.yml
+++ set +x
diff --git a/test/integration/targets/callback_default/callback_default.out.check_nomarkers_dry.stdout b/test/integration/targets/callback_default/callback_default.out.check_nomarkers_dry.stdout
new file mode 100644
index 0000000..e984d49
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.check_nomarkers_dry.stdout
@@ -0,0 +1,74 @@
+
+PLAY [A common play] ***********************************************************
+
+TASK [debug] *******************************************************************
+ok: [testhost] => {
+ "msg": "ansible_check_mode: True"
+}
+
+TASK [Command] *****************************************************************
+skipping: [testhost]
+
+TASK [Command with check_mode: false] ******************************************
+changed: [testhost]
+
+TASK [Command with check_mode: true] *******************************************
+skipping: [testhost]
+
+PLAY [Play with check_mode: true (runs always in check_mode)] ******************
+
+TASK [debug] *******************************************************************
+ok: [testhost] => {
+ "msg": "ansible_check_mode: True"
+}
+
+TASK [Command] *****************************************************************
+skipping: [testhost]
+
+TASK [Command with check_mode: false] ******************************************
+changed: [testhost]
+
+TASK [Command with check_mode: true] *******************************************
+skipping: [testhost]
+
+PLAY [Play with check_mode: false (runs always in wet mode)] *******************
+
+TASK [debug] *******************************************************************
+ok: [testhost] => {
+ "msg": "ansible_check_mode: True"
+}
+
+TASK [Command] *****************************************************************
+changed: [testhost]
+
+TASK [Command with check_mode: false] ******************************************
+changed: [testhost]
+
+TASK [Command with check_mode: true] *******************************************
+skipping: [testhost]
+
+PLAY [Play with a block with check_mode: true] *********************************
+
+TASK [Command] *****************************************************************
+skipping: [testhost]
+
+TASK [Command with check_mode: false] ******************************************
+changed: [testhost]
+
+TASK [Command with check_mode: true] *******************************************
+skipping: [testhost]
+
+PLAY [Play with a block with check_mode: false] ********************************
+
+TASK [Command] *****************************************************************
+changed: [testhost]
+
+TASK [Command with check_mode: false] ******************************************
+changed: [testhost]
+
+TASK [Command with check_mode: true] *******************************************
+skipping: [testhost]
+
+PLAY RECAP *********************************************************************
+testhost : ok=10 changed=7 unreachable=0 failed=0 skipped=8 rescued=0 ignored=0
+
diff --git a/test/integration/targets/callback_default/callback_default.out.check_nomarkers_wet.stderr b/test/integration/targets/callback_default/callback_default.out.check_nomarkers_wet.stderr
new file mode 100644
index 0000000..e430942
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.check_nomarkers_wet.stderr
@@ -0,0 +1,2 @@
++ ansible-playbook -i inventory test_dryrun.yml
+++ set +x
diff --git a/test/integration/targets/callback_default/callback_default.out.check_nomarkers_wet.stdout b/test/integration/targets/callback_default/callback_default.out.check_nomarkers_wet.stdout
new file mode 100644
index 0000000..2b331bb
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.check_nomarkers_wet.stdout
@@ -0,0 +1,74 @@
+
+PLAY [A common play] ***********************************************************
+
+TASK [debug] *******************************************************************
+ok: [testhost] => {
+ "msg": "ansible_check_mode: False"
+}
+
+TASK [Command] *****************************************************************
+changed: [testhost]
+
+TASK [Command with check_mode: false] ******************************************
+changed: [testhost]
+
+TASK [Command with check_mode: true] *******************************************
+skipping: [testhost]
+
+PLAY [Play with check_mode: true (runs always in check_mode)] ******************
+
+TASK [debug] *******************************************************************
+ok: [testhost] => {
+ "msg": "ansible_check_mode: False"
+}
+
+TASK [Command] *****************************************************************
+skipping: [testhost]
+
+TASK [Command with check_mode: false] ******************************************
+changed: [testhost]
+
+TASK [Command with check_mode: true] *******************************************
+skipping: [testhost]
+
+PLAY [Play with check_mode: false (runs always in wet mode)] *******************
+
+TASK [debug] *******************************************************************
+ok: [testhost] => {
+ "msg": "ansible_check_mode: False"
+}
+
+TASK [Command] *****************************************************************
+changed: [testhost]
+
+TASK [Command with check_mode: false] ******************************************
+changed: [testhost]
+
+TASK [Command with check_mode: true] *******************************************
+skipping: [testhost]
+
+PLAY [Play with a block with check_mode: true] *********************************
+
+TASK [Command] *****************************************************************
+skipping: [testhost]
+
+TASK [Command with check_mode: false] ******************************************
+changed: [testhost]
+
+TASK [Command with check_mode: true] *******************************************
+skipping: [testhost]
+
+PLAY [Play with a block with check_mode: false] ********************************
+
+TASK [Command] *****************************************************************
+changed: [testhost]
+
+TASK [Command with check_mode: false] ******************************************
+changed: [testhost]
+
+TASK [Command with check_mode: true] *******************************************
+skipping: [testhost]
+
+PLAY RECAP *********************************************************************
+testhost : ok=11 changed=8 unreachable=0 failed=0 skipped=7 rescued=0 ignored=0
+
diff --git a/test/integration/targets/callback_default/callback_default.out.default.stderr b/test/integration/targets/callback_default/callback_default.out.default.stderr
new file mode 100644
index 0000000..d3e07d4
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.default.stderr
@@ -0,0 +1,2 @@
++ ansible-playbook -i inventory test.yml
+++ set +x
diff --git a/test/integration/targets/callback_default/callback_default.out.default.stdout b/test/integration/targets/callback_default/callback_default.out.default.stdout
new file mode 100644
index 0000000..5502b34
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.default.stdout
@@ -0,0 +1,108 @@
+
+PLAY [testhost] ****************************************************************
+
+TASK [Changed task] ************************************************************
+changed: [testhost]
+
+TASK [Ok task] *****************************************************************
+ok: [testhost]
+
+TASK [Failed task] *************************************************************
+fatal: [testhost]: FAILED! => {"changed": false, "msg": "no reason"}
+...ignoring
+
+TASK [Skipped task] ************************************************************
+skipping: [testhost]
+
+TASK [Task with var in name (foo bar)] *****************************************
+changed: [testhost]
+
+TASK [Loop task] ***************************************************************
+changed: [testhost] => (item=foo-1)
+changed: [testhost] => (item=foo-2)
+changed: [testhost] => (item=foo-3)
+
+TASK [debug loop] **************************************************************
+changed: [testhost] => (item=debug-1) => {
+ "msg": "debug-1"
+}
+failed: [testhost] (item=debug-2) => {
+ "msg": "debug-2"
+}
+ok: [testhost] => (item=debug-3) => {
+ "msg": "debug-3"
+}
+skipping: [testhost] => (item=debug-4)
+fatal: [testhost]: FAILED! => {"msg": "One or more items failed"}
+...ignoring
+
+TASK [EXPECTED FAILURE Failed task to be rescued] ******************************
+fatal: [testhost]: FAILED! => {"changed": false, "msg": "Failed as requested from task"}
+
+TASK [Rescue task] *************************************************************
+changed: [testhost]
+
+TASK [include_tasks] ***********************************************************
+included: .../test/integration/targets/callback_default/include_me.yml for testhost => (item=1)
+
+TASK [debug] *******************************************************************
+ok: [testhost] => {
+ "item": 1
+}
+
+TASK [copy] ********************************************************************
+changed: [testhost]
+
+TASK [replace] *****************************************************************
+--- before: .../test_diff.txt
++++ after: .../test_diff.txt
+@@ -1 +1 @@
+-foo
+\ No newline at end of file
++bar
+\ No newline at end of file
+
+changed: [testhost]
+
+TASK [replace] *****************************************************************
+ok: [testhost]
+
+TASK [debug] *******************************************************************
+skipping: [testhost]
+
+TASK [debug] *******************************************************************
+skipping: [testhost]
+
+TASK [debug] *******************************************************************
+skipping: [testhost] => (item=1)
+skipping: [testhost] => (item=2)
+skipping: [testhost]
+
+RUNNING HANDLER [Test handler 1] ***********************************************
+changed: [testhost]
+
+RUNNING HANDLER [Test handler 2] ***********************************************
+ok: [testhost]
+
+RUNNING HANDLER [Test handler 3] ***********************************************
+changed: [testhost]
+
+PLAY [testhost] ****************************************************************
+
+TASK [First free task] *********************************************************
+changed: [testhost]
+
+TASK [Second free task] ********************************************************
+changed: [testhost]
+
+TASK [Include some tasks] ******************************************************
+included: .../test/integration/targets/callback_default/include_me.yml for testhost => (item=1)
+
+TASK [debug] *******************************************************************
+ok: [testhost] => {
+ "item": 1
+}
+
+PLAY RECAP *********************************************************************
+testhost : ok=19 changed=11 unreachable=0 failed=0 skipped=4 rescued=1 ignored=2
+
diff --git a/test/integration/targets/callback_default/callback_default.out.display_path_on_failure.stderr b/test/integration/targets/callback_default/callback_default.out.display_path_on_failure.stderr
new file mode 100644
index 0000000..d3e07d4
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.display_path_on_failure.stderr
@@ -0,0 +1,2 @@
++ ansible-playbook -i inventory test.yml
+++ set +x
diff --git a/test/integration/targets/callback_default/callback_default.out.display_path_on_failure.stdout b/test/integration/targets/callback_default/callback_default.out.display_path_on_failure.stdout
new file mode 100644
index 0000000..22f3f51
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.display_path_on_failure.stdout
@@ -0,0 +1,111 @@
+
+PLAY [testhost] ****************************************************************
+
+TASK [Changed task] ************************************************************
+changed: [testhost]
+
+TASK [Ok task] *****************************************************************
+ok: [testhost]
+
+TASK [Failed task] *************************************************************
+task path: TEST_PATH/test.yml:16
+fatal: [testhost]: FAILED! => {"changed": false, "msg": "no reason"}
+...ignoring
+
+TASK [Skipped task] ************************************************************
+skipping: [testhost]
+
+TASK [Task with var in name (foo bar)] *****************************************
+changed: [testhost]
+
+TASK [Loop task] ***************************************************************
+changed: [testhost] => (item=foo-1)
+changed: [testhost] => (item=foo-2)
+changed: [testhost] => (item=foo-3)
+
+TASK [debug loop] **************************************************************
+changed: [testhost] => (item=debug-1) => {
+ "msg": "debug-1"
+}
+failed: [testhost] (item=debug-2) => {
+ "msg": "debug-2"
+}
+ok: [testhost] => (item=debug-3) => {
+ "msg": "debug-3"
+}
+skipping: [testhost] => (item=debug-4)
+task path: TEST_PATH/test.yml:38
+fatal: [testhost]: FAILED! => {"msg": "One or more items failed"}
+...ignoring
+
+TASK [EXPECTED FAILURE Failed task to be rescued] ******************************
+task path: TEST_PATH/test.yml:54
+fatal: [testhost]: FAILED! => {"changed": false, "msg": "Failed as requested from task"}
+
+TASK [Rescue task] *************************************************************
+changed: [testhost]
+
+TASK [include_tasks] ***********************************************************
+included: .../test/integration/targets/callback_default/include_me.yml for testhost => (item=1)
+
+TASK [debug] *******************************************************************
+ok: [testhost] => {
+ "item": 1
+}
+
+TASK [copy] ********************************************************************
+changed: [testhost]
+
+TASK [replace] *****************************************************************
+--- before: .../test_diff.txt
++++ after: .../test_diff.txt
+@@ -1 +1 @@
+-foo
+\ No newline at end of file
++bar
+\ No newline at end of file
+
+changed: [testhost]
+
+TASK [replace] *****************************************************************
+ok: [testhost]
+
+TASK [debug] *******************************************************************
+skipping: [testhost]
+
+TASK [debug] *******************************************************************
+skipping: [testhost]
+
+TASK [debug] *******************************************************************
+skipping: [testhost] => (item=1)
+skipping: [testhost] => (item=2)
+skipping: [testhost]
+
+RUNNING HANDLER [Test handler 1] ***********************************************
+changed: [testhost]
+
+RUNNING HANDLER [Test handler 2] ***********************************************
+ok: [testhost]
+
+RUNNING HANDLER [Test handler 3] ***********************************************
+changed: [testhost]
+
+PLAY [testhost] ****************************************************************
+
+TASK [First free task] *********************************************************
+changed: [testhost]
+
+TASK [Second free task] ********************************************************
+changed: [testhost]
+
+TASK [Include some tasks] ******************************************************
+included: .../test/integration/targets/callback_default/include_me.yml for testhost => (item=1)
+
+TASK [debug] *******************************************************************
+ok: [testhost] => {
+ "item": 1
+}
+
+PLAY RECAP *********************************************************************
+testhost : ok=19 changed=11 unreachable=0 failed=0 skipped=4 rescued=1 ignored=2
+
diff --git a/test/integration/targets/callback_default/callback_default.out.failed_to_stderr.stderr b/test/integration/targets/callback_default/callback_default.out.failed_to_stderr.stderr
new file mode 100644
index 0000000..9a39d78
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.failed_to_stderr.stderr
@@ -0,0 +1,8 @@
++ ansible-playbook -i inventory test.yml
+++ set +x
+fatal: [testhost]: FAILED! => {"changed": false, "msg": "no reason"}
+failed: [testhost] (item=debug-2) => {
+ "msg": "debug-2"
+}
+fatal: [testhost]: FAILED! => {"msg": "One or more items failed"}
+fatal: [testhost]: FAILED! => {"changed": false, "msg": "Failed as requested from task"}
diff --git a/test/integration/targets/callback_default/callback_default.out.failed_to_stderr.stdout b/test/integration/targets/callback_default/callback_default.out.failed_to_stderr.stdout
new file mode 100644
index 0000000..87af5be
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.failed_to_stderr.stdout
@@ -0,0 +1,102 @@
+
+PLAY [testhost] ****************************************************************
+
+TASK [Changed task] ************************************************************
+changed: [testhost]
+
+TASK [Ok task] *****************************************************************
+ok: [testhost]
+
+TASK [Failed task] *************************************************************
+...ignoring
+
+TASK [Skipped task] ************************************************************
+skipping: [testhost]
+
+TASK [Task with var in name (foo bar)] *****************************************
+changed: [testhost]
+
+TASK [Loop task] ***************************************************************
+changed: [testhost] => (item=foo-1)
+changed: [testhost] => (item=foo-2)
+changed: [testhost] => (item=foo-3)
+
+TASK [debug loop] **************************************************************
+changed: [testhost] => (item=debug-1) => {
+ "msg": "debug-1"
+}
+ok: [testhost] => (item=debug-3) => {
+ "msg": "debug-3"
+}
+skipping: [testhost] => (item=debug-4)
+...ignoring
+
+TASK [EXPECTED FAILURE Failed task to be rescued] ******************************
+
+TASK [Rescue task] *************************************************************
+changed: [testhost]
+
+TASK [include_tasks] ***********************************************************
+included: .../test/integration/targets/callback_default/include_me.yml for testhost => (item=1)
+
+TASK [debug] *******************************************************************
+ok: [testhost] => {
+ "item": 1
+}
+
+TASK [copy] ********************************************************************
+changed: [testhost]
+
+TASK [replace] *****************************************************************
+--- before: .../test_diff.txt
++++ after: .../test_diff.txt
+@@ -1 +1 @@
+-foo
+\ No newline at end of file
++bar
+\ No newline at end of file
+
+changed: [testhost]
+
+TASK [replace] *****************************************************************
+ok: [testhost]
+
+TASK [debug] *******************************************************************
+skipping: [testhost]
+
+TASK [debug] *******************************************************************
+skipping: [testhost]
+
+TASK [debug] *******************************************************************
+skipping: [testhost] => (item=1)
+skipping: [testhost] => (item=2)
+skipping: [testhost]
+
+RUNNING HANDLER [Test handler 1] ***********************************************
+changed: [testhost]
+
+RUNNING HANDLER [Test handler 2] ***********************************************
+ok: [testhost]
+
+RUNNING HANDLER [Test handler 3] ***********************************************
+changed: [testhost]
+
+PLAY [testhost] ****************************************************************
+
+TASK [First free task] *********************************************************
+changed: [testhost]
+
+TASK [Second free task] ********************************************************
+changed: [testhost]
+
+TASK [Include some tasks] ******************************************************
+included: .../test/integration/targets/callback_default/include_me.yml for testhost => (item=1)
+
+TASK [debug] *******************************************************************
+ok: [testhost] => {
+ "item": 1
+}
+
+PLAY RECAP *********************************************************************
+testhost : ok=19 changed=11 unreachable=0 failed=0 skipped=4 rescued=1 ignored=2
+
diff --git a/test/integration/targets/callback_default/callback_default.out.fqcn_free.stdout b/test/integration/targets/callback_default/callback_default.out.fqcn_free.stdout
new file mode 100644
index 0000000..0ec0447
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.fqcn_free.stdout
@@ -0,0 +1,35 @@
+
+PLAY [nonlockstep] *************************************************************
+
+TASK [command] *****************************************************************
+changed: [testhost10]
+
+TASK [command] *****************************************************************
+changed: [testhost10]
+
+TASK [command] *****************************************************************
+changed: [testhost10]
+
+TASK [command] *****************************************************************
+changed: [testhost11]
+
+TASK [command] *****************************************************************
+changed: [testhost11]
+
+TASK [command] *****************************************************************
+changed: [testhost11]
+
+TASK [command] *****************************************************************
+changed: [testhost12]
+
+TASK [command] *****************************************************************
+changed: [testhost12]
+
+TASK [command] *****************************************************************
+changed: [testhost12]
+
+PLAY RECAP *********************************************************************
+testhost10 : ok=3 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
+testhost11 : ok=3 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
+testhost12 : ok=3 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
+
diff --git a/test/integration/targets/callback_default/callback_default.out.free.stdout b/test/integration/targets/callback_default/callback_default.out.free.stdout
new file mode 100644
index 0000000..0ec0447
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.free.stdout
@@ -0,0 +1,35 @@
+
+PLAY [nonlockstep] *************************************************************
+
+TASK [command] *****************************************************************
+changed: [testhost10]
+
+TASK [command] *****************************************************************
+changed: [testhost10]
+
+TASK [command] *****************************************************************
+changed: [testhost10]
+
+TASK [command] *****************************************************************
+changed: [testhost11]
+
+TASK [command] *****************************************************************
+changed: [testhost11]
+
+TASK [command] *****************************************************************
+changed: [testhost11]
+
+TASK [command] *****************************************************************
+changed: [testhost12]
+
+TASK [command] *****************************************************************
+changed: [testhost12]
+
+TASK [command] *****************************************************************
+changed: [testhost12]
+
+PLAY RECAP *********************************************************************
+testhost10 : ok=3 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
+testhost11 : ok=3 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
+testhost12 : ok=3 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
+
diff --git a/test/integration/targets/callback_default/callback_default.out.hide_ok.stderr b/test/integration/targets/callback_default/callback_default.out.hide_ok.stderr
new file mode 100644
index 0000000..d3e07d4
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.hide_ok.stderr
@@ -0,0 +1,2 @@
++ ansible-playbook -i inventory test.yml
+++ set +x
diff --git a/test/integration/targets/callback_default/callback_default.out.hide_ok.stdout b/test/integration/targets/callback_default/callback_default.out.hide_ok.stdout
new file mode 100644
index 0000000..3921f73
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.hide_ok.stdout
@@ -0,0 +1,86 @@
+
+PLAY [testhost] ****************************************************************
+
+TASK [Changed task] ************************************************************
+changed: [testhost]
+
+TASK [Failed task] *************************************************************
+fatal: [testhost]: FAILED! => {"changed": false, "msg": "no reason"}
+...ignoring
+
+TASK [Skipped task] ************************************************************
+skipping: [testhost]
+
+TASK [Task with var in name (foo bar)] *****************************************
+changed: [testhost]
+
+TASK [Loop task] ***************************************************************
+changed: [testhost] => (item=foo-1)
+changed: [testhost] => (item=foo-2)
+changed: [testhost] => (item=foo-3)
+
+TASK [debug loop] **************************************************************
+changed: [testhost] => (item=debug-1) => {
+ "msg": "debug-1"
+}
+failed: [testhost] (item=debug-2) => {
+ "msg": "debug-2"
+}
+skipping: [testhost] => (item=debug-4)
+fatal: [testhost]: FAILED! => {"msg": "One or more items failed"}
+...ignoring
+
+TASK [EXPECTED FAILURE Failed task to be rescued] ******************************
+fatal: [testhost]: FAILED! => {"changed": false, "msg": "Failed as requested from task"}
+
+TASK [Rescue task] *************************************************************
+changed: [testhost]
+
+TASK [include_tasks] ***********************************************************
+included: .../test/integration/targets/callback_default/include_me.yml for testhost => (item=1)
+
+TASK [copy] ********************************************************************
+changed: [testhost]
+
+TASK [replace] *****************************************************************
+--- before: .../test_diff.txt
++++ after: .../test_diff.txt
+@@ -1 +1 @@
+-foo
+\ No newline at end of file
++bar
+\ No newline at end of file
+
+changed: [testhost]
+
+TASK [debug] *******************************************************************
+skipping: [testhost]
+
+TASK [debug] *******************************************************************
+skipping: [testhost]
+
+TASK [debug] *******************************************************************
+skipping: [testhost] => (item=1)
+skipping: [testhost] => (item=2)
+skipping: [testhost]
+
+RUNNING HANDLER [Test handler 1] ***********************************************
+changed: [testhost]
+
+RUNNING HANDLER [Test handler 3] ***********************************************
+changed: [testhost]
+
+PLAY [testhost] ****************************************************************
+
+TASK [First free task] *********************************************************
+changed: [testhost]
+
+TASK [Second free task] ********************************************************
+changed: [testhost]
+
+TASK [Include some tasks] ******************************************************
+included: .../test/integration/targets/callback_default/include_me.yml for testhost => (item=1)
+
+PLAY RECAP *********************************************************************
+testhost : ok=19 changed=11 unreachable=0 failed=0 skipped=4 rescued=1 ignored=2
+
diff --git a/test/integration/targets/callback_default/callback_default.out.hide_skipped.stderr b/test/integration/targets/callback_default/callback_default.out.hide_skipped.stderr
new file mode 100644
index 0000000..d3e07d4
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.hide_skipped.stderr
@@ -0,0 +1,2 @@
++ ansible-playbook -i inventory test.yml
+++ set +x
diff --git a/test/integration/targets/callback_default/callback_default.out.hide_skipped.stdout b/test/integration/targets/callback_default/callback_default.out.hide_skipped.stdout
new file mode 100644
index 0000000..b291869
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.hide_skipped.stdout
@@ -0,0 +1,93 @@
+
+PLAY [testhost] ****************************************************************
+
+TASK [Changed task] ************************************************************
+changed: [testhost]
+
+TASK [Ok task] *****************************************************************
+ok: [testhost]
+
+TASK [Failed task] *************************************************************
+fatal: [testhost]: FAILED! => {"changed": false, "msg": "no reason"}
+...ignoring
+
+TASK [Task with var in name (foo bar)] *****************************************
+changed: [testhost]
+
+TASK [Loop task] ***************************************************************
+changed: [testhost] => (item=foo-1)
+changed: [testhost] => (item=foo-2)
+changed: [testhost] => (item=foo-3)
+
+TASK [debug loop] **************************************************************
+changed: [testhost] => (item=debug-1) => {
+ "msg": "debug-1"
+}
+failed: [testhost] (item=debug-2) => {
+ "msg": "debug-2"
+}
+ok: [testhost] => (item=debug-3) => {
+ "msg": "debug-3"
+}
+fatal: [testhost]: FAILED! => {"msg": "One or more items failed"}
+...ignoring
+
+TASK [EXPECTED FAILURE Failed task to be rescued] ******************************
+fatal: [testhost]: FAILED! => {"changed": false, "msg": "Failed as requested from task"}
+
+TASK [Rescue task] *************************************************************
+changed: [testhost]
+
+TASK [include_tasks] ***********************************************************
+included: .../test/integration/targets/callback_default/include_me.yml for testhost => (item=1)
+
+TASK [debug] *******************************************************************
+ok: [testhost] => {
+ "item": 1
+}
+
+TASK [copy] ********************************************************************
+changed: [testhost]
+
+TASK [replace] *****************************************************************
+--- before: .../test_diff.txt
++++ after: .../test_diff.txt
+@@ -1 +1 @@
+-foo
+\ No newline at end of file
++bar
+\ No newline at end of file
+
+changed: [testhost]
+
+TASK [replace] *****************************************************************
+ok: [testhost]
+
+RUNNING HANDLER [Test handler 1] ***********************************************
+changed: [testhost]
+
+RUNNING HANDLER [Test handler 2] ***********************************************
+ok: [testhost]
+
+RUNNING HANDLER [Test handler 3] ***********************************************
+changed: [testhost]
+
+PLAY [testhost] ****************************************************************
+
+TASK [First free task] *********************************************************
+changed: [testhost]
+
+TASK [Second free task] ********************************************************
+changed: [testhost]
+
+TASK [Include some tasks] ******************************************************
+included: .../test/integration/targets/callback_default/include_me.yml for testhost => (item=1)
+
+TASK [debug] *******************************************************************
+ok: [testhost] => {
+ "item": 1
+}
+
+PLAY RECAP *********************************************************************
+testhost : ok=19 changed=11 unreachable=0 failed=0 skipped=4 rescued=1 ignored=2
+
diff --git a/test/integration/targets/callback_default/callback_default.out.hide_skipped_ok.stderr b/test/integration/targets/callback_default/callback_default.out.hide_skipped_ok.stderr
new file mode 100644
index 0000000..d3e07d4
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.hide_skipped_ok.stderr
@@ -0,0 +1,2 @@
++ ansible-playbook -i inventory test.yml
+++ set +x
diff --git a/test/integration/targets/callback_default/callback_default.out.hide_skipped_ok.stdout b/test/integration/targets/callback_default/callback_default.out.hide_skipped_ok.stdout
new file mode 100644
index 0000000..e3eb937
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.hide_skipped_ok.stdout
@@ -0,0 +1,71 @@
+
+PLAY [testhost] ****************************************************************
+
+TASK [Changed task] ************************************************************
+changed: [testhost]
+
+TASK [Failed task] *************************************************************
+fatal: [testhost]: FAILED! => {"changed": false, "msg": "no reason"}
+...ignoring
+
+TASK [Task with var in name (foo bar)] *****************************************
+changed: [testhost]
+
+TASK [Loop task] ***************************************************************
+changed: [testhost] => (item=foo-1)
+changed: [testhost] => (item=foo-2)
+changed: [testhost] => (item=foo-3)
+
+TASK [debug loop] **************************************************************
+changed: [testhost] => (item=debug-1) => {
+ "msg": "debug-1"
+}
+failed: [testhost] (item=debug-2) => {
+ "msg": "debug-2"
+}
+fatal: [testhost]: FAILED! => {"msg": "One or more items failed"}
+...ignoring
+
+TASK [EXPECTED FAILURE Failed task to be rescued] ******************************
+fatal: [testhost]: FAILED! => {"changed": false, "msg": "Failed as requested from task"}
+
+TASK [Rescue task] *************************************************************
+changed: [testhost]
+
+TASK [include_tasks] ***********************************************************
+included: .../test/integration/targets/callback_default/include_me.yml for testhost => (item=1)
+
+TASK [copy] ********************************************************************
+changed: [testhost]
+
+TASK [replace] *****************************************************************
+--- before: .../test_diff.txt
++++ after: .../test_diff.txt
+@@ -1 +1 @@
+-foo
+\ No newline at end of file
++bar
+\ No newline at end of file
+
+changed: [testhost]
+
+RUNNING HANDLER [Test handler 1] ***********************************************
+changed: [testhost]
+
+RUNNING HANDLER [Test handler 3] ***********************************************
+changed: [testhost]
+
+PLAY [testhost] ****************************************************************
+
+TASK [First free task] *********************************************************
+changed: [testhost]
+
+TASK [Second free task] ********************************************************
+changed: [testhost]
+
+TASK [Include some tasks] ******************************************************
+included: .../test/integration/targets/callback_default/include_me.yml for testhost => (item=1)
+
+PLAY RECAP *********************************************************************
+testhost : ok=19 changed=11 unreachable=0 failed=0 skipped=4 rescued=1 ignored=2
+
diff --git a/test/integration/targets/callback_default/callback_default.out.host_pinned.stdout b/test/integration/targets/callback_default/callback_default.out.host_pinned.stdout
new file mode 100644
index 0000000..0ec0447
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.host_pinned.stdout
@@ -0,0 +1,35 @@
+
+PLAY [nonlockstep] *************************************************************
+
+TASK [command] *****************************************************************
+changed: [testhost10]
+
+TASK [command] *****************************************************************
+changed: [testhost10]
+
+TASK [command] *****************************************************************
+changed: [testhost10]
+
+TASK [command] *****************************************************************
+changed: [testhost11]
+
+TASK [command] *****************************************************************
+changed: [testhost11]
+
+TASK [command] *****************************************************************
+changed: [testhost11]
+
+TASK [command] *****************************************************************
+changed: [testhost12]
+
+TASK [command] *****************************************************************
+changed: [testhost12]
+
+TASK [command] *****************************************************************
+changed: [testhost12]
+
+PLAY RECAP *********************************************************************
+testhost10 : ok=3 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
+testhost11 : ok=3 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
+testhost12 : ok=3 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
+
diff --git a/test/integration/targets/callback_default/callback_default.out.result_format_yaml.stderr b/test/integration/targets/callback_default/callback_default.out.result_format_yaml.stderr
new file mode 100644
index 0000000..d3e07d4
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.result_format_yaml.stderr
@@ -0,0 +1,2 @@
++ ansible-playbook -i inventory test.yml
+++ set +x
diff --git a/test/integration/targets/callback_default/callback_default.out.result_format_yaml.stdout b/test/integration/targets/callback_default/callback_default.out.result_format_yaml.stdout
new file mode 100644
index 0000000..87ddc60
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.result_format_yaml.stdout
@@ -0,0 +1,108 @@
+
+PLAY [testhost] ****************************************************************
+
+TASK [Changed task] ************************************************************
+changed: [testhost]
+
+TASK [Ok task] *****************************************************************
+ok: [testhost]
+
+TASK [Failed task] *************************************************************
+fatal: [testhost]: FAILED! =>
+ changed: false
+ msg: no reason
+...ignoring
+
+TASK [Skipped task] ************************************************************
+skipping: [testhost]
+
+TASK [Task with var in name (foo bar)] *****************************************
+changed: [testhost]
+
+TASK [Loop task] ***************************************************************
+changed: [testhost] => (item=foo-1)
+changed: [testhost] => (item=foo-2)
+changed: [testhost] => (item=foo-3)
+
+TASK [debug loop] **************************************************************
+changed: [testhost] => (item=debug-1) =>
+ msg: debug-1
+failed: [testhost] (item=debug-2) =>
+ msg: debug-2
+ok: [testhost] => (item=debug-3) =>
+ msg: debug-3
+skipping: [testhost] => (item=debug-4)
+fatal: [testhost]: FAILED! =>
+ msg: One or more items failed
+...ignoring
+
+TASK [EXPECTED FAILURE Failed task to be rescued] ******************************
+fatal: [testhost]: FAILED! =>
+ changed: false
+ msg: Failed as requested from task
+
+TASK [Rescue task] *************************************************************
+changed: [testhost]
+
+TASK [include_tasks] ***********************************************************
+included: .../test/integration/targets/callback_default/include_me.yml for testhost => (item=1)
+
+TASK [debug] *******************************************************************
+ok: [testhost] =>
+ item: 1
+
+TASK [copy] ********************************************************************
+changed: [testhost]
+
+TASK [replace] *****************************************************************
+--- before: .../test_diff.txt
++++ after: .../test_diff.txt
+@@ -1 +1 @@
+-foo
+\ No newline at end of file
++bar
+\ No newline at end of file
+
+changed: [testhost]
+
+TASK [replace] *****************************************************************
+ok: [testhost]
+
+TASK [debug] *******************************************************************
+skipping: [testhost]
+
+TASK [debug] *******************************************************************
+skipping: [testhost]
+
+TASK [debug] *******************************************************************
+skipping: [testhost] => (item=1)
+skipping: [testhost] => (item=2)
+skipping: [testhost]
+
+RUNNING HANDLER [Test handler 1] ***********************************************
+changed: [testhost]
+
+RUNNING HANDLER [Test handler 2] ***********************************************
+ok: [testhost]
+
+RUNNING HANDLER [Test handler 3] ***********************************************
+changed: [testhost]
+
+PLAY [testhost] ****************************************************************
+
+TASK [First free task] *********************************************************
+changed: [testhost]
+
+TASK [Second free task] ********************************************************
+changed: [testhost]
+
+TASK [Include some tasks] ******************************************************
+included: .../test/integration/targets/callback_default/include_me.yml for testhost => (item=1)
+
+TASK [debug] *******************************************************************
+ok: [testhost] =>
+ item: 1
+
+PLAY RECAP *********************************************************************
+testhost : ok=19 changed=11 unreachable=0 failed=0 skipped=4 rescued=1 ignored=2
+
diff --git a/test/integration/targets/callback_default/callback_default.out.result_format_yaml_lossy_verbose.stderr b/test/integration/targets/callback_default/callback_default.out.result_format_yaml_lossy_verbose.stderr
new file mode 100644
index 0000000..4884dfe
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.result_format_yaml_lossy_verbose.stderr
@@ -0,0 +1,2 @@
++ ansible-playbook -i inventory test.yml -v
+++ set +x
diff --git a/test/integration/targets/callback_default/callback_default.out.result_format_yaml_lossy_verbose.stdout b/test/integration/targets/callback_default/callback_default.out.result_format_yaml_lossy_verbose.stdout
new file mode 100644
index 0000000..71a4ef9
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.result_format_yaml_lossy_verbose.stdout
@@ -0,0 +1,300 @@
+
+
+PLAY [testhost] ****************************************************************
+
+TASK [Changed task] ************************************************************
+changed: [testhost] =>
+ changed: true
+ cmd:
+ - echo
+ - foo
+ delta: '0:00:00.000000'
+ end: '0000-00-00 00:00:00.000000'
+ msg: ''
+ rc: 0
+ start: '0000-00-00 00:00:00.000000'
+ stderr: ''
+ stderr_lines: <omitted>
+ stdout: foo
+ stdout_lines: <omitted>
+
+TASK [Ok task] *****************************************************************
+ok: [testhost] =>
+ changed: false
+ cmd:
+ - echo
+ - foo
+ delta: '0:00:00.000000'
+ end: '0000-00-00 00:00:00.000000'
+ msg: ''
+ rc: 0
+ start: '0000-00-00 00:00:00.000000'
+ stderr: ''
+ stderr_lines: <omitted>
+ stdout: foo
+ stdout_lines: <omitted>
+
+TASK [Failed task] *************************************************************
+fatal: [testhost]: FAILED! =>
+ changed: false
+ msg: no reason
+...ignoring
+
+TASK [Skipped task] ************************************************************
+skipping: [testhost] =>
+ changed: false
+ skip_reason: Conditional result was False
+
+TASK [Task with var in name (foo bar)] *****************************************
+changed: [testhost] =>
+ changed: true
+ cmd:
+ - echo
+ - foo
+ delta: '0:00:00.000000'
+ end: '0000-00-00 00:00:00.000000'
+ msg: ''
+ rc: 0
+ start: '0000-00-00 00:00:00.000000'
+ stderr: ''
+ stderr_lines: <omitted>
+ stdout: foo
+ stdout_lines: <omitted>
+
+TASK [Loop task] ***************************************************************
+changed: [testhost] => (item=foo-1) =>
+ ansible_loop_var: item
+ changed: true
+ cmd:
+ - echo
+ - foo
+ delta: '0:00:00.000000'
+ end: '0000-00-00 00:00:00.000000'
+ item: 1
+ msg: ''
+ rc: 0
+ start: '0000-00-00 00:00:00.000000'
+ stderr: ''
+ stderr_lines: <omitted>
+ stdout: foo
+ stdout_lines: <omitted>
+changed: [testhost] => (item=foo-2) =>
+ ansible_loop_var: item
+ changed: true
+ cmd:
+ - echo
+ - foo
+ delta: '0:00:00.000000'
+ end: '0000-00-00 00:00:00.000000'
+ item: 2
+ msg: ''
+ rc: 0
+ start: '0000-00-00 00:00:00.000000'
+ stderr: ''
+ stderr_lines: <omitted>
+ stdout: foo
+ stdout_lines: <omitted>
+changed: [testhost] => (item=foo-3) =>
+ ansible_loop_var: item
+ changed: true
+ cmd:
+ - echo
+ - foo
+ delta: '0:00:00.000000'
+ end: '0000-00-00 00:00:00.000000'
+ item: 3
+ msg: ''
+ rc: 0
+ start: '0000-00-00 00:00:00.000000'
+ stderr: ''
+ stderr_lines: <omitted>
+ stdout: foo
+ stdout_lines: <omitted>
+
+TASK [debug loop] **************************************************************
+changed: [testhost] => (item=debug-1) =>
+ msg: debug-1
+failed: [testhost] (item=debug-2) =>
+ msg: debug-2
+ok: [testhost] => (item=debug-3) =>
+ msg: debug-3
+skipping: [testhost] => (item=debug-4) =>
+ ansible_loop_var: item
+ item: 4
+fatal: [testhost]: FAILED! =>
+ msg: One or more items failed
+...ignoring
+
+TASK [EXPECTED FAILURE Failed task to be rescued] ******************************
+fatal: [testhost]: FAILED! =>
+ changed: false
+ msg: Failed as requested from task
+
+TASK [Rescue task] *************************************************************
+changed: [testhost] =>
+ changed: true
+ cmd:
+ - echo
+ - rescued
+ delta: '0:00:00.000000'
+ end: '0000-00-00 00:00:00.000000'
+ msg: ''
+ rc: 0
+ start: '0000-00-00 00:00:00.000000'
+ stderr: ''
+ stderr_lines: <omitted>
+ stdout: rescued
+ stdout_lines: <omitted>
+
+TASK [include_tasks] ***********************************************************
+included: .../test/integration/targets/callback_default/include_me.yml for testhost => (item=1)
+
+TASK [debug] *******************************************************************
+ok: [testhost] =>
+ item: 1
+
+TASK [copy] ********************************************************************
+changed: [testhost] =>
+ changed: true
+ checksum: 0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33
+ dest: .../test_diff.txt
+ gid: 0
+ group: root
+ md5sum: acbd18db4cc2f85cedef654fccc4a4d8
+ mode: '0644'
+ owner: root
+ size: 3
+ src: .../source
+ state: file
+ uid: 0
+
+TASK [replace] *****************************************************************
+--- before: .../test_diff.txt
++++ after: .../test_diff.txt
+@@ -1 +1 @@
+-foo
+\ No newline at end of file
++bar
+\ No newline at end of file
+
+changed: [testhost] =>
+ changed: true
+ msg: 1 replacements made
+ rc: 0
+
+TASK [replace] *****************************************************************
+ok: [testhost] =>
+ changed: false
+ msg: 1 replacements made
+ rc: 0
+
+TASK [debug] *******************************************************************
+skipping: [testhost] =>
+ skipped_reason: No items in the list
+
+TASK [debug] *******************************************************************
+skipping: [testhost] =>
+ skipped_reason: No items in the list
+
+TASK [debug] *******************************************************************
+skipping: [testhost] => (item=1) =>
+ ansible_loop_var: item
+ item: 1
+skipping: [testhost] => (item=2) =>
+ ansible_loop_var: item
+ item: 2
+skipping: [testhost] =>
+ msg: All items skipped
+
+RUNNING HANDLER [Test handler 1] ***********************************************
+changed: [testhost] =>
+ changed: true
+ cmd:
+ - echo
+ - foo
+ delta: '0:00:00.000000'
+ end: '0000-00-00 00:00:00.000000'
+ msg: ''
+ rc: 0
+ start: '0000-00-00 00:00:00.000000'
+ stderr: ''
+ stderr_lines: <omitted>
+ stdout: foo
+ stdout_lines: <omitted>
+
+RUNNING HANDLER [Test handler 2] ***********************************************
+ok: [testhost] =>
+ changed: false
+ cmd:
+ - echo
+ - foo
+ delta: '0:00:00.000000'
+ end: '0000-00-00 00:00:00.000000'
+ msg: ''
+ rc: 0
+ start: '0000-00-00 00:00:00.000000'
+ stderr: ''
+ stderr_lines: <omitted>
+ stdout: foo
+ stdout_lines: <omitted>
+
+RUNNING HANDLER [Test handler 3] ***********************************************
+changed: [testhost] =>
+ changed: true
+ cmd:
+ - echo
+ - foo
+ delta: '0:00:00.000000'
+ end: '0000-00-00 00:00:00.000000'
+ msg: ''
+ rc: 0
+ start: '0000-00-00 00:00:00.000000'
+ stderr: ''
+ stderr_lines: <omitted>
+ stdout: foo
+ stdout_lines: <omitted>
+
+PLAY [testhost] ****************************************************************
+
+TASK [First free task] *********************************************************
+changed: [testhost] =>
+ changed: true
+ cmd:
+ - echo
+ - foo
+ delta: '0:00:00.000000'
+ end: '0000-00-00 00:00:00.000000'
+ msg: ''
+ rc: 0
+ start: '0000-00-00 00:00:00.000000'
+ stderr: ''
+ stderr_lines: <omitted>
+ stdout: foo
+ stdout_lines: <omitted>
+
+TASK [Second free task] ********************************************************
+changed: [testhost] =>
+ changed: true
+ cmd:
+ - echo
+ - foo
+ delta: '0:00:00.000000'
+ end: '0000-00-00 00:00:00.000000'
+ msg: ''
+ rc: 0
+ start: '0000-00-00 00:00:00.000000'
+ stderr: ''
+ stderr_lines: <omitted>
+ stdout: foo
+ stdout_lines: <omitted>
+
+TASK [Include some tasks] ******************************************************
+included: .../test/integration/targets/callback_default/include_me.yml for testhost => (item=1)
+
+TASK [debug] *******************************************************************
+ok: [testhost] =>
+ item: 1
+
+PLAY RECAP *********************************************************************
+testhost : ok=19 changed=11 unreachable=0 failed=0 skipped=4 rescued=1 ignored=2
+
diff --git a/test/integration/targets/callback_default/callback_default.out.result_format_yaml_verbose.stderr b/test/integration/targets/callback_default/callback_default.out.result_format_yaml_verbose.stderr
new file mode 100644
index 0000000..4884dfe
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.result_format_yaml_verbose.stderr
@@ -0,0 +1,2 @@
++ ansible-playbook -i inventory test.yml -v
+++ set +x
diff --git a/test/integration/targets/callback_default/callback_default.out.result_format_yaml_verbose.stdout b/test/integration/targets/callback_default/callback_default.out.result_format_yaml_verbose.stdout
new file mode 100644
index 0000000..7a99cc7
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.result_format_yaml_verbose.stdout
@@ -0,0 +1,312 @@
+
+
+PLAY [testhost] ****************************************************************
+
+TASK [Changed task] ************************************************************
+changed: [testhost] =>
+ changed: true
+ cmd:
+ - echo
+ - foo
+ delta: '0:00:00.000000'
+ end: '0000-00-00 00:00:00.000000'
+ msg: ''
+ rc: 0
+ start: '0000-00-00 00:00:00.000000'
+ stderr: ''
+ stderr_lines: []
+ stdout: foo
+ stdout_lines:
+ - foo
+
+TASK [Ok task] *****************************************************************
+ok: [testhost] =>
+ changed: false
+ cmd:
+ - echo
+ - foo
+ delta: '0:00:00.000000'
+ end: '0000-00-00 00:00:00.000000'
+ msg: ''
+ rc: 0
+ start: '0000-00-00 00:00:00.000000'
+ stderr: ''
+ stderr_lines: []
+ stdout: foo
+ stdout_lines:
+ - foo
+
+TASK [Failed task] *************************************************************
+fatal: [testhost]: FAILED! =>
+ changed: false
+ msg: no reason
+...ignoring
+
+TASK [Skipped task] ************************************************************
+skipping: [testhost] =>
+ changed: false
+ skip_reason: Conditional result was False
+
+TASK [Task with var in name (foo bar)] *****************************************
+changed: [testhost] =>
+ changed: true
+ cmd:
+ - echo
+ - foo
+ delta: '0:00:00.000000'
+ end: '0000-00-00 00:00:00.000000'
+ msg: ''
+ rc: 0
+ start: '0000-00-00 00:00:00.000000'
+ stderr: ''
+ stderr_lines: []
+ stdout: foo
+ stdout_lines:
+ - foo
+
+TASK [Loop task] ***************************************************************
+changed: [testhost] => (item=foo-1) =>
+ ansible_loop_var: item
+ changed: true
+ cmd:
+ - echo
+ - foo
+ delta: '0:00:00.000000'
+ end: '0000-00-00 00:00:00.000000'
+ item: 1
+ msg: ''
+ rc: 0
+ start: '0000-00-00 00:00:00.000000'
+ stderr: ''
+ stderr_lines: []
+ stdout: foo
+ stdout_lines:
+ - foo
+changed: [testhost] => (item=foo-2) =>
+ ansible_loop_var: item
+ changed: true
+ cmd:
+ - echo
+ - foo
+ delta: '0:00:00.000000'
+ end: '0000-00-00 00:00:00.000000'
+ item: 2
+ msg: ''
+ rc: 0
+ start: '0000-00-00 00:00:00.000000'
+ stderr: ''
+ stderr_lines: []
+ stdout: foo
+ stdout_lines:
+ - foo
+changed: [testhost] => (item=foo-3) =>
+ ansible_loop_var: item
+ changed: true
+ cmd:
+ - echo
+ - foo
+ delta: '0:00:00.000000'
+ end: '0000-00-00 00:00:00.000000'
+ item: 3
+ msg: ''
+ rc: 0
+ start: '0000-00-00 00:00:00.000000'
+ stderr: ''
+ stderr_lines: []
+ stdout: foo
+ stdout_lines:
+ - foo
+
+TASK [debug loop] **************************************************************
+changed: [testhost] => (item=debug-1) =>
+ msg: debug-1
+failed: [testhost] (item=debug-2) =>
+ msg: debug-2
+ok: [testhost] => (item=debug-3) =>
+ msg: debug-3
+skipping: [testhost] => (item=debug-4) =>
+ ansible_loop_var: item
+ item: 4
+fatal: [testhost]: FAILED! =>
+ msg: One or more items failed
+...ignoring
+
+TASK [EXPECTED FAILURE Failed task to be rescued] ******************************
+fatal: [testhost]: FAILED! =>
+ changed: false
+ msg: Failed as requested from task
+
+TASK [Rescue task] *************************************************************
+changed: [testhost] =>
+ changed: true
+ cmd:
+ - echo
+ - rescued
+ delta: '0:00:00.000000'
+ end: '0000-00-00 00:00:00.000000'
+ msg: ''
+ rc: 0
+ start: '0000-00-00 00:00:00.000000'
+ stderr: ''
+ stderr_lines: []
+ stdout: rescued
+ stdout_lines:
+ - rescued
+
+TASK [include_tasks] ***********************************************************
+included: .../test/integration/targets/callback_default/include_me.yml for testhost => (item=1)
+
+TASK [debug] *******************************************************************
+ok: [testhost] =>
+ item: 1
+
+TASK [copy] ********************************************************************
+changed: [testhost] =>
+ changed: true
+ checksum: 0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33
+ dest: .../test_diff.txt
+ gid: 0
+ group: root
+ md5sum: acbd18db4cc2f85cedef654fccc4a4d8
+ mode: '0644'
+ owner: root
+ size: 3
+ src: .../source
+ state: file
+ uid: 0
+
+TASK [replace] *****************************************************************
+--- before: .../test_diff.txt
++++ after: .../test_diff.txt
+@@ -1 +1 @@
+-foo
+\ No newline at end of file
++bar
+\ No newline at end of file
+
+changed: [testhost] =>
+ changed: true
+ msg: 1 replacements made
+ rc: 0
+
+TASK [replace] *****************************************************************
+ok: [testhost] =>
+ changed: false
+ msg: 1 replacements made
+ rc: 0
+
+TASK [debug] *******************************************************************
+skipping: [testhost] =>
+ skipped_reason: No items in the list
+
+TASK [debug] *******************************************************************
+skipping: [testhost] =>
+ skipped_reason: No items in the list
+
+TASK [debug] *******************************************************************
+skipping: [testhost] => (item=1) =>
+ ansible_loop_var: item
+ item: 1
+skipping: [testhost] => (item=2) =>
+ ansible_loop_var: item
+ item: 2
+skipping: [testhost] =>
+ msg: All items skipped
+
+RUNNING HANDLER [Test handler 1] ***********************************************
+changed: [testhost] =>
+ changed: true
+ cmd:
+ - echo
+ - foo
+ delta: '0:00:00.000000'
+ end: '0000-00-00 00:00:00.000000'
+ msg: ''
+ rc: 0
+ start: '0000-00-00 00:00:00.000000'
+ stderr: ''
+ stderr_lines: []
+ stdout: foo
+ stdout_lines:
+ - foo
+
+RUNNING HANDLER [Test handler 2] ***********************************************
+ok: [testhost] =>
+ changed: false
+ cmd:
+ - echo
+ - foo
+ delta: '0:00:00.000000'
+ end: '0000-00-00 00:00:00.000000'
+ msg: ''
+ rc: 0
+ start: '0000-00-00 00:00:00.000000'
+ stderr: ''
+ stderr_lines: []
+ stdout: foo
+ stdout_lines:
+ - foo
+
+RUNNING HANDLER [Test handler 3] ***********************************************
+changed: [testhost] =>
+ changed: true
+ cmd:
+ - echo
+ - foo
+ delta: '0:00:00.000000'
+ end: '0000-00-00 00:00:00.000000'
+ msg: ''
+ rc: 0
+ start: '0000-00-00 00:00:00.000000'
+ stderr: ''
+ stderr_lines: []
+ stdout: foo
+ stdout_lines:
+ - foo
+
+PLAY [testhost] ****************************************************************
+
+TASK [First free task] *********************************************************
+changed: [testhost] =>
+ changed: true
+ cmd:
+ - echo
+ - foo
+ delta: '0:00:00.000000'
+ end: '0000-00-00 00:00:00.000000'
+ msg: ''
+ rc: 0
+ start: '0000-00-00 00:00:00.000000'
+ stderr: ''
+ stderr_lines: []
+ stdout: foo
+ stdout_lines:
+ - foo
+
+TASK [Second free task] ********************************************************
+changed: [testhost] =>
+ changed: true
+ cmd:
+ - echo
+ - foo
+ delta: '0:00:00.000000'
+ end: '0000-00-00 00:00:00.000000'
+ msg: ''
+ rc: 0
+ start: '0000-00-00 00:00:00.000000'
+ stderr: ''
+ stderr_lines: []
+ stdout: foo
+ stdout_lines:
+ - foo
+
+TASK [Include some tasks] ******************************************************
+included: .../test/integration/targets/callback_default/include_me.yml for testhost => (item=1)
+
+TASK [debug] *******************************************************************
+ok: [testhost] =>
+ item: 1
+
+PLAY RECAP *********************************************************************
+testhost : ok=19 changed=11 unreachable=0 failed=0 skipped=4 rescued=1 ignored=2
+
diff --git a/test/integration/targets/callback_default/callback_default.out.yaml_result_format_yaml_verbose.stderr b/test/integration/targets/callback_default/callback_default.out.yaml_result_format_yaml_verbose.stderr
new file mode 100644
index 0000000..6d767d2
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.yaml_result_format_yaml_verbose.stderr
@@ -0,0 +1,2 @@
++ ansible-playbook -i inventory test_yaml.yml -v
+++ set +x
diff --git a/test/integration/targets/callback_default/callback_default.out.yaml_result_format_yaml_verbose.stdout b/test/integration/targets/callback_default/callback_default.out.yaml_result_format_yaml_verbose.stdout
new file mode 100644
index 0000000..36437e5
--- /dev/null
+++ b/test/integration/targets/callback_default/callback_default.out.yaml_result_format_yaml_verbose.stdout
@@ -0,0 +1,29 @@
+
+
+PLAY [testhost] ****************************************************************
+
+TASK [Sample task name] ********************************************************
+ok: [testhost] =>
+ msg: sample debug msg
+
+TASK [Umlaut output] ***********************************************************
+ok: [testhost] =>
+ msg: |-
+ äöü
+ éêè
+ ßï☺
+
+TASK [Test to_yaml] ************************************************************
+ok: [testhost] =>
+ msg: |-
+ 'line 1
+
+ line 2
+
+ line 3
+
+ '
+
+PLAY RECAP *********************************************************************
+testhost : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
+
diff --git a/test/integration/targets/callback_default/include_me.yml b/test/integration/targets/callback_default/include_me.yml
new file mode 100644
index 0000000..51470f3
--- /dev/null
+++ b/test/integration/targets/callback_default/include_me.yml
@@ -0,0 +1,2 @@
+- debug:
+ var: item
diff --git a/test/integration/targets/callback_default/inventory b/test/integration/targets/callback_default/inventory
new file mode 100644
index 0000000..5d93afd
--- /dev/null
+++ b/test/integration/targets/callback_default/inventory
@@ -0,0 +1,10 @@
+[local]
+testhost ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+
+[nonexistent]
+testhost5 ansible_host=169.254.199.200 # no connection is ever established with this host
+
+[nonlockstep]
+testhost10 i=0 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+testhost11 i=3.0 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+testhost12 i=12.0 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
diff --git a/test/integration/targets/callback_default/no_implicit_meta_banners.yml b/test/integration/targets/callback_default/no_implicit_meta_banners.yml
new file mode 100644
index 0000000..87883dc
--- /dev/null
+++ b/test/integration/targets/callback_default/no_implicit_meta_banners.yml
@@ -0,0 +1,11 @@
+# This playbooks generates a noop task in the linear strategy, the output is tested that the banner for noop task is not printed https://github.com/ansible/ansible/pull/71344
+- hosts: all
+ gather_facts: no
+ tasks:
+ - block:
+ - name: EXPECTED FAILURE # sate shippable
+ fail:
+ when: inventory_hostname == 'host1'
+ rescue:
+ - name: rescue
+ debug:
diff --git a/test/integration/targets/callback_default/runme.sh b/test/integration/targets/callback_default/runme.sh
new file mode 100755
index 0000000..0ee4259
--- /dev/null
+++ b/test/integration/targets/callback_default/runme.sh
@@ -0,0 +1,241 @@
+#!/usr/bin/env bash
+
+# This test compares "known good" output with various settings against output
+# with the current code. It's brittle by nature, but this is probably the
+# "best" approach possible.
+#
+# Notes:
+# * options passed to this script (such as -v) are ignored, as they would change
+# the output and break the test
+# * the number of asterisks after a "banner" differs depending on the number of
+# columns on the TTY, so we must adjust the columns for the current session
+# for consistency
+
+set -eux
+
+run_test() {
+ local testname=$1
+ local playbook=$2
+
+ # output was recorded w/o cowsay, ensure we reproduce the same
+ export ANSIBLE_NOCOWS=1
+
+ # The shenanigans with redirection and 'tee' are to capture STDOUT and
+ # STDERR separately while still displaying both to the console
+ { ansible-playbook -i inventory "$playbook" "${@:3}" \
+ > >(set +x; tee "${OUTFILE}.${testname}.stdout"); } \
+ 2> >(set +x; tee "${OUTFILE}.${testname}.stderr" >&2)
+ # Scrub deprication warning that shows up in Python 2.6 on CentOS 6
+ sed -i -e '/RandomPool_DeprecationWarning/d' "${OUTFILE}.${testname}.stderr"
+ sed -i -e 's/included: .*\/test\/integration/included: ...\/test\/integration/g' "${OUTFILE}.${testname}.stdout"
+ sed -i -e 's/@@ -1,1 +1,1 @@/@@ -1 +1 @@/g' "${OUTFILE}.${testname}.stdout"
+ sed -i -e 's/: .*\/test_diff\.txt/: ...\/test_diff.txt/g' "${OUTFILE}.${testname}.stdout"
+ sed -i -e "s#${ANSIBLE_PLAYBOOK_DIR}#TEST_PATH#g" "${OUTFILE}.${testname}.stdout"
+ sed -i -e 's/^Using .*//g' "${OUTFILE}.${testname}.stdout"
+ sed -i -e 's/[0-9]:[0-9]\{2\}:[0-9]\{2\}\.[0-9]\{6\}/0:00:00.000000/g' "${OUTFILE}.${testname}.stdout"
+ sed -i -e 's/[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\} [0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}\.[0-9]\{6\}/0000-00-00 00:00:00.000000/g' "${OUTFILE}.${testname}.stdout"
+ sed -i -e 's#: .*/source$#: .../source#g' "${OUTFILE}.${testname}.stdout"
+ sed -i -e '/secontext:/d' "${OUTFILE}.${testname}.stdout"
+ sed -i -e 's/group: wheel/group: root/g' "${OUTFILE}.${testname}.stdout"
+
+ diff -u "${ORIGFILE}.${testname}.stdout" "${OUTFILE}.${testname}.stdout" || diff_failure
+ diff -u "${ORIGFILE}.${testname}.stderr" "${OUTFILE}.${testname}.stderr" || diff_failure
+}
+
+run_test_dryrun() {
+ local testname=$1
+ # optional, pass --check to run a dry run
+ local chk=${2:-}
+
+ # outout was recorded w/o cowsay, ensure we reproduce the same
+ export ANSIBLE_NOCOWS=1
+
+ # This needed to satisfy shellcheck that can not accept unquoted variable
+ cmd="ansible-playbook -i inventory ${chk} test_dryrun.yml"
+
+ # The shenanigans with redirection and 'tee' are to capture STDOUT and
+ # STDERR separately while still displaying both to the console
+ { $cmd \
+ > >(set +x; tee "${OUTFILE}.${testname}.stdout"); } \
+ 2> >(set +x; tee "${OUTFILE}.${testname}.stderr" >&2)
+ # Scrub deprication warning that shows up in Python 2.6 on CentOS 6
+ sed -i -e '/RandomPool_DeprecationWarning/d' "${OUTFILE}.${testname}.stderr"
+
+ diff -u "${ORIGFILE}.${testname}.stdout" "${OUTFILE}.${testname}.stdout" || diff_failure
+ diff -u "${ORIGFILE}.${testname}.stderr" "${OUTFILE}.${testname}.stderr" || diff_failure
+}
+
+diff_failure() {
+ if [[ $INIT = 0 ]]; then
+ echo "FAILURE...diff mismatch!"
+ exit 1
+ fi
+}
+
+cleanup() {
+ if [[ $INIT = 0 ]]; then
+ rm -rf "${OUTFILE}.*"
+ fi
+
+ if [[ -f "${BASEFILE}.unreachable.stdout" ]]; then
+ rm -rf "${BASEFILE}.unreachable.stdout"
+ fi
+
+ if [[ -f "${BASEFILE}.unreachable.stderr" ]]; then
+ rm -rf "${BASEFILE}.unreachable.stderr"
+ fi
+
+ # Restore TTY cols
+ if [[ -n ${TTY_COLS:-} ]]; then
+ stty cols "${TTY_COLS}"
+ fi
+}
+
+adjust_tty_cols() {
+ if [[ -t 1 ]]; then
+ # Preserve existing TTY cols
+ TTY_COLS=$( stty -a | grep -Eo '; columns [0-9]+;' | cut -d';' -f2 | cut -d' ' -f3 )
+ # Override TTY cols to make comparing ansible-playbook output easier
+ # This value matches the default in the code when there is no TTY
+ stty cols 79
+ fi
+}
+
+BASEFILE=callback_default.out
+
+ORIGFILE="${BASEFILE}"
+OUTFILE="${BASEFILE}.new"
+
+trap 'cleanup' EXIT
+
+# The --init flag will (re)generate the "good" output files used by the tests
+INIT=0
+if [[ ${1:-} == "--init" ]]; then
+ shift
+ OUTFILE=$ORIGFILE
+ INIT=1
+fi
+
+adjust_tty_cols
+
+# Force the 'default' callback plugin, since that's what we're testing
+export ANSIBLE_STDOUT_CALLBACK=default
+# Disable color in output for consistency
+export ANSIBLE_FORCE_COLOR=0
+export ANSIBLE_NOCOLOR=1
+
+# Default settings
+export ANSIBLE_DISPLAY_SKIPPED_HOSTS=1
+export ANSIBLE_DISPLAY_OK_HOSTS=1
+export ANSIBLE_DISPLAY_FAILED_STDERR=0
+export ANSIBLE_CHECK_MODE_MARKERS=0
+
+run_test default test.yml
+
+# Check for async output
+# NOTE: regex to match 1 or more digits works for both BSD and GNU grep
+ansible-playbook -i inventory test_async.yml 2>&1 | tee async_test.out
+grep "ASYNC OK .* jid=[0-9]\{1,\}" async_test.out
+grep "ASYNC FAILED .* jid=[0-9]\{1,\}" async_test.out
+rm -f async_test.out
+
+# Hide skipped
+export ANSIBLE_DISPLAY_SKIPPED_HOSTS=0
+
+run_test hide_skipped test.yml
+
+# Hide skipped/ok
+export ANSIBLE_DISPLAY_SKIPPED_HOSTS=0
+export ANSIBLE_DISPLAY_OK_HOSTS=0
+
+run_test hide_skipped_ok test.yml
+
+# Hide ok
+export ANSIBLE_DISPLAY_SKIPPED_HOSTS=1
+export ANSIBLE_DISPLAY_OK_HOSTS=0
+
+run_test hide_ok test.yml
+
+# Failed to stderr
+export ANSIBLE_DISPLAY_SKIPPED_HOSTS=1
+export ANSIBLE_DISPLAY_OK_HOSTS=1
+export ANSIBLE_DISPLAY_FAILED_STDERR=1
+
+run_test failed_to_stderr test.yml
+export ANSIBLE_DISPLAY_FAILED_STDERR=0
+
+
+# Test displaying task path on failure
+export ANSIBLE_SHOW_TASK_PATH_ON_FAILURE=1
+run_test display_path_on_failure test.yml
+export ANSIBLE_SHOW_TASK_PATH_ON_FAILURE=0
+
+
+# Default settings with unreachable tasks
+export ANSIBLE_DISPLAY_SKIPPED_HOSTS=1
+export ANSIBLE_DISPLAY_OK_HOSTS=1
+export ANSIBLE_DISPLAY_FAILED_STDERR=1
+export ANSIBLE_TIMEOUT=1
+
+# Check if UNREACHBLE is available in stderr
+set +e
+ansible-playbook -i inventory test_2.yml > >(set +x; tee "${BASEFILE}.unreachable.stdout";) 2> >(set +x; tee "${BASEFILE}.unreachable.stderr" >&2) || true
+set -e
+if test "$(grep -c 'UNREACHABLE' "${BASEFILE}.unreachable.stderr")" -ne 1; then
+ echo "Test failed"
+ exit 1
+fi
+export ANSIBLE_DISPLAY_FAILED_STDERR=0
+
+export ANSIBLE_CALLBACK_RESULT_FORMAT=yaml
+run_test result_format_yaml test.yml
+export ANSIBLE_CALLBACK_RESULT_FORMAT=json
+
+export ANSIBLE_CALLBACK_RESULT_FORMAT=yaml
+export ANSIBLE_CALLBACK_FORMAT_PRETTY=1
+run_test result_format_yaml_lossy_verbose test.yml -v
+run_test yaml_result_format_yaml_verbose test_yaml.yml -v
+export ANSIBLE_CALLBACK_RESULT_FORMAT=json
+unset ANSIBLE_CALLBACK_FORMAT_PRETTY
+
+export ANSIBLE_CALLBACK_RESULT_FORMAT=yaml
+export ANSIBLE_CALLBACK_FORMAT_PRETTY=0
+run_test result_format_yaml_verbose test.yml -v
+export ANSIBLE_CALLBACK_RESULT_FORMAT=json
+unset ANSIBLE_CALLBACK_FORMAT_PRETTY
+
+
+## DRY RUN tests
+#
+# Default settings with dry run tasks
+export ANSIBLE_DISPLAY_SKIPPED_HOSTS=1
+export ANSIBLE_DISPLAY_OK_HOSTS=1
+export ANSIBLE_DISPLAY_FAILED_STDERR=1
+# Enable Check mode markers
+export ANSIBLE_CHECK_MODE_MARKERS=1
+
+# Test the wet run with check markers
+run_test_dryrun check_markers_wet
+
+# Test the dry run with check markers
+run_test_dryrun check_markers_dry --check
+
+# Disable Check mode markers
+export ANSIBLE_CHECK_MODE_MARKERS=0
+
+# Test the wet run without check markers
+run_test_dryrun check_nomarkers_wet
+
+# Test the dry run without check markers
+run_test_dryrun check_nomarkers_dry --check
+
+# Make sure implicit meta tasks are not printed
+ansible-playbook -i host1,host2 no_implicit_meta_banners.yml > meta_test.out
+cat meta_test.out
+[ "$(grep -c 'TASK \[meta\]' meta_test.out)" -eq 0 ]
+rm -f meta_test.out
+
+# Ensure free/host_pinned non-lockstep strategies display correctly
+diff -u callback_default.out.free.stdout <(ANSIBLE_STRATEGY=free ansible-playbook -i inventory test_non_lockstep.yml 2>/dev/null)
+diff -u callback_default.out.fqcn_free.stdout <(ANSIBLE_STRATEGY=ansible.builtin.free ansible-playbook -i inventory test_non_lockstep.yml 2>/dev/null)
+diff -u callback_default.out.host_pinned.stdout <(ANSIBLE_STRATEGY=host_pinned ansible-playbook -i inventory test_non_lockstep.yml 2>/dev/null)
diff --git a/test/integration/targets/callback_default/test.yml b/test/integration/targets/callback_default/test.yml
new file mode 100644
index 0000000..79cea27
--- /dev/null
+++ b/test/integration/targets/callback_default/test.yml
@@ -0,0 +1,128 @@
+---
+- hosts: testhost
+ gather_facts: no
+ vars:
+ foo: foo bar
+ tasks:
+ - name: Changed task
+ command: echo foo
+ changed_when: true
+ notify: test handlers
+
+ - name: Ok task
+ command: echo foo
+ changed_when: false
+
+ - name: Failed task
+ fail:
+ msg: no reason
+ ignore_errors: yes
+
+ - name: Skipped task
+ command: echo foo
+ when: false
+
+ - name: Task with var in name ({{ foo }})
+ command: echo foo
+
+ - name: Loop task
+ command: echo foo
+ loop:
+ - 1
+ - 2
+ - 3
+ loop_control:
+ label: foo-{{ item }}
+
+ # detect "changed" debug tasks being hidden with display_ok_tasks=false
+ - name: debug loop
+ debug:
+ msg: debug-{{ item }}
+ changed_when: item == 1
+ failed_when: item == 2
+ when: item != 4
+ ignore_errors: yes
+ loop:
+ - 1
+ - 2
+ - 3
+ - 4
+ loop_control:
+ label: debug-{{ item }}
+
+ - block:
+ - name: EXPECTED FAILURE Failed task to be rescued
+ fail:
+ rescue:
+ - name: Rescue task
+ command: echo rescued
+
+ - include_tasks: include_me.yml
+ loop:
+ - 1
+
+ - copy:
+ dest: '{{ lookup("env", "OUTPUT_DIR") }}/test_diff.txt'
+ content: foo
+
+ - replace:
+ path: '{{ lookup("env", "OUTPUT_DIR") }}/test_diff.txt'
+ regexp: '^foo$'
+ replace: bar
+ diff: true
+
+ - replace:
+ path: '{{ lookup("env", "OUTPUT_DIR") }}/test_diff.txt'
+ regexp: '^bar$'
+ replace: baz
+ diff: true
+ changed_when: false
+
+ - debug:
+ msg: "{{ item }}"
+ loop: []
+
+ - debug:
+ msg: "{{ item }}"
+ loop: "{{ empty_list }}"
+ vars:
+ empty_list: []
+
+ - debug:
+ msg: "{{ item }}"
+ when: False
+ loop:
+ - 1
+ - 2
+
+ handlers:
+ - name: Test handler 1
+ command: echo foo
+ listen: test handlers
+
+ - name: Test handler 2
+ command: echo foo
+ changed_when: false
+ listen: test handlers
+
+ - name: Test handler 3
+ command: echo foo
+ listen: test handlers
+
+# An issue was found previously for tasks in a play using strategy 'free' after
+# a non-'free' play in the same playbook, so we protect against a regression.
+- hosts: testhost
+ gather_facts: no
+ strategy: free
+ tasks:
+ - name: First free task
+ command: echo foo
+
+ - name: Second free task
+ command: echo foo
+
+ # Ensure include_tasks task names get shown (#71277)
+ - name: Include some tasks
+ include_tasks: include_me.yml
+ loop:
+ - 1
diff --git a/test/integration/targets/callback_default/test_2.yml b/test/integration/targets/callback_default/test_2.yml
new file mode 100644
index 0000000..2daded7
--- /dev/null
+++ b/test/integration/targets/callback_default/test_2.yml
@@ -0,0 +1,6 @@
+- hosts: nonexistent
+ gather_facts: no
+ tasks:
+ - name: Test task for unreachable host
+ command: echo foo
+ ignore_errors: True
diff --git a/test/integration/targets/callback_default/test_async.yml b/test/integration/targets/callback_default/test_async.yml
new file mode 100644
index 0000000..57294a4
--- /dev/null
+++ b/test/integration/targets/callback_default/test_async.yml
@@ -0,0 +1,14 @@
+---
+- hosts: testhost
+ gather_facts: no
+ tasks:
+ - name: test success async output
+ command: sleep 1
+ async: 10
+ poll: 1
+
+ - name: test failure async output
+ command: sleep 10
+ async: 1
+ poll: 1
+ ignore_errors: yes
diff --git a/test/integration/targets/callback_default/test_dryrun.yml b/test/integration/targets/callback_default/test_dryrun.yml
new file mode 100644
index 0000000..e6e3294
--- /dev/null
+++ b/test/integration/targets/callback_default/test_dryrun.yml
@@ -0,0 +1,93 @@
+---
+- name: A common play
+ hosts: testhost
+ gather_facts: no
+ tasks:
+ - debug:
+ msg: 'ansible_check_mode: {{ansible_check_mode}}'
+
+ - name: Command
+ command: ls -l
+
+ - name: "Command with check_mode: false"
+ command: ls -l
+ check_mode: false
+
+ - name: "Command with check_mode: true"
+ command: ls -l
+ check_mode: true
+
+
+- name: "Play with check_mode: true (runs always in check_mode)"
+ hosts: testhost
+ gather_facts: no
+ check_mode: true
+ tasks:
+ - debug:
+ msg: 'ansible_check_mode: {{ansible_check_mode}}'
+
+ - name: Command
+ command: ls -l
+
+ - name: "Command with check_mode: false"
+ command: ls -l
+ check_mode: false
+
+ - name: "Command with check_mode: true"
+ command: ls -l
+ check_mode: true
+
+
+- name: "Play with check_mode: false (runs always in wet mode)"
+ hosts: testhost
+ gather_facts: no
+ check_mode: false
+ tasks:
+ - debug:
+ msg: 'ansible_check_mode: {{ansible_check_mode}}'
+
+ - name: Command
+ command: ls -l
+
+ - name: "Command with check_mode: false"
+ command: ls -l
+ check_mode: false
+
+ - name: "Command with check_mode: true"
+ command: ls -l
+ check_mode: true
+
+
+- name: "Play with a block with check_mode: true"
+ hosts: testhost
+ gather_facts: no
+ tasks:
+ - block:
+ - name: Command
+ command: ls -l
+
+ - name: "Command with check_mode: false"
+ command: ls -l
+ check_mode: false
+
+ - name: "Command with check_mode: true"
+ command: ls -l
+ check_mode: true
+ check_mode: true
+
+- name: "Play with a block with check_mode: false"
+ hosts: testhost
+ gather_facts: no
+ tasks:
+ - block:
+ - name: Command
+ command: ls -l
+
+ - name: "Command with check_mode: false"
+ command: ls -l
+ check_mode: false
+
+ - name: "Command with check_mode: true"
+ command: ls -l
+ check_mode: true
+ check_mode: false
diff --git a/test/integration/targets/callback_default/test_non_lockstep.yml b/test/integration/targets/callback_default/test_non_lockstep.yml
new file mode 100644
index 0000000..b656ee9
--- /dev/null
+++ b/test/integration/targets/callback_default/test_non_lockstep.yml
@@ -0,0 +1,7 @@
+---
+- hosts: nonlockstep
+ gather_facts: false
+ tasks:
+ - command: sleep {{ i }}
+ - command: sleep {{ i }}
+ - command: sleep {{ i }}
diff --git a/test/integration/targets/callback_default/test_yaml.yml b/test/integration/targets/callback_default/test_yaml.yml
new file mode 100644
index 0000000..6405906
--- /dev/null
+++ b/test/integration/targets/callback_default/test_yaml.yml
@@ -0,0 +1,19 @@
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - name: Sample task name
+ debug:
+ msg: sample debug msg
+
+ - name: Umlaut output
+ debug:
+ msg: "äöü\néêè\nßï☺"
+
+ - name: Test to_yaml
+ debug:
+ msg: "{{ data | to_yaml }}"
+ vars:
+ data: |
+ line 1
+ line 2
+ line 3
diff --git a/test/integration/targets/changed_when/aliases b/test/integration/targets/changed_when/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/changed_when/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/changed_when/meta/main.yml b/test/integration/targets/changed_when/meta/main.yml
new file mode 100644
index 0000000..07faa21
--- /dev/null
+++ b/test/integration/targets/changed_when/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - prepare_tests
diff --git a/test/integration/targets/changed_when/tasks/main.yml b/test/integration/targets/changed_when/tasks/main.yml
new file mode 100644
index 0000000..bc8da71
--- /dev/null
+++ b/test/integration/targets/changed_when/tasks/main.yml
@@ -0,0 +1,111 @@
+# test code for the changed_when parameter
+# (c) 2014, James Tanner <tanner.jc@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: ensure shell is always changed
+ shell: ls -al /tmp
+ register: shell_result
+
+- debug: var=shell_result
+
+- name: changed should always be true for shell
+ assert:
+ that:
+ - "shell_result.changed"
+
+- name: test changed_when override for shell
+ shell: ls -al /tmp
+ changed_when: False
+ register: shell_result
+
+- debug: var=shell_result
+
+- name: changed should be false
+ assert:
+ that:
+ - "not shell_result.changed"
+
+- name: Add hosts to test group and ensure it appears as changed
+ group_by:
+ key: "cw_test1_{{ inventory_hostname }}"
+ register: groupby
+
+- name: verify its changed
+ assert:
+ that:
+ - groupby is changed
+
+- name: Add hosts to test group and ensure it does NOT appear as changed
+ group_by:
+ key: "cw_test2_{{ inventory_hostname }}"
+ changed_when: False
+ register: groupby
+
+- name: verify its not changed
+ assert:
+ that:
+ - groupby is not changed
+
+- name: invalid conditional
+ command: echo foo
+ changed_when: boomboomboom
+ register: invalid_conditional
+ ignore_errors: true
+
+- assert:
+ that:
+ - invalid_conditional is failed
+ - invalid_conditional.stdout is defined
+ - invalid_conditional.changed_when_result is contains('boomboomboom')
+
+- add_host:
+ name: 'host_{{item}}'
+ loop:
+ - 1
+ - 2
+ changed_when: item == 2
+ register: add_host_loop_res
+
+- assert:
+ that:
+ - add_host_loop_res.results[0] is not changed
+ - add_host_loop_res.results[1] is changed
+ - add_host_loop_res is changed
+
+- group_by:
+ key: "test_{{ item }}"
+ loop:
+ - 1
+ - 2
+ changed_when: item == 2
+ register: group_by_loop_res
+
+- assert:
+ that:
+ - group_by_loop_res.results[0] is not changed
+ - group_by_loop_res.results[1] is changed
+ - group_by_loop_res is changed
+
+- name: use changed in changed_when
+ add_host:
+ name: 'host_3'
+ changed_when: add_host_loop_res is changed
+ register: add_host_loop_res
+
+- assert:
+ that:
+ - add_host_loop_res is changed
diff --git a/test/integration/targets/check_mode/aliases b/test/integration/targets/check_mode/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/check_mode/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/check_mode/check_mode-not-on-cli.yml b/test/integration/targets/check_mode/check_mode-not-on-cli.yml
new file mode 100644
index 0000000..1b0c734
--- /dev/null
+++ b/test/integration/targets/check_mode/check_mode-not-on-cli.yml
@@ -0,0 +1,37 @@
+---
+# Run withhout --check
+- hosts: testhost
+ gather_facts: False
+ tasks:
+ - command: 'echo ran'
+ register: command_out
+
+ - debug: var=command_out
+ - name: check that this did not run in check mode
+ assert:
+ that:
+ - '"ran" in command_out["stdout"]'
+
+- hosts: testhost
+ gather_facts: False
+ check_mode: True
+ tasks:
+ - command: 'echo ran'
+ register: command_out
+
+ - name: check that play level check_mode overrode the cli
+ assert:
+ that:
+ - '"check mode" in command_out["msg"]'
+
+- hosts: testhost
+ gather_facts: False
+ tasks:
+ - command: 'echo ran'
+ register: command_out
+ check_mode: True
+
+ - name: check that task level check_mode overrode the cli
+ assert:
+ that:
+ - '"check mode" in command_out["msg"]'
diff --git a/test/integration/targets/check_mode/check_mode-on-cli.yml b/test/integration/targets/check_mode/check_mode-on-cli.yml
new file mode 100644
index 0000000..0af34b8
--- /dev/null
+++ b/test/integration/targets/check_mode/check_mode-on-cli.yml
@@ -0,0 +1,36 @@
+---
+# Run with --check
+- hosts: testhost
+ gather_facts: False
+ tasks:
+ - command: 'echo ran'
+ register: command_out
+
+ - name: check that this did not run in check mode
+ assert:
+ that:
+ - '"check mode" in command_out["msg"]'
+
+- hosts: testhost
+ gather_facts: False
+ check_mode: False
+ tasks:
+ - command: 'echo ran'
+ register: command_out
+
+ - name: check that play level check_mode overrode the cli
+ assert:
+ that:
+ - '"ran" in command_out["stdout"]'
+
+- hosts: testhost
+ gather_facts: False
+ tasks:
+ - command: 'echo ran'
+ register: command_out
+ check_mode: False
+
+ - name: check that task level check_mode overrode the cli
+ assert:
+ that:
+ - '"ran" in command_out["stdout"]'
diff --git a/test/integration/targets/check_mode/check_mode.yml b/test/integration/targets/check_mode/check_mode.yml
new file mode 100644
index 0000000..a577750
--- /dev/null
+++ b/test/integration/targets/check_mode/check_mode.yml
@@ -0,0 +1,7 @@
+- name: Test that check works with check_mode specified in roles
+ hosts: testhost
+ vars:
+ - output_dir: .
+ roles:
+ - { role: test_always_run, tags: test_always_run }
+ - { role: test_check_mode, tags: test_check_mode }
diff --git a/test/integration/targets/check_mode/roles/test_always_run/meta/main.yml b/test/integration/targets/check_mode/roles/test_always_run/meta/main.yml
new file mode 100644
index 0000000..d06fd48
--- /dev/null
+++ b/test/integration/targets/check_mode/roles/test_always_run/meta/main.yml
@@ -0,0 +1,17 @@
+# test code for the check_mode: no option
+# (c) 2014, James Cammarata <jcammarata@ansible.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
diff --git a/test/integration/targets/check_mode/roles/test_always_run/tasks/main.yml b/test/integration/targets/check_mode/roles/test_always_run/tasks/main.yml
new file mode 100644
index 0000000..59bfb1d
--- /dev/null
+++ b/test/integration/targets/check_mode/roles/test_always_run/tasks/main.yml
@@ -0,0 +1,29 @@
+# test code for the check_mode: no option
+# (c) 2014, James Cammarata <jcammarata@ansible.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: run a command while in check mode
+ shell: echo "running"
+ check_mode: no
+ register: result
+
+- name: assert that the command was run
+ assert:
+ that:
+ - "result.changed == true"
+ - "result.stdout == 'running'"
+ - "result.rc == 0"
diff --git a/test/integration/targets/check_mode/roles/test_check_mode/files/foo.txt b/test/integration/targets/check_mode/roles/test_check_mode/files/foo.txt
new file mode 100644
index 0000000..3e96db9
--- /dev/null
+++ b/test/integration/targets/check_mode/roles/test_check_mode/files/foo.txt
@@ -0,0 +1 @@
+templated_var_loaded
diff --git a/test/integration/targets/check_mode/roles/test_check_mode/tasks/main.yml b/test/integration/targets/check_mode/roles/test_check_mode/tasks/main.yml
new file mode 100644
index 0000000..f926d14
--- /dev/null
+++ b/test/integration/targets/check_mode/roles/test_check_mode/tasks/main.yml
@@ -0,0 +1,50 @@
+# test code for the template module
+# (c) 2014, Michael DeHaan <michael.dehaan@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: fill in a basic template in check mode
+ template: src=foo.j2 dest={{output_dir}}/checkmode_foo.templated mode=0644
+ register: template_result
+
+- name: check whether file exists
+ stat: path={{output_dir}}/checkmode_foo.templated
+ register: foo
+
+- name: verify that the file was marked as changed in check mode
+ assert:
+ that:
+ - "template_result is changed"
+ - "not foo.stat.exists"
+
+- name: Actually create the file, disable check mode
+ template: src=foo.j2 dest={{output_dir}}/checkmode_foo.templated2 mode=0644
+ check_mode: no
+ register: checkmode_disabled
+
+- name: fill in template with new content
+ template: src=foo.j2 dest={{output_dir}}/checkmode_foo.templated2 mode=0644
+ register: template_result2
+
+- name: remove templated file
+ file: path={{output_dir}}/checkmode_foo.templated2 state=absent
+ check_mode: no
+
+- name: verify that the file was not changed
+ assert:
+ that:
+ - "checkmode_disabled is changed"
+ - "template_result2 is not changed"
diff --git a/test/integration/targets/check_mode/roles/test_check_mode/templates/foo.j2 b/test/integration/targets/check_mode/roles/test_check_mode/templates/foo.j2
new file mode 100644
index 0000000..55aab8f
--- /dev/null
+++ b/test/integration/targets/check_mode/roles/test_check_mode/templates/foo.j2
@@ -0,0 +1 @@
+{{ templated_var }}
diff --git a/test/integration/targets/check_mode/roles/test_check_mode/vars/main.yml b/test/integration/targets/check_mode/roles/test_check_mode/vars/main.yml
new file mode 100644
index 0000000..1e8f64c
--- /dev/null
+++ b/test/integration/targets/check_mode/roles/test_check_mode/vars/main.yml
@@ -0,0 +1 @@
+templated_var: templated_var_loaded
diff --git a/test/integration/targets/check_mode/runme.sh b/test/integration/targets/check_mode/runme.sh
new file mode 100755
index 0000000..954ac6f
--- /dev/null
+++ b/test/integration/targets/check_mode/runme.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook check_mode.yml -i ../../inventory -v --check "$@"
+ansible-playbook check_mode-on-cli.yml -i ../../inventory -v --check "$@"
+ansible-playbook check_mode-not-on-cli.yml -i ../../inventory -v "$@"
diff --git a/test/integration/targets/cli/aliases b/test/integration/targets/cli/aliases
new file mode 100644
index 0000000..e85a92f
--- /dev/null
+++ b/test/integration/targets/cli/aliases
@@ -0,0 +1,6 @@
+destructive
+needs/root
+needs/ssh
+needs/target/setup_pexpect
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/cli/runme.sh b/test/integration/targets/cli/runme.sh
new file mode 100755
index 0000000..c4f0867
--- /dev/null
+++ b/test/integration/targets/cli/runme.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ANSIBLE_ROLES_PATH=../ ansible-playbook setup.yml
+
+python test-cli.py
+
+ansible-playbook test_syntax/syntax_check.yml --syntax-check -i ../../inventory -v "$@"
diff --git a/test/integration/targets/cli/setup.yml b/test/integration/targets/cli/setup.yml
new file mode 100644
index 0000000..901cfd1
--- /dev/null
+++ b/test/integration/targets/cli/setup.yml
@@ -0,0 +1,42 @@
+- hosts: localhost
+ gather_facts: yes
+ roles:
+ - setup_pexpect
+
+ tasks:
+ - name: Test ansible-playbook and ansible with -K
+ block:
+ - name: Create user to connect as
+ user:
+ name: cliuser1
+ shell: /bin/bash
+ groups: wheel
+ append: yes
+ password: "{{ 'secretpassword' | password_hash('sha512', 'mysecretsalt') }}"
+ - name: Create user to become
+ user:
+ name: cliuser2
+ shell: /bin/bash
+ password: "{{ 'secretpassword' | password_hash('sha512', 'mysecretsalt') }}"
+ # Sometimes this file doesn't get removed, and we need it gone to ssh
+ - name: Remove /run/nologin
+ file:
+ path: /run/nologin
+ state: absent
+ # Make Ansible run Python to run Ansible
+ - name: Run the test
+ shell: python test_k_and_K.py {{ ansible_python_interpreter }}
+ always:
+ - name: Remove users
+ user:
+ name: "{{ item }}"
+ state: absent
+ with_items:
+ - cliuser1
+ - cliuser2
+ # For now, we don't test this everywhere, because `user` works differently
+ # on some platforms, as does sudo/sudoers. On Fedora, we can just add
+ # the user to 'wheel' and things magically work.
+ # TODO: In theory, we should test this with all the different 'become'
+ # plugins in base.
+ when: ansible_distribution == 'Fedora'
diff --git a/test/integration/targets/cli/test-cli.py b/test/integration/targets/cli/test-cli.py
new file mode 100644
index 0000000..9893d66
--- /dev/null
+++ b/test/integration/targets/cli/test-cli.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+# Copyright (c) 2019 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+
+import pexpect
+
+os.environ['ANSIBLE_NOCOLOR'] = '1'
+out = pexpect.run(
+ 'ansible localhost -m debug -a msg="{{ ansible_password }}" -k',
+ events={
+ 'SSH password:': '{{ 1 + 2 }}\n'
+ }
+)
+
+assert b'{{ 1 + 2 }}' in out
diff --git a/test/integration/targets/cli/test_k_and_K.py b/test/integration/targets/cli/test_k_and_K.py
new file mode 100644
index 0000000..f7077fb
--- /dev/null
+++ b/test/integration/targets/cli/test_k_and_K.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+import sys
+
+import pexpect
+
+os.environ['ANSIBLE_NOCOLOR'] = '1'
+
+out = pexpect.run(
+ 'ansible -c ssh -i localhost, -u cliuser1 -e ansible_python_interpreter={0} '
+ '-m command -a whoami -Kkb --become-user cliuser2 localhost'.format(sys.argv[1]),
+ events={
+ 'SSH password:': 'secretpassword\n',
+ 'BECOME password': 'secretpassword\n',
+ },
+ timeout=10
+)
+
+print(out)
+
+assert b'cliuser2' in out
diff --git a/test/integration/targets/cli/test_syntax/files/vaultsecret b/test/integration/targets/cli/test_syntax/files/vaultsecret
new file mode 100644
index 0000000..9daeafb
--- /dev/null
+++ b/test/integration/targets/cli/test_syntax/files/vaultsecret
@@ -0,0 +1 @@
+test
diff --git a/test/integration/targets/cli/test_syntax/group_vars/all/testvault.yml b/test/integration/targets/cli/test_syntax/group_vars/all/testvault.yml
new file mode 100644
index 0000000..dab41f8
--- /dev/null
+++ b/test/integration/targets/cli/test_syntax/group_vars/all/testvault.yml
@@ -0,0 +1,6 @@
+$ANSIBLE_VAULT;1.1;AES256
+63666636353064303132316633383734303731353066643832633666616162373531306563616139
+3038343765633138376561336530366162646332353132620a643661663366336237636562393662
+61656465393864613832383565306133656332656534326530346638336165346264386666343266
+3066336331313830310a666265653532643434303233306635366635616261373166613564326238
+62306134303765306537396162623232396639316239616534613631336166616561
diff --git a/test/integration/targets/cli/test_syntax/roles/some_role/tasks/main.yml b/test/integration/targets/cli/test_syntax/roles/some_role/tasks/main.yml
new file mode 100644
index 0000000..307927c
--- /dev/null
+++ b/test/integration/targets/cli/test_syntax/roles/some_role/tasks/main.yml
@@ -0,0 +1 @@
+- debug: msg='in role'
diff --git a/test/integration/targets/cli/test_syntax/syntax_check.yml b/test/integration/targets/cli/test_syntax/syntax_check.yml
new file mode 100644
index 0000000..8e26cf0
--- /dev/null
+++ b/test/integration/targets/cli/test_syntax/syntax_check.yml
@@ -0,0 +1,7 @@
+- name: "main"
+ hosts: all
+ gather_facts: false
+ tasks:
+ - import_role:
+ name: some_role
+ delegate_to: testhost
diff --git a/test/integration/targets/collection/aliases b/test/integration/targets/collection/aliases
new file mode 100644
index 0000000..136c05e
--- /dev/null
+++ b/test/integration/targets/collection/aliases
@@ -0,0 +1 @@
+hidden
diff --git a/test/integration/targets/collection/setup.sh b/test/integration/targets/collection/setup.sh
new file mode 100755
index 0000000..f1b33a5
--- /dev/null
+++ b/test/integration/targets/collection/setup.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+# Source this file from collection integration tests.
+#
+# It simplifies several aspects of collection testing:
+#
+# 1) Collection tests must be executed outside of the ansible source tree.
+# Otherwise ansible-test will test the ansible source instead of the test collection.
+# The temporary directory provided by ansible-test resides within the ansible source tree.
+#
+# 2) Sanity test ignore files for collections must be versioned based on the ansible-core version being used.
+# This script generates an ignore file with the correct filename for the current ansible-core version.
+#
+# 3) Sanity tests which are multi-version require an ignore entry per Python version.
+# This script replicates these ignore entries for each supported Python version based on the ignored path.
+
+set -eu -o pipefail
+
+export TEST_DIR
+export WORK_DIR
+
+TEST_DIR="$PWD"
+WORK_DIR="$(mktemp -d)"
+
+trap 'rm -rf "${WORK_DIR}"' EXIT
+
+cp -a "${TEST_DIR}/ansible_collections" "${WORK_DIR}"
+cd "${WORK_DIR}/ansible_collections/ns/col"
+
+"${TEST_DIR}/../collection/update-ignore.py"
diff --git a/test/integration/targets/collection/update-ignore.py b/test/integration/targets/collection/update-ignore.py
new file mode 100755
index 0000000..92a702c
--- /dev/null
+++ b/test/integration/targets/collection/update-ignore.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python
+"""Rewrite a sanity ignore file to expand Python versions for import ignores and write the file out with the correct Ansible version in the name."""
+
+import os
+import sys
+
+from ansible import release
+
+
+def main():
+ ansible_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(release.__file__))))
+ source_root = os.path.join(ansible_root, 'test', 'lib')
+
+ sys.path.insert(0, source_root)
+
+ from ansible_test._internal import constants
+
+ src_path = 'tests/sanity/ignore.txt'
+
+ if not os.path.exists(src_path):
+ print(f'Skipping updates on non-existent ignore file: {src_path}')
+ return
+
+ directory = os.path.dirname(src_path)
+ name, ext = os.path.splitext(os.path.basename(src_path))
+ major_minor = '.'.join(release.__version__.split('.')[:2])
+ dst_path = os.path.join(directory, f'{name}-{major_minor}{ext}')
+
+ with open(src_path) as src_file:
+ src_lines = src_file.read().splitlines()
+
+ dst_lines = []
+
+ for line in src_lines:
+ path, rule = line.split(' ')
+
+ if rule != 'import':
+ dst_lines.append(line)
+ continue
+
+ if path.startswith('plugins/module'):
+ python_versions = constants.SUPPORTED_PYTHON_VERSIONS
+ else:
+ python_versions = constants.CONTROLLER_PYTHON_VERSIONS
+
+ for python_version in python_versions:
+ dst_lines.append(f'{line}-{python_version}')
+
+ ignores = '\n'.join(dst_lines) + '\n'
+
+ with open(dst_path, 'w') as dst_file:
+ dst_file.write(ignores)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/a.statichost.yml b/test/integration/targets/collections/a.statichost.yml
new file mode 100644
index 0000000..683878a
--- /dev/null
+++ b/test/integration/targets/collections/a.statichost.yml
@@ -0,0 +1,3 @@
+# use a plugin defined in a content-adjacent collection to ensure we added it properly
+plugin: testns.content_adj.statichost
+hostname: dynamic_host_a
diff --git a/test/integration/targets/collections/aliases b/test/integration/targets/collections/aliases
new file mode 100644
index 0000000..996481b
--- /dev/null
+++ b/test/integration/targets/collections/aliases
@@ -0,0 +1,4 @@
+posix
+shippable/posix/group1
+shippable/windows/group1
+windows
diff --git a/test/integration/targets/collections/ansiballz_dupe/collections/ansible_collections/duplicate/name/plugins/modules/ping.py b/test/integration/targets/collections/ansiballz_dupe/collections/ansible_collections/duplicate/name/plugins/modules/ping.py
new file mode 100644
index 0000000..d0fdba7
--- /dev/null
+++ b/test/integration/targets/collections/ansiballz_dupe/collections/ansible_collections/duplicate/name/plugins/modules/ping.py
@@ -0,0 +1,3 @@
+#!/usr/bin/python
+from ansible.module_utils.basic import AnsibleModule
+AnsibleModule({}).exit_json(ping='duplicate.name.pong')
diff --git a/test/integration/targets/collections/ansiballz_dupe/test_ansiballz_cache_dupe_shortname.yml b/test/integration/targets/collections/ansiballz_dupe/test_ansiballz_cache_dupe_shortname.yml
new file mode 100644
index 0000000..2552624
--- /dev/null
+++ b/test/integration/targets/collections/ansiballz_dupe/test_ansiballz_cache_dupe_shortname.yml
@@ -0,0 +1,15 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - ping:
+ register: result1
+
+ - ping:
+ collections:
+ - duplicate.name
+ register: result2
+
+ - assert:
+ that:
+ - result1.ping == 'pong'
+ - result2.ping == 'duplicate.name.pong'
diff --git a/test/integration/targets/collections/cache.statichost.yml b/test/integration/targets/collections/cache.statichost.yml
new file mode 100644
index 0000000..b2adcfa
--- /dev/null
+++ b/test/integration/targets/collections/cache.statichost.yml
@@ -0,0 +1,7 @@
+# use inventory and cache plugins defined in a content-adjacent collection
+plugin: testns.content_adj.statichost
+hostname: cache_host_a
+cache_plugin: testns.content_adj.custom_jsonfile
+cache: yes
+cache_connection: inventory_cache
+cache_prefix: 'prefix_'
diff --git a/test/integration/targets/collections/check_populated_inventory.yml b/test/integration/targets/collections/check_populated_inventory.yml
new file mode 100644
index 0000000..ab33081
--- /dev/null
+++ b/test/integration/targets/collections/check_populated_inventory.yml
@@ -0,0 +1,11 @@
+---
+- hosts: localhost
+ connection: local
+ gather_facts: no
+ tasks:
+ - assert:
+ that:
+ - "groups.all | length == 2"
+ - "groups.ungrouped == groups.all"
+ - "'cache_host_a' in groups.all"
+ - "'dynamic_host_a' in groups.all"
diff --git a/test/integration/targets/collections/collection_root_sys/ansible_collections/testns/coll_in_sys/plugins/modules/systestmodule.py b/test/integration/targets/collections/collection_root_sys/ansible_collections/testns/coll_in_sys/plugins/modules/systestmodule.py
new file mode 100644
index 0000000..cba3812
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_sys/ansible_collections/testns/coll_in_sys/plugins/modules/systestmodule.py
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+
+def main():
+ print(json.dumps(dict(changed=False, source='sys')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collection_root_sys/ansible_collections/testns/testcoll/plugins/modules/maskedmodule.py b/test/integration/targets/collections/collection_root_sys/ansible_collections/testns/testcoll/plugins/modules/maskedmodule.py
new file mode 100644
index 0000000..e3db81b
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_sys/ansible_collections/testns/testcoll/plugins/modules/maskedmodule.py
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+
+def main():
+ print(json.dumps(dict(changed=False, failed=True, msg='this collection should be masked by testcoll in the user content root')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collection_root_sys/ansible_collections/testns/testcoll/plugins/modules/testmodule.py b/test/integration/targets/collections/collection_root_sys/ansible_collections/testns/testcoll/plugins/modules/testmodule.py
new file mode 100644
index 0000000..cba3812
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_sys/ansible_collections/testns/testcoll/plugins/modules/testmodule.py
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+
+def main():
+ print(json.dumps(dict(changed=False, source='sys')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collection_root_sys/ansible_collections/testns/testcoll/roles/maskedrole/tasks/main.yml b/test/integration/targets/collections/collection_root_sys/ansible_collections/testns/testcoll/roles/maskedrole/tasks/main.yml
new file mode 100644
index 0000000..21fe324
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_sys/ansible_collections/testns/testcoll/roles/maskedrole/tasks/main.yml
@@ -0,0 +1,2 @@
+- fail:
+ msg: this role should never be visible or runnable
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/ansible/builtin/plugins/modules/ping.py b/test/integration/targets/collections/collection_root_user/ansible_collections/ansible/builtin/plugins/modules/ping.py
new file mode 100644
index 0000000..0747670
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/ansible/builtin/plugins/modules/ping.py
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+
+def main():
+ print(json.dumps(dict(changed=False, source='overridden ansible.builtin (should not be possible)')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/ansible/bullcoll/plugins/modules/bullmodule.py b/test/integration/targets/collections/collection_root_user/ansible_collections/ansible/bullcoll/plugins/modules/bullmodule.py
new file mode 100644
index 0000000..5ea354e
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/ansible/bullcoll/plugins/modules/bullmodule.py
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+
+def main():
+ print(json.dumps(dict(changed=False, source='user_ansible_bullcoll')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/othercoll/plugins/module_utils/formerly_testcoll_pkg/__init__.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/othercoll/plugins/module_utils/formerly_testcoll_pkg/__init__.py
new file mode 100644
index 0000000..aa5c3ee
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/othercoll/plugins/module_utils/formerly_testcoll_pkg/__init__.py
@@ -0,0 +1 @@
+thing = "hello from testns.othercoll.formerly_testcoll_pkg.thing"
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/othercoll/plugins/module_utils/formerly_testcoll_pkg/submod.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/othercoll/plugins/module_utils/formerly_testcoll_pkg/submod.py
new file mode 100644
index 0000000..eb49a16
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/othercoll/plugins/module_utils/formerly_testcoll_pkg/submod.py
@@ -0,0 +1 @@
+thing = "hello from formerly_testcoll_pkg.submod.thing"
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testbroken/plugins/filter/broken_filter.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testbroken/plugins/filter/broken_filter.py
new file mode 100644
index 0000000..51fe852
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testbroken/plugins/filter/broken_filter.py
@@ -0,0 +1,13 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+class FilterModule(object):
+
+ def filters(self):
+ return {
+ 'broken': lambda x: 'broken',
+ }
+
+
+raise Exception('This is a broken filter plugin.')
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/meta/runtime.yml b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/meta/runtime.yml
new file mode 100644
index 0000000..f5b617d
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/meta/runtime.yml
@@ -0,0 +1,52 @@
+plugin_routing:
+ action:
+ uses_redirected_action:
+ redirect: testns.testcoll.subclassed_normal
+ callback:
+ removedcallback:
+ tombstone:
+ removal_date: '2020-01-01'
+ connection:
+ redirected_local:
+ redirect: ansible.builtin.local
+ modules:
+ multilevel1:
+ redirect: testns.testcoll.multilevel2
+ multilevel2:
+ redirect: testns.testcoll.multilevel3
+ multilevel3:
+ redirect: testns.testcoll.ping
+ uses_redirected_action:
+ redirect: ansible.builtin.ping
+ setup.ps1: ansible.windows.setup
+ looped_ping:
+ redirect: testns.testcoll.looped_ping2
+ looped_ping2:
+ redirect: testns.testcoll.looped_ping
+ bogus_redirect:
+ redirect: bogus.collection.shouldbomb
+ deprecated_ping:
+ deprecation:
+ removal_date: 2020-12-31
+ warning_text: old_ping will be removed in a future release of this collection. Use new_ping instead.
+ foobar_facts:
+ redirect: foobar_info
+ aliased_ping:
+ redirect: ansible.builtin.ping
+ dead_ping:
+ tombstone:
+ removal_date: 2019-12-31
+ warning_text: dead_ping has been removed
+ module_utils:
+ moved_out_root:
+ redirect: testns.content_adj.sub1.foomodule
+ formerly_testcoll_pkg:
+ redirect: ansible_collections.testns.othercoll.plugins.module_utils.formerly_testcoll_pkg
+ formerly_testcoll_pkg.submod:
+ redirect: ansible_collections.testns.othercoll.plugins.module_utils.formerly_testcoll_pkg.submod
+ missing_redirect_target_collection:
+ redirect: bogusns.boguscoll.bogusmu
+ missing_redirect_target_module:
+ redirect: testns.othercoll.bogusmu
+
+requires_ansible: '>=2.11'
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/default_collection_playbook.yml b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/default_collection_playbook.yml
new file mode 100644
index 0000000..1d1aee7
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/default_collection_playbook.yml
@@ -0,0 +1,49 @@
+# verify default collection action/module lookup works
+# since we're running this playbook inside a collection, it will set that collection as the default search for all playboooks
+# and non-collection roles to allow for easy migration of old integration tests to collections
+- hosts: testhost
+ tasks:
+ - testmodule:
+
+- hosts: testhost
+ vars:
+ test_role_input: task static default collection
+ tasks:
+ - import_role:
+ name: testrole # unqualified role lookup should work; inheriting from the containing collection
+ - assert:
+ that:
+ - test_role_output.msg == test_role_input
+ - vars:
+ test_role_input: task static legacy embedded default collection
+ block:
+ - import_role:
+ name: non_coll_role
+ - assert:
+ that:
+ - test_role_output.msg == test_role_input
+
+- hosts: testhost
+ vars:
+ test_role_input: keyword static default collection
+ roles:
+ - testrole
+ tasks:
+ - debug: var=test_role_input
+ - debug: var=test_role_output
+ - assert:
+ that:
+ - test_role_output.msg == test_role_input
+
+- hosts: testhost
+ vars:
+ test_role_input: task dynamic default collection
+ tasks:
+ - include_role:
+ name: testrole # unqualified role lookup should work; inheriting from the containing collection
+ - include_role:
+ name: non_coll_role
+ - assert:
+ that:
+ - testmodule_out_from_non_coll_role is success
+ - embedded_module_out_from_non_coll_role is success
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/play.yml b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/play.yml
new file mode 100644
index 0000000..6be246c
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/play.yml
@@ -0,0 +1,4 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - set_fact: play='tldr'
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role/library/embedded_module.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role/library/embedded_module.py
new file mode 100644
index 0000000..54402d1
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role/library/embedded_module.py
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+
+def main():
+ print(json.dumps(dict(changed=False, source='collection_embedded_non_collection_role')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role/tasks/main.yml b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role/tasks/main.yml
new file mode 100644
index 0000000..3fab7fe
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role/tasks/main.yml
@@ -0,0 +1,29 @@
+- testmodule:
+ register: testmodule_out_from_non_coll_role
+
+- embedded_module:
+ register: embedded_module_out_from_non_coll_role
+
+- name: check collections list from role meta
+ plugin_lookup:
+ register: pluginlookup_out
+
+- debug: var=pluginlookup_out
+
+- debug:
+ msg: '{{ test_role_input | default("(undefined)") }}'
+ register: test_role_output
+
+- assert:
+ that:
+ - test_role_input is not defined or test_role_input == test_role_output.msg
+
+- vars:
+ test_role_input: include another non-coll role
+ block:
+ - include_role:
+ name: non_coll_role_to_call
+
+ - assert:
+ that:
+ - non_coll_role_to_call_test_role_output.msg == test_role_input
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role_to_call/tasks/main.yml b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role_to_call/tasks/main.yml
new file mode 100644
index 0000000..2b1c15f
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role_to_call/tasks/main.yml
@@ -0,0 +1,7 @@
+- debug:
+ msg: '{{ test_role_input | default("(undefined)") }}'
+ register: non_coll_role_to_call_test_role_output
+
+- assert:
+ that:
+ - 'non_coll_role_to_call_test_role_output.msg == "include another non-coll role"'
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/type/play.yml b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/type/play.yml
new file mode 100644
index 0000000..dd6d563
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/type/play.yml
@@ -0,0 +1,4 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - set_fact: play_type='in type subdir'
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/type/subtype/play.yml b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/type/subtype/play.yml
new file mode 100644
index 0000000..0e33a76
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/type/subtype/play.yml
@@ -0,0 +1,4 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - set_fact: play_type_subtype='in subtype subdir'
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/action_subdir/subdir_ping_action.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/action_subdir/subdir_ping_action.py
new file mode 100644
index 0000000..5af7334
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/action_subdir/subdir_ping_action.py
@@ -0,0 +1,19 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.action import ActionBase
+
+
+class ActionModule(ActionBase):
+ TRANSFERS_FILES = False
+ _VALID_ARGS = frozenset()
+
+ def run(self, tmp=None, task_vars=None):
+ if task_vars is None:
+ task_vars = dict()
+
+ result = super(ActionModule, self).run(None, task_vars)
+
+ result = dict(changed=False)
+
+ return result
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/bypass_host_loop.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/bypass_host_loop.py
new file mode 100644
index 0000000..b15493d
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/bypass_host_loop.py
@@ -0,0 +1,17 @@
+# Copyright: (c) 2020, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.action import ActionBase
+
+
+class ActionModule(ActionBase):
+
+ BYPASS_HOST_LOOP = True
+
+ def run(self, tmp=None, task_vars=None):
+ result = super(ActionModule, self).run(tmp, task_vars)
+ result['bypass_inventory_hostname'] = task_vars['inventory_hostname']
+ return result
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/plugin_lookup.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/plugin_lookup.py
new file mode 100644
index 0000000..3fa41e8
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/plugin_lookup.py
@@ -0,0 +1,40 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.action import ActionBase
+from ansible.plugins import loader
+
+
+class ActionModule(ActionBase):
+ TRANSFERS_FILES = False
+ _VALID_ARGS = frozenset(('type', 'name'))
+
+ def run(self, tmp=None, task_vars=None):
+ if task_vars is None:
+ task_vars = dict()
+
+ result = super(ActionModule, self).run(None, task_vars)
+
+ plugin_type = self._task.args.get('type')
+ name = self._task.args.get('name')
+
+ result = dict(changed=False, collection_list=self._task.collections)
+
+ if all([plugin_type, name]):
+ attr_name = '{0}_loader'.format(plugin_type)
+
+ typed_loader = getattr(loader, attr_name, None)
+
+ if not typed_loader:
+ return (dict(failed=True, msg='invalid plugin type {0}'.format(plugin_type)))
+
+ context = typed_loader.find_plugin_with_context(name, collection_list=self._task.collections)
+
+ if not context.resolved:
+ result['plugin_path'] = None
+ result['redirect_list'] = []
+ else:
+ result['plugin_path'] = context.plugin_resolved_path
+ result['redirect_list'] = context.redirect_list
+
+ return result
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/subclassed_normal.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/subclassed_normal.py
new file mode 100644
index 0000000..f0eff30
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/subclassed_normal.py
@@ -0,0 +1,11 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.action.normal import ActionModule as NormalAction
+
+
+class ActionModule(NormalAction):
+ def run(self, *args, **kwargs):
+ result = super(ActionModule, self).run(*args, **kwargs)
+ result['hacked'] = 'I got run under a subclassed normal, yay'
+ return result
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/uses_redirected_import.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/uses_redirected_import.py
new file mode 100644
index 0000000..701d7b4
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/uses_redirected_import.py
@@ -0,0 +1,20 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.action import ActionBase
+from ansible.module_utils.formerly_core import thingtocall
+
+
+class ActionModule(ActionBase):
+ TRANSFERS_FILES = False
+ _VALID_ARGS = frozenset()
+
+ def run(self, tmp=None, task_vars=None):
+ if task_vars is None:
+ task_vars = dict()
+
+ result = super(ActionModule, self).run(None, task_vars)
+
+ result = dict(changed=False, ttc_res=thingtocall())
+
+ return result
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/callback/usercallback.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/callback/usercallback.py
new file mode 100644
index 0000000..b534df2
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/callback/usercallback.py
@@ -0,0 +1,26 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.callback import CallbackBase
+
+DOCUMENTATION = '''
+ callback: usercallback
+ callback_type: notification
+ short_description: does stuff
+ description:
+ - does some stuff
+'''
+
+
+class CallbackModule(CallbackBase):
+ CALLBACK_VERSION = 2.0
+ CALLBACK_TYPE = 'aggregate'
+ CALLBACK_NAME = 'usercallback'
+
+ def __init__(self):
+
+ super(CallbackModule, self).__init__()
+ self._display.display("loaded usercallback from collection, yay")
+
+ def v2_runner_on_ok(self, result):
+ self._display.display("usercallback says ok")
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/connection/localconn.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/connection/localconn.py
new file mode 100644
index 0000000..fc19a99
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/connection/localconn.py
@@ -0,0 +1,41 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.module_utils._text import to_native
+from ansible.plugins.connection import ConnectionBase
+
+DOCUMENTATION = """
+ connection: localconn
+ short_description: do stuff local
+ description:
+ - does stuff
+ options:
+ connectionvar:
+ description:
+ - something we set
+ default: the_default
+ vars:
+ - name: ansible_localconn_connectionvar
+"""
+
+
+class Connection(ConnectionBase):
+ transport = 'local'
+ has_pipelining = True
+
+ def _connect(self):
+ return self
+
+ def exec_command(self, cmd, in_data=None, sudoable=True):
+ stdout = 'localconn ran {0}'.format(to_native(cmd))
+ stderr = 'connectionvar is {0}'.format(to_native(self.get_option('connectionvar')))
+ return (0, stdout, stderr)
+
+ def put_file(self, in_path, out_path):
+ raise NotImplementedError('just a test')
+
+ def fetch_file(self, in_path, out_path):
+ raise NotImplementedError('just a test')
+
+ def close(self):
+ self._connected = False
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/doc_fragments/frag.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/doc_fragments/frag.py
new file mode 100644
index 0000000..4549f2d
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/doc_fragments/frag.py
@@ -0,0 +1,18 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+class ModuleDocFragment(object):
+ DOCUMENTATION = r'''
+options:
+ normal_doc_frag:
+ description:
+ - an option
+'''
+
+ OTHER_DOCUMENTATION = r'''
+options:
+ other_doc_frag:
+ description:
+ - another option
+'''
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/filter/filter_subdir/my_subdir_filters.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/filter/filter_subdir/my_subdir_filters.py
new file mode 100644
index 0000000..a5498a4
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/filter/filter_subdir/my_subdir_filters.py
@@ -0,0 +1,14 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+def test_subdir_filter(data):
+ return "{0}_via_testfilter_from_subdir".format(data)
+
+
+class FilterModule(object):
+
+ def filters(self):
+ return {
+ 'test_subdir_filter': test_subdir_filter
+ }
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/filter/myfilters.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/filter/myfilters.py
new file mode 100644
index 0000000..0ce239e
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/filter/myfilters.py
@@ -0,0 +1,14 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+def testfilter(data):
+ return "{0}_via_testfilter_from_userdir".format(data)
+
+
+class FilterModule(object):
+
+ def filters(self):
+ return {
+ 'testfilter': testfilter
+ }
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/filter/myfilters2.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/filter/myfilters2.py
new file mode 100644
index 0000000..0723922
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/filter/myfilters2.py
@@ -0,0 +1,14 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+def testfilter2(data):
+ return "{0}_via_testfilter2_from_userdir".format(data)
+
+
+class FilterModule(object):
+
+ def filters(self):
+ return {
+ 'testfilter2': testfilter2
+ }
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/lookup/lookup_subdir/my_subdir_lookup.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/lookup/lookup_subdir/my_subdir_lookup.py
new file mode 100644
index 0000000..dd9818c
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/lookup/lookup_subdir/my_subdir_lookup.py
@@ -0,0 +1,11 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.lookup import LookupBase
+
+
+class LookupModule(LookupBase):
+
+ def run(self, terms, variables, **kwargs):
+
+ return ['subdir_lookup_from_user_dir']
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/lookup/mylookup.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/lookup/mylookup.py
new file mode 100644
index 0000000..1cf3d28
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/lookup/mylookup.py
@@ -0,0 +1,11 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.lookup import LookupBase
+
+
+class LookupModule(LookupBase):
+
+ def run(self, terms, variables, **kwargs):
+
+ return ['mylookup_from_user_dir']
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/lookup/mylookup2.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/lookup/mylookup2.py
new file mode 100644
index 0000000..bda671f
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/lookup/mylookup2.py
@@ -0,0 +1,12 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+from ansible.plugins.lookup import LookupBase
+
+
+class LookupModule(LookupBase):
+
+ def run(self, terms, variables, **kwargs):
+
+ return ['mylookup2_from_user_dir']
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/AnotherCSMU.cs b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/AnotherCSMU.cs
new file mode 100644
index 0000000..68d2bc7
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/AnotherCSMU.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace ansible_collections.testns.testcoll.plugins.module_utils.AnotherCSMU
+{
+ public class AnotherThing
+ {
+ public static string CallMe()
+ {
+ return "Hello from nested user-collection-hosted AnotherCSMU";
+ }
+ }
+}
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyCSMU.cs b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyCSMU.cs
new file mode 100644
index 0000000..2b7843d
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyCSMU.cs
@@ -0,0 +1,19 @@
+using System;
+
+using ansible_collections.testns.testcoll.plugins.module_utils.AnotherCSMU;
+using ansible_collections.testns.testcoll.plugins.module_utils.subpkg.subcs;
+
+//TypeAccelerator -Name MyCSMU -TypeName CustomThing
+
+namespace ansible_collections.testns.testcoll.plugins.module_utils.MyCSMU
+{
+ public class CustomThing
+ {
+ public static string HelloWorld()
+ {
+ string res1 = AnotherThing.CallMe();
+ string res2 = NestedUtil.HelloWorld();
+ return String.Format("Hello from user_mu collection-hosted MyCSMU, also {0} and {1}", res1, res2);
+ }
+ }
+}
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyCSMUOptional.cs b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyCSMUOptional.cs
new file mode 100644
index 0000000..0a3e758
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyCSMUOptional.cs
@@ -0,0 +1,19 @@
+using System;
+
+using ansible_collections.testns.testcoll.plugins.module_utils.AnotherCSMU;
+using ansible_collections.testns.testcoll.plugins.module_utils.subpkg.subcs;
+
+//TypeAccelerator -Name MyCSMU -TypeName CustomThing
+
+namespace ansible_collections.testns.testcoll.plugins.module_utils.MyCSMU
+{
+ public class CustomThing
+ {
+ public static string HelloWorld()
+ {
+ string res1 = AnotherThing.CallMe();
+ string res2 = NestedUtil.HelloWorld();
+ return String.Format("Hello from user_mu collection-hosted MyCSMUOptional, also {0} and {1}", res1, res2);
+ }
+ }
+}
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyPSMU.psm1 b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyPSMU.psm1
new file mode 100644
index 0000000..09da66d
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyPSMU.psm1
@@ -0,0 +1,9 @@
+Function Invoke-FromUserPSMU {
+ <#
+ .SYNOPSIS
+ Test function
+ #>
+ return "from user_mu"
+}
+
+Export-ModuleMember -Function Invoke-FromUserPSMU
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyPSMUOptional.psm1 b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyPSMUOptional.psm1
new file mode 100644
index 0000000..1e36159
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyPSMUOptional.psm1
@@ -0,0 +1,16 @@
+#AnsibleRequires -CSharpUtil Ansible.Invalid -Optional
+#AnsibleRequires -Powershell Ansible.ModuleUtils.Invalid -Optional
+#AnsibleRequires -CSharpUtil ansible_collections.testns.testcoll.plugins.module_utils.invalid -Optional
+#AnsibleRequires -CSharpUtil ansible_collections.testns.testcoll.plugins.module_utils.invalid.invalid -Optional
+#AnsibleRequires -Powershell ansible_collections.testns.testcoll.plugins.module_utils.invalid -Optional
+#AnsibleRequires -Powershell ansible_collections.testns.testcoll.plugins.module_utils.invalid.invalid -Optional
+
+Function Invoke-FromUserPSMU {
+ <#
+ .SYNOPSIS
+ Test function
+ #>
+ return "from optional user_mu"
+}
+
+Export-ModuleMember -Function Invoke-FromUserPSMU
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/base.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/base.py
new file mode 100644
index 0000000..0654d18
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/base.py
@@ -0,0 +1,12 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible_collections.testns.testcoll.plugins.module_utils import secondary
+import ansible_collections.testns.testcoll.plugins.module_utils.secondary
+
+
+def thingtocall():
+ if secondary != ansible_collections.testns.testcoll.plugins.module_utils.secondary:
+ raise Exception()
+
+ return "thingtocall in base called " + ansible_collections.testns.testcoll.plugins.module_utils.secondary.thingtocall()
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/leaf.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/leaf.py
new file mode 100644
index 0000000..ad84710
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/leaf.py
@@ -0,0 +1,6 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+def thingtocall():
+ return "thingtocall in leaf"
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/nested_same/__init__.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/nested_same/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/nested_same/__init__.py
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/nested_same/nested_same/__init__.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/nested_same/nested_same/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/nested_same/nested_same/__init__.py
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/nested_same/nested_same/nested_same.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/nested_same/nested_same/nested_same.py
new file mode 100644
index 0000000..7740756
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/nested_same/nested_same/nested_same.py
@@ -0,0 +1,6 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+def nested_same():
+ return 'hello from nested_same'
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/secondary.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/secondary.py
new file mode 100644
index 0000000..9a31568
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/secondary.py
@@ -0,0 +1,6 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+def thingtocall():
+ return "thingtocall in secondary"
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg/__init__.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg/__init__.py
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg/subcs.cs b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg/subcs.cs
new file mode 100644
index 0000000..ebeb8ce
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg/subcs.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace ansible_collections.testns.testcoll.plugins.module_utils.subpkg.subcs
+{
+ public class NestedUtil
+ {
+ public static string HelloWorld()
+ {
+ string res = "Hello from subpkg.subcs";
+ return res;
+ }
+ }
+}
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg/submod.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg/submod.py
new file mode 100644
index 0000000..3c24bc4
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg/submod.py
@@ -0,0 +1,6 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+def thingtocall():
+ return "thingtocall in subpkg.submod"
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg/subps.psm1 b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg/subps.psm1
new file mode 100644
index 0000000..1db0ab9
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg/subps.psm1
@@ -0,0 +1,9 @@
+Function Invoke-SubUserPSMU {
+ <#
+ .SYNOPSIS
+ Test function
+ #>
+ return "from subpkg.subps.psm1"
+}
+
+Export-ModuleMember -Function Invoke-SubUserPSMU
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg_with_init.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg_with_init.py
new file mode 100644
index 0000000..b48a717
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg_with_init.py
@@ -0,0 +1,11 @@
+# NB: this module should never be loaded, since we'll see the subpkg_with_init package dir first
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+def thingtocall():
+ raise Exception('this should never be called (loaded discrete module instead of package module)')
+
+
+def anotherthingtocall():
+ raise Exception('this should never be called (loaded discrete module instead of package module)')
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg_with_init/__init__.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg_with_init/__init__.py
new file mode 100644
index 0000000..d424796
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg_with_init/__init__.py
@@ -0,0 +1,10 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+# exercise relative imports in package init; they behave differently
+from .mod_in_subpkg_with_init import thingtocall as submod_thingtocall
+from ..subpkg.submod import thingtocall as cousin_submod_thingtocall # pylint: disable=relative-beyond-top-level
+
+
+def thingtocall():
+ return "thingtocall in subpkg_with_init"
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg_with_init/mod_in_subpkg_with_init.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg_with_init/mod_in_subpkg_with_init.py
new file mode 100644
index 0000000..27747da
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg_with_init/mod_in_subpkg_with_init.py
@@ -0,0 +1,6 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+def thingtocall():
+ return "thingtocall in mod_in_subpkg_with_init"
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/deprecated_ping.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/deprecated_ping.py
new file mode 100644
index 0000000..9698ba6
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/deprecated_ping.py
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+
+def main():
+ print(json.dumps(dict(changed=False, source='user', is_deprecated=True)))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/module_subdir/subdir_ping_module.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/module_subdir/subdir_ping_module.py
new file mode 100644
index 0000000..5a70174
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/module_subdir/subdir_ping_module.py
@@ -0,0 +1,14 @@
+#!/usr/bin/python
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+
+def main():
+ print(json.dumps(dict(changed=False, source='user')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/ping.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/ping.py
new file mode 100644
index 0000000..2ca079c
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/ping.py
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+
+def main():
+ print(json.dumps(dict(changed=False, source='user')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/testmodule.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/testmodule.py
new file mode 100644
index 0000000..e2efada
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/testmodule.py
@@ -0,0 +1,21 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+DOCUMENTATION = r'''
+module: testmodule
+description: for testing
+extends_documentation_fragment:
+ - testns.testcoll.frag
+ - testns.testcoll.frag.other_documentation
+'''
+
+
+def main():
+ print(json.dumps(dict(changed=False, source='user')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/testmodule_bad_docfrags.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/testmodule_bad_docfrags.py
new file mode 100644
index 0000000..46ccb76
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/testmodule_bad_docfrags.py
@@ -0,0 +1,25 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+DOCUMENTATION = r'''
+module: testmodule
+description: for testing
+extends_documentation_fragment:
+ - noncollbogusfrag
+ - noncollbogusfrag.bogusvar
+ - bogusns.testcoll.frag
+ - testns.boguscoll.frag
+ - testns.testcoll.bogusfrag
+ - testns.testcoll.frag.bogusvar
+'''
+
+
+def main():
+ print(json.dumps(dict(changed=False, source='user')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_base_mu_granular_nested_import.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_base_mu_granular_nested_import.py
new file mode 100644
index 0000000..4054e36
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_base_mu_granular_nested_import.py
@@ -0,0 +1,19 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+import sys
+
+from ansible_collections.testns.testcoll.plugins.module_utils.base import thingtocall
+
+
+def main():
+ mu_result = thingtocall()
+ print(json.dumps(dict(changed=False, source='user', mu_result=mu_result)))
+
+ sys.exit()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_collection_redirected_mu.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_collection_redirected_mu.py
new file mode 100644
index 0000000..b169fde
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_collection_redirected_mu.py
@@ -0,0 +1,21 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+import sys
+
+from ansible_collections.testns.testcoll.plugins.module_utils.moved_out_root import importme
+from ..module_utils.formerly_testcoll_pkg import thing as movedthing # pylint: disable=relative-beyond-top-level
+from ..module_utils.formerly_testcoll_pkg.submod import thing as submodmovedthing # pylint: disable=relative-beyond-top-level
+
+
+def main():
+ mu_result = importme()
+ print(json.dumps(dict(changed=False, source='user', mu_result=mu_result, mu_result2=movedthing, mu_result3=submodmovedthing)))
+
+ sys.exit()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_core_redirected_mu.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_core_redirected_mu.py
new file mode 100644
index 0000000..28a0772
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_core_redirected_mu.py
@@ -0,0 +1,19 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+import sys
+
+from ansible.module_utils.formerly_core import thingtocall
+
+
+def main():
+ mu_result = thingtocall()
+ print(json.dumps(dict(changed=False, source='user', mu_result=mu_result)))
+
+ sys.exit()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_flat_import.bak b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_flat_import.bak
new file mode 100644
index 0000000..703f454
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_flat_import.bak
@@ -0,0 +1,3 @@
+# Intentionally blank, and intentionally attempting to shadow
+# uses_leaf_mu_flat_import.py. MODULE_IGNORE_EXTS should prevent this file
+# from ever being loaded.
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_flat_import.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_flat_import.py
new file mode 100644
index 0000000..295d432
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_flat_import.py
@@ -0,0 +1,19 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+import sys
+
+import ansible_collections.testns.testcoll.plugins.module_utils.leaf
+
+
+def main():
+ mu_result = ansible_collections.testns.testcoll.plugins.module_utils.leaf.thingtocall()
+ print(json.dumps(dict(changed=False, source='user', mu_result=mu_result)))
+
+ sys.exit()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_flat_import.yml b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_flat_import.yml
new file mode 100644
index 0000000..703f454
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_flat_import.yml
@@ -0,0 +1,3 @@
+# Intentionally blank, and intentionally attempting to shadow
+# uses_leaf_mu_flat_import.py. MODULE_IGNORE_EXTS should prevent this file
+# from ever being loaded.
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_granular_import.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_granular_import.py
new file mode 100644
index 0000000..3794f49
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_granular_import.py
@@ -0,0 +1,19 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+import sys
+
+from ansible_collections.testns.testcoll.plugins.module_utils.leaf import thingtocall as aliasedthing
+
+
+def main():
+ mu_result = aliasedthing()
+ print(json.dumps(dict(changed=False, source='user', mu_result=mu_result)))
+
+ sys.exit()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_module_import_from.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_module_import_from.py
new file mode 100644
index 0000000..559e3e5
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_module_import_from.py
@@ -0,0 +1,31 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+import sys
+
+from ansible_collections.testns.testcoll.plugins.module_utils import leaf, secondary
+# FIXME: this one needs pkginit synthesis to work
+# from ansible_collections.testns.testcoll.plugins.module_utils.subpkg import submod
+from ansible_collections.testns.testcoll.plugins.module_utils.subpkg_with_init import (thingtocall as spwi_thingtocall,
+ submod_thingtocall as spwi_submod_thingtocall,
+ cousin_submod_thingtocall as spwi_cousin_submod_thingtocall)
+
+
+def main():
+ mu_result = leaf.thingtocall()
+ mu2_result = secondary.thingtocall()
+ mu3_result = "thingtocall in subpkg.submod" # FIXME: this one needs pkginit synthesis to work
+ # mu3_result = submod.thingtocall()
+ mu4_result = spwi_thingtocall()
+ mu5_result = spwi_submod_thingtocall()
+ mu6_result = spwi_cousin_submod_thingtocall()
+ print(json.dumps(dict(changed=False, source='user', mu_result=mu_result, mu2_result=mu2_result,
+ mu3_result=mu3_result, mu4_result=mu4_result, mu5_result=mu5_result, mu6_result=mu6_result)))
+
+ sys.exit()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing.py
new file mode 100644
index 0000000..b945eb6
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing.py
@@ -0,0 +1,16 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+import sys
+
+from ..module_utils import bogusmu # pylint: disable=relative-beyond-top-level
+
+
+def main():
+ raise Exception('should never get here')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing_redirect_collection.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing_redirect_collection.py
new file mode 100644
index 0000000..59cb3c5
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing_redirect_collection.py
@@ -0,0 +1,16 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+import sys
+
+from ..module_utils import missing_redirect_target_collection # pylint: disable=relative-beyond-top-level
+
+
+def main():
+ raise Exception('should never get here')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing_redirect_module.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing_redirect_module.py
new file mode 100644
index 0000000..31ffd17
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing_redirect_module.py
@@ -0,0 +1,16 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+import sys
+
+from ..module_utils import missing_redirect_target_module # pylint: disable=relative-beyond-top-level
+
+
+def main():
+ raise Exception('should never get here')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_nested_same_as_func.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_nested_same_as_func.py
new file mode 100644
index 0000000..26fa53c
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_nested_same_as_func.py
@@ -0,0 +1,19 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+import sys
+
+from ansible_collections.testns.testcoll.plugins.module_utils.nested_same.nested_same.nested_same import nested_same
+
+
+def main():
+ mu_result = nested_same()
+ print(json.dumps(dict(changed=False, source='user', mu_result=mu_result)))
+
+ sys.exit()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_nested_same_as_module.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_nested_same_as_module.py
new file mode 100644
index 0000000..e017c14
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_nested_same_as_module.py
@@ -0,0 +1,19 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+import sys
+
+from ansible_collections.testns.testcoll.plugins.module_utils.nested_same.nested_same import nested_same
+
+
+def main():
+ mu_result = nested_same.nested_same()
+ print(json.dumps(dict(changed=False, source='user', mu_result=mu_result)))
+
+ sys.exit()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_csbasic_only.ps1 b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_csbasic_only.ps1
new file mode 100644
index 0000000..df17583
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_csbasic_only.ps1
@@ -0,0 +1,22 @@
+#!powershell
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$spec = @{
+ options = @{
+ data = @{ type = "str"; default = "pong" }
+ }
+ supports_check_mode = $true
+}
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+$data = $module.Params.data
+
+if ($data -eq "crash") {
+ throw "boom"
+}
+
+$module.Result.ping = $data
+$module.Result.source = "user"
+$module.ExitJson() \ No newline at end of file
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_selfcontained.ps1 b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_selfcontained.ps1
new file mode 100644
index 0000000..986d515
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_selfcontained.ps1
@@ -0,0 +1,9 @@
+#!powershell
+
+$res = @{
+ changed = $false
+ source = "user"
+ msg = "hi from selfcontained.ps1"
+}
+
+ConvertTo-Json $res \ No newline at end of file
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_selfcontained.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_selfcontained.py
new file mode 100644
index 0000000..ce99bfa
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_selfcontained.py
@@ -0,0 +1 @@
+# docs for Windows module would go here; just ensure we don't accidentally load this instead of the .ps1
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_uses_coll_csmu.ps1 b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_uses_coll_csmu.ps1
new file mode 100644
index 0000000..af00627
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_uses_coll_csmu.ps1
@@ -0,0 +1,26 @@
+#!powershell
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#AnsibleRequires -CSharpUtil ansible_collections.testns.testcoll.plugins.module_utils.MyCSMU
+#AnsibleRequires -CSharpUtil ansible_collections.testns.testcoll.plugins.module_utils.subpkg.subcs
+
+$spec = @{
+ options = @{
+ data = @{ type = "str"; default = "called from $([ansible_collections.testns.testcoll.plugins.module_utils.MyCSMU.CustomThing]::HelloWorld())" }
+ }
+ supports_check_mode = $true
+}
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+$data = $module.Params.data
+
+if ($data -eq "crash") {
+ throw "boom"
+}
+
+$module.Result.ping = $data
+$module.Result.source = "user"
+$module.Result.subpkg = [ansible_collections.testns.testcoll.plugins.module_utils.subpkg.subcs.NestedUtil]::HelloWorld()
+$module.Result.type_accelerator = "called from $([MyCSMU]::HelloWorld())"
+$module.ExitJson()
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_uses_coll_psmu.ps1 b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_uses_coll_psmu.ps1
new file mode 100644
index 0000000..cbca7b7
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_uses_coll_psmu.ps1
@@ -0,0 +1,25 @@
+#!powershell
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#AnsibleRequires -Powershell ansible_collections.testns.testcoll.plugins.module_utils.MyPSMU
+#AnsibleRequires -PowerShell ansible_collections.testns.testcoll.plugins.module_utils.subpkg.subps
+
+$spec = @{
+ options = @{
+ data = @{ type = "str"; default = "called from $(Invoke-FromUserPSMU)" }
+ }
+ supports_check_mode = $true
+}
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+$data = $module.Params.data
+
+if ($data -eq "crash") {
+ throw "boom"
+}
+
+$module.Result.ping = $data
+$module.Result.source = "user"
+$module.Result.subpkg = Invoke-SubUserPSMU
+$module.ExitJson()
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_uses_optional.ps1 b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_uses_optional.ps1
new file mode 100644
index 0000000..c44dcfe
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_uses_optional.ps1
@@ -0,0 +1,33 @@
+#!powershell
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Test builtin C# still works with -Optional
+#AnsibleRequires -CSharpUtil Ansible.Basic -Optional
+
+# Test no failure when importing an invalid builtin C# and pwsh util with -Optional
+#AnsibleRequires -CSharpUtil Ansible.Invalid -Optional
+#AnsibleRequires -PowerShell Ansible.ModuleUtils.Invalid -Optional
+
+# Test valid module_util still works with -Optional
+#AnsibleRequires -CSharpUtil ansible_collections.testns.testcoll.plugins.module_utils.MyCSMUOptional -Optional
+#AnsibleRequires -Powershell ansible_collections.testns.testcoll.plugins.module_utils.MyPSMUOptional -Optional
+
+# Test no failure when importing an invalid collection C# and pwsh util with -Optional
+#AnsibleRequires -CSharpUtil ansible_collections.testns.testcoll.plugins.module_utils.invalid -Optional
+#AnsibleRequires -CSharpUtil ansible_collections.testns.testcoll.plugins.module_utils.invalid.invalid -Optional
+#AnsibleRequires -Powershell ansible_collections.testns.testcoll.plugins.module_utils.invalid -Optional
+#AnsibleRequires -Powershell ansible_collections.testns.testcoll.plugins.module_utils.invalid.invalid -Optional
+
+$spec = @{
+ options = @{
+ data = @{ type = "str"; default = "called $(Invoke-FromUserPSMU)" }
+ }
+ supports_check_mode = $true
+}
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$module.Result.data = $module.Params.data
+$module.Result.csharp = [MyCSMU]::HelloWorld()
+
+$module.ExitJson()
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/test/mytests.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/test/mytests.py
new file mode 100644
index 0000000..ba610fb
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/test/mytests.py
@@ -0,0 +1,13 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+def testtest(data):
+ return data == 'from_user'
+
+
+class TestModule(object):
+ def tests(self):
+ return {
+ 'testtest': testtest
+ }
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/test/mytests2.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/test/mytests2.py
new file mode 100644
index 0000000..183944f
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/test/mytests2.py
@@ -0,0 +1,13 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+def testtest(data):
+ return data == 'from_user2'
+
+
+class TestModule(object):
+ def tests(self):
+ return {
+ 'testtest2': testtest
+ }
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/test/test_subdir/my_subdir_tests.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/test/test_subdir/my_subdir_tests.py
new file mode 100644
index 0000000..98a8f89
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/test/test_subdir/my_subdir_tests.py
@@ -0,0 +1,13 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+def subdir_test(data):
+ return data == 'subdir_from_user'
+
+
+class TestModule(object):
+ def tests(self):
+ return {
+ 'subdir_test': subdir_test
+ }
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/vars/custom_vars.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/vars/custom_vars.py
new file mode 100644
index 0000000..da4e776
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/vars/custom_vars.py
@@ -0,0 +1,48 @@
+# Copyright 2019 RedHat, inc
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+#############################################
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ vars: custom_vars
+ version_added: "2.10"
+ short_description: load host and group vars
+ description: test loading host and group vars from a collection
+ options:
+ stage:
+ choices: ['all', 'inventory', 'task']
+ type: str
+ ini:
+ - key: stage
+ section: custom_vars
+ env:
+ - name: ANSIBLE_VARS_PLUGIN_STAGE
+'''
+
+from ansible.plugins.vars import BaseVarsPlugin
+
+
+class VarsModule(BaseVarsPlugin):
+
+ # Vars plugins in collections are only loaded when they are enabled by the user.
+ # If a vars plugin sets REQUIRES_ENABLED = False, a warning should occur (assuming it is loaded).
+ REQUIRES_ENABLED = False
+
+ def get_vars(self, loader, path, entities, cache=True):
+ super(VarsModule, self).get_vars(loader, path, entities)
+ return {'collection': 'collection_root_user'}
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/call_standalone/tasks/main.yml b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/call_standalone/tasks/main.yml
new file mode 100644
index 0000000..f5dcc0f
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/call_standalone/tasks/main.yml
@@ -0,0 +1,6 @@
+- include_role:
+ name: standalone
+
+- assert:
+ that:
+ - standalone_role_var is defined
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/calls_intra_collection_dep_role_unqualified/meta/main.yml b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/calls_intra_collection_dep_role_unqualified/meta/main.yml
new file mode 100644
index 0000000..b3a8819
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/calls_intra_collection_dep_role_unqualified/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - testrole # since testrole lives in this collection, we'll check there first
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/calls_intra_collection_dep_role_unqualified/tasks/main.yml b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/calls_intra_collection_dep_role_unqualified/tasks/main.yml
new file mode 100644
index 0000000..99297f7
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/calls_intra_collection_dep_role_unqualified/tasks/main.yml
@@ -0,0 +1,7 @@
+- debug:
+ msg: '{{ outer_role_input | default("(undefined)") }}'
+ register: outer_role_output
+
+- assert:
+ that:
+ - outer_role_input is not defined or outer_role_input == outer_role_output.msg
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/common_handlers/handlers/main.yml b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/common_handlers/handlers/main.yml
new file mode 100644
index 0000000..186368f
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/common_handlers/handlers/main.yml
@@ -0,0 +1,27 @@
+# This handler should only be called 1 time, if it's called more than once
+# this task should fail on subsequent executions
+- name: test_fqcn_handler
+ set_fact:
+ handler_counter: '{{ handler_counter|int + 1 }}'
+ failed_when: handler_counter|int > 1
+
+# The following handler contains the role name and should be callable as:
+# 'common_handlers test_fqcn_handler'
+# 'common_handlers : common_handlers test_fqcn_handler`
+# 'testns.testcoll.common_handlers : common_handlers test_fqcn_handler'
+- name: common_handlers test_fqcn_handler
+ set_fact:
+ handler_counter: '{{ handler_counter|int + 1}}'
+ failed_when: handler_counter|int > 2
+
+# The following handler starts with 'role name : ' and should _not_ be listed as:
+# 'common_handlers : common_handlers : test_fqcn_handler`
+# 'testns.testcoll.common_handlers : common_handlers : test_fqcn_handler'
+- name: 'common_handlers : test_fqcn_handler'
+ meta: noop
+
+# The following handler starts with 'fqcn : ' and should _not_ be listed as:
+# 'common_handlers : testns.testcoll.common_handlers : test_fqcn_handler`
+# 'testns.testcoll.common_handlers : testns.testcoll.common_handlers : test_fqcn_handler'
+- name: 'testns.testcoll.common_handlers : test_fqcn_handler'
+ meta: noop
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/role_subdir/subdir_testrole/tasks/main.yml b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/role_subdir/subdir_testrole/tasks/main.yml
new file mode 100644
index 0000000..64f5242
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/role_subdir/subdir_testrole/tasks/main.yml
@@ -0,0 +1,10 @@
+- debug:
+ msg: '{{ test_role_input | default("(undefined)") }}'
+ register: test_role_output
+
+- set_fact:
+ testrole_source: collection
+
+- assert:
+ that:
+ - test_role_input is not defined or test_role_input == test_role_output.msg
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/test_fqcn_handlers/meta/main.yml b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/test_fqcn_handlers/meta/main.yml
new file mode 100644
index 0000000..9218f3d
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/test_fqcn_handlers/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - testns.testcoll.common_handlers
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/test_fqcn_handlers/tasks/main.yml b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/test_fqcn_handlers/tasks/main.yml
new file mode 100644
index 0000000..6eadb7c
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/test_fqcn_handlers/tasks/main.yml
@@ -0,0 +1,16 @@
+- name: Fire fqcn handler 1
+ debug:
+ msg: Fire fqcn handler
+ changed_when: true
+ notify:
+ - 'testns.testcoll.common_handlers : test_fqcn_handler'
+ - 'common_handlers : test_fqcn_handler'
+ - 'test_fqcn_handler'
+
+- debug:
+ msg: Fire fqcn handler with role name
+ changed_when: true
+ notify:
+ - 'testns.testcoll.common_handlers : common_handlers test_fqcn_handler'
+ - 'common_handlers : common_handlers test_fqcn_handler'
+ - 'common_handlers test_fqcn_handler'
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/testrole/meta/main.yml b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/testrole/meta/main.yml
new file mode 100644
index 0000000..8c22c1c
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/testrole/meta/main.yml
@@ -0,0 +1,4 @@
+collections:
+- ansible.builtin
+- testns.coll_in_sys
+- bogus.fromrolemeta
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/testrole/tasks/main.yml b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/testrole/tasks/main.yml
new file mode 100644
index 0000000..7c05abb
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/testrole/tasks/main.yml
@@ -0,0 +1,39 @@
+# test using builtin module of multiple types in a role in a collection
+# https://github.com/ansible/ansible/issues/65298
+- name: Run setup module because there is both setup.ps1 and setup.py
+ setup:
+ gather_subset: min
+
+- name: check collections list from role meta
+ plugin_lookup:
+ register: pluginlookup_out
+
+- name: call role-local ping module
+ ping:
+ register: ping_out
+
+- name: call unqualified module in another collection listed in role meta (testns.coll_in_sys)
+ systestmodule:
+ register: systestmodule_out
+
+# verify that pluginloader caching doesn't prevent us from explicitly calling a builtin plugin with the same name
+- name: call builtin ping module explicitly
+ ansible.builtin.ping:
+ register: builtinping_out
+
+- debug:
+ msg: '{{ test_role_input | default("(undefined)") }}'
+ register: test_role_output
+
+- set_fact:
+ testrole_source: collection
+
+# FIXME: add tests to ensure that block/task level stuff in a collection-hosted role properly inherit role default/meta values
+
+- assert:
+ that:
+ - pluginlookup_out.collection_list == ['testns.testcoll', 'ansible.builtin', 'testns.coll_in_sys', 'bogus.fromrolemeta']
+ - ping_out.source is defined and ping_out.source == 'user'
+ - systestmodule_out.source is defined and systestmodule_out.source == 'sys'
+ - builtinping_out.ping is defined and builtinping_out.ping == 'pong'
+ - test_role_input is not defined or test_role_input == test_role_output.msg
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/testrole_main_yaml/meta/main.yml b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/testrole_main_yaml/meta/main.yml
new file mode 100644
index 0000000..8c22c1c
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/testrole_main_yaml/meta/main.yml
@@ -0,0 +1,4 @@
+collections:
+- ansible.builtin
+- testns.coll_in_sys
+- bogus.fromrolemeta
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/testrole_main_yaml/tasks/main.yml b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/testrole_main_yaml/tasks/main.yml
new file mode 100644
index 0000000..31e3af5
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/testrole_main_yaml/tasks/main.yml
@@ -0,0 +1,33 @@
+- name: check collections list from role meta
+ plugin_lookup:
+ register: pluginlookup_out
+
+- name: call role-local ping module
+ ping:
+ register: ping_out
+
+- name: call unqualified module in another collection listed in role meta (testns.coll_in_sys)
+ systestmodule:
+ register: systestmodule_out
+
+# verify that pluginloader caching doesn't prevent us from explicitly calling a builtin plugin with the same name
+- name: call builtin ping module explicitly
+ ansible.builtin.ping:
+ register: builtinping_out
+
+- debug:
+ msg: '{{ test_role_input | default("(undefined)") }}'
+ register: test_role_output
+
+- set_fact:
+ testrole_source: collection
+
+# FIXME: add tests to ensure that block/task level stuff in a collection-hosted role properly inherit role default/meta values
+
+- assert:
+ that:
+ - pluginlookup_out.collection_list == ['testns.testcoll', 'ansible.builtin', 'testns.coll_in_sys', 'bogus.fromrolemeta']
+ - ping_out.source is defined and ping_out.source == 'user'
+ - systestmodule_out.source is defined and systestmodule_out.source == 'sys'
+ - builtinping_out.ping is defined and builtinping_out.ping == 'pong'
+ - test_role_input is not defined or test_role_input == test_role_output.msg
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testredirect/meta/runtime.yml b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testredirect/meta/runtime.yml
new file mode 100644
index 0000000..bb4bd6d
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testredirect/meta/runtime.yml
@@ -0,0 +1,23 @@
+plugin_routing:
+ modules:
+ ping:
+ redirect: testns.testcoll.ping
+ filter:
+ multi_redirect_filter:
+ redirect: testns.testredirect.redirect_filter1
+ deprecation:
+ warning_text: deprecation1
+ redirect_filter1:
+ redirect: testns.testredirect.redirect_filter2
+ deprecation:
+ warning_text: deprecation2
+ redirect_filter2:
+ redirect: testns.testcoll.testfilter
+ deprecation:
+ warning_text: deprecation3
+ dead_end:
+ redirect: testns.testredirect.bad_redirect
+ recursive_redirect:
+ redirect: testns.testredirect.recursive_redirect
+ invalid_redirect:
+ redirect: contextual_redirect
diff --git a/test/integration/targets/collections/collections/ansible_collections/me/mycoll1/plugins/action/action1.py b/test/integration/targets/collections/collections/ansible_collections/me/mycoll1/plugins/action/action1.py
new file mode 100644
index 0000000..e9f9731
--- /dev/null
+++ b/test/integration/targets/collections/collections/ansible_collections/me/mycoll1/plugins/action/action1.py
@@ -0,0 +1,29 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.action import ActionBase
+
+
+class ActionModule(ActionBase):
+
+ def run(self, tmp=None, task_vars=None):
+ ''' handler for file transfer operations '''
+ if task_vars is None:
+ task_vars = dict()
+
+ result = super(ActionModule, self).run(tmp, task_vars)
+
+ if result.get('skipped'):
+ return result
+
+ module_args = self._task.args.copy()
+
+ result.update(
+ self._execute_module(
+ module_name='me.mycoll2.module1',
+ module_args=module_args,
+ task_vars=task_vars,
+ )
+ )
+
+ return result
diff --git a/test/integration/targets/collections/collections/ansible_collections/me/mycoll1/plugins/modules/action1.py b/test/integration/targets/collections/collections/ansible_collections/me/mycoll1/plugins/modules/action1.py
new file mode 100644
index 0000000..66bb5a4
--- /dev/null
+++ b/test/integration/targets/collections/collections/ansible_collections/me/mycoll1/plugins/modules/action1.py
@@ -0,0 +1,24 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['stableinterface'],
+ 'supported_by': 'core'}
+
+DOCUMENTATION = '''
+---
+module: action1
+short_description: Action Test module
+description:
+ - Action Test module
+author:
+ - Ansible Core Team
+'''
+
+EXAMPLES = '''
+'''
+
+RETURN = '''
+'''
diff --git a/test/integration/targets/collections/collections/ansible_collections/me/mycoll2/plugins/modules/module1.py b/test/integration/targets/collections/collections/ansible_collections/me/mycoll2/plugins/modules/module1.py
new file mode 100644
index 0000000..00bb993
--- /dev/null
+++ b/test/integration/targets/collections/collections/ansible_collections/me/mycoll2/plugins/modules/module1.py
@@ -0,0 +1,43 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['stableinterface'],
+ 'supported_by': 'core'}
+
+DOCUMENTATION = '''
+---
+module: module1
+short_description: module1 Test module
+description:
+ - module1 Test module
+author:
+ - Ansible Core Team
+'''
+
+EXAMPLES = '''
+'''
+
+RETURN = '''
+'''
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ desc=dict(type='str'),
+ ),
+ )
+
+ results = dict(msg="you just ran me.mycoll2.module1", desc=module.params.get('desc'))
+
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/cache/custom_jsonfile.py b/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/cache/custom_jsonfile.py
new file mode 100644
index 0000000..7605dc4
--- /dev/null
+++ b/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/cache/custom_jsonfile.py
@@ -0,0 +1,63 @@
+# (c) 2014, Brian Coca, Josh Drake, et al
+# (c) 2017 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ cache: jsonfile
+ short_description: JSON formatted files.
+ description:
+ - This cache uses JSON formatted, per host, files saved to the filesystem.
+ version_added: "1.9"
+ author: Ansible Core (@ansible-core)
+ options:
+ _uri:
+ required: True
+ description:
+ - Path in which the cache plugin will save the JSON files
+ env:
+ - name: ANSIBLE_CACHE_PLUGIN_CONNECTION
+ ini:
+ - key: fact_caching_connection
+ section: defaults
+ _prefix:
+ description: User defined prefix to use when creating the JSON files
+ env:
+ - name: ANSIBLE_CACHE_PLUGIN_PREFIX
+ ini:
+ - key: fact_caching_prefix
+ section: defaults
+ _timeout:
+ default: 86400
+ description: Expiration timeout for the cache plugin data
+ env:
+ - name: ANSIBLE_CACHE_PLUGIN_TIMEOUT
+ ini:
+ - key: fact_caching_timeout
+ section: defaults
+ type: integer
+'''
+
+import codecs
+import json
+
+from ansible.parsing.ajson import AnsibleJSONEncoder, AnsibleJSONDecoder
+from ansible.plugins.cache import BaseFileCacheModule
+
+
+class CacheModule(BaseFileCacheModule):
+ """
+ A caching module backed by json files.
+ """
+
+ def _load(self, filepath):
+ # Valid JSON is always UTF-8 encoded.
+ with codecs.open(filepath, 'r', encoding='utf-8') as f:
+ return json.load(f, cls=AnsibleJSONDecoder)
+
+ def _dump(self, value, filepath):
+ with codecs.open(filepath, 'w', encoding='utf-8') as f:
+ f.write(json.dumps(value, cls=AnsibleJSONEncoder, sort_keys=True, indent=4))
diff --git a/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/inventory/statichost.py b/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/inventory/statichost.py
new file mode 100644
index 0000000..ae6941f
--- /dev/null
+++ b/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/inventory/statichost.py
@@ -0,0 +1,68 @@
+# Copyright (c) 2018 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ inventory: statichost
+ short_description: Add a single host
+ description: Add a single host
+ extends_documentation_fragment:
+ - inventory_cache
+ options:
+ plugin:
+ description: plugin name (must be statichost)
+ required: true
+ hostname:
+ description: Toggle display of stderr even when script was successful
+ required: True
+'''
+
+from ansible.errors import AnsibleParserError
+from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable
+
+
+class InventoryModule(BaseInventoryPlugin, Cacheable):
+
+ NAME = 'testns.content_adj.statichost'
+
+ def __init__(self):
+
+ super(InventoryModule, self).__init__()
+
+ self._hosts = set()
+
+ def verify_file(self, path):
+ ''' Verify if file is usable by this plugin, base does minimal accessibility check '''
+
+ if not path.endswith('.statichost.yml') and not path.endswith('.statichost.yaml'):
+ return False
+ return super(InventoryModule, self).verify_file(path)
+
+ def parse(self, inventory, loader, path, cache=None):
+
+ super(InventoryModule, self).parse(inventory, loader, path)
+
+ # Initialize and validate options
+ self._read_config_data(path)
+
+ # Exercise cache
+ cache_key = self.get_cache_key(path)
+ attempt_to_read_cache = self.get_option('cache') and cache
+ cache_needs_update = self.get_option('cache') and not cache
+ if attempt_to_read_cache:
+ try:
+ host_to_add = self._cache[cache_key]
+ except KeyError:
+ cache_needs_update = True
+ if not attempt_to_read_cache or cache_needs_update:
+ host_to_add = self.get_option('hostname')
+
+ # this is where the magic happens
+ self.inventory.add_host(host_to_add, 'all')
+ self._cache[cache_key] = host_to_add
+
+ # self.inventory.add_group()...
+ # self.inventory.add_child()...
+ # self.inventory.set_variable()..
diff --git a/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/module_utils/__init__.py b/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/module_utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/module_utils/__init__.py
diff --git a/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/module_utils/sub1/__init__.py b/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/module_utils/sub1/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/module_utils/sub1/__init__.py
diff --git a/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/module_utils/sub1/foomodule.py b/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/module_utils/sub1/foomodule.py
new file mode 100644
index 0000000..eeffe01
--- /dev/null
+++ b/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/module_utils/sub1/foomodule.py
@@ -0,0 +1,6 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+def importme():
+ return "hello from {0}".format(__name__)
diff --git a/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/modules/contentadjmodule.py b/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/modules/contentadjmodule.py
new file mode 100644
index 0000000..0fa98eb
--- /dev/null
+++ b/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/modules/contentadjmodule.py
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+
+def main():
+ print(json.dumps(dict(changed=False, source='content_adj')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/vars/custom_adj_vars.py b/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/vars/custom_adj_vars.py
new file mode 100644
index 0000000..0cd9a1d
--- /dev/null
+++ b/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/vars/custom_adj_vars.py
@@ -0,0 +1,45 @@
+# Copyright 2019 RedHat, inc
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+#############################################
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ vars: custom_adj_vars
+ version_added: "2.10"
+ short_description: load host and group vars
+ description: test loading host and group vars from a collection
+ options:
+ stage:
+ default: all
+ choices: ['all', 'inventory', 'task']
+ type: str
+ ini:
+ - key: stage
+ section: custom_adj_vars
+ env:
+ - name: ANSIBLE_VARS_PLUGIN_STAGE
+'''
+
+from ansible.plugins.vars import BaseVarsPlugin
+
+
+class VarsModule(BaseVarsPlugin):
+
+ def get_vars(self, loader, path, entities, cache=True):
+ super(VarsModule, self).get_vars(loader, path, entities)
+ return {'collection': 'adjacent', 'adj_var': 'value'}
diff --git a/test/integration/targets/collections/custom_vars_plugins/v1_vars_plugin.py b/test/integration/targets/collections/custom_vars_plugins/v1_vars_plugin.py
new file mode 100644
index 0000000..b5792d8
--- /dev/null
+++ b/test/integration/targets/collections/custom_vars_plugins/v1_vars_plugin.py
@@ -0,0 +1,37 @@
+# Copyright 2019 RedHat, inc
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+#############################################
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ vars: v1_vars_plugin
+ version_added: "2.10"
+ short_description: load host and group vars
+ description:
+ - 3rd party vars plugin to test loading host and group vars without requiring whitelisting and without a plugin-specific stage option
+ options:
+'''
+
+from ansible.plugins.vars import BaseVarsPlugin
+
+
+class VarsModule(BaseVarsPlugin):
+
+ def get_vars(self, loader, path, entities, cache=True):
+ super(VarsModule, self).get_vars(loader, path, entities)
+ return {'collection': False, 'name': 'v1_vars_plugin', 'v1_vars_plugin': True}
diff --git a/test/integration/targets/collections/custom_vars_plugins/v2_vars_plugin.py b/test/integration/targets/collections/custom_vars_plugins/v2_vars_plugin.py
new file mode 100644
index 0000000..fc14016
--- /dev/null
+++ b/test/integration/targets/collections/custom_vars_plugins/v2_vars_plugin.py
@@ -0,0 +1,45 @@
+# Copyright 2019 RedHat, inc
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+#############################################
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ vars: v2_vars_plugin
+ version_added: "2.10"
+ short_description: load host and group vars
+ description:
+ - 3rd party vars plugin to test loading host and group vars without requiring whitelisting and with a plugin-specific stage option
+ options:
+ stage:
+ choices: ['all', 'inventory', 'task']
+ type: str
+ ini:
+ - key: stage
+ section: other_vars_plugin
+ env:
+ - name: ANSIBLE_VARS_PLUGIN_STAGE
+'''
+
+from ansible.plugins.vars import BaseVarsPlugin
+
+
+class VarsModule(BaseVarsPlugin):
+
+ def get_vars(self, loader, path, entities, cache=True):
+ super(VarsModule, self).get_vars(loader, path, entities)
+ return {'collection': False, 'name': 'v2_vars_plugin', 'v2_vars_plugin': True}
diff --git a/test/integration/targets/collections/filter_plugins/override_formerly_core_masked_filter.py b/test/integration/targets/collections/filter_plugins/override_formerly_core_masked_filter.py
new file mode 100644
index 0000000..600b1fd
--- /dev/null
+++ b/test/integration/targets/collections/filter_plugins/override_formerly_core_masked_filter.py
@@ -0,0 +1,13 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+def override_formerly_core_masked_filter(*args, **kwargs):
+ return 'hello from overridden formerly_core_masked_filter'
+
+
+class FilterModule(object):
+ def filters(self):
+ return {
+ 'formerly_core_masked_filter': override_formerly_core_masked_filter
+ }
diff --git a/test/integration/targets/collections/import_collection_pb.yml b/test/integration/targets/collections/import_collection_pb.yml
new file mode 100644
index 0000000..511d948
--- /dev/null
+++ b/test/integration/targets/collections/import_collection_pb.yml
@@ -0,0 +1,17 @@
+- import_playbook: testns.testcoll.default_collection_playbook.yml
+- import_playbook: testns.testcoll.default_collection_playbook
+
+# test subdirs
+- import_playbook: "testns.testcoll.play"
+- import_playbook: "testns.testcoll.type.play"
+- import_playbook: "testns.testcoll.type.subtype.play"
+
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: check values from imports
+ assert:
+ that:
+ - play is defined
+ - play_type is defined
+ - play_type_subtype is defined
diff --git a/test/integration/targets/collections/includeme.yml b/test/integration/targets/collections/includeme.yml
new file mode 100644
index 0000000..219ee58
--- /dev/null
+++ b/test/integration/targets/collections/includeme.yml
@@ -0,0 +1,6 @@
+- testns.testcoll.plugin_lookup:
+ register: included_plugin_lookup_out
+
+- assert:
+ that:
+ - included_plugin_lookup_out.collection_list == ['bogus.bogus', 'ansible.legacy']
diff --git a/test/integration/targets/collections/inventory_test.yml b/test/integration/targets/collections/inventory_test.yml
new file mode 100644
index 0000000..b508927
--- /dev/null
+++ b/test/integration/targets/collections/inventory_test.yml
@@ -0,0 +1,26 @@
+- name: test a collection-hosted connection plugin against hosts from collection-hosted inventory plugins
+ hosts: dynamic_host_a, dynamic_host_redirected
+ gather_facts: no
+ vars:
+ ansible_connection: testns.testcoll.localconn
+ ansible_localconn_connectionvar: from_play
+ tasks:
+ - raw: echo 'hello world'
+ register: connection_out
+
+ - assert:
+ that:
+ - connection_out.stdout == "localconn ran echo 'hello world'"
+ # ensure that the connection var we overrode above made it into the running config
+ - connection_out.stderr == "connectionvar is from_play"
+
+
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - assert:
+ that:
+ - hostvars['dynamic_host_a'] is defined
+ - hostvars['dynamic_host_a'].connection_out.stdout == "localconn ran echo 'hello world'"
+ - hostvars['dynamic_host_redirected'] is defined
+ - hostvars['dynamic_host_redirected'].connection_out.stdout == "localconn ran echo 'hello world'"
diff --git a/test/integration/targets/collections/invocation_tests.yml b/test/integration/targets/collections/invocation_tests.yml
new file mode 100644
index 0000000..c80e1ed
--- /dev/null
+++ b/test/integration/targets/collections/invocation_tests.yml
@@ -0,0 +1,5 @@
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - name: run action that invokes module from another collection
+ me.mycoll1.action1: desc="this should run me.mycoll2.module1"
diff --git a/test/integration/targets/collections/library/ping.py b/test/integration/targets/collections/library/ping.py
new file mode 100644
index 0000000..7a416a6
--- /dev/null
+++ b/test/integration/targets/collections/library/ping.py
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+
+def main():
+ print(json.dumps(dict(changed=False, source='legacy_library_dir')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/noop.yml b/test/integration/targets/collections/noop.yml
new file mode 100644
index 0000000..81c6e47
--- /dev/null
+++ b/test/integration/targets/collections/noop.yml
@@ -0,0 +1,4 @@
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - debug:
diff --git a/test/integration/targets/collections/posix.yml b/test/integration/targets/collections/posix.yml
new file mode 100644
index 0000000..903fb4f
--- /dev/null
+++ b/test/integration/targets/collections/posix.yml
@@ -0,0 +1,443 @@
+- hosts: testhost
+ tasks:
+ # basic test of FQ module lookup and that we got the right one (user-dir hosted)
+ - name: exec FQ module in a user-dir testns collection
+ testns.testcoll.testmodule:
+ register: testmodule_out
+
+ # verifies that distributed collection subpackages are visible under a multi-location namespace (testns exists in user and sys locations)
+ - name: exec FQ module in a sys-dir testns collection
+ testns.coll_in_sys.systestmodule:
+ register: systestmodule_out
+
+ # verifies that content-adjacent collections were automatically added to the installed content roots
+ - name: exec FQ module from content-adjacent collection
+ testns.content_adj.contentadjmodule:
+ register: contentadjmodule_out
+
+ # content should only be loaded from the first visible instance of a collection
+ - name: attempt to look up FQ module in a masked collection
+ testns.testcoll.plugin_lookup:
+ type: module
+ name: testns.testcoll.maskedmodule
+ register: maskedmodule_out
+
+ # ensure the ansible ns can have real collections added to it
+ - name: call an external module in the ansible namespace
+ ansible.bullcoll.bullmodule:
+ register: bullmodule_out
+
+ # ensure the ansible ns cannot override ansible.builtin externally
+ - name: call an external module in the ansible.builtin collection (should use the built in module)
+ ansible.builtin.ping:
+ register: builtin_ping_out
+
+ # action in a collection subdir
+ - name: test subdir action FQ
+ testns.testcoll.action_subdir.subdir_ping_action:
+ register: subdir_ping_action_out
+
+ # module in a collection subdir
+ - name: test subdir module FQ
+ testns.testcoll.module_subdir.subdir_ping_module:
+ register: subdir_ping_module_out
+
+ # module with a granular module_utils import (from (this collection).module_utils.leaf import thingtocall)
+ - name: exec module with granular module utils import from this collection
+ testns.testcoll.uses_leaf_mu_granular_import:
+ register: granular_out
+
+ # module with a granular nested module_utils import (from (this collection).module_utils.base import thingtocall,
+ # where base imports secondary from the same collection's module_utils)
+ - name: exec module with nested module utils from this collection
+ testns.testcoll.uses_base_mu_granular_nested_import:
+ register: granular_nested_out
+
+ # module with a flat module_utils import (import (this collection).module_utils.leaf)
+ - name: exec module with flat module_utils import from this collection
+ testns.testcoll.uses_leaf_mu_flat_import:
+ register: flat_out
+
+ # module with a full-module module_utils import using 'from' (from (this collection).module_utils import leaf)
+ - name: exec module with full-module module_utils import using 'from' from this collection
+ testns.testcoll.uses_leaf_mu_module_import_from:
+ register: from_out
+
+ # module with multiple levels of the same nested package name and imported as a function
+ - name: exec module with multiple levels of the same nested package name imported as a function
+ testns.testcoll.uses_nested_same_as_func:
+ register: from_nested_func
+
+ # module with multiple levels of the same nested package name and imported as a module
+ - name: exec module with multiple levels of the same nested package name imported as a module
+ testns.testcoll.uses_nested_same_as_module:
+ register: from_nested_module
+
+ # module using a bunch of collection-level redirected module_utils
+ - name: exec module using a bunch of collection-level redirected module_utils
+ testns.testcoll.uses_collection_redirected_mu:
+ register: from_redirected_mu
+
+ # module with bogus MU
+ - name: exec module with bogus MU
+ testns.testcoll.uses_mu_missing:
+ ignore_errors: true
+ register: from_missing_mu
+
+ # module with redirected MU, redirect collection not found
+ - name: exec module with a missing redirect target collection
+ testns.testcoll.uses_mu_missing_redirect_collection:
+ ignore_errors: true
+ register: from_missing_redir_collection
+
+ # module with redirected MU, redirect module not found
+ - name: exec module with a missing redirect target module
+ testns.testcoll.uses_mu_missing_redirect_module:
+ ignore_errors: true
+ register: from_missing_redir_module
+
+ - assert:
+ that:
+ - testmodule_out.source == 'user'
+ - systestmodule_out.source == 'sys'
+ - contentadjmodule_out.source == 'content_adj'
+ - not maskedmodule_out.plugin_path
+ - bullmodule_out.source == 'user_ansible_bullcoll'
+ - builtin_ping_out.source is not defined
+ - builtin_ping_out.ping == 'pong'
+ - subdir_ping_action_out is not changed
+ - subdir_ping_module_out is not changed
+ - granular_out.mu_result == 'thingtocall in leaf'
+ - granular_nested_out.mu_result == 'thingtocall in base called thingtocall in secondary'
+ - flat_out.mu_result == 'thingtocall in leaf'
+ - from_out.mu_result == 'thingtocall in leaf'
+ - from_out.mu2_result == 'thingtocall in secondary'
+ - from_out.mu3_result == 'thingtocall in subpkg.submod'
+ - from_out.mu4_result == 'thingtocall in subpkg_with_init'
+ - from_out.mu5_result == 'thingtocall in mod_in_subpkg_with_init'
+ - from_out.mu6_result == 'thingtocall in subpkg.submod'
+ - from_nested_func.mu_result == 'hello from nested_same'
+ - from_nested_module.mu_result == 'hello from nested_same'
+ - from_redirected_mu.mu_result == 'hello from ansible_collections.testns.content_adj.plugins.module_utils.sub1.foomodule'
+ - from_redirected_mu.mu_result2 == 'hello from testns.othercoll.formerly_testcoll_pkg.thing'
+ - from_redirected_mu.mu_result3 == 'hello from formerly_testcoll_pkg.submod.thing'
+ - from_missing_mu is failed
+ - "'Could not find imported module support' in from_missing_mu.msg"
+ - from_missing_redir_collection is failed
+ - "'unable to locate collection bogusns.boguscoll' in from_missing_redir_collection.msg"
+ - from_missing_redir_module is failed
+ - "'Could not find imported module support code for ansible_collections.testns.testcoll.plugins.modules.uses_mu_missing_redirect_module' in from_missing_redir_module.msg"
+
+
+- hosts: testhost
+ tasks:
+ - name: exercise filters/tests/lookups
+ assert:
+ that:
+ - "'data' | testns.testcoll.testfilter == 'data_via_testfilter_from_userdir'"
+ - "'data' | testns.testcoll.testfilter2 == 'data_via_testfilter2_from_userdir'"
+ - "'data' | testns.testcoll.filter_subdir.test_subdir_filter == 'data_via_testfilter_from_subdir'"
+ - "'from_user' is testns.testcoll.testtest"
+ - "'from_user2' is testns.testcoll.testtest2"
+ - "'subdir_from_user' is testns.testcoll.test_subdir.subdir_test"
+ - lookup('testns.testcoll.mylookup') == 'mylookup_from_user_dir'
+ - lookup('testns.testcoll.mylookup2') == 'mylookup2_from_user_dir'
+ - lookup('testns.testcoll.lookup_subdir.my_subdir_lookup') == 'subdir_lookup_from_user_dir'
+
+ - debug:
+ msg: "{{ 'foo'|testns.testbroken.broken }}"
+ register: result
+ ignore_errors: true
+
+ - assert:
+ that:
+ - |
+ 'This is a broken filter plugin.' in result.msg
+
+ - debug:
+ msg: "{{ 'foo'|missing.collection.filter }}"
+ register: result
+ ignore_errors: true
+
+ - assert:
+ that:
+ - result is failed
+
+# ensure that the synthetic ansible.builtin collection limits to builtin plugins, that ansible.legacy loads overrides
+# from legacy plugin dirs, and that a same-named plugin loaded from a real collection is not masked by the others
+- hosts: testhost
+ tasks:
+ - name: test unqualified ping from library dir
+ ping:
+ register: unqualified_ping_out
+
+ - name: test legacy-qualified ping from library dir
+ ansible.legacy.ping:
+ register: legacy_ping_out
+
+ - name: test builtin ping
+ ansible.builtin.ping:
+ register: builtin_ping_out
+
+ - name: test collection-based ping
+ testns.testcoll.ping:
+ register: collection_ping_out
+
+ - assert:
+ that:
+ - unqualified_ping_out.source == 'legacy_library_dir'
+ - legacy_ping_out.source == 'legacy_library_dir'
+ - builtin_ping_out.ping == 'pong'
+ - collection_ping_out.source == 'user'
+
+# verify the default value for the collections list is empty
+- hosts: testhost
+ tasks:
+ - name: sample default collections value
+ testns.testcoll.plugin_lookup:
+ register: coll_default_out
+
+ - assert:
+ that:
+ # in original release, collections defaults to empty, which is mostly equivalent to ansible.legacy
+ - not coll_default_out.collection_list
+
+
+# ensure that inheritance/masking works as expected, that the proper default values are injected when missing,
+# and that the order is preserved if one of the magic values is explicitly specified
+- name: verify collections keyword play/block/task inheritance and magic values
+ hosts: testhost
+ collections:
+ - bogus.fromplay
+ tasks:
+ - name: sample play collections value
+ testns.testcoll.plugin_lookup:
+ register: coll_play_out
+
+ - name: collections override block-level
+ collections:
+ - bogus.fromblock
+ block:
+ - name: sample block collections value
+ testns.testcoll.plugin_lookup:
+ register: coll_block_out
+
+ - name: sample task collections value
+ collections:
+ - bogus.fromtask
+ testns.testcoll.plugin_lookup:
+ register: coll_task_out
+
+ - name: sample task with explicit core
+ collections:
+ - ansible.builtin
+ - bogus.fromtaskexplicitcore
+ testns.testcoll.plugin_lookup:
+ register: coll_task_core
+
+ - name: sample task with explicit legacy
+ collections:
+ - ansible.legacy
+ - bogus.fromtaskexplicitlegacy
+ testns.testcoll.plugin_lookup:
+ register: coll_task_legacy
+
+ - assert:
+ that:
+ # ensure that parent value inheritance is masked properly by explicit setting
+ - coll_play_out.collection_list == ['bogus.fromplay', 'ansible.legacy']
+ - coll_block_out.collection_list == ['bogus.fromblock', 'ansible.legacy']
+ - coll_task_out.collection_list == ['bogus.fromtask', 'ansible.legacy']
+ - coll_task_core.collection_list == ['ansible.builtin', 'bogus.fromtaskexplicitcore']
+ - coll_task_legacy.collection_list == ['ansible.legacy', 'bogus.fromtaskexplicitlegacy']
+
+- name: verify unqualified plugin resolution behavior
+ hosts: testhost
+ collections:
+ - testns.testcoll
+ - testns.coll_in_sys
+ - testns.contentadj
+ tasks:
+ # basic test of unqualified module lookup and that we got the right one (user-dir hosted, there's another copy of
+ # this one in the same-named collection in sys dir that should be masked
+ - name: exec unqualified module in a user-dir testns collection
+ testmodule:
+ register: testmodule_out
+
+ # use another collection to verify that we're looking in all collections listed on the play
+ - name: exec unqualified module in a sys-dir testns collection
+ systestmodule:
+ register: systestmodule_out
+
+ - assert:
+ that:
+ - testmodule_out.source == 'user'
+ - systestmodule_out.source == 'sys'
+
+# test keyword-static execution of a FQ collection-backed role with "tasks/main.yaml"
+- name: verify collection-backed role execution (keyword static)
+ hosts: testhost
+ collections:
+ # set to ansible.builtin only to ensure that roles function properly without inheriting the play's collections config
+ - ansible.builtin
+ vars:
+ test_role_input: keyword static
+ roles:
+ - role: testns.testcoll.testrole_main_yaml
+ tasks:
+ - name: ensure role executed
+ assert:
+ that:
+ - test_role_output.msg == test_role_input
+ - testrole_source == 'collection'
+
+
+# test dynamic execution of a FQ collection-backed role
+- name: verify collection-backed role execution (dynamic)
+ hosts: testhost
+ collections:
+ # set to ansible.builtin only to ensure that roles function properly without inheriting the play's collections config
+ - ansible.builtin
+ vars:
+ test_role_input: dynamic
+ tasks:
+ - include_role:
+ name: testns.testcoll.testrole
+ - name: ensure role executed
+ assert:
+ that:
+ - test_role_output.msg == test_role_input
+ - testrole_source == 'collection'
+
+# test task-static execution of a FQ collection-backed role
+- name: verify collection-backed role execution (task static)
+ hosts: testhost
+ collections:
+ - ansible.builtin
+ vars:
+ test_role_input: task static
+ tasks:
+ - import_role:
+ name: testns.testcoll.testrole
+ - name: ensure role executed
+ assert:
+ that:
+ - test_role_output.msg == test_role_input
+ - testrole_source == 'collection'
+
+
+# test a legacy playbook-adjacent role, ensure that play collections config is not inherited
+- name: verify legacy playbook-adjacent role behavior
+ hosts: testhost
+ collections:
+ - bogus.bogus
+ vars:
+ test_role_input: legacy playbook-adjacent
+ roles:
+ - testrole
+# FIXME: this should technically work to look up a playbook-adjacent role
+# - ansible.legacy.testrole
+ tasks:
+ - name: ensure role executed
+ assert:
+ that:
+ - test_role_output.msg == test_role_input
+ - testrole_source == 'legacy roles dir'
+
+
+# test dynamic execution of a FQ collection-backed role
+- name: verify collection-backed role execution in subdir (include)
+ hosts: testhost
+ vars:
+ test_role_input: dynamic (subdir)
+ tasks:
+ - include_role:
+ name: testns.testcoll.role_subdir.subdir_testrole
+ - name: ensure role executed
+ assert:
+ that:
+ - test_role_output.msg == test_role_input
+ - testrole_source == 'collection'
+
+
+# test collection-relative role deps (keyword static)
+- name: verify collection-relative role deps
+ hosts: testhost
+ vars:
+ outer_role_input: keyword static outer
+ test_role_input: keyword static inner
+ roles:
+ - testns.testcoll.calls_intra_collection_dep_role_unqualified
+ tasks:
+ - assert:
+ that:
+ - outer_role_output.msg == outer_role_input
+ - test_role_output.msg == test_role_input
+ - testrole_source == 'collection'
+
+# test collection-relative role deps (task static)
+- name: verify collection-relative role deps
+ hosts: testhost
+ vars:
+ outer_role_input: task static outer
+ test_role_input: task static inner
+ tasks:
+ - import_role:
+ name: testns.testcoll.calls_intra_collection_dep_role_unqualified
+ - assert:
+ that:
+ - outer_role_output.msg == outer_role_input
+ - test_role_output.msg == test_role_input
+ - testrole_source == 'collection'
+
+# test collection-relative role deps (task dynamic)
+- name: verify collection-relative role deps
+ hosts: testhost
+ vars:
+ outer_role_input: task dynamic outer
+ test_role_input: task dynamic inner
+ tasks:
+ - include_role:
+ name: testns.testcoll.calls_intra_collection_dep_role_unqualified
+ - assert:
+ that:
+ - outer_role_output.msg == outer_role_input
+ - test_role_output.msg == test_role_input
+ - testrole_source == 'collection'
+
+
+- name: validate static task include behavior
+ hosts: testhost
+ collections:
+ - bogus.bogus
+ tasks:
+ - import_tasks: includeme.yml
+
+
+- name: validate dynamic task include behavior
+ hosts: testhost
+ collections:
+ - bogus.bogus
+ tasks:
+ - include_tasks: includeme.yml
+
+
+- import_playbook: test_collection_meta.yml
+- name: Test FQCN handlers
+ hosts: testhost
+ vars:
+ handler_counter: 0
+ roles:
+ - testns.testcoll.test_fqcn_handlers
+
+- name: Ensure a collection role can call a standalone role
+ hosts: testhost
+ roles:
+ - testns.testcoll.call_standalone
+
+# Issue https://github.com/ansible/ansible/issues/69054
+- name: Test collection as string
+ hosts: testhost
+ collections: foo
+ tasks:
+ - debug: msg="Test"
diff --git a/test/integration/targets/collections/redirected.statichost.yml b/test/integration/targets/collections/redirected.statichost.yml
new file mode 100644
index 0000000..8cfab46
--- /dev/null
+++ b/test/integration/targets/collections/redirected.statichost.yml
@@ -0,0 +1,3 @@
+# use a plugin redirected by core to a collection to ensure inventory redirection and redirected config names are working
+plugin: formerly_core_inventory # this is defined in the ansible-core runtime.yml routing to point at testns.content_adj.statichost
+hostname: dynamic_host_redirected
diff --git a/test/integration/targets/collections/roles/standalone/tasks/main.yml b/test/integration/targets/collections/roles/standalone/tasks/main.yml
new file mode 100644
index 0000000..b4dd23d
--- /dev/null
+++ b/test/integration/targets/collections/roles/standalone/tasks/main.yml
@@ -0,0 +1,2 @@
+- set_fact:
+ standalone_role_var: True
diff --git a/test/integration/targets/collections/roles/testrole/tasks/main.yml b/test/integration/targets/collections/roles/testrole/tasks/main.yml
new file mode 100644
index 0000000..cbf6b8e
--- /dev/null
+++ b/test/integration/targets/collections/roles/testrole/tasks/main.yml
@@ -0,0 +1,28 @@
+- debug:
+ msg: executing testrole from legacy playbook-adjacent roles dir
+
+- name: exec a FQ module from a legacy role
+ testns.testcoll.testmodule:
+ register: coll_module_out
+
+- name: exec a legacy playbook-adjacent module from a legacy role
+ ping:
+ register: ping_out
+
+- name: sample collections list inside a legacy role (should be empty)
+ testns.testcoll.plugin_lookup:
+ register: plugin_lookup_out
+
+- debug:
+ msg: '{{ test_role_input | default("(undefined)") }}'
+ register: test_role_output
+
+- set_fact:
+ testrole_source: legacy roles dir
+
+- assert:
+ that:
+ - coll_module_out.source == 'user'
+ # ensure we used the library/ ping override, not the builtin or one from another collection
+ - ping_out.source == 'legacy_library_dir'
+ - not plugin_lookup_out.collection_list
diff --git a/test/integration/targets/collections/runme.sh b/test/integration/targets/collections/runme.sh
new file mode 100755
index 0000000..5f11abe
--- /dev/null
+++ b/test/integration/targets/collections/runme.sh
@@ -0,0 +1,150 @@
+#!/usr/bin/env bash
+
+set -eux
+
+export ANSIBLE_COLLECTIONS_PATH=$PWD/collection_root_user:$PWD/collection_root_sys
+export ANSIBLE_GATHERING=explicit
+export ANSIBLE_GATHER_SUBSET=minimal
+export ANSIBLE_HOST_PATTERN_MISMATCH=error
+unset ANSIBLE_COLLECTIONS_ON_ANSIBLE_VERSION_MISMATCH
+
+# ensure we can call collection module
+ansible localhost -m testns.testcoll.testmodule
+
+# ensure we can call collection module with ansible_collections in path
+ANSIBLE_COLLECTIONS_PATH=$PWD/collection_root_sys/ansible_collections ansible localhost -m testns.testcoll.testmodule
+
+
+echo "--- validating callbacks"
+# validate FQ callbacks in ansible-playbook
+ANSIBLE_CALLBACKS_ENABLED=testns.testcoll.usercallback ansible-playbook noop.yml | grep "usercallback says ok"
+# use adhoc for the rest of these tests, must force it to load other callbacks
+export ANSIBLE_LOAD_CALLBACK_PLUGINS=1
+# validate redirected callback
+ANSIBLE_CALLBACKS_ENABLED=formerly_core_callback ansible localhost -m debug 2>&1 | grep -- "usercallback says ok"
+## validate missing redirected callback
+ANSIBLE_CALLBACKS_ENABLED=formerly_core_missing_callback ansible localhost -m debug 2>&1 | grep -- "Skipping callback plugin 'formerly_core_missing_callback'"
+## validate redirected + removed callback (fatal)
+ANSIBLE_CALLBACKS_ENABLED=formerly_core_removed_callback ansible localhost -m debug 2>&1 | grep -- "testns.testcoll.removedcallback has been removed"
+# validate avoiding duplicate loading of callback, even if using diff names
+[ "$(ANSIBLE_CALLBACKS_ENABLED=testns.testcoll.usercallback,formerly_core_callback ansible localhost -m debug 2>&1 | grep -c 'usercallback says ok')" = "1" ]
+# ensure non existing callback does not crash ansible
+ANSIBLE_CALLBACKS_ENABLED=charlie.gomez.notme ansible localhost -m debug 2>&1 | grep -- "Skipping callback plugin 'charlie.gomez.notme'"
+
+unset ANSIBLE_LOAD_CALLBACK_PLUGINS
+# adhoc normally shouldn't load non-default plugins- let's be sure
+output=$(ANSIBLE_CALLBACKS_ENABLED=testns.testcoll.usercallback ansible localhost -m debug)
+if [[ "${output}" =~ "usercallback says ok" ]]; then echo fail; exit 1; fi
+
+echo "--- validating docs"
+# test documentation
+ansible-doc testns.testcoll.testmodule -vvv | grep -- "- normal_doc_frag"
+# same with symlink
+ln -s "${PWD}/testcoll2" ./collection_root_sys/ansible_collections/testns/testcoll2
+ansible-doc testns.testcoll2.testmodule2 -vvv | grep "Test module"
+# now test we can list with symlink
+ansible-doc -l -vvv| grep "testns.testcoll2.testmodule2"
+
+echo "testing bad doc_fragments (expected ERROR message follows)"
+# test documentation failure
+ansible-doc testns.testcoll.testmodule_bad_docfrags -vvv 2>&1 | grep -- "unknown doc_fragment"
+
+echo "--- validating default collection"
+# test adhoc default collection resolution (use unqualified collection module with playbook dir under its collection)
+
+echo "testing adhoc default collection support with explicit playbook dir"
+ANSIBLE_PLAYBOOK_DIR=./collection_root_user/ansible_collections/testns/testcoll ansible localhost -m testmodule
+
+# we need multiple plays, and conditional import_playbook is noisy and causes problems, so choose here which one to use...
+if [[ ${INVENTORY_PATH} == *.winrm ]]; then
+ export TEST_PLAYBOOK=windows.yml
+else
+ export TEST_PLAYBOOK=posix.yml
+
+ echo "testing default collection support"
+ ansible-playbook -i "${INVENTORY_PATH}" collection_root_user/ansible_collections/testns/testcoll/playbooks/default_collection_playbook.yml "$@"
+fi
+
+# test redirects and warnings for filter redirects
+echo "testing redirect and deprecation display"
+ANSIBLE_DEPRECATION_WARNINGS=yes ansible localhost -m debug -a msg='{{ "data" | testns.testredirect.multi_redirect_filter }}' -vvvvv 2>&1 | tee out.txt
+cat out.txt
+
+test "$(grep out.txt -ce 'deprecation1' -ce 'deprecation2' -ce 'deprecation3')" == 3
+grep out.txt -e 'redirecting (type: filter) testns.testredirect.multi_redirect_filter to testns.testredirect.redirect_filter1'
+grep out.txt -e 'redirecting (type: filter) testns.testredirect.redirect_filter1 to testns.testredirect.redirect_filter2'
+grep out.txt -e 'redirecting (type: filter) testns.testredirect.redirect_filter2 to testns.testcoll.testfilter'
+
+echo "--- validating collections support in playbooks/roles"
+# run test playbooks
+ansible-playbook -i "${INVENTORY_PATH}" -v "${TEST_PLAYBOOK}" "$@"
+
+if [[ ${INVENTORY_PATH} != *.winrm ]]; then
+ ansible-playbook -i "${INVENTORY_PATH}" -v invocation_tests.yml "$@"
+fi
+
+echo "--- validating bypass_host_loop with collection search"
+ansible-playbook -i host1,host2, -v test_bypass_host_loop.yml "$@"
+
+echo "--- validating inventory"
+# test collection inventories
+ansible-playbook inventory_test.yml -i a.statichost.yml -i redirected.statichost.yml "$@"
+
+if [[ ${INVENTORY_PATH} != *.winrm ]]; then
+ # base invocation tests
+ ansible-playbook -i "${INVENTORY_PATH}" -v invocation_tests.yml "$@"
+
+ # run playbook from collection, test default again, but with FQCN
+ ansible-playbook -i "${INVENTORY_PATH}" testns.testcoll.default_collection_playbook.yml "$@"
+
+ # run playbook from collection, test default again, but with FQCN and no extension
+ ansible-playbook -i "${INVENTORY_PATH}" testns.testcoll.default_collection_playbook "$@"
+
+ # run playbook that imports from collection
+ ansible-playbook -i "${INVENTORY_PATH}" import_collection_pb.yml "$@"
+fi
+
+# test collection inventories
+ansible-playbook inventory_test.yml -i a.statichost.yml -i redirected.statichost.yml "$@"
+
+# test plugin loader redirect_list
+ansible-playbook test_redirect_list.yml -v "$@"
+
+# test ansiballz cache dupe
+ansible-playbook ansiballz_dupe/test_ansiballz_cache_dupe_shortname.yml -v "$@"
+
+# test adjacent with --playbook-dir
+export ANSIBLE_COLLECTIONS_PATH=''
+ANSIBLE_INVENTORY_ANY_UNPARSED_IS_FAILED=1 ansible-inventory --list --export --playbook-dir=. -v "$@"
+
+# use an inventory source with caching enabled
+ansible-playbook -i a.statichost.yml -i ./cache.statichost.yml -v check_populated_inventory.yml
+
+# Check that the inventory source with caching enabled was stored
+if [[ "$(find ./inventory_cache -type f ! -path "./inventory_cache/.keep" | wc -l)" -ne "1" ]]; then
+ echo "Failed to find the expected single cache"
+ exit 1
+fi
+
+CACHEFILE="$(find ./inventory_cache -type f ! -path './inventory_cache/.keep')"
+
+if [[ $CACHEFILE != ./inventory_cache/prefix_* ]]; then
+ echo "Unexpected cache file"
+ exit 1
+fi
+
+# Check the cache for the expected hosts
+
+if [[ "$(grep -wc "cache_host_a" "$CACHEFILE")" -ne "1" ]]; then
+ echo "Failed to cache host as expected"
+ exit 1
+fi
+
+if [[ "$(grep -wc "dynamic_host_a" "$CACHEFILE")" -ne "0" ]]; then
+ echo "Cached an incorrect source"
+ exit 1
+fi
+
+./vars_plugin_tests.sh
+
+./test_task_resolved_plugin.sh
diff --git a/test/integration/targets/collections/test_bypass_host_loop.yml b/test/integration/targets/collections/test_bypass_host_loop.yml
new file mode 100644
index 0000000..71f48d5
--- /dev/null
+++ b/test/integration/targets/collections/test_bypass_host_loop.yml
@@ -0,0 +1,19 @@
+- name: Test collection lookup bypass host list
+ hosts: all
+ connection: local
+ gather_facts: false
+ collections:
+ - testns.testcoll
+ tasks:
+ - bypass_host_loop:
+ register: bypass
+
+ - run_once: true
+ vars:
+ bypass_hosts: '{{ hostvars|dictsort|map(attribute="1.bypass.bypass_inventory_hostname")|select("defined")|unique }}'
+ block:
+ - debug:
+ var: bypass_hosts
+
+ - assert:
+ that: bypass_hosts|length == 1
diff --git a/test/integration/targets/collections/test_collection_meta.yml b/test/integration/targets/collections/test_collection_meta.yml
new file mode 100644
index 0000000..e4c4d30
--- /dev/null
+++ b/test/integration/targets/collections/test_collection_meta.yml
@@ -0,0 +1,75 @@
+- hosts: localhost
+ gather_facts: no
+ collections:
+ - testns.testcoll
+ vars:
+ # redirect connection
+ ansible_connection: testns.testcoll.redirected_local
+ tasks:
+ - assert:
+ that: ('data' | testns.testcoll.testfilter) == 'data_via_testfilter_from_userdir'
+
+ # redirect module (multiple levels)
+ - multilevel1:
+ # redirect action
+ - uses_redirected_action:
+ # redirect import (consumed via action)
+ - uses_redirected_import:
+ # redirect lookup
+ - assert:
+ that: lookup('formerly_core_lookup') == 'mylookup_from_user_dir'
+ # redirect filter
+ - assert:
+ that: ('yes' | formerly_core_filter) == True
+ # redirect filter (multiple levels)
+ - assert:
+ that: ('data' | testns.testredirect.multi_redirect_filter) == 'data_via_testfilter_from_userdir'
+ # invalid filter redirect
+ - debug: msg="{{ 'data' | testns.testredirect.dead_end }}"
+ ignore_errors: yes
+ register: redirect_failure
+ - assert:
+ that:
+ - redirect_failure is failed
+ - "'Could not load \"testns.testredirect.dead_end\"' in redirect_failure.msg"
+ # recursive filter redirect
+ - debug: msg="{{ 'data' | testns.testredirect.recursive_redirect }}"
+ ignore_errors: yes
+ register: redirect_failure
+ - assert:
+ that:
+ - redirect_failure is failed
+ - '"recursive collection redirect found for ''testns.testredirect.recursive_redirect''" in redirect_failure.msg'
+ # invalid filter redirect
+ - debug: msg="{{ 'data' | testns.testredirect.invalid_redirect }}"
+ ignore_errors: yes
+ register: redirect_failure
+ - assert:
+ that:
+ - redirect_failure is failed
+ - error in redirect_failure.msg
+ vars:
+ error: "Collection testns.testredirect contains invalid redirect for testns.testredirect.invalid_redirect: contextual_redirect"
+ # legacy filter should mask redirected
+ - assert:
+ that: ('' | formerly_core_masked_filter) == 'hello from overridden formerly_core_masked_filter'
+ # redirect test
+ - assert:
+ that:
+ - "'stuff' is formerly_core_test('tuf')"
+ - "'hello override' is formerly_core_masked_test"
+ # redirect module (formerly internal)
+ - formerly_core_ping:
+ # redirect module from collection (with subdir)
+ - testns.testcoll.module_subdir.subdir_ping_module:
+ # redirect module_utils plugin (consumed via module)
+ - uses_core_redirected_mu:
+ # deprecated module (issues warning)
+ - deprecated_ping:
+ # redirect module (internal alias)
+ - aliased_ping:
+ # redirect module (cycle detection, fatal)
+# - looped_ping:
+
+ # removed module (fatal)
+# - dead_ping:
diff --git a/test/integration/targets/collections/test_plugins/override_formerly_core_masked_test.py b/test/integration/targets/collections/test_plugins/override_formerly_core_masked_test.py
new file mode 100644
index 0000000..11c7f7a
--- /dev/null
+++ b/test/integration/targets/collections/test_plugins/override_formerly_core_masked_test.py
@@ -0,0 +1,16 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+def override_formerly_core_masked_test(value, *args, **kwargs):
+ if value != 'hello override':
+ raise Exception('expected "hello override" only...')
+
+ return True
+
+
+class TestModule(object):
+ def tests(self):
+ return {
+ 'formerly_core_masked_test': override_formerly_core_masked_test
+ }
diff --git a/test/integration/targets/collections/test_redirect_list.yml b/test/integration/targets/collections/test_redirect_list.yml
new file mode 100644
index 0000000..8a24b96
--- /dev/null
+++ b/test/integration/targets/collections/test_redirect_list.yml
@@ -0,0 +1,86 @@
+---
+- hosts: localhost
+ gather_facts: no
+ module_defaults:
+ testns.testcoll.plugin_lookup:
+ type: module
+ tasks:
+ - name: test builtin
+ testns.testcoll.plugin_lookup:
+ name: dnf
+ register: result
+ failed_when:
+ - result['redirect_list'] != ['dnf'] or result['plugin_path'].endswith('library/dnf.py')
+
+ - name: test builtin with collections kw
+ testns.testcoll.plugin_lookup:
+ name: dnf
+ register: result
+ failed_when:
+ - result['redirect_list'] != ['dnf'] or result['plugin_path'].endswith('library/dnf.py')
+ collections:
+ - testns.unrelatedcoll
+
+ - name: test redirected builtin
+ testns.testcoll.plugin_lookup:
+ name: formerly_core_ping
+ register: result
+ failed_when: result['redirect_list'] != expected_redirect_list
+ vars:
+ expected_redirect_list:
+ - formerly_core_ping
+ - ansible.builtin.formerly_core_ping
+ - testns.testcoll.ping
+
+ - name: test redirected builtin with collections kw
+ testns.testcoll.plugin_lookup:
+ name: formerly_core_ping
+ register: result
+ failed_when: result['redirect_list'] != expected_redirect_list
+ vars:
+ expected_redirect_list:
+ - formerly_core_ping
+ - ansible.builtin.formerly_core_ping
+ - testns.testcoll.ping
+ collections:
+ - testns.unrelatedcoll
+ - testns.testcoll
+
+ - name: test collection module with collections kw
+ testns.testcoll.plugin_lookup:
+ name: ping
+ register: result
+ failed_when: result['redirect_list'] != expected_redirect_list
+ vars:
+ expected_redirect_list:
+ - ping
+ - testns.testcoll.ping
+ collections:
+ - testns.unrelatedcoll
+ - testns.testcoll
+
+ - name: test redirected collection module with collections kw
+ testns.testcoll.plugin_lookup:
+ name: ping
+ register: result
+ failed_when: result['redirect_list'] != expected_redirect_list
+ vars:
+ expected_redirect_list:
+ - ping
+ - testns.testredirect.ping
+ - testns.testcoll.ping
+ collections:
+ - testns.unrelatedcoll
+ - testns.testredirect
+
+ - name: test legacy module with collections kw
+ testns.testcoll.plugin_lookup:
+ name: ping
+ register: result
+ failed_when:
+ - result['redirect_list'] != expected_redirect_list or not result['plugin_path'].endswith('library/ping.py')
+ vars:
+ expected_redirect_list:
+ - ping
+ collections:
+ - testns.unrelatedcoll
diff --git a/test/integration/targets/collections/test_task_resolved_plugin.sh b/test/integration/targets/collections/test_task_resolved_plugin.sh
new file mode 100755
index 0000000..444b4f1
--- /dev/null
+++ b/test/integration/targets/collections/test_task_resolved_plugin.sh
@@ -0,0 +1,48 @@
+#!/usr/bin/env bash
+
+set -eux
+
+export ANSIBLE_CALLBACKS_ENABLED=display_resolved_action
+
+ansible-playbook test_task_resolved_plugin/unqualified.yml "$@" | tee out.txt
+action_resolution=(
+ "legacy_action == legacy_action"
+ "legacy_module == legacy_module"
+ "debug == ansible.builtin.debug"
+ "ping == ansible.builtin.ping"
+)
+for result in "${action_resolution[@]}"; do
+ grep -q out.txt -e "$result"
+done
+
+ansible-playbook test_task_resolved_plugin/unqualified_and_collections_kw.yml "$@" | tee out.txt
+action_resolution=(
+ "legacy_action == legacy_action"
+ "legacy_module == legacy_module"
+ "debug == ansible.builtin.debug"
+ "ping == ansible.builtin.ping"
+ "collection_action == test_ns.test_coll.collection_action"
+ "collection_module == test_ns.test_coll.collection_module"
+ "formerly_action == test_ns.test_coll.collection_action"
+ "formerly_module == test_ns.test_coll.collection_module"
+)
+for result in "${action_resolution[@]}"; do
+ grep -q out.txt -e "$result"
+done
+
+ansible-playbook test_task_resolved_plugin/fqcn.yml "$@" | tee out.txt
+action_resolution=(
+ "ansible.legacy.legacy_action == legacy_action"
+ "ansible.legacy.legacy_module == legacy_module"
+ "ansible.legacy.debug == ansible.builtin.debug"
+ "ansible.legacy.ping == ansible.builtin.ping"
+ "ansible.builtin.debug == ansible.builtin.debug"
+ "ansible.builtin.ping == ansible.builtin.ping"
+ "test_ns.test_coll.collection_action == test_ns.test_coll.collection_action"
+ "test_ns.test_coll.collection_module == test_ns.test_coll.collection_module"
+ "test_ns.test_coll.formerly_action == test_ns.test_coll.collection_action"
+ "test_ns.test_coll.formerly_module == test_ns.test_coll.collection_module"
+)
+for result in "${action_resolution[@]}"; do
+ grep -q out.txt -e "$result"
+done
diff --git a/test/integration/targets/collections/test_task_resolved_plugin/action_plugins/legacy_action.py b/test/integration/targets/collections/test_task_resolved_plugin/action_plugins/legacy_action.py
new file mode 100644
index 0000000..fa4d514
--- /dev/null
+++ b/test/integration/targets/collections/test_task_resolved_plugin/action_plugins/legacy_action.py
@@ -0,0 +1,14 @@
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.action import ActionBase
+
+
+class ActionModule(ActionBase):
+ TRANSFERS_FILES = False
+ _VALID_ARGS = frozenset()
+
+ def run(self, tmp=None, task_vars=None):
+ return {'changed': False}
diff --git a/test/integration/targets/collections/test_task_resolved_plugin/callback_plugins/display_resolved_action.py b/test/integration/targets/collections/test_task_resolved_plugin/callback_plugins/display_resolved_action.py
new file mode 100644
index 0000000..23cce10
--- /dev/null
+++ b/test/integration/targets/collections/test_task_resolved_plugin/callback_plugins/display_resolved_action.py
@@ -0,0 +1,37 @@
+# (c) 2020 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ name: display_resolved_action
+ type: aggregate
+ short_description: Displays the requested and resolved actions at the end of a playbook.
+ description:
+ - Displays the requested and resolved actions in the format "requested == resolved".
+ requirements:
+ - Enable in configuration.
+'''
+
+from ansible import constants as C
+from ansible.plugins.callback import CallbackBase
+
+
+class CallbackModule(CallbackBase):
+
+ CALLBACK_VERSION = 2.0
+ CALLBACK_TYPE = 'aggregate'
+ CALLBACK_NAME = 'display_resolved_action'
+ CALLBACK_NEEDS_ENABLED = True
+
+ def __init__(self, *args, **kwargs):
+ super(CallbackModule, self).__init__(*args, **kwargs)
+ self.requested_to_resolved = {}
+
+ def v2_playbook_on_task_start(self, task, is_conditional):
+ self.requested_to_resolved[task.action] = task.resolved_action
+
+ def v2_playbook_on_stats(self, stats):
+ for requested, resolved in self.requested_to_resolved.items():
+ self._display.display("%s == %s" % (requested, resolved), screen_only=True)
diff --git a/test/integration/targets/collections/test_task_resolved_plugin/collections/ansible_collections/test_ns/test_coll/meta/runtime.yml b/test/integration/targets/collections/test_task_resolved_plugin/collections/ansible_collections/test_ns/test_coll/meta/runtime.yml
new file mode 100644
index 0000000..8c27dba
--- /dev/null
+++ b/test/integration/targets/collections/test_task_resolved_plugin/collections/ansible_collections/test_ns/test_coll/meta/runtime.yml
@@ -0,0 +1,7 @@
+plugin_routing:
+ modules:
+ formerly_module:
+ redirect: test_ns.test_coll.collection_module
+ action:
+ formerly_action:
+ redirect: test_ns.test_coll.collection_action
diff --git a/test/integration/targets/collections/test_task_resolved_plugin/collections/ansible_collections/test_ns/test_coll/plugins/action/collection_action.py b/test/integration/targets/collections/test_task_resolved_plugin/collections/ansible_collections/test_ns/test_coll/plugins/action/collection_action.py
new file mode 100644
index 0000000..fa4d514
--- /dev/null
+++ b/test/integration/targets/collections/test_task_resolved_plugin/collections/ansible_collections/test_ns/test_coll/plugins/action/collection_action.py
@@ -0,0 +1,14 @@
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.action import ActionBase
+
+
+class ActionModule(ActionBase):
+ TRANSFERS_FILES = False
+ _VALID_ARGS = frozenset()
+
+ def run(self, tmp=None, task_vars=None):
+ return {'changed': False}
diff --git a/test/integration/targets/collections/test_task_resolved_plugin/collections/ansible_collections/test_ns/test_coll/plugins/modules/collection_module.py b/test/integration/targets/collections/test_task_resolved_plugin/collections/ansible_collections/test_ns/test_coll/plugins/modules/collection_module.py
new file mode 100644
index 0000000..8f31226
--- /dev/null
+++ b/test/integration/targets/collections/test_task_resolved_plugin/collections/ansible_collections/test_ns/test_coll/plugins/modules/collection_module.py
@@ -0,0 +1,29 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+---
+module: collection_module
+short_description: A module to test a task's resolved action name.
+description: A module to test a task's resolved action name.
+options: {}
+author: Ansible Core Team
+notes:
+ - Supports C(check_mode).
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(supports_check_mode=True, argument_spec={})
+ module.exit_json(changed=False)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/test_task_resolved_plugin/fqcn.yml b/test/integration/targets/collections/test_task_resolved_plugin/fqcn.yml
new file mode 100644
index 0000000..ab9e925
--- /dev/null
+++ b/test/integration/targets/collections/test_task_resolved_plugin/fqcn.yml
@@ -0,0 +1,14 @@
+---
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - ansible.legacy.legacy_action:
+ - ansible.legacy.legacy_module:
+ - ansible.legacy.debug:
+ - ansible.legacy.ping:
+ - ansible.builtin.debug:
+ - ansible.builtin.ping:
+ - test_ns.test_coll.collection_action:
+ - test_ns.test_coll.collection_module:
+ - test_ns.test_coll.formerly_action:
+ - test_ns.test_coll.formerly_module:
diff --git a/test/integration/targets/collections/test_task_resolved_plugin/library/legacy_module.py b/test/integration/targets/collections/test_task_resolved_plugin/library/legacy_module.py
new file mode 100644
index 0000000..4fd7587
--- /dev/null
+++ b/test/integration/targets/collections/test_task_resolved_plugin/library/legacy_module.py
@@ -0,0 +1,29 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+---
+module: legacy_module
+short_description: A module to test a task's resolved action name.
+description: A module to test a task's resolved action name.
+options: {}
+author: Ansible Core Team
+notes:
+ - Supports C(check_mode).
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(supports_check_mode=True, argument_spec={})
+ module.exit_json(changed=False)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/test_task_resolved_plugin/unqualified.yml b/test/integration/targets/collections/test_task_resolved_plugin/unqualified.yml
new file mode 100644
index 0000000..076b8cc
--- /dev/null
+++ b/test/integration/targets/collections/test_task_resolved_plugin/unqualified.yml
@@ -0,0 +1,8 @@
+---
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - legacy_action:
+ - legacy_module:
+ - debug:
+ - ping:
diff --git a/test/integration/targets/collections/test_task_resolved_plugin/unqualified_and_collections_kw.yml b/test/integration/targets/collections/test_task_resolved_plugin/unqualified_and_collections_kw.yml
new file mode 100644
index 0000000..5af4eda
--- /dev/null
+++ b/test/integration/targets/collections/test_task_resolved_plugin/unqualified_and_collections_kw.yml
@@ -0,0 +1,14 @@
+---
+- hosts: localhost
+ gather_facts: no
+ collections:
+ - test_ns.test_coll
+ tasks:
+ - legacy_action:
+ - legacy_module:
+ - debug:
+ - ping:
+ - collection_action:
+ - collection_module:
+ - formerly_action:
+ - formerly_module:
diff --git a/test/integration/targets/collections/testcoll2/MANIFEST.json b/test/integration/targets/collections/testcoll2/MANIFEST.json
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/collections/testcoll2/MANIFEST.json
diff --git a/test/integration/targets/collections/testcoll2/plugins/modules/testmodule2.py b/test/integration/targets/collections/testcoll2/plugins/modules/testmodule2.py
new file mode 100644
index 0000000..7f6eb02
--- /dev/null
+++ b/test/integration/targets/collections/testcoll2/plugins/modules/testmodule2.py
@@ -0,0 +1,33 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['stableinterface'],
+ 'supported_by': 'core'}
+
+DOCUMENTATION = '''
+---
+module: testmodule2
+short_description: Test module
+description:
+ - Test module
+author:
+ - Ansible Core Team
+'''
+
+EXAMPLES = '''
+'''
+
+RETURN = '''
+'''
+
+import json
+
+
+def main():
+ print(json.dumps(dict(changed=False, source='sys')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/vars_plugin_tests.sh b/test/integration/targets/collections/vars_plugin_tests.sh
new file mode 100755
index 0000000..b808897
--- /dev/null
+++ b/test/integration/targets/collections/vars_plugin_tests.sh
@@ -0,0 +1,87 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# Collections vars plugins must be enabled using the FQCN in the 'enabled' list, because PluginLoader.all() does not search collections
+
+# Let vars plugins run for inventory by using the global setting
+export ANSIBLE_RUN_VARS_PLUGINS=start
+
+# Test vars plugin in a playbook-adjacent collection
+export ANSIBLE_VARS_ENABLED=testns.content_adj.custom_adj_vars
+
+ansible-inventory -i a.statichost.yml --list --playbook-dir=./ 2>&1 | tee out.txt
+
+grep '"collection": "adjacent"' out.txt
+grep '"adj_var": "value"' out.txt
+grep -v "REQUIRES_ENABLED is not supported" out.txt
+
+# Test vars plugin in a collection path
+export ANSIBLE_VARS_ENABLED=testns.testcoll.custom_vars
+export ANSIBLE_COLLECTIONS_PATH=$PWD/collection_root_user:$PWD/collection_root_sys
+
+ansible-inventory -i a.statichost.yml --list --playbook-dir=./ 2>&1 | tee out.txt
+
+grep '"collection": "collection_root_user"' out.txt
+grep -v '"adj_var": "value"' out.txt
+grep "REQUIRES_ENABLED is not supported" out.txt
+
+# Test enabled vars plugins order reflects the order in which variables are merged
+export ANSIBLE_VARS_ENABLED=testns.content_adj.custom_adj_vars,testns.testcoll.custom_vars
+
+ansible-inventory -i a.statichost.yml --list --playbook-dir=./ | tee out.txt
+
+grep '"collection": "collection_root_user"' out.txt
+grep '"adj_var": "value"' out.txt
+grep -v '"collection": "adjacent"' out.txt
+
+# Test that 3rd party plugins in plugin_path do not need to require enabling by default
+# Plugins shipped with Ansible and in the custom plugin dir should be used first
+export ANSIBLE_VARS_PLUGINS=./custom_vars_plugins
+
+ansible-inventory -i a.statichost.yml --list --playbook-dir=./ | tee out.txt
+
+grep '"name": "v2_vars_plugin"' out.txt
+grep '"collection": "collection_root_user"' out.txt
+grep '"adj_var": "value"' out.txt
+
+# Test plugins in plugin paths that opt-in to require enabling
+unset ANSIBLE_VARS_ENABLED
+unset ANSIBLE_COLLECTIONS_PATH
+
+
+# Test vars plugins that support the stage setting don't run for inventory when stage is set to 'task'
+# and that the vars plugins that don't support the stage setting don't run for inventory when the global setting is 'demand'
+ANSIBLE_VARS_PLUGIN_STAGE=task ansible-inventory -i a.statichost.yml --list --playbook-dir=./ | tee out.txt
+
+grep -v '"v1_vars_plugin": true' out.txt
+grep -v '"v2_vars_plugin": true' out.txt
+grep -v '"collection": "adjacent"' out.txt
+grep -v '"collection": "collection_root_user"' out.txt
+grep -v '"adj_var": "value"' out.txt
+
+# Test that the global setting allows v1 and v2 plugins to run after importing inventory
+ANSIBLE_RUN_VARS_PLUGINS=start ansible-inventory -i a.statichost.yml --list --playbook-dir=./ | tee out.txt
+
+grep '"v1_vars_plugin": true' out.txt
+grep '"v2_vars_plugin": true' out.txt
+grep '"name": "v2_vars_plugin"' out.txt
+
+# Test that vars plugins in collections and in the vars plugin path are available for tasks
+cat << EOF > "test_task_vars.yml"
+---
+- hosts: localhost
+ connection: local
+ gather_facts: no
+ tasks:
+ - debug: msg="{{ name }}"
+ - debug: msg="{{ collection }}"
+ - debug: msg="{{ adj_var }}"
+EOF
+
+export ANSIBLE_VARS_ENABLED=testns.content_adj.custom_adj_vars
+
+ANSIBLE_VARS_PLUGIN_STAGE=task ANSIBLE_VARS_PLUGINS=./custom_vars_plugins ansible-playbook test_task_vars.yml | grep "ok=3"
+ANSIBLE_RUN_VARS_PLUGINS=start ANSIBLE_VARS_PLUGIN_STAGE=inventory ANSIBLE_VARS_PLUGINS=./custom_vars_plugins ansible-playbook test_task_vars.yml | grep "ok=3"
+ANSIBLE_RUN_VARS_PLUGINS=demand ANSIBLE_VARS_PLUGIN_STAGE=inventory ANSIBLE_VARS_PLUGINS=./custom_vars_plugins ansible-playbook test_task_vars.yml | grep "ok=3"
+ANSIBLE_VARS_PLUGINS=./custom_vars_plugins ansible-playbook test_task_vars.yml | grep "ok=3"
diff --git a/test/integration/targets/collections/windows.yml b/test/integration/targets/collections/windows.yml
new file mode 100644
index 0000000..cf98ca1
--- /dev/null
+++ b/test/integration/targets/collections/windows.yml
@@ -0,0 +1,34 @@
+- hosts: windows
+ tasks:
+ - testns.testcoll.win_selfcontained:
+ register: selfcontained_out
+
+ - testns.testcoll.win_csbasic_only:
+ register: csbasic_only_out
+
+ - testns.testcoll.win_uses_coll_psmu:
+ register: uses_coll_psmu
+
+ - testns.testcoll.win_uses_coll_csmu:
+ register: uses_coll_csmu
+
+ - testns.testcoll.win_uses_optional:
+ register: uses_coll_optional
+
+ - assert:
+ that:
+ - selfcontained_out.source == 'user'
+ - csbasic_only_out.source == 'user'
+ # win_uses_coll_psmu
+ - uses_coll_psmu.source == 'user'
+ - "'user_mu' in uses_coll_psmu.ping"
+ - uses_coll_psmu.subpkg == 'from subpkg.subps.psm1'
+ # win_uses_coll_csmu
+ - uses_coll_csmu.source == 'user'
+ - "'user_mu' in uses_coll_csmu.ping"
+ - "'Hello from subpkg.subcs' in uses_coll_csmu.ping"
+ - uses_coll_csmu.subpkg == 'Hello from subpkg.subcs'
+ - uses_coll_csmu.type_accelerator == uses_coll_csmu.ping
+ # win_uses_optional
+ - uses_coll_optional.data == "called from optional user_mu"
+ - uses_coll_optional.csharp == "Hello from user_mu collection-hosted MyCSMUOptional, also Hello from nested user-collection-hosted AnotherCSMU and Hello from subpkg.subcs"
diff --git a/test/integration/targets/collections_plugin_namespace/aliases b/test/integration/targets/collections_plugin_namespace/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/collections_plugin_namespace/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/plugins/filter/test_filter.py b/test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/plugins/filter/test_filter.py
new file mode 100644
index 0000000..dca094b
--- /dev/null
+++ b/test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/plugins/filter/test_filter.py
@@ -0,0 +1,15 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+def filter_name(a):
+ return __name__
+
+
+class FilterModule(object):
+ def filters(self):
+ filters = {
+ 'filter_name': filter_name,
+ }
+
+ return filters
diff --git a/test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/plugins/lookup/lookup_name.py b/test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/plugins/lookup/lookup_name.py
new file mode 100644
index 0000000..d0af703
--- /dev/null
+++ b/test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/plugins/lookup/lookup_name.py
@@ -0,0 +1,9 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.lookup import LookupBase
+
+
+class LookupModule(LookupBase):
+ def run(self, terms, variables, **kwargs):
+ return [__name__]
diff --git a/test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/plugins/lookup/lookup_no_future_boilerplate.py b/test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/plugins/lookup/lookup_no_future_boilerplate.py
new file mode 100644
index 0000000..79e80f6
--- /dev/null
+++ b/test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/plugins/lookup/lookup_no_future_boilerplate.py
@@ -0,0 +1,10 @@
+# do not add future boilerplate to this plugin
+# specifically, do not add absolute_import, as the purpose of this plugin is to test implicit relative imports on Python 2.x
+__metaclass__ = type
+
+from ansible.plugins.lookup import LookupBase
+
+
+class LookupModule(LookupBase):
+ def run(self, terms, variables, **kwargs):
+ return [__name__]
diff --git a/test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/plugins/test/test_test.py b/test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/plugins/test/test_test.py
new file mode 100644
index 0000000..1739072
--- /dev/null
+++ b/test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/plugins/test/test_test.py
@@ -0,0 +1,13 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+def test_name_ok(value):
+ return __name__ == 'ansible_collections.my_ns.my_col.plugins.test.test_test'
+
+
+class TestModule:
+ def tests(self):
+ return {
+ 'test_name_ok': test_name_ok,
+ }
diff --git a/test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/roles/test/tasks/main.yml b/test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/roles/test/tasks/main.yml
new file mode 100644
index 0000000..d80f547
--- /dev/null
+++ b/test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/roles/test/tasks/main.yml
@@ -0,0 +1,12 @@
+- set_fact:
+ filter_name: "{{ 1 | my_ns.my_col.filter_name }}"
+ lookup_name: "{{ lookup('my_ns.my_col.lookup_name') }}"
+ lookup_no_future_boilerplate: "{{ lookup('my_ns.my_col.lookup_no_future_boilerplate') }}"
+ test_name_ok: "{{ 1 is my_ns.my_col.test_name_ok }}"
+
+- assert:
+ that:
+ - filter_name == 'ansible_collections.my_ns.my_col.plugins.filter.test_filter'
+ - lookup_name == 'ansible_collections.my_ns.my_col.plugins.lookup.lookup_name'
+ - lookup_no_future_boilerplate == 'ansible_collections.my_ns.my_col.plugins.lookup.lookup_no_future_boilerplate'
+ - test_name_ok
diff --git a/test/integration/targets/collections_plugin_namespace/runme.sh b/test/integration/targets/collections_plugin_namespace/runme.sh
new file mode 100755
index 0000000..96e83d3
--- /dev/null
+++ b/test/integration/targets/collections_plugin_namespace/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ANSIBLE_COLLECTIONS_PATH="${PWD}/collection_root" ansible-playbook test.yml -i ../../inventory "$@"
diff --git a/test/integration/targets/collections_plugin_namespace/test.yml b/test/integration/targets/collections_plugin_namespace/test.yml
new file mode 100644
index 0000000..d1c3f1b
--- /dev/null
+++ b/test/integration/targets/collections_plugin_namespace/test.yml
@@ -0,0 +1,3 @@
+- hosts: testhost
+ roles:
+ - my_ns.my_col.test
diff --git a/test/integration/targets/collections_relative_imports/aliases b/test/integration/targets/collections_relative_imports/aliases
new file mode 100644
index 0000000..996481b
--- /dev/null
+++ b/test/integration/targets/collections_relative_imports/aliases
@@ -0,0 +1,4 @@
+posix
+shippable/posix/group1
+shippable/windows/group1
+windows
diff --git a/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/PSRel1.psm1 b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/PSRel1.psm1
new file mode 100644
index 0000000..bf81264
--- /dev/null
+++ b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/PSRel1.psm1
@@ -0,0 +1,11 @@
+#AnsibleRequires -PowerShell .sub_pkg.PSRel2
+
+Function Invoke-FromPSRel1 {
+ <#
+ .SYNOPSIS
+ Test function
+ #>
+ return "$(Invoke-FromPSRel2) -> Invoke-FromPSRel1"
+}
+
+Export-ModuleMember -Function Invoke-FromPSRel1
diff --git a/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/PSRel4.psm1 b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/PSRel4.psm1
new file mode 100644
index 0000000..bcb5ec1
--- /dev/null
+++ b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/PSRel4.psm1
@@ -0,0 +1,12 @@
+#AnsibleRequires -CSharpUtil .sub_pkg.CSRel5 -Optional
+#AnsibleRequires -PowerShell .sub_pkg.PSRelInvalid -Optional
+
+Function Invoke-FromPSRel4 {
+ <#
+ .SYNOPSIS
+ Test function
+ #>
+ return "Invoke-FromPSRel4"
+}
+
+Export-ModuleMember -Function Invoke-FromPSRel4
diff --git a/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/my_util1.py b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/my_util1.py
new file mode 100644
index 0000000..196b4ab
--- /dev/null
+++ b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/my_util1.py
@@ -0,0 +1,6 @@
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+def one():
+ return 1
diff --git a/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/my_util2.py b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/my_util2.py
new file mode 100644
index 0000000..0d985bf
--- /dev/null
+++ b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/my_util2.py
@@ -0,0 +1,8 @@
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from .my_util1 import one
+
+
+def two():
+ return one() * 2
diff --git a/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/my_util3.py b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/my_util3.py
new file mode 100644
index 0000000..1529d7b
--- /dev/null
+++ b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/my_util3.py
@@ -0,0 +1,8 @@
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from . import my_util2
+
+
+def three():
+ return my_util2.two() + 1
diff --git a/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/sub_pkg/PSRel2.psm1 b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/sub_pkg/PSRel2.psm1
new file mode 100644
index 0000000..d0aa368
--- /dev/null
+++ b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/sub_pkg/PSRel2.psm1
@@ -0,0 +1,11 @@
+#AnsibleRequires -PowerShell ansible_collections.my_ns.my_col2.plugins.module_utils.PSRel3
+
+Function Invoke-FromPSRel2 {
+ <#
+ .SYNOPSIS
+ Test function
+ #>
+ return "$(Invoke-FromPSRel3) -> Invoke-FromPSRel2"
+}
+
+Export-ModuleMember -Function Invoke-FromPSRel2
diff --git a/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/modules/my_module.py b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/modules/my_module.py
new file mode 100644
index 0000000..0cdf500
--- /dev/null
+++ b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/modules/my_module.py
@@ -0,0 +1,24 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+from ..module_utils.my_util2 import two
+from ..module_utils import my_util3
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(),
+ supports_check_mode=True
+ )
+
+ result = dict(
+ two=two(),
+ three=my_util3.three(),
+ )
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/modules/win_relative.ps1 b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/modules/win_relative.ps1
new file mode 100644
index 0000000..383df0a
--- /dev/null
+++ b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/modules/win_relative.ps1
@@ -0,0 +1,10 @@
+#!powershell
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#AnsibleRequires -PowerShell ..module_utils.PSRel1
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
+
+$module.Result.data = Invoke-FromPSRel1
+
+$module.ExitJson()
diff --git a/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/modules/win_relative_optional.ps1 b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/modules/win_relative_optional.ps1
new file mode 100644
index 0000000..9086ca4
--- /dev/null
+++ b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/modules/win_relative_optional.ps1
@@ -0,0 +1,17 @@
+#!powershell
+
+#AnsibleRequires -CSharpUtil Ansible.Basic -Optional
+#AnsibleRequires -PowerShell ..module_utils.PSRel4 -optional
+
+# These do not exist
+#AnsibleRequires -CSharpUtil ..invalid_package.name -Optional
+#AnsibleRequires -CSharpUtil ..module_utils.InvalidName -optional
+#AnsibleRequires -PowerShell ..invalid_package.pwsh_name -optional
+#AnsibleRequires -PowerShell ..module_utils.InvalidPwshName -Optional
+
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
+
+$module.Result.data = Invoke-FromPSRel4
+
+$module.ExitJson()
diff --git a/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/roles/test/tasks/main.yml b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/roles/test/tasks/main.yml
new file mode 100644
index 0000000..9ba0f7e
--- /dev/null
+++ b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/roles/test/tasks/main.yml
@@ -0,0 +1,4 @@
+- name: fully qualified module usage with relative imports
+ my_ns.my_col.my_module:
+- name: collection relative module usage with relative imports
+ my_module:
diff --git a/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col2/plugins/module_utils/PSRel3.psm1 b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col2/plugins/module_utils/PSRel3.psm1
new file mode 100644
index 0000000..46edd5a
--- /dev/null
+++ b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col2/plugins/module_utils/PSRel3.psm1
@@ -0,0 +1,11 @@
+#AnsibleRequires -CSharpUtil .sub_pkg.CSRel4
+
+Function Invoke-FromPSRel3 {
+ <#
+ .SYNOPSIS
+ Test function
+ #>
+ return "$([CSRel4]::Invoke()) -> Invoke-FromPSRel3"
+}
+
+Export-ModuleMember -Function Invoke-FromPSRel3
diff --git a/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col2/plugins/module_utils/sub_pkg/CSRel4.cs b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col2/plugins/module_utils/sub_pkg/CSRel4.cs
new file mode 100644
index 0000000..c50024b
--- /dev/null
+++ b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col2/plugins/module_utils/sub_pkg/CSRel4.cs
@@ -0,0 +1,14 @@
+using System;
+
+//TypeAccelerator -Name CSRel4 -TypeName TestClass
+
+namespace ansible_collections.my_ns.my_col.plugins.module_utils.sub_pkg.CSRel4
+{
+ public class TestClass
+ {
+ public static string Invoke()
+ {
+ return "CSRel4.Invoke()";
+ }
+ }
+}
diff --git a/test/integration/targets/collections_relative_imports/runme.sh b/test/integration/targets/collections_relative_imports/runme.sh
new file mode 100755
index 0000000..754efaf
--- /dev/null
+++ b/test/integration/targets/collections_relative_imports/runme.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# we need multiple plays, and conditional import_playbook is noisy and causes problems, so choose here which one to use...
+if [[ ${INVENTORY_PATH} == *.winrm ]]; then
+ export TEST_PLAYBOOK=windows.yml
+else
+ export TEST_PLAYBOOK=test.yml
+
+fi
+
+ANSIBLE_COLLECTIONS_PATH="${PWD}/collection_root" ansible-playbook "${TEST_PLAYBOOK}" -i "${INVENTORY_PATH}" "$@"
diff --git a/test/integration/targets/collections_relative_imports/test.yml b/test/integration/targets/collections_relative_imports/test.yml
new file mode 100644
index 0000000..d1c3f1b
--- /dev/null
+++ b/test/integration/targets/collections_relative_imports/test.yml
@@ -0,0 +1,3 @@
+- hosts: testhost
+ roles:
+ - my_ns.my_col.test
diff --git a/test/integration/targets/collections_relative_imports/windows.yml b/test/integration/targets/collections_relative_imports/windows.yml
new file mode 100644
index 0000000..3a3c548
--- /dev/null
+++ b/test/integration/targets/collections_relative_imports/windows.yml
@@ -0,0 +1,20 @@
+- hosts: windows
+ gather_facts: no
+ tasks:
+ - name: test out relative imports on Windows modules
+ my_ns.my_col.win_relative:
+ register: win_relative
+
+ - name: assert relative imports on Windows modules
+ assert:
+ that:
+ - win_relative.data == 'CSRel4.Invoke() -> Invoke-FromPSRel3 -> Invoke-FromPSRel2 -> Invoke-FromPSRel1'
+
+ - name: test out relative imports on Windows modules with optional import
+ my_ns.my_col.win_relative_optional:
+ register: win_relative_optional
+
+ - name: assert relative imports on Windows modules with optional import
+ assert:
+ that:
+ - win_relative_optional.data == 'Invoke-FromPSRel4'
diff --git a/test/integration/targets/collections_runtime_pythonpath/aliases b/test/integration/targets/collections_runtime_pythonpath/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/collections_runtime_pythonpath/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-boo/ansible_collections/python/dist/plugins/modules/boo.py b/test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-boo/ansible_collections/python/dist/plugins/modules/boo.py
new file mode 100644
index 0000000..a2313b1
--- /dev/null
+++ b/test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-boo/ansible_collections/python/dist/plugins/modules/boo.py
@@ -0,0 +1,28 @@
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""Say hello."""
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec={
+ 'name': {'default': 'world'},
+ },
+ )
+ name = module.params['name']
+
+ module.exit_json(
+ msg='Greeting {name} completed.'.
+ format(name=name.title()),
+ greeting='Hello, {name}!'.format(name=name),
+ )
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-boo/pyproject.toml b/test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-boo/pyproject.toml
new file mode 100644
index 0000000..feec734
--- /dev/null
+++ b/test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-boo/pyproject.toml
@@ -0,0 +1,6 @@
+[build-system]
+requires = [
+ "setuptools >= 44",
+ "wheel",
+]
+build-backend = "setuptools.build_meta"
diff --git a/test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-boo/setup.cfg b/test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-boo/setup.cfg
new file mode 100644
index 0000000..d25ebb0
--- /dev/null
+++ b/test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-boo/setup.cfg
@@ -0,0 +1,15 @@
+[metadata]
+name = ansible-collections.python.dist
+version = 1.0.0rc2.post3.dev4
+
+[options]
+package_dir =
+ = .
+packages =
+ ansible_collections
+ ansible_collections.python
+ ansible_collections.python.dist
+ ansible_collections.python.dist.plugins
+ ansible_collections.python.dist.plugins.modules
+zip_safe = True
+include_package_data = True
diff --git a/test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-foo/ansible_collections/python/dist/plugins/modules/boo.py b/test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-foo/ansible_collections/python/dist/plugins/modules/boo.py
new file mode 100644
index 0000000..1ef0333
--- /dev/null
+++ b/test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-foo/ansible_collections/python/dist/plugins/modules/boo.py
@@ -0,0 +1,28 @@
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""Say hello in Ukrainian."""
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec={
+ 'name': {'default': 'Ñвіт'},
+ },
+ )
+ name = module.params['name']
+
+ module.exit_json(
+ msg='Greeting {name} completed.'.
+ format(name=name.title()),
+ greeting='Привіт, {name}!'.format(name=name),
+ )
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections_runtime_pythonpath/runme.sh b/test/integration/targets/collections_runtime_pythonpath/runme.sh
new file mode 100755
index 0000000..38c6c64
--- /dev/null
+++ b/test/integration/targets/collections_runtime_pythonpath/runme.sh
@@ -0,0 +1,60 @@
+#!/usr/bin/env bash
+
+set -eux -o pipefail
+
+
+export PIP_DISABLE_PIP_VERSION_CHECK=1
+
+
+source virtualenv.sh
+
+
+>&2 echo \
+ === Test that the module \
+ gets picked up if discoverable \
+ via PYTHONPATH env var ===
+PYTHONPATH="${PWD}/ansible-collection-python-dist-boo:$PYTHONPATH" \
+ansible \
+ -m python.dist.boo \
+ -a 'name=Bob' \
+ -c local localhost \
+ "$@" | grep -E '"greeting": "Hello, Bob!",'
+
+
+>&2 echo \
+ === Test that the module \
+ gets picked up if installed \
+ into site-packages ===
+python -m pip install pep517
+( # Build a binary Python dist (a wheel) using PEP517:
+ cp -r ansible-collection-python-dist-boo "${OUTPUT_DIR}/"
+ cd "${OUTPUT_DIR}/ansible-collection-python-dist-boo"
+ python -m pep517.build --binary --out-dir dist .
+)
+# Install a pre-built dist with pip:
+python -m pip install \
+ --no-index \
+ -f "${OUTPUT_DIR}/ansible-collection-python-dist-boo/dist/" \
+ --only-binary=ansible-collections.python.dist \
+ ansible-collections.python.dist
+python -m pip show ansible-collections.python.dist
+ansible \
+ -m python.dist.boo \
+ -a 'name=Frodo' \
+ -c local localhost \
+ "$@" | grep -E '"greeting": "Hello, Frodo!",'
+
+
+>&2 echo \
+ === Test that ansible_collections \
+ root takes precedence over \
+ PYTHONPATH/site-packages ===
+# This is done by injecting a module with the same FQCN
+# into another collection root.
+ANSIBLE_COLLECTIONS_PATH="${PWD}/ansible-collection-python-dist-foo" \
+PYTHONPATH="${PWD}/ansible-collection-python-dist-boo:$PYTHONPATH" \
+ansible \
+ -m python.dist.boo \
+ -a 'name=Степан' \
+ -c local localhost \
+ "$@" | grep -E '"greeting": "Привіт, Степан!",'
diff --git a/test/integration/targets/command_nonexisting/aliases b/test/integration/targets/command_nonexisting/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/command_nonexisting/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/command_nonexisting/tasks/main.yml b/test/integration/targets/command_nonexisting/tasks/main.yml
new file mode 100644
index 0000000..d21856e
--- /dev/null
+++ b/test/integration/targets/command_nonexisting/tasks/main.yml
@@ -0,0 +1,4 @@
+- command: commandthatdoesnotexist --would-be-awkward
+ register: res
+ changed_when: "'changed' in res.stdout"
+ failed_when: "res.stdout != '' or res.stderr != ''" \ No newline at end of file
diff --git a/test/integration/targets/command_shell/aliases b/test/integration/targets/command_shell/aliases
new file mode 100644
index 0000000..a1bd994
--- /dev/null
+++ b/test/integration/targets/command_shell/aliases
@@ -0,0 +1,3 @@
+command
+shippable/posix/group2
+shell
diff --git a/test/integration/targets/command_shell/files/create_afile.sh b/test/integration/targets/command_shell/files/create_afile.sh
new file mode 100755
index 0000000..e6fae44
--- /dev/null
+++ b/test/integration/targets/command_shell/files/create_afile.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+echo "win" > "$1" \ No newline at end of file
diff --git a/test/integration/targets/command_shell/files/remove_afile.sh b/test/integration/targets/command_shell/files/remove_afile.sh
new file mode 100755
index 0000000..4a7fea6
--- /dev/null
+++ b/test/integration/targets/command_shell/files/remove_afile.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+rm "$1" \ No newline at end of file
diff --git a/test/integration/targets/command_shell/files/test.sh b/test/integration/targets/command_shell/files/test.sh
new file mode 100755
index 0000000..ade17e9
--- /dev/null
+++ b/test/integration/targets/command_shell/files/test.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+echo -n "win" \ No newline at end of file
diff --git a/test/integration/targets/command_shell/meta/main.yml b/test/integration/targets/command_shell/meta/main.yml
new file mode 100644
index 0000000..cb6005d
--- /dev/null
+++ b/test/integration/targets/command_shell/meta/main.yml
@@ -0,0 +1,3 @@
+dependencies:
+ - prepare_tests
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/command_shell/tasks/main.yml b/test/integration/targets/command_shell/tasks/main.yml
new file mode 100644
index 0000000..12a944c
--- /dev/null
+++ b/test/integration/targets/command_shell/tasks/main.yml
@@ -0,0 +1,548 @@
+# Test code for the command and shell modules.
+
+# Copyright: (c) 2014, Richard Isaacson <richard.c.isaacson@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+- name: use command with unsupported executable arg
+ command: ls /dev/null
+ args:
+ executable: /bogus
+ register: executable
+
+- name: assert executable warning was reported
+ assert:
+ that:
+ - executable.stdout == '/dev/null'
+ - executable.warnings | length() == 1
+ - "'no longer supported' in executable.warnings[0]"
+
+- name: use command with no command
+ command:
+ args:
+ chdir: /
+ register: no_command
+ ignore_errors: true
+
+- name: assert executable fails with no command
+ assert:
+ that:
+ - no_command is failed
+ - no_command.msg == 'no command given'
+ - no_command.rc == 256
+
+- name: use argv
+ command:
+ argv:
+ - echo
+ - testing
+ register: argv_command
+ ignore_errors: true
+
+- name: assert executable works with argv
+ assert:
+ that:
+ - "argv_command.stdout == 'testing'"
+
+- name: use argv and command string
+ command: echo testing
+ args:
+ argv:
+ - echo
+ - testing
+ register: argv_and_string_command
+ ignore_errors: true
+
+- name: assert executable fails with both argv and command string
+ assert:
+ that:
+ - argv_and_string_command is failed
+ - argv_and_string_command.msg == 'only command or argv can be given, not both'
+ - argv_and_string_command.rc == 256
+
+- set_fact:
+ remote_tmp_dir_test: "{{ remote_tmp_dir }}/test_command_shell"
+
+- name: make sure our testing sub-directory does not exist
+ file:
+ path: "{{ remote_tmp_dir_test }}"
+ state: absent
+
+- name: create our testing sub-directory
+ file:
+ path: "{{ remote_tmp_dir_test }}"
+ state: directory
+
+- name: prep our test script
+ copy:
+ src: test.sh
+ dest: "{{ remote_tmp_dir_test }}"
+ mode: '0755'
+
+- name: prep our test script
+ copy:
+ src: create_afile.sh
+ dest: "{{ remote_tmp_dir_test }}"
+ mode: '0755'
+
+- name: prep our test script
+ copy:
+ src: remove_afile.sh
+ dest: "{{ remote_tmp_dir_test }}"
+ mode: '0755'
+
+- name: locate bash
+ shell: which bash
+ register: bash
+
+##
+## command
+##
+
+- name: execute the test.sh script via command
+ command: "{{ remote_tmp_dir_test }}/test.sh"
+ register: command_result0
+
+- name: assert that the script executed correctly
+ assert:
+ that:
+ - command_result0.rc == 0
+ - command_result0.stderr == ''
+ - command_result0.stdout == 'win'
+
+# executable
+
+# FIXME doesn't have the expected stdout.
+
+#- name: execute the test.sh script with executable via command
+# command: "{{remote_tmp_dir_test }}/test.sh executable={{ bash.stdout }}"
+# register: command_result1
+#
+#- name: assert that the script executed correctly with command
+# assert:
+# that:
+# - "command_result1.rc == 0"
+# - "command_result1.stderr == ''"
+# - "command_result1.stdout == 'win'"
+
+# chdir
+
+- name: execute the test.sh script with chdir via command
+ command: ./test.sh
+ args:
+ chdir: "{{ remote_tmp_dir_test }}"
+ register: command_result2
+
+- name: Check invalid chdir
+ command: echo
+ args:
+ chdir: "{{ remote_tmp_dir }}/nope"
+ ignore_errors: yes
+ register: chdir_invalid
+
+- name: assert that the script executed correctly with chdir
+ assert:
+ that:
+ - command_result2.rc == 0
+ - command_result2.stderr == ''
+ - command_result2.stdout == 'win'
+ - chdir_invalid is failed
+ - chdir_invalid.msg is search('Unable to change directory')
+
+# creates
+
+- name: verify that afile.txt is absent
+ file:
+ path: "{{ remote_tmp_dir_test }}/afile.txt"
+ state: absent
+
+- name: create afile.txt with create_afile.sh via command (check mode)
+ command: "{{ remote_tmp_dir_test }}/create_afile.sh {{remote_tmp_dir_test }}/afile.txt"
+ args:
+ creates: "{{ remote_tmp_dir_test }}/afile.txt"
+ register: check_mode_result
+ check_mode: yes
+
+- assert:
+ that:
+ - check_mode_result.changed
+ - "'skipped' not in check_mode_result"
+
+- name: verify that afile.txt still does not exist
+ stat:
+ path: "{{remote_tmp_dir_test}}/afile.txt"
+ register: stat_result
+ failed_when: stat_result.stat.exists
+
+- name: create afile.txt with create_afile.sh via command
+ command: "{{ remote_tmp_dir_test }}/create_afile.sh {{remote_tmp_dir_test }}/afile.txt"
+ args:
+ creates: "{{ remote_tmp_dir_test }}/afile.txt"
+
+- name: verify that afile.txt is present
+ file:
+ path: "{{ remote_tmp_dir_test }}/afile.txt"
+ state: file
+
+- name: re-run previous command using creates with globbing (check mode)
+ command: "{{ remote_tmp_dir_test }}/create_afile.sh {{ remote_tmp_dir_test }}/afile.txt"
+ args:
+ creates: "{{ remote_tmp_dir_test }}/afile.*"
+ register: check_mode_result
+ check_mode: yes
+
+- assert:
+ that:
+ - not check_mode_result.changed
+ - "'skipped' not in check_mode_result"
+
+- name: re-run previous command using creates with globbing
+ command: "{{ remote_tmp_dir_test }}/create_afile.sh {{ remote_tmp_dir_test }}/afile.txt"
+ args:
+ creates: "{{ remote_tmp_dir_test }}/afile.*"
+ register: command_result3
+
+- name: assert that creates with globbing is working
+ assert:
+ that:
+ - command_result3 is not changed
+
+# removes
+
+- name: remove afile.txt with remote_afile.sh via command (check mode)
+ command: "{{ remote_tmp_dir_test }}/remove_afile.sh {{ remote_tmp_dir_test }}/afile.txt"
+ args:
+ removes: "{{ remote_tmp_dir_test }}/afile.txt"
+ register: check_mode_result
+ check_mode: yes
+
+- assert:
+ that:
+ - check_mode_result.changed
+ - "'skipped' not in check_mode_result"
+
+- name: verify that afile.txt still exists
+ stat:
+ path: "{{remote_tmp_dir_test}}/afile.txt"
+ register: stat_result
+ failed_when: not stat_result.stat.exists
+
+- name: remove afile.txt with remote_afile.sh via command
+ command: "{{ remote_tmp_dir_test }}/remove_afile.sh {{ remote_tmp_dir_test }}/afile.txt"
+ args:
+ removes: "{{ remote_tmp_dir_test }}/afile.txt"
+
+- name: verify that afile.txt is absent
+ file: path={{remote_tmp_dir_test}}/afile.txt state=absent
+
+- name: re-run previous command using removes with globbing (check mode)
+ command: "{{ remote_tmp_dir_test }}/remove_afile.sh {{ remote_tmp_dir_test }}/afile.txt"
+ args:
+ removes: "{{ remote_tmp_dir_test }}/afile.*"
+ register: check_mode_result
+ check_mode: yes
+
+- assert:
+ that:
+ - not check_mode_result.changed
+ - "'skipped' not in check_mode_result"
+
+- name: re-run previous command using removes with globbing
+ command: "{{ remote_tmp_dir_test }}/remove_afile.sh {{ remote_tmp_dir_test }}/afile.txt"
+ args:
+ removes: "{{ remote_tmp_dir_test }}/afile.*"
+ register: command_result4
+
+- name: assert that removes with globbing is working
+ assert:
+ that:
+ - command_result4.changed != True
+
+- name: pass stdin to cat via command
+ command: cat
+ args:
+ stdin: 'foobar'
+ register: command_result5
+
+- name: assert that stdin is passed
+ assert:
+ that:
+ - command_result5.stdout == 'foobar'
+
+- name: send to stdin literal multiline block
+ command: "{{ ansible_python.executable }} -c 'import hashlib, sys; print(hashlib.sha1((sys.stdin.buffer if hasattr(sys.stdin, \"buffer\") else sys.stdin).read()).hexdigest())'"
+ args:
+ stdin: |-
+ this is the first line
+ this is the second line
+
+ this line is after an empty line
+ this line is the last line
+ register: command_result6
+
+- name: assert the multiline input was passed correctly
+ assert:
+ that:
+ - "command_result6.stdout == '9cd0697c6a9ff6689f0afb9136fa62e0b3fee903'"
+
+##
+## shell
+##
+
+- name: Execute the test.sh script
+ shell: "{{ remote_tmp_dir_test }}/test.sh"
+ register: shell_result0
+
+- name: Assert that the script executed correctly
+ assert:
+ that:
+ - shell_result0 is changed
+ - shell_result0.cmd == '{{ remote_tmp_dir_test }}/test.sh'
+ - shell_result0.rc == 0
+ - shell_result0.stderr == ''
+ - shell_result0.stdout == 'win'
+
+# executable
+
+# FIXME doesn't pass the expected stdout
+
+#- name: execute the test.sh script
+# shell: "{{remote_tmp_dir_test }}/test.sh executable={{ bash.stdout }}"
+# register: shell_result1
+#
+#- name: assert that the shell executed correctly
+# assert:
+# that:
+# - "shell_result1.rc == 0"
+# - "shell_result1.stderr == ''"
+# - "shell_result1.stdout == 'win'"
+
+# chdir
+
+- name: Execute the test.sh script with chdir
+ shell: ./test.sh
+ args:
+ chdir: "{{ remote_tmp_dir_test }}"
+ register: shell_result2
+
+- name: Assert that the shell executed correctly with chdir
+ assert:
+ that:
+ - shell_result2 is changed
+ - shell_result2.cmd == './test.sh'
+ - shell_result2.rc == 0
+ - shell_result2.stderr == ''
+ - shell_result2.stdout == 'win'
+
+# creates
+
+- name: Verify that afile.txt is absent
+ file:
+ path: "{{ remote_tmp_dir_test }}/afile.txt"
+ state: absent
+
+- name: Execute the test.sh script with chdir
+ shell: "{{ remote_tmp_dir_test }}/test.sh > {{ remote_tmp_dir_test }}/afile.txt"
+ args:
+ chdir: "{{ remote_tmp_dir_test }}"
+ creates: "{{ remote_tmp_dir_test }}/afile.txt"
+
+- name: Verify that afile.txt is present
+ file:
+ path: "{{ remote_tmp_dir_test }}/afile.txt"
+ state: file
+
+# multiline
+
+- name: Remove test file previously created
+ file:
+ path: "{{ remote_tmp_dir_test }}/afile.txt"
+ state: absent
+
+- name: Execute a shell command using a literal multiline block
+ args:
+ executable: "{{ bash.stdout }}"
+ shell: |
+ echo this is a \
+ "multiline echo" \
+ "with a new line
+ in quotes" \
+ | {{ ansible_python.executable }} -c 'import hashlib, sys; print(hashlib.sha1((sys.stdin.buffer if hasattr(sys.stdin, "buffer") else sys.stdin).read()).hexdigest())'
+ echo "this is a second line"
+ register: shell_result5
+
+- name: Assert the multiline shell command ran as expected
+ assert:
+ that:
+ - shell_result5 is changed
+ - shell_result5.rc == 0
+ - shell_result5.cmd == 'echo this is a "multiline echo" "with a new line\nin quotes" | ' + ansible_python.executable + ' -c \'import hashlib, sys; print(hashlib.sha1((sys.stdin.buffer if hasattr(sys.stdin, "buffer") else sys.stdin).read()).hexdigest())\'\necho "this is a second line"\n'
+ - shell_result5.stdout == '5575bb6b71c9558db0b6fbbf2f19909eeb4e3b98\nthis is a second line'
+
+- name: Execute a shell command using a literal multiline block with arguments in it
+ shell: |
+ executable="{{ bash.stdout }}"
+ creates={{ remote_tmp_dir_test }}/afile.txt
+ echo "test"
+ register: shell_result6
+
+- name: Assert the multiline shell command with arguments in it run as expected
+ assert:
+ that:
+ - shell_result6 is changed
+ - shell_result6.rc == 0
+ - shell_result6.cmd == 'echo "test"\n'
+ - shell_result6.stdout == 'test'
+
+- name: Execute a shell command using a multiline block where whitespaces matter
+ shell: |
+ cat <<EOF
+ One
+ Two
+ Three
+ EOF
+ register: shell_result7
+
+- name: Assert the multiline shell command outputs with whitespaces preserved
+ assert:
+ that:
+ - shell_result7 is changed
+ - shell_result7.rc == 0
+ - shell_result7.cmd == 'cat <<EOF\nOne\n Two\n Three\nEOF\n'
+ - shell_result7.stdout == 'One\n Two\n Three'
+
+- name: execute a shell command with no trailing newline to stdin
+ shell: cat > {{remote_tmp_dir_test }}/afile.txt
+ args:
+ stdin: test
+ stdin_add_newline: no
+
+- name: make sure content matches expected
+ copy:
+ dest: "{{remote_tmp_dir_test }}/afile.txt"
+ content: test
+ register: shell_result7
+ failed_when:
+ - shell_result7 is failed or
+ shell_result7 is changed
+
+- name: execute a shell command with trailing newline to stdin
+ shell: cat > {{remote_tmp_dir_test }}/afile.txt
+ args:
+ stdin: test
+ stdin_add_newline: yes
+
+- name: make sure content matches expected
+ copy:
+ dest: "{{remote_tmp_dir_test }}/afile.txt"
+ content: |
+ test
+ register: shell_result8
+ failed_when:
+ - shell_result8 is failed or
+ shell_result8 is changed
+
+- name: execute a shell command with trailing newline to stdin, default
+ shell: cat > {{remote_tmp_dir_test }}/afile.txt
+ args:
+ stdin: test
+
+- name: make sure content matches expected
+ copy:
+ dest: "{{remote_tmp_dir_test }}/afile.txt"
+ content: |
+ test
+ register: shell_result9
+ failed_when:
+ - shell_result9 is failed or
+ shell_result9 is changed
+
+- name: remove the previously created file
+ file:
+ path: "{{ remote_tmp_dir_test }}/afile.txt"
+ state: absent
+
+- name: test check mode skip message
+ command:
+ cmd: "true"
+ check_mode: yes
+ register: result
+
+- name: assert check message exists
+ assert:
+ that:
+ - "'Command would have run if not in check mode' in result.msg"
+ - result.skipped
+ - not result.changed
+
+- name: test check mode creates/removes message
+ command:
+ cmd: "true"
+ creates: yes
+ check_mode: yes
+ register: result
+
+- name: assert check message exists
+ assert:
+ that:
+ - "'Command would have run if not in check mode' in result.msg"
+ - "'skipped' not in result"
+ - result.changed
+
+- name: command symlink handling
+ block:
+ - name: Create target folders
+ file:
+ path: '{{remote_tmp_dir}}/www_root/site'
+ state: directory
+
+ - name: Create symlink
+ file:
+ path: '{{remote_tmp_dir}}/www'
+ state: link
+ src: '{{remote_tmp_dir}}/www_root'
+
+ - name: check parent using chdir
+ shell: dirname "$PWD"
+ args:
+ chdir: '{{remote_tmp_dir}}/www/site'
+ register: parent_dir_chdir
+
+ - name: check parent using cd
+ shell: cd "{{remote_tmp_dir}}/www/site" && dirname "$PWD"
+ register: parent_dir_cd
+
+ - name: check expected outputs
+ assert:
+ that:
+ - parent_dir_chdir.stdout != parent_dir_cd.stdout
+ # These tests use endswith, to get around /private/tmp on macos
+ - 'parent_dir_cd.stdout.endswith(remote_tmp_dir ~ "/www")'
+ - 'parent_dir_chdir.stdout.endswith(remote_tmp_dir ~ "/www_root")'
+
+- name: Set print error command for Python 2
+ set_fact:
+ print_error_command: print >> sys.stderr, msg
+ when: ansible_facts.python_version is version('3', '<')
+
+- name: Set print error command for Python 3
+ set_fact:
+ print_error_command: print(msg, file=sys.stderr)
+ when: ansible_facts.python_version is version('3', '>=')
+
+- name: run command with strip
+ command: '{{ ansible_python_interpreter }} -c "import sys; msg=''hello \n \r''; print(msg); {{ print_error_command }}"'
+ register: command_strip
+
+- name: run command without strip
+ command: '{{ ansible_python_interpreter }} -c "import sys; msg=''hello \n \r''; print(msg); {{ print_error_command }}"'
+ args:
+ strip_empty_ends: no
+ register: command_no_strip
+
+- name: Verify strip behavior worked as expected
+ assert:
+ that:
+ - command_strip.stdout == 'hello \n '
+ - command_strip.stderr == 'hello \n '
+ - command_no_strip.stdout== 'hello \n \r\n'
+ - command_no_strip.stderr == 'hello \n \r\n'
diff --git a/test/integration/targets/common_network/aliases b/test/integration/targets/common_network/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/common_network/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/common_network/tasks/main.yml b/test/integration/targets/common_network/tasks/main.yml
new file mode 100644
index 0000000..97b3dd0
--- /dev/null
+++ b/test/integration/targets/common_network/tasks/main.yml
@@ -0,0 +1,4 @@
+- assert:
+ that:
+ - '"00:00:00:a1:2b:cc" is is_mac'
+ - '"foo" is not is_mac'
diff --git a/test/integration/targets/common_network/test_plugins/is_mac.py b/test/integration/targets/common_network/test_plugins/is_mac.py
new file mode 100644
index 0000000..6a4d409
--- /dev/null
+++ b/test/integration/targets/common_network/test_plugins/is_mac.py
@@ -0,0 +1,14 @@
+# Copyright: (c) 2020, Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.common.network import is_mac
+
+
+class TestModule(object):
+ def tests(self):
+ return {
+ 'is_mac': is_mac,
+ }
diff --git a/test/integration/targets/conditionals/aliases b/test/integration/targets/conditionals/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/conditionals/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/conditionals/play.yml b/test/integration/targets/conditionals/play.yml
new file mode 100644
index 0000000..455818c
--- /dev/null
+++ b/test/integration/targets/conditionals/play.yml
@@ -0,0 +1,667 @@
+# (c) 2014, James Cammarata <jcammarata@ansible.com>
+# (c) 2019, Ansible Project
+
+- hosts: testhost
+ gather_facts: false
+ vars_files:
+ - vars/main.yml
+ tasks:
+ - name: test conditional '=='
+ shell: echo 'testing'
+ when: 1 == 1
+ register: result
+
+ - name: assert conditional '==' ran
+ assert:
+ that:
+ - result is changed
+ - "result.stdout == 'testing'"
+ - "result.rc == 0"
+
+ - name: test bad conditional '=='
+ shell: echo 'testing'
+ when: 0 == 1
+ register: result
+
+ - name: assert bad conditional '==' did NOT run
+ assert:
+ that:
+ - result is skipped
+
+ - name: test conditional '!='
+ shell: echo 'testing'
+ when: 0 != 1
+ register: result
+
+ - name: assert conditional '!=' ran
+ assert:
+ that:
+ - result is changed
+ - "result.stdout == 'testing'"
+ - "result.rc == 0"
+
+ - name: test bad conditional '!='
+ shell: echo 'testing'
+ when: 1 != 1
+ register: result
+
+ - name: assert bad conditional '!=' did NOT run
+ assert:
+ that:
+ - result is skipped
+
+ - name: test conditional 'in'
+ shell: echo 'testing'
+ when: 1 in [1,2,3]
+ register: result
+
+ - name: assert conditional 'in' ran
+ assert:
+ that:
+ - result is changed
+ - "result.stdout == 'testing'"
+ - "result.rc == 0"
+
+ - name: test bad conditional 'in'
+ shell: echo 'testing'
+ when: 1 in [7,8,9]
+ register: result
+
+ - name: assert bad conditional 'in' did NOT run
+ assert:
+ that:
+ - result is skipped
+
+ - name: test conditional 'not in'
+ shell: echo 'testing'
+ when: 0 not in [1,2,3]
+ register: result
+
+ - name: assert conditional 'not in' ran
+ assert:
+ that:
+ - result is changed
+ - "result.stdout == 'testing'"
+ - "result.rc == 0"
+
+ - name: test bad conditional 'not in'
+ shell: echo 'testing'
+ when: 1 not in [1,2,3]
+ register: result
+
+ - name: assert bad conditional 'not in' did NOT run
+ assert:
+ that:
+ - result is skipped
+
+ - name: test conditional 'is defined'
+ shell: echo 'testing'
+ when: test_bare is defined
+ register: result
+
+ - name: assert conditional 'is defined' ran
+ assert:
+ that:
+ - result is changed
+ - "result.stdout == 'testing'"
+ - "result.rc == 0"
+
+ - name: test bad conditional 'is defined'
+ shell: echo 'testing'
+ when: foo_asdf_xyz is defined
+ register: result
+
+ - name: assert bad conditional 'is defined' did NOT run
+ assert:
+ that:
+ - result is skipped
+
+ - name: test conditional 'is not defined'
+ shell: echo 'testing'
+ when: foo_asdf_xyz is not defined
+ register: result
+
+ - name: assert conditional 'is not defined' ran
+ assert:
+ that:
+ - result is changed
+ - "result.stdout == 'testing'"
+ - "result.rc == 0"
+
+ - name: test bad conditional 'is not defined'
+ shell: echo 'testing'
+ when: test_bare is not defined
+ register: result
+
+ - name: assert bad conditional 'is not defined' did NOT run
+ assert:
+ that:
+ - result is skipped
+
+ - name: test bad conditional 'is undefined'
+ shell: echo 'testing'
+ when: test_bare is undefined
+ register: result
+
+ - name: assert bad conditional 'is undefined' did NOT run
+ assert:
+ that:
+ - result is skipped
+
+ - name: test bare conditional
+ shell: echo 'testing'
+ when: test_bare
+ register: result
+
+ - name: assert bare conditional ran
+ assert:
+ that:
+ - result is changed
+ - "result.stdout == 'testing'"
+ - "result.rc == 0"
+
+ - name: not test bare conditional
+ shell: echo 'testing'
+ when: not test_bare
+ register: result
+
+ - name: assert did not run
+ assert:
+ that:
+ - result is skipped
+
+ - name: empty string is false
+ shell: echo 'testing'
+ when: string_lit_empty
+ register: result
+
+ - name: assert did not run
+ assert:
+ that:
+ - result is skipped
+
+ - name: not empty string is true
+ shell: echo 'testing'
+ when: not string_lit_empty
+ register: result
+
+ - name: assert ran
+ assert:
+ that:
+ - result is not skipped
+
+ - name: literal 0 is false
+ shell: echo 'testing'
+ when: int_lit_0
+ register: result
+
+ - name: assert did not run
+ assert:
+ that:
+ - result is skipped
+
+ - name: not literal 0 is true
+ shell: echo 'testing'
+ when: not int_lit_0
+ register: result
+
+ - name: assert ran
+ assert:
+ that:
+ - result is not skipped
+
+ - name: literal 1 is true
+ shell: echo 'testing'
+ when: int_lit_1
+ register: result
+
+ - name: assert ran
+ assert:
+ that:
+ - result is not skipped
+
+ - name: not literal 1 is false
+ shell: echo 'testing'
+ when: not int_lit_1
+ register: result
+
+ - name: assert did not run
+ assert:
+ that:
+ - result is skipped
+
+ - name: null is false
+ shell: echo 'testing'
+ when: lit_null
+ register: result
+
+ - name: assert did not run
+ assert:
+ that:
+ - result is skipped
+
+ - name: literal string "true" is true
+ shell: echo 'testing'
+ when: string_lit_true
+ register: result
+
+ - name: assert ran
+ assert:
+ that:
+ - result is not skipped
+
+ - name: not literal string "true" is false
+ shell: echo 'testing'
+ when: not string_lit_true
+ register: result
+
+ - name: assert did not run
+ assert:
+ that:
+ - result is skipped
+
+ - name: literal string "false" is true (nonempty string)
+ shell: echo 'testing'
+ when: string_lit_false
+ register: result
+
+ - name: assert ran
+ assert:
+ that:
+ - result is not skipped
+
+ - name: not literal string "false" is false
+ shell: echo 'testing'
+ when: not string_lit_false
+ register: result
+
+ - name: assert did not run
+ assert:
+ that:
+ - result is skipped
+
+ - name: not literal string "true" is false
+ shell: echo 'testing'
+ when: not string_lit_true
+ register: result
+
+ - name: assert did not run
+ assert:
+ that:
+ - result is skipped
+
+ - name: test conditional using a variable
+ shell: echo 'testing'
+ when: test_bare_var == 123
+ register: result
+
+ - name: assert conditional using a variable ran
+ assert:
+ that:
+ - result is changed
+ - "result.stdout == 'testing'"
+ - "result.rc == 0"
+
+ - name: test good conditional based on nested variables
+ shell: echo 'testing'
+ when: test_bare_nested_good
+ register: result
+
+ - name: assert good conditional based on nested var ran
+ assert:
+ that:
+ - result is changed
+ - "result.stdout == 'testing'"
+ - "result.rc == 0"
+
+ - name: test bad conditional based on nested variables
+ shell: echo 'testing'
+ when: test_bare_nested_bad
+ register: result
+
+ - debug: var={{item}}
+ loop:
+ - result
+ - test_bare_nested_bad
+
+ - name: assert that the bad nested conditional ran (it is a non-empty string, so truthy)
+ assert:
+ that:
+ - result is not skipped
+
+ - name: test bad conditional based on nested variables with bool filter
+ shell: echo 'testing'
+ when: test_bare_nested_bad|bool
+ register: result
+
+ - name: assert that the bad nested conditional did NOT run as bool forces evaluation
+ assert:
+ that:
+ - result is skipped
+
+
+ #-----------------------------------------------------------------------
+ # proper booleanification tests (issue #8629)
+
+ - name: set fact to string 'false'
+ set_fact: bool_test1=false
+
+ - name: set fact to string 'False'
+ set_fact: bool_test2=False
+
+ - name: set fact to a proper boolean using complex args
+ set_fact:
+ bool_test3: false
+
+ - name: "test boolean value 'false' string using 'when: var'"
+ command: echo 'hi'
+ when: bool_test1
+ register: result
+
+ - name: assert that the task did not run for 'false'
+ assert:
+ that:
+ - result is skipped
+
+ - name: "test boolean value 'false' string using 'when: not var'"
+ command: echo 'hi'
+ when: not bool_test1
+ register: result
+
+ - name: assert that the task DID run for not 'false'
+ assert:
+ that:
+ - result is changed
+
+ - name: "test boolean value of 'False' string using 'when: var'"
+ command: echo 'hi'
+ when: bool_test2
+ register: result
+
+ - name: assert that the task did not run for 'False'
+ assert:
+ that:
+ - result is skipped
+
+ - name: "test boolean value 'False' string using 'when: not var'"
+ command: echo 'hi'
+ when: not bool_test2
+ register: result
+
+ - name: assert that the task DID run for not 'False'
+ assert:
+ that:
+ - result is changed
+
+ - name: "test proper boolean value of complex arg using 'when: var'"
+ command: echo 'hi'
+ when: bool_test3
+ register: result
+
+ - name: assert that the task did not run for proper boolean false
+ assert:
+ that:
+ - result is skipped
+
+ - name: "test proper boolean value of complex arg using 'when: not var'"
+ command: echo 'hi'
+ when: not bool_test3
+ register: result
+
+ - name: assert that the task DID run for not false
+ assert:
+ that:
+ - result is changed
+
+ - set_fact: skipped_bad_attribute=True
+ - block:
+ - name: test a with_items loop using a variable with a missing attribute
+ debug: var=item
+ with_items: "{{cond_bad_attribute.results | default('')}}"
+ register: result
+ - set_fact: skipped_bad_attribute=False
+ - name: assert the task was skipped
+ assert:
+ that:
+ - skipped_bad_attribute
+ when: cond_bad_attribute is defined and 'results' in cond_bad_attribute
+
+ - name: test a with_items loop skipping a single item
+ debug: var=item
+ with_items: "{{cond_list_of_items.results}}"
+ when: item != 'b'
+ register: result
+
+ - debug: var=result
+
+ - name: assert only a single item was skipped
+ assert:
+ that:
+ - result.results|length == 3
+ - result.results[1].skipped
+
+ - name: test complex templated condition
+ debug: msg="it works"
+ when: vars_file_var in things1|union([vars_file_var])
+
+ - name: test dict with invalid key is undefined
+ vars:
+ mydict:
+ a: foo
+ b: bar
+ debug: var=mydict['c']
+ register: result
+ when: mydict['c'] is undefined
+
+ - name: assert the task did not fail
+ assert:
+ that:
+ - result is success
+
+ - name: test dict with invalid key does not run with conditional is defined
+ vars:
+ mydict:
+ a: foo
+ b: bar
+ debug: var=mydict['c']
+ when: mydict['c'] is defined
+ register: result
+
+ - name: assert the task was skipped
+ assert:
+ that:
+ - result is skipped
+
+ - name: test list with invalid element does not run with conditional is defined
+ vars:
+ mylist: []
+ debug: var=mylist[0]
+ when: mylist[0] is defined
+ register: result
+
+ - name: assert the task was skipped
+ assert:
+ that:
+ - result is skipped
+
+ - name: test list with invalid element is undefined
+ vars:
+ mylist: []
+ debug: var=mylist[0]
+ when: mylist[0] is undefined
+ register: result
+
+ - name: assert the task did not fail
+ assert:
+ that:
+ - result is success
+
+
+ - name: Deal with multivar equality
+ tags: ['leveldiff']
+ vars:
+ toplevel_hash:
+ hash_var_one: justastring
+ hash_var_two: something.with.dots
+ hash_var_three: something:with:colons
+ hash_var_four: something/with/slashes
+ hash_var_five: something with spaces
+ hash_var_six: yes
+ hash_var_seven: no
+ toplevel_var_one: justastring
+ toplevel_var_two: something.with.dots
+ toplevel_var_three: something:with:colons
+ toplevel_var_four: something/with/slashes
+ toplevel_var_five: something with spaces
+ toplevel_var_six: yes
+ toplevel_var_seven: no
+ block:
+
+ - name: var subkey simple string
+ debug:
+ var: toplevel_hash.hash_var_one
+ register: sub
+ when: toplevel_hash.hash_var_one
+
+ - name: toplevel simple string
+ debug:
+ var: toplevel_var_one
+ when: toplevel_var_one
+ register: top
+ ignore_errors: yes
+
+ - name: ensure top and multi work same
+ assert:
+ that:
+ - top is not skipped
+ - sub is not skipped
+ - top is not failed
+ - sub is not failed
+
+ - name: var subkey string with dots
+ debug:
+ var: toplevel_hash.hash_var_two
+ register: sub
+ when: toplevel_hash.hash_var_two
+
+ - debug:
+ var: toplevel_var_two
+ when: toplevel_var_two
+ register: top
+ ignore_errors: yes
+
+ - name: ensure top and multi work same
+ assert:
+ that:
+ - top is not skipped
+ - sub is not skipped
+ - top is not failed
+ - sub is not failed
+
+ - name: var subkey string with dots
+ debug:
+ var: toplevel_hash.hash_var_three
+ register: sub
+ when: toplevel_hash.hash_var_three
+
+ - debug:
+ var: toplevel_var_three
+ when: toplevel_var_three
+ register: top
+ ignore_errors: yes
+
+ - name: ensure top and multi work same
+ assert:
+ that:
+ - top is not skipped
+ - sub is not skipped
+ - top is not failed
+ - sub is not failed
+
+ - name: var subkey string with colon
+ debug:
+ var: toplevel_hash.hash_var_four
+ register: sub
+ when: toplevel_hash.hash_var_four
+
+ - debug:
+ var: toplevel_var_four
+ when: toplevel_var_four
+ register: top
+ ignore_errors: yes
+
+ - name: ensure top and multi work same
+ assert:
+ that:
+ - top is not skipped
+ - sub is not skipped
+ - top is not failed
+ - sub is not failed
+
+ - name: var subkey string with spaces
+ debug:
+ var: toplevel_hash.hash_var_five
+ register: sub
+ when: toplevel_hash.hash_var_five
+
+ - debug:
+ var: toplevel_var_five
+ when: toplevel_var_five
+ register: top
+ ignore_errors: yes
+
+ - name: ensure top and multi work same
+ assert:
+ that:
+ - top is not skipped
+ - sub is not skipped
+ - top is not failed
+ - sub is not failed
+
+ - name: var subkey with 'yes' value
+ debug:
+ var: toplevel_hash.hash_var_six
+ register: sub
+ when: toplevel_hash.hash_var_six
+
+ - debug:
+ var: toplevel_var_six
+ register: top
+ when: toplevel_var_six
+
+ - name: ensure top and multi work same
+ assert:
+ that:
+ - top is not skipped
+ - sub is not skipped
+
+ - name: var subkey with 'no' value
+ debug:
+ var: toplevel_hash.hash_var_seven
+ register: sub
+ when: toplevel_hash.hash_var_seven
+
+ - debug:
+ var: toplevel_var_seven
+ register: top
+ when: toplevel_var_seven
+
+ - name: ensure top and multi work same
+ assert:
+ that:
+ - top is skipped
+ - sub is skipped
+
+ - name: test that 'comparison expression' item works with_items
+ assert:
+ that:
+ - item
+ with_items:
+ - 1 == 1
+
+ - name: test that 'comparison expression' item works in loop
+ assert:
+ that:
+ - item
+ loop:
+ - 1 == 1
diff --git a/test/integration/targets/conditionals/runme.sh b/test/integration/targets/conditionals/runme.sh
new file mode 100755
index 0000000..4858fbf
--- /dev/null
+++ b/test/integration/targets/conditionals/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook -i ../../inventory play.yml "$@"
diff --git a/test/integration/targets/conditionals/vars/main.yml b/test/integration/targets/conditionals/vars/main.yml
new file mode 100644
index 0000000..2af6cee
--- /dev/null
+++ b/test/integration/targets/conditionals/vars/main.yml
@@ -0,0 +1,29 @@
+---
+# foo is a dictionary that will be used to check that
+# a conditional passes a with_items loop on a variable
+# with a missing attribute (ie. foo.results)
+cond_bad_attribute:
+ bar: a
+
+cond_list_of_items:
+ results:
+ - a
+ - b
+ - c
+
+things1:
+ - 1
+ - 2
+vars_file_var: 321
+
+test_bare: true
+test_bare_var: 123
+test_bare_nested_good: "test_bare_var == 123"
+test_bare_nested_bad: "{{test_bare_var}} == 321"
+
+string_lit_true: "true"
+string_lit_false: "false"
+string_lit_empty: ""
+lit_null: null
+int_lit_0: 0
+int_lit_1: 1
diff --git a/test/integration/targets/config/aliases b/test/integration/targets/config/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/config/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/config/files/types.env b/test/integration/targets/config/files/types.env
new file mode 100644
index 0000000..b5fc43e
--- /dev/null
+++ b/test/integration/targets/config/files/types.env
@@ -0,0 +1,11 @@
+# valid(list): does nothihng, just for testing values
+ANSIBLE_TYPES_VALID=
+
+# mustunquote(list): does nothihng, just for testing values
+ANSIBLE_TYPES_MUSTUNQUOTE=
+
+# notvalid(list): does nothihng, just for testing values
+ANSIBLE_TYPES_NOTVALID=
+
+# totallynotvalid(list): does nothihng, just for testing values
+ANSIBLE_TYPES_TOTALLYNOTVALID=
diff --git a/test/integration/targets/config/files/types.ini b/test/integration/targets/config/files/types.ini
new file mode 100644
index 0000000..c04b6d5
--- /dev/null
+++ b/test/integration/targets/config/files/types.ini
@@ -0,0 +1,13 @@
+[list_values]
+# (list) does nothihng, just for testing values
+mustunquote=
+
+# (list) does nothihng, just for testing values
+notvalid=
+
+# (list) does nothihng, just for testing values
+totallynotvalid=
+
+# (list) does nothihng, just for testing values
+valid=
+
diff --git a/test/integration/targets/config/files/types.vars b/test/integration/targets/config/files/types.vars
new file mode 100644
index 0000000..d1427fc
--- /dev/null
+++ b/test/integration/targets/config/files/types.vars
@@ -0,0 +1,15 @@
+# valid(list): does nothihng, just for testing values
+ansible_types_valid: ''
+
+
+# mustunquote(list): does nothihng, just for testing values
+ansible_types_mustunquote: ''
+
+
+# notvalid(list): does nothihng, just for testing values
+ansible_types_notvalid: ''
+
+
+# totallynotvalid(list): does nothihng, just for testing values
+ansible_types_totallynotvalid: ''
+
diff --git a/test/integration/targets/config/files/types_dump.txt b/test/integration/targets/config/files/types_dump.txt
new file mode 100644
index 0000000..2139f4d
--- /dev/null
+++ b/test/integration/targets/config/files/types_dump.txt
@@ -0,0 +1,8 @@
+
+types:
+_____
+_terms(default) = None
+mustunquote(default) = None
+notvalid(default) = None
+totallynotvalid(default) = None
+valid(default) = None
diff --git a/test/integration/targets/config/inline_comment_ansible.cfg b/test/integration/targets/config/inline_comment_ansible.cfg
new file mode 100644
index 0000000..01a95c4
--- /dev/null
+++ b/test/integration/targets/config/inline_comment_ansible.cfg
@@ -0,0 +1,2 @@
+[defaults]
+cowsay_enabled_stencils = ansibull ; BOOM
diff --git a/test/integration/targets/config/lookup_plugins/bogus.py b/test/integration/targets/config/lookup_plugins/bogus.py
new file mode 100644
index 0000000..34dc98a
--- /dev/null
+++ b/test/integration/targets/config/lookup_plugins/bogus.py
@@ -0,0 +1,51 @@
+# (c) 2021 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+
+__metaclass__ = type
+
+DOCUMENTATION = """
+ name: bogus
+ author: Ansible Core Team
+ version_added: histerical
+ short_description: returns what you gave it
+ description:
+ - this is mostly a noop
+ options:
+ _terms:
+ description: stuff to pass through
+ test_list:
+ description: does nothihng, just for testing values
+ type: list
+ choices:
+ - Dan
+ - Yevgeni
+ - Carla
+ - Manuela
+"""
+
+EXAMPLES = """
+- name: like some other plugins, this is mostly useless
+ debug: msg={{ q('bogus', [1,2,3])}}
+"""
+
+RETURN = """
+ _list:
+ description: basically the same as you fed in
+ type: list
+ elements: raw
+"""
+
+from ansible.plugins.lookup import LookupBase
+
+
+class LookupModule(LookupBase):
+
+ def run(self, terms, variables=None, **kwargs):
+
+ self.set_options(var_options=variables, direct=kwargs)
+ dump = self.get_option('test_list')
+
+ return terms
diff --git a/test/integration/targets/config/lookup_plugins/types.py b/test/integration/targets/config/lookup_plugins/types.py
new file mode 100644
index 0000000..d309229
--- /dev/null
+++ b/test/integration/targets/config/lookup_plugins/types.py
@@ -0,0 +1,82 @@
+# (c) 2021 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+
+__metaclass__ = type
+
+DOCUMENTATION = """
+ name: types
+ author: Ansible Core Team
+ version_added: histerical
+ short_description: returns what you gave it
+ description:
+ - this is mostly a noop
+ options:
+ _terms:
+ description: stuff to pass through
+ valid:
+ description: does nothihng, just for testing values
+ type: list
+ ini:
+ - section: list_values
+ key: valid
+ env:
+ - name: ANSIBLE_TYPES_VALID
+ vars:
+ - name: ansible_types_valid
+ mustunquote:
+ description: does nothihng, just for testing values
+ type: list
+ ini:
+ - section: list_values
+ key: mustunquote
+ env:
+ - name: ANSIBLE_TYPES_MUSTUNQUOTE
+ vars:
+ - name: ansible_types_mustunquote
+ notvalid:
+ description: does nothihng, just for testing values
+ type: list
+ ini:
+ - section: list_values
+ key: notvalid
+ env:
+ - name: ANSIBLE_TYPES_NOTVALID
+ vars:
+ - name: ansible_types_notvalid
+ totallynotvalid:
+ description: does nothihng, just for testing values
+ type: list
+ ini:
+ - section: list_values
+ key: totallynotvalid
+ env:
+ - name: ANSIBLE_TYPES_TOTALLYNOTVALID
+ vars:
+ - name: ansible_types_totallynotvalid
+"""
+
+EXAMPLES = """
+- name: like some other plugins, this is mostly useless
+ debug: msg={{ q('types', [1,2,3])}}
+"""
+
+RETURN = """
+ _list:
+ description: basically the same as you fed in
+ type: list
+ elements: raw
+"""
+
+from ansible.plugins.lookup import LookupBase
+
+
+class LookupModule(LookupBase):
+
+ def run(self, terms, variables=None, **kwargs):
+
+ self.set_options(var_options=variables, direct=kwargs)
+
+ return terms
diff --git a/test/integration/targets/config/runme.sh b/test/integration/targets/config/runme.sh
new file mode 100755
index 0000000..122e15d
--- /dev/null
+++ b/test/integration/targets/config/runme.sh
@@ -0,0 +1,43 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# ignore empty env var and use default
+# shellcheck disable=SC1007
+ANSIBLE_TIMEOUT= ansible -m ping testhost -i ../../inventory "$@"
+
+# env var is wrong type, this should be a fatal error pointing at the setting
+ANSIBLE_TIMEOUT='lola' ansible -m ping testhost -i ../../inventory "$@" 2>&1|grep 'Invalid type for configuration option setting: DEFAULT_TIMEOUT (from env: ANSIBLE_TIMEOUT)'
+
+# https://github.com/ansible/ansible/issues/69577
+ANSIBLE_REMOTE_TMP="$HOME/.ansible/directory_with_no_space" ansible -m ping testhost -i ../../inventory "$@"
+
+ANSIBLE_REMOTE_TMP="$HOME/.ansible/directory with space" ansible -m ping testhost -i ../../inventory "$@"
+
+ANSIBLE_CONFIG=nonexistent.cfg ansible-config dump --only-changed -v | grep 'No config file found; using defaults'
+
+# https://github.com/ansible/ansible/pull/73715
+ANSIBLE_CONFIG=inline_comment_ansible.cfg ansible-config dump --only-changed | grep "'ansibull'"
+
+# test type headers are only displayed with --only-changed -t all for changed options
+env -i PATH="$PATH" PYTHONPATH="$PYTHONPATH" ansible-config dump --only-changed -t all | grep -v "CONNECTION"
+env -i PATH="$PATH" PYTHONPATH="$PYTHONPATH" ANSIBLE_SSH_PIPELINING=True ansible-config dump --only-changed -t all | grep "CONNECTION"
+
+# test the config option validation
+ansible-playbook validation.yml "$@"
+
+# test types from config (just lists for now)
+ANSIBLE_CONFIG=type_munging.cfg ansible-playbook types.yml "$@"
+
+cleanup() {
+ rm -f files/*.new.*
+}
+
+trap 'cleanup' EXIT
+
+# check a-c init per format
+for format in "vars" "ini" "env"
+do
+ ANSIBLE_LOOKUP_PLUGINS=./ ansible-config init types -t lookup -f "${format}" > "files/types.new.${format}"
+ diff -u "files/types.${format}" "files/types.new.${format}"
+done
diff --git a/test/integration/targets/config/type_munging.cfg b/test/integration/targets/config/type_munging.cfg
new file mode 100644
index 0000000..d6aeaab
--- /dev/null
+++ b/test/integration/targets/config/type_munging.cfg
@@ -0,0 +1,8 @@
+[defaults]
+nothing = here
+
+[list_values]
+valid = 1, 2, 3
+mustunquote = '1', '2', '3'
+notvalid = [1, 2, 3]
+totallynotvalid = ['1', '2', '3']
diff --git a/test/integration/targets/config/types.yml b/test/integration/targets/config/types.yml
new file mode 100644
index 0000000..650a96f
--- /dev/null
+++ b/test/integration/targets/config/types.yml
@@ -0,0 +1,25 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: ensures we got the list we expected
+ block:
+ - name: initialize plugin
+ debug: msg={{ lookup('types', 'starting test') }}
+
+ - set_fact:
+ valid: '{{ lookup("config", "valid", plugin_type="lookup", plugin_name="types") }}'
+ mustunquote: '{{ lookup("config", "mustunquote", plugin_type="lookup", plugin_name="types") }}'
+ notvalid: '{{ lookup("config", "notvalid", plugin_type="lookup", plugin_name="types") }}'
+ totallynotvalid: '{{ lookup("config", "totallynotvalid", plugin_type="lookup", plugin_name="types") }}'
+
+ - assert:
+ that:
+ - 'valid|type_debug == "list"'
+ - 'mustunquote|type_debug == "list"'
+ - 'notvalid|type_debug == "list"'
+ - 'totallynotvalid|type_debug == "list"'
+ - valid[0]|int == 1
+ - mustunquote[0]|int == 1
+ - "notvalid[0] == '[1'"
+ # using 'and true' to avoid quote hell
+ - totallynotvalid[0] == "['1'" and True
diff --git a/test/integration/targets/config/validation.yml b/test/integration/targets/config/validation.yml
new file mode 100644
index 0000000..1c81e66
--- /dev/null
+++ b/test/integration/targets/config/validation.yml
@@ -0,0 +1,17 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: does nothing but an empty assign, should fail only if lookup gets invalid options
+ set_fact: whatever={{ lookup('bogus', 1, test_list=['Dan', 'Manuela']) }}
+
+ - name: now pass invalid option and fail!
+ set_fact: whatever={{ lookup('bogus', 1, test_list=['Dan', 'Manuela', 'Yoko']) }}
+ register: bad_input
+ ignore_errors: true
+
+ - name: ensure it fails as expected
+ assert:
+ that:
+ - bad_input is failed
+ - '"Invalid value " in bad_input.msg'
+ - '"valid values are:" in bad_input.msg'
diff --git a/test/integration/targets/connection/aliases b/test/integration/targets/connection/aliases
new file mode 100644
index 0000000..136c05e
--- /dev/null
+++ b/test/integration/targets/connection/aliases
@@ -0,0 +1 @@
+hidden
diff --git a/test/integration/targets/connection/test.sh b/test/integration/targets/connection/test.sh
new file mode 100755
index 0000000..6e16a87
--- /dev/null
+++ b/test/integration/targets/connection/test.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+
+set -eux
+
+[ -f "${INVENTORY}" ]
+
+ansible-playbook test_connection.yml -i "${INVENTORY}" "$@"
+
+# Check that connection vars do not appear in the output
+# https://github.com/ansible/ansible/pull/70853
+trap "rm out.txt" EXIT
+
+ansible all -i "${INVENTORY}" -m set_fact -a "testing=value" -v | tee out.txt
+if grep 'ansible_host' out.txt
+then
+ echo "FAILURE: Connection vars in output"
+ exit 1
+else
+ echo "SUCCESS: Connection vars not found"
+fi
+
+ansible-playbook test_reset_connection.yml -i "${INVENTORY}" "$@"
diff --git a/test/integration/targets/connection/test_connection.yml b/test/integration/targets/connection/test_connection.yml
new file mode 100644
index 0000000..2169942
--- /dev/null
+++ b/test/integration/targets/connection/test_connection.yml
@@ -0,0 +1,43 @@
+- hosts: "{{ target_hosts }}"
+ gather_facts: no
+ serial: 1
+ tasks:
+
+ ### raw with unicode arg and output
+
+ - name: raw with unicode arg and output
+ raw: echo 汉语
+ register: command
+ - name: check output of raw with unicode arg and output
+ assert:
+ that:
+ - "'汉语' in command.stdout"
+ - command is changed # as of 2.2, raw should default to changed: true for consistency w/ shell/command/script modules
+
+ ### copy local file with unicode filename and content
+
+ - name: create local file with unicode filename and content
+ local_action: lineinfile dest={{ local_tmp }}-汉语/汉语.txt create=true line=汉语
+ - name: remove remote file with unicode filename and content
+ action: "{{ action_prefix }}file path={{ remote_tmp }}-汉语/汉语.txt state=absent"
+ - name: create remote directory with unicode name
+ action: "{{ action_prefix }}file path={{ remote_tmp }}-汉语 state=directory"
+ - name: copy local file with unicode filename and content
+ action: "{{ action_prefix }}copy src={{ local_tmp }}-汉语/汉语.txt dest={{ remote_tmp }}-汉语/汉语.txt"
+
+ ### fetch remote file with unicode filename and content
+
+ - name: remove local file with unicode filename and content
+ local_action: file path={{ local_tmp }}-汉语/汉语.txt state=absent
+ - name: fetch remote file with unicode filename and content
+ fetch: src={{ remote_tmp }}-汉语/汉语.txt dest={{ local_tmp }}-汉语/汉语.txt fail_on_missing=true validate_checksum=true flat=true
+
+ ### remove local and remote temp files
+
+ - name: remove local temp file
+ local_action: file path={{ local_tmp }}-汉语 state=absent
+ - name: remove remote temp file
+ action: "{{ action_prefix }}file path={{ remote_tmp }}-汉语 state=absent"
+
+ ### test wait_for_connection plugin
+ - wait_for_connection:
diff --git a/test/integration/targets/connection/test_reset_connection.yml b/test/integration/targets/connection/test_reset_connection.yml
new file mode 100644
index 0000000..2f6cb8d
--- /dev/null
+++ b/test/integration/targets/connection/test_reset_connection.yml
@@ -0,0 +1,5 @@
+- hosts: "{{ target_hosts }}"
+ gather_facts: no
+ tasks:
+ # https://github.com/ansible/ansible/issues/65812
+ - meta: reset_connection
diff --git a/test/integration/targets/connection_delegation/action_plugins/delegation_action.py b/test/integration/targets/connection_delegation/action_plugins/delegation_action.py
new file mode 100644
index 0000000..9d419e7
--- /dev/null
+++ b/test/integration/targets/connection_delegation/action_plugins/delegation_action.py
@@ -0,0 +1,12 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.action import ActionBase
+
+
+class ActionModule(ActionBase):
+
+ def run(self, tmp=None, task_vars=None):
+ return {
+ 'remote_password': self._connection.get_option('remote_password'),
+ }
diff --git a/test/integration/targets/connection_delegation/aliases b/test/integration/targets/connection_delegation/aliases
new file mode 100644
index 0000000..6c96566
--- /dev/null
+++ b/test/integration/targets/connection_delegation/aliases
@@ -0,0 +1,6 @@
+shippable/posix/group3
+context/controller
+skip/freebsd # No sshpass
+skip/osx # No sshpass
+skip/macos # No sshpass
+skip/rhel # No sshpass
diff --git a/test/integration/targets/connection_delegation/connection_plugins/delegation_connection.py b/test/integration/targets/connection_delegation/connection_plugins/delegation_connection.py
new file mode 100644
index 0000000..f61846c
--- /dev/null
+++ b/test/integration/targets/connection_delegation/connection_plugins/delegation_connection.py
@@ -0,0 +1,45 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = """
+author: Ansible Core Team
+connection: delegation_connection
+short_description: Test connection for delegated host check
+description:
+- Some further description that you don't care about.
+options:
+ remote_password:
+ description: The remote password
+ type: str
+ vars:
+ - name: ansible_password
+ # Tests that an aliased key gets the -k option which hardcodes the value to password
+ aliases:
+ - password
+"""
+
+from ansible.plugins.connection import ConnectionBase
+
+
+class Connection(ConnectionBase):
+
+ transport = 'delegation_connection'
+ has_pipelining = True
+
+ def __init__(self, *args, **kwargs):
+ super(Connection, self).__init__(*args, **kwargs)
+
+ def _connect(self):
+ super(Connection, self)._connect()
+
+ def exec_command(self, cmd, in_data=None, sudoable=True):
+ super(Connection, self).exec_command(cmd, in_data, sudoable)
+
+ def put_file(self, in_path, out_path):
+ super(Connection, self).put_file(in_path, out_path)
+
+ def fetch_file(self, in_path, out_path):
+ super(Connection, self).fetch_file(in_path, out_path)
+
+ def close(self):
+ super(Connection, self).close()
diff --git a/test/integration/targets/connection_delegation/inventory.ini b/test/integration/targets/connection_delegation/inventory.ini
new file mode 100644
index 0000000..e7f846d
--- /dev/null
+++ b/test/integration/targets/connection_delegation/inventory.ini
@@ -0,0 +1 @@
+my_host ansible_host=127.0.0.1 ansible_connection=delegation_connection
diff --git a/test/integration/targets/connection_delegation/runme.sh b/test/integration/targets/connection_delegation/runme.sh
new file mode 100755
index 0000000..4d50724
--- /dev/null
+++ b/test/integration/targets/connection_delegation/runme.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+set -ux
+
+echo "Checking if sshpass is present"
+command -v sshpass 2>&1 || exit 0
+echo "sshpass is present, continuing with test"
+
+sshpass -p my_password ansible-playbook -i inventory.ini test.yml -k "$@"
diff --git a/test/integration/targets/connection_delegation/test.yml b/test/integration/targets/connection_delegation/test.yml
new file mode 100644
index 0000000..678bef5
--- /dev/null
+++ b/test/integration/targets/connection_delegation/test.yml
@@ -0,0 +1,23 @@
+---
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - name: test connection receives -k from play_context when delegating
+ delegation_action:
+ delegate_to: my_host
+ register: result
+
+ - assert:
+ that:
+ - result.remote_password == 'my_password'
+
+ - name: ensure vars set for that host take precedence over -k
+ delegation_action:
+ delegate_to: my_host
+ vars:
+ ansible_password: other_password
+ register: result
+
+ - assert:
+ that:
+ - result.remote_password == 'other_password'
diff --git a/test/integration/targets/connection_local/aliases b/test/integration/targets/connection_local/aliases
new file mode 100644
index 0000000..9390a2b
--- /dev/null
+++ b/test/integration/targets/connection_local/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+needs/target/connection
diff --git a/test/integration/targets/connection_local/runme.sh b/test/integration/targets/connection_local/runme.sh
new file mode 100755
index 0000000..a2c32ad
--- /dev/null
+++ b/test/integration/targets/connection_local/runme.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+
+set -eux
+
+group=local
+
+cd ../connection
+
+INVENTORY="../connection_${group}/test_connection.inventory" ./test.sh \
+ -e target_hosts="${group}" \
+ -e action_prefix= \
+ -e local_tmp=/tmp/ansible-local \
+ -e remote_tmp=/tmp/ansible-remote \
+ "$@"
diff --git a/test/integration/targets/connection_local/test_connection.inventory b/test/integration/targets/connection_local/test_connection.inventory
new file mode 100644
index 0000000..64a2722
--- /dev/null
+++ b/test/integration/targets/connection_local/test_connection.inventory
@@ -0,0 +1,7 @@
+[local]
+local-pipelining ansible_ssh_pipelining=true
+local-no-pipelining ansible_ssh_pipelining=false
+[local:vars]
+ansible_host=localhost
+ansible_connection=local
+ansible_python_interpreter="{{ ansible_playbook_python }}"
diff --git a/test/integration/targets/connection_paramiko_ssh/aliases b/test/integration/targets/connection_paramiko_ssh/aliases
new file mode 100644
index 0000000..3851c95
--- /dev/null
+++ b/test/integration/targets/connection_paramiko_ssh/aliases
@@ -0,0 +1,5 @@
+needs/ssh
+shippable/posix/group5
+needs/target/setup_paramiko
+needs/target/connection
+destructive # potentially installs/uninstalls OS packages via setup_paramiko
diff --git a/test/integration/targets/connection_paramiko_ssh/runme.sh b/test/integration/targets/connection_paramiko_ssh/runme.sh
new file mode 100755
index 0000000..123f6e2
--- /dev/null
+++ b/test/integration/targets/connection_paramiko_ssh/runme.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+set -eux
+
+source ../setup_paramiko/setup.sh
+
+./test.sh
diff --git a/test/integration/targets/connection_paramiko_ssh/test.sh b/test/integration/targets/connection_paramiko_ssh/test.sh
new file mode 100755
index 0000000..de1ae67
--- /dev/null
+++ b/test/integration/targets/connection_paramiko_ssh/test.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+
+set -eux
+
+group=paramiko_ssh
+
+cd ../connection
+
+INVENTORY="../connection_${group}/test_connection.inventory" ./test.sh \
+ -e target_hosts="${group}" \
+ -e action_prefix= \
+ -e local_tmp=/tmp/ansible-local \
+ -e remote_tmp=/tmp/ansible-remote \
+ "$@"
diff --git a/test/integration/targets/connection_paramiko_ssh/test_connection.inventory b/test/integration/targets/connection_paramiko_ssh/test_connection.inventory
new file mode 100644
index 0000000..a3f34ab
--- /dev/null
+++ b/test/integration/targets/connection_paramiko_ssh/test_connection.inventory
@@ -0,0 +1,7 @@
+[paramiko_ssh]
+paramiko_ssh-pipelining ansible_ssh_pipelining=true
+paramiko_ssh-no-pipelining ansible_ssh_pipelining=false
+[paramiko_ssh:vars]
+ansible_host=localhost
+ansible_connection=paramiko_ssh
+ansible_python_interpreter="{{ ansible_playbook_python }}"
diff --git a/test/integration/targets/connection_psrp/aliases b/test/integration/targets/connection_psrp/aliases
new file mode 100644
index 0000000..b3e9b8b
--- /dev/null
+++ b/test/integration/targets/connection_psrp/aliases
@@ -0,0 +1,4 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
+needs/target/connection
diff --git a/test/integration/targets/connection_psrp/files/empty.txt b/test/integration/targets/connection_psrp/files/empty.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/connection_psrp/files/empty.txt
diff --git a/test/integration/targets/connection_psrp/runme.sh b/test/integration/targets/connection_psrp/runme.sh
new file mode 100755
index 0000000..35984bb
--- /dev/null
+++ b/test/integration/targets/connection_psrp/runme.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# make sure hosts are using psrp connections
+ansible -i ../../inventory.winrm localhost \
+ -m template \
+ -a "src=test_connection.inventory.j2 dest=${OUTPUT_DIR}/test_connection.inventory" \
+ "$@"
+
+python.py -m pip install pypsrp
+cd ../connection
+
+INVENTORY="${OUTPUT_DIR}/test_connection.inventory" ./test.sh \
+ -e target_hosts=windows \
+ -e action_prefix=win_ \
+ -e local_tmp=/tmp/ansible-local \
+ -e remote_tmp=c:/windows/temp/ansible-remote \
+ "$@"
+
+cd ../connection_psrp
+
+ansible-playbook -i "${OUTPUT_DIR}/test_connection.inventory" tests.yml \
+ "$@"
diff --git a/test/integration/targets/connection_psrp/test_connection.inventory.j2 b/test/integration/targets/connection_psrp/test_connection.inventory.j2
new file mode 100644
index 0000000..d2d3a49
--- /dev/null
+++ b/test/integration/targets/connection_psrp/test_connection.inventory.j2
@@ -0,0 +1,9 @@
+[windows]
+{% for host in vars.groups.windows %}
+{{ host }} ansible_host={{ hostvars[host]['ansible_host'] }} ansible_port={{ hostvars[host]['ansible_port'] }} ansible_user={{ hostvars[host]['ansible_user'] }} ansible_password={{ hostvars[host]['ansible_password'] }}
+{% endfor %}
+
+[windows:vars]
+ansible_connection=psrp
+ansible_psrp_auth=negotiate
+ansible_psrp_cert_validation=ignore
diff --git a/test/integration/targets/connection_psrp/tests.yml b/test/integration/targets/connection_psrp/tests.yml
new file mode 100644
index 0000000..dabbf40
--- /dev/null
+++ b/test/integration/targets/connection_psrp/tests.yml
@@ -0,0 +1,133 @@
+---
+# these are extra tests for psrp that aren't covered under test/integration/targets/connection/*
+- name: test out psrp specific tests
+ hosts: windows
+ serial: 1
+ gather_facts: no
+
+ tasks:
+ - name: test complex objects in raw output
+ # until PyYAML is upgraded to 4.x we need to use the \U escape for a unicode codepoint
+ # and enclose in a quote to it translates the \U
+ raw: "
+ [PSCustomObject]@{string = 'string'};
+ [PSCustomObject]@{unicode = 'poo - \U0001F4A9'};
+ [PSCustomObject]@{integer = 1};
+ [PSCustomObject]@{list = @(1, 2)};
+ Get-Service -Name winrm;
+ Write-Output -InputObject 'string - \U0001F4A9';"
+ register: raw_out
+
+ - name: assert complex objects in raw output
+ assert:
+ that:
+ - raw_out.stdout_lines|count == 6
+ - "raw_out.stdout_lines[0] == 'string: string'"
+ - "raw_out.stdout_lines[1] == 'unicode: poo - \U0001F4A9'"
+ - "raw_out.stdout_lines[2] == 'integer: 1'"
+ - "raw_out.stdout_lines[3] == \"list: [1, 2]\""
+ - raw_out.stdout_lines[4] == "winrm"
+ - raw_out.stdout_lines[5] == "string - \U0001F4A9"
+
+ # Become only works on Server 2008 when running with basic auth, skip this host for now as it is too complicated to
+ # override the auth protocol in the tests.
+ - name: check if we running on Server 2008
+ win_shell: '[System.Environment]::OSVersion.Version -ge [Version]"6.1"'
+ register: os_version
+
+ - name: test out become with psrp
+ win_whoami:
+ when: os_version|bool
+ register: whoami_out
+ become: yes
+ become_method: runas
+ become_user: SYSTEM
+
+ - name: assert test out become with psrp
+ assert:
+ that:
+ - whoami_out.account.sid == "S-1-5-18"
+ when: os_version|bool
+
+ - name: test out async with psrp
+ win_shell: Start-Sleep -Seconds 2; Write-Output abc
+ async: 10
+ poll: 1
+ register: async_out
+
+ - name: assert est out async with psrp
+ assert:
+ that:
+ - async_out.stdout_lines == ["abc"]
+
+ - name: Output unicode characters from Powershell using PSRP
+ win_command: "powershell.exe -ExecutionPolicy ByPass -Command \"Write-Host '\U0001F4A9'\""
+ register: command_unicode_output
+
+ - name: Assert unicode output
+ assert:
+ that:
+ - command_unicode_output is changed
+ - command_unicode_output.rc == 0
+ - "command_unicode_output.stdout == '\U0001F4A9\n'"
+ - command_unicode_output.stderr == ''
+
+ - name: Output unicode characters from Powershell using PSRP
+ win_shell: "Write-Host '\U0001F4A9'"
+ register: shell_unicode_output
+
+ - name: Assert unicode output
+ assert:
+ that:
+ - shell_unicode_output is changed
+ - shell_unicode_output.rc == 0
+ - "shell_unicode_output.stdout == '\U0001F4A9\n'"
+ - shell_unicode_output.stderr == ''
+
+ - name: copy empty file
+ win_copy:
+ src: empty.txt
+ dest: C:\Windows\TEMP\empty.txt
+ register: copy_empty
+
+ - name: get result of copy empty file
+ win_stat:
+ path: C:\Windows\TEMP\empty.txt
+ get_checksum: yes
+ register: copy_empty_actual
+
+ - name: assert copy empty file
+ assert:
+ that:
+ - copy_empty.checksum == 'da39a3ee5e6b4b0d3255bfef95601890afd80709'
+ - copy_empty_actual.stat.checksum == 'da39a3ee5e6b4b0d3255bfef95601890afd80709'
+ - copy_empty_actual.stat.size == 0
+
+ - block:
+ - name: fetch empty file
+ fetch:
+ src: C:\Windows\TEMP\empty.txt
+ dest: /tmp/empty.txt
+ flat: yes
+ register: fetch_empty
+
+ - name: get result of fetch empty file
+ stat:
+ path: /tmp/empty.txt
+ get_checksum: yes
+ register: fetch_empty_actual
+ delegate_to: localhost
+
+ - name: assert fetch empty file
+ assert:
+ that:
+ - fetch_empty.checksum == 'da39a3ee5e6b4b0d3255bfef95601890afd80709'
+ - fetch_empty_actual.stat.checksum == 'da39a3ee5e6b4b0d3255bfef95601890afd80709'
+ - fetch_empty_actual.stat.size == 0
+
+ always:
+ - name: remove tmp file
+ file:
+ path: /tmp/empty.txt
+ state: absent
+ delegate_to: localhost
diff --git a/test/integration/targets/connection_remote_is_local/aliases b/test/integration/targets/connection_remote_is_local/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/connection_remote_is_local/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/connection_remote_is_local/connection_plugins/remote_is_local.py b/test/integration/targets/connection_remote_is_local/connection_plugins/remote_is_local.py
new file mode 100644
index 0000000..818bca4
--- /dev/null
+++ b/test/integration/targets/connection_remote_is_local/connection_plugins/remote_is_local.py
@@ -0,0 +1,25 @@
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+ name: remote_is_local
+ short_description: remote is local
+ description:
+ - remote_is_local
+ author: ansible (@core)
+ version_added: historical
+ extends_documentation_fragment:
+ - connection_pipelining
+ notes:
+ - The remote user is ignored, the user with which the ansible CLI was executed is used instead.
+'''
+
+
+from ansible.plugins.connection.local import Connection as LocalConnection
+
+
+class Connection(LocalConnection):
+ _remote_is_local = True
diff --git a/test/integration/targets/connection_remote_is_local/tasks/main.yml b/test/integration/targets/connection_remote_is_local/tasks/main.yml
new file mode 100644
index 0000000..265713a
--- /dev/null
+++ b/test/integration/targets/connection_remote_is_local/tasks/main.yml
@@ -0,0 +1,15 @@
+- command: ansible-playbook {{ role_path }}/test.yml -vvv -i {{ '-i '.join(ansible_inventory_sources) }}
+ environment:
+ ANSIBLE_REMOTE_TEMP: /i/dont/exist
+ ANSIBLE_NOCOLOR: 'true'
+ register: result
+
+- assert:
+ that:
+ - >-
+ result.stdout is search('PUT ' ~ ansible_local ~ ' TO ' ~ ansible_local)
+ - >-
+ '/i/dont/exist' not in result.stdout
+ vars:
+ local_tmp: '{{ q("config", "remote_tmp", plugin_type="shell", plugin_name="sh")|first|expanduser|realpath }}'
+ ansible_local: '{{ local_tmp }}/ansible-local-\S+'
diff --git a/test/integration/targets/connection_remote_is_local/test.yml b/test/integration/targets/connection_remote_is_local/test.yml
new file mode 100644
index 0000000..b76ba5f
--- /dev/null
+++ b/test/integration/targets/connection_remote_is_local/test.yml
@@ -0,0 +1,8 @@
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - ping:
+ vars:
+ ansible_connection: remote_is_local
+ ansible_pipelining: false
+ ansible_remote_tmp: /i/dont/exist
diff --git a/test/integration/targets/connection_ssh/aliases b/test/integration/targets/connection_ssh/aliases
new file mode 100644
index 0000000..bd04bed
--- /dev/null
+++ b/test/integration/targets/connection_ssh/aliases
@@ -0,0 +1,3 @@
+needs/ssh
+shippable/posix/group3
+needs/target/connection
diff --git a/test/integration/targets/connection_ssh/check_ssh_defaults.yml b/test/integration/targets/connection_ssh/check_ssh_defaults.yml
new file mode 100644
index 0000000..937f1f7
--- /dev/null
+++ b/test/integration/targets/connection_ssh/check_ssh_defaults.yml
@@ -0,0 +1,29 @@
+- hosts: ssh
+ gather_facts: false
+ vars:
+ ansible_connection: ssh
+ ansible_ssh_timeout: 10
+ tasks:
+ - name: contain the maddness
+ block:
+ - name: test all is good
+ ping:
+
+ - name: start the fun
+ meta: reset_connection
+
+ - name: now test we can use wrong port from ssh/config
+ ping:
+ ignore_unreachable: True
+ vars:
+ ansible_ssh_args: "-F {{playbook_dir}}/files/port_overrride_ssh.cfg"
+ register: expected
+
+ - name: check all is as expected
+ assert:
+ that:
+ - expected['unreachable']|bool
+ - "'2222' in expected['msg']"
+ always:
+ - name: make sure we don't cache the bad connection
+ meta: reset_connection
diff --git a/test/integration/targets/connection_ssh/files/port_overrride_ssh.cfg b/test/integration/targets/connection_ssh/files/port_overrride_ssh.cfg
new file mode 100644
index 0000000..7f8422e
--- /dev/null
+++ b/test/integration/targets/connection_ssh/files/port_overrride_ssh.cfg
@@ -0,0 +1,2 @@
+Host *
+ Port 2222
diff --git a/test/integration/targets/connection_ssh/posix.sh b/test/integration/targets/connection_ssh/posix.sh
new file mode 100755
index 0000000..8f036fb
--- /dev/null
+++ b/test/integration/targets/connection_ssh/posix.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+
+set -eux
+
+group=ssh
+
+cd ../connection
+
+INVENTORY="../connection_${group}/test_connection.inventory" ./test.sh \
+ -e target_hosts="${group}" \
+ -e action_prefix= \
+ -e local_tmp=/tmp/ansible-local \
+ -e remote_tmp=/tmp/ansible-remote \
+ "$@"
diff --git a/test/integration/targets/connection_ssh/runme.sh b/test/integration/targets/connection_ssh/runme.sh
new file mode 100755
index 0000000..ad817c8
--- /dev/null
+++ b/test/integration/targets/connection_ssh/runme.sh
@@ -0,0 +1,81 @@
+#!/usr/bin/env bash
+
+set -ux
+
+# We skip this whole section if the test node doesn't have sshpass on it.
+if command -v sshpass > /dev/null; then
+ # Check if our sshpass supports -P
+ sshpass -P foo > /dev/null
+ sshpass_supports_prompt=$?
+ if [[ $sshpass_supports_prompt -eq 0 ]]; then
+ # If the prompt is wrong, we'll end up hanging (due to sshpass hanging).
+ # We should probably do something better here, like timing out in Ansible,
+ # but this has been the behavior for a long time, before we supported custom
+ # password prompts.
+ #
+ # So we search for a custom password prompt that is clearly wrong and call
+ # ansible with timeout. If we time out, our custom prompt was successfully
+ # searched for. It's a weird way of doing things, but it does ensure
+ # that the flag gets passed to sshpass.
+ timeout 5 ansible -m ping \
+ -e ansible_connection=ssh \
+ -e ansible_sshpass_prompt=notThis: \
+ -e ansible_password=foo \
+ -e ansible_user=definitelynotroot \
+ -i test_connection.inventory \
+ ssh-pipelining
+ ret=$?
+ # 124 is EXIT_TIMEDOUT from gnu coreutils
+ # 143 is 128+SIGTERM(15) from BusyBox
+ if [[ $ret -ne 124 && $ret -ne 143 ]]; then
+ echo "Expected to time out and we did not. Exiting with failure."
+ exit 1
+ fi
+ else
+ ansible -m ping \
+ -e ansible_connection=ssh \
+ -e ansible_sshpass_prompt=notThis: \
+ -e ansible_password=foo \
+ -e ansible_user=definitelynotroot \
+ -i test_connection.inventory \
+ ssh-pipelining | grep 'customized password prompts'
+ ret=$?
+ [[ $ret -eq 0 ]] || exit $ret
+ fi
+fi
+
+set -e
+
+if [[ "$(scp -O 2>&1)" == "usage: scp "* ]]; then
+ # scp supports the -O option (and thus the -T option as well)
+ # work-around required
+ # see: https://www.openssh.com/txt/release-9.0
+ scp_args=("-e" "ansible_scp_extra_args=-TO")
+elif [[ "$(scp -T 2>&1)" == "usage: scp "* ]]; then
+ # scp supports the -T option
+ # work-around required
+ # see: https://github.com/ansible/ansible/issues/52640
+ scp_args=("-e" "ansible_scp_extra_args=-T")
+else
+ # scp does not support the -T or -O options
+ # no work-around required
+ # however we need to put something in the array to keep older versions of bash happy
+ scp_args=("-e" "")
+fi
+
+# sftp
+./posix.sh "$@"
+# scp
+ANSIBLE_SCP_IF_SSH=true ./posix.sh "$@" "${scp_args[@]}"
+# piped
+ANSIBLE_SSH_TRANSFER_METHOD=piped ./posix.sh "$@"
+
+# test config defaults override
+ansible-playbook check_ssh_defaults.yml "$@" -i test_connection.inventory
+
+# ensure we can load from ini cfg
+ANSIBLE_CONFIG=./test_ssh_defaults.cfg ansible-playbook verify_config.yml "$@"
+
+# ensure we handle cp with spaces correctly, otherwise would fail with
+# `"Failed to connect to the host via ssh: command-line line 0: keyword controlpath extra arguments at end of line"`
+ANSIBLE_SSH_CONTROL_PATH='/tmp/ssh cp with spaces' ansible -m ping all -e ansible_connection=ssh -i test_connection.inventory "$@"
diff --git a/test/integration/targets/connection_ssh/test_connection.inventory b/test/integration/targets/connection_ssh/test_connection.inventory
new file mode 100644
index 0000000..a1a4ff1
--- /dev/null
+++ b/test/integration/targets/connection_ssh/test_connection.inventory
@@ -0,0 +1,7 @@
+[ssh]
+ssh-pipelining ansible_ssh_pipelining=true
+ssh-no-pipelining ansible_ssh_pipelining=false
+[ssh:vars]
+ansible_host=localhost
+ansible_connection=ssh
+ansible_python_interpreter="{{ ansible_playbook_python }}"
diff --git a/test/integration/targets/connection_ssh/test_ssh_defaults.cfg b/test/integration/targets/connection_ssh/test_ssh_defaults.cfg
new file mode 100644
index 0000000..362f946
--- /dev/null
+++ b/test/integration/targets/connection_ssh/test_ssh_defaults.cfg
@@ -0,0 +1,5 @@
+[ssh_connection]
+ssh_common_args=fromconfig
+ssh_extra_args=fromconfig
+scp_extra_args=fromconfig
+sftp_extra_args=fromconfig
diff --git a/test/integration/targets/connection_ssh/verify_config.yml b/test/integration/targets/connection_ssh/verify_config.yml
new file mode 100644
index 0000000..0bf7958
--- /dev/null
+++ b/test/integration/targets/connection_ssh/verify_config.yml
@@ -0,0 +1,21 @@
+- hosts: localhost
+ gather_facts: false
+ vars:
+ ssh_configs:
+ - ssh_common_args
+ - ssh_extra_args
+ - sftp_extra_args
+ - scp_extra_args
+ tasks:
+ - debug:
+ msg: '{{item ~ ": " ~ lookup("config", item, plugin_type="connection", plugin_name="ssh")}}'
+ verbosity: 3
+ loop: '{{ssh_configs}}'
+ tags: [ configfile ]
+
+ - name: check config from file
+ assert:
+ that:
+ - 'lookup("config", item, plugin_type="connection", plugin_name="ssh") == "fromconfig"'
+ loop: '{{ssh_configs}}'
+ tags: [ configfile ]
diff --git a/test/integration/targets/connection_windows_ssh/aliases b/test/integration/targets/connection_windows_ssh/aliases
new file mode 100644
index 0000000..af3f193
--- /dev/null
+++ b/test/integration/targets/connection_windows_ssh/aliases
@@ -0,0 +1,5 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
+needs/target/connection
+needs/target/setup_remote_tmp_dir
diff --git a/test/integration/targets/connection_windows_ssh/runme.sh b/test/integration/targets/connection_windows_ssh/runme.sh
new file mode 100755
index 0000000..766193f
--- /dev/null
+++ b/test/integration/targets/connection_windows_ssh/runme.sh
@@ -0,0 +1,54 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# We need to run these tests with both the powershell and cmd shell type
+
+### cmd tests - no DefaultShell set ###
+ansible -i ../../inventory.winrm localhost \
+ -m template \
+ -a "src=test_connection.inventory.j2 dest=${OUTPUT_DIR}/test_connection.inventory" \
+ -e "test_shell_type=cmd" \
+ "$@"
+
+# https://github.com/PowerShell/Win32-OpenSSH/wiki/DefaultShell
+ansible -i ../../inventory.winrm windows \
+ -m win_regedit \
+ -a "path=HKLM:\\\\SOFTWARE\\\\OpenSSH name=DefaultShell state=absent" \
+ "$@"
+
+# Need to flush the connection to ensure we get a new shell for the next tests
+ansible -i "${OUTPUT_DIR}/test_connection.inventory" windows \
+ -m meta -a "reset_connection" \
+ "$@"
+
+# sftp
+./windows.sh "$@"
+# scp
+ANSIBLE_SSH_TRANSFER_METHOD=scp ./windows.sh "$@"
+# other tests not part of the generic connection test framework
+ansible-playbook -i "${OUTPUT_DIR}/test_connection.inventory" tests.yml \
+ "$@"
+
+### powershell tests - explicit DefaultShell set ###
+# we do this last as the default shell on our CI instances is set to PowerShell
+ansible -i ../../inventory.winrm localhost \
+ -m template \
+ -a "src=test_connection.inventory.j2 dest=${OUTPUT_DIR}/test_connection.inventory" \
+ -e "test_shell_type=powershell" \
+ "$@"
+
+# ensure the default shell is set to PowerShell
+ansible -i ../../inventory.winrm windows \
+ -m win_regedit \
+ -a "path=HKLM:\\\\SOFTWARE\\\\OpenSSH name=DefaultShell data=C:\\\\Windows\\\\System32\\\\WindowsPowerShell\\\\v1.0\\\\powershell.exe" \
+ "$@"
+
+ansible -i "${OUTPUT_DIR}/test_connection.inventory" windows \
+ -m meta -a "reset_connection" \
+ "$@"
+
+./windows.sh "$@"
+ANSIBLE_SSH_TRANSFER_METHOD=scp ./windows.sh "$@"
+ansible-playbook -i "${OUTPUT_DIR}/test_connection.inventory" tests.yml \
+ "$@"
diff --git a/test/integration/targets/connection_windows_ssh/test_connection.inventory.j2 b/test/integration/targets/connection_windows_ssh/test_connection.inventory.j2
new file mode 100644
index 0000000..5893eaf
--- /dev/null
+++ b/test/integration/targets/connection_windows_ssh/test_connection.inventory.j2
@@ -0,0 +1,12 @@
+[windows]
+{% for host in vars.groups.windows %}
+{{ host }} ansible_host={{ hostvars[host]['ansible_host'] }} ansible_user={{ hostvars[host]['ansible_user'] }}{{ ' ansible_ssh_private_key_file=' ~ hostvars[host]['ansible_ssh_private_key_file'] if (hostvars[host]['ansible_ssh_private_key_file']|default()) else '' }}
+{% endfor %}
+
+[windows:vars]
+ansible_shell_type={{ test_shell_type }}
+ansible_connection=ssh
+ansible_port=22
+# used to preserve the existing environment and not touch existing files
+ansible_ssh_extra_args="-o UserKnownHostsFile=/dev/null"
+ansible_ssh_host_key_checking=False
diff --git a/test/integration/targets/connection_windows_ssh/tests.yml b/test/integration/targets/connection_windows_ssh/tests.yml
new file mode 100644
index 0000000..e9b538b
--- /dev/null
+++ b/test/integration/targets/connection_windows_ssh/tests.yml
@@ -0,0 +1,32 @@
+---
+- name: test out Windows SSH specific tests
+ hosts: windows
+ serial: 1
+ gather_facts: no
+
+ tasks:
+ - name: test out become with Windows SSH
+ win_whoami:
+ register: win_ssh_become
+ become: yes
+ become_method: runas
+ become_user: SYSTEM
+
+ - name: assert test out become with Windows SSH
+ assert:
+ that:
+ - win_ssh_become.account.sid == "S-1-5-18"
+
+ - name: test out async with Windows SSH
+ win_shell: Write-Host café
+ async: 20
+ poll: 3
+ register: win_ssh_async
+
+ - name: assert test out async with Windows SSH
+ assert:
+ that:
+ - win_ssh_async is changed
+ - win_ssh_async.rc == 0
+ - win_ssh_async.stdout == "café\n"
+ - win_ssh_async.stderr == ""
diff --git a/test/integration/targets/connection_windows_ssh/tests_fetch.yml b/test/integration/targets/connection_windows_ssh/tests_fetch.yml
new file mode 100644
index 0000000..0b4fe94
--- /dev/null
+++ b/test/integration/targets/connection_windows_ssh/tests_fetch.yml
@@ -0,0 +1,41 @@
+# This must be a play as we need to invoke it with the ANSIBLE_SCP_IF_SSH env
+# to control the mechanism used. Unfortunately while ansible_scp_if_ssh is
+# documented, it isn't actually used hence the separate invocation
+---
+- name: further fetch tests with metachar characters in filename
+ hosts: windows
+ force_handlers: yes
+ serial: 1
+ gather_facts: no
+
+ tasks:
+ - name: setup remote tmp dir
+ import_role:
+ name: ../../setup_remote_tmp_dir
+
+ - name: create remote file with metachar in name
+ win_copy:
+ content: some content
+ dest: '{{ remote_tmp_dir }}\file ^with &whoami'
+
+ - name: test fetch against a file with cmd metacharacters
+ block:
+ - name: fetch file with metachar in name
+ fetch:
+ src: '{{ remote_tmp_dir }}\file ^with &whoami'
+ dest: ansible-test.txt
+ flat: yes
+ register: fetch_res
+
+ - name: assert fetch file with metachar in name
+ assert:
+ that:
+ - fetch_res is changed
+ - fetch_res.checksum == '94e66df8cd09d410c62d9e0dc59d3a884e458e05'
+
+ always:
+ - name: remove local copy of file
+ file:
+ path: ansible-test.txt
+ state: absent
+ delegate_to: localhost
diff --git a/test/integration/targets/connection_windows_ssh/windows.sh b/test/integration/targets/connection_windows_ssh/windows.sh
new file mode 100755
index 0000000..d2db50f
--- /dev/null
+++ b/test/integration/targets/connection_windows_ssh/windows.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+
+set -eux
+
+cd ../connection
+
+# A recent patch to OpenSSH causes a validation error when running through Ansible. It seems like if the path is quoted
+# then it will fail with 'protocol error: filename does not match request'. We currently ignore this by setting
+# 'ansible_scp_extra_args=-T' to ignore this check but this should be removed once that bug is fixed and our test
+# container has been updated.
+# https://unix.stackexchange.com/questions/499958/why-does-scps-strict-filename-checking-reject-quoted-last-component-but-not-oth
+# https://github.com/openssh/openssh-portable/commit/391ffc4b9d31fa1f4ad566499fef9176ff8a07dc
+INVENTORY="${OUTPUT_DIR}/test_connection.inventory" ./test.sh \
+ -e target_hosts=windows \
+ -e action_prefix=win_ \
+ -e local_tmp=/tmp/ansible-local \
+ -e remote_tmp=c:/windows/temp/ansible-remote \
+ -e ansible_scp_extra_args=-T \
+ "$@"
+
+cd ../connection_windows_ssh
+
+ansible-playbook -i "${OUTPUT_DIR}/test_connection.inventory" tests_fetch.yml \
+ -e ansible_scp_extra_args=-T \
+ "$@"
diff --git a/test/integration/targets/connection_winrm/aliases b/test/integration/targets/connection_winrm/aliases
new file mode 100644
index 0000000..af3f193
--- /dev/null
+++ b/test/integration/targets/connection_winrm/aliases
@@ -0,0 +1,5 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
+needs/target/connection
+needs/target/setup_remote_tmp_dir
diff --git a/test/integration/targets/connection_winrm/runme.sh b/test/integration/targets/connection_winrm/runme.sh
new file mode 100755
index 0000000..36a7aa8
--- /dev/null
+++ b/test/integration/targets/connection_winrm/runme.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# make sure hosts are using winrm connections
+ansible -i ../../inventory.winrm localhost \
+ -m template \
+ -a "src=test_connection.inventory.j2 dest=${OUTPUT_DIR}/test_connection.inventory" \
+ "$@"
+
+cd ../connection
+
+INVENTORY="${OUTPUT_DIR}/test_connection.inventory" ./test.sh \
+ -e target_hosts=windows \
+ -e action_prefix=win_ \
+ -e local_tmp=/tmp/ansible-local \
+ -e remote_tmp=c:/windows/temp/ansible-remote \
+ "$@"
+
+cd ../connection_winrm
+
+ansible-playbook -i "${OUTPUT_DIR}/test_connection.inventory" tests.yml \
+ "$@"
diff --git a/test/integration/targets/connection_winrm/test_connection.inventory.j2 b/test/integration/targets/connection_winrm/test_connection.inventory.j2
new file mode 100644
index 0000000..7c4f3dc
--- /dev/null
+++ b/test/integration/targets/connection_winrm/test_connection.inventory.j2
@@ -0,0 +1,10 @@
+[windows]
+{% for host in vars.groups.windows %}
+{{ host }} ansible_host={{ hostvars[host]['ansible_host'] }} ansible_port={{ hostvars[host]['ansible_port'] }} ansible_user={{ hostvars[host]['ansible_user'] }} ansible_password={{ hostvars[host]['ansible_password'] }}
+{% endfor %}
+
+[windows:vars]
+ansible_connection=winrm
+# we don't know if we're using an encrypted connection or not, so we'll use message encryption
+ansible_winrm_transport=ntlm
+ansible_winrm_server_cert_validation=ignore
diff --git a/test/integration/targets/connection_winrm/tests.yml b/test/integration/targets/connection_winrm/tests.yml
new file mode 100644
index 0000000..78f92a4
--- /dev/null
+++ b/test/integration/targets/connection_winrm/tests.yml
@@ -0,0 +1,28 @@
+---
+- name: test out Windows WinRM specific tests
+ hosts: windows
+ force_handlers: yes
+ serial: 1
+ gather_facts: no
+
+ tasks:
+ - name: setup remote tmp dir
+ import_role:
+ name: ../../setup_remote_tmp_dir
+
+ - name: copy across empty file
+ win_copy:
+ content: ''
+ dest: '{{ remote_tmp_dir }}\empty.txt'
+ register: winrm_copy_empty
+
+ - name: get result of copy across empty file
+ win_stat:
+ path: '{{ remote_tmp_dir }}\empty.txt'
+ register: winrm_copy_empty_actual
+
+ - name: assert copy across empty file
+ assert:
+ that:
+ - winrm_copy_empty is changed
+ - winrm_copy_empty_actual.stat.size == 0
diff --git a/test/integration/targets/controller/aliases b/test/integration/targets/controller/aliases
new file mode 100644
index 0000000..5a47349
--- /dev/null
+++ b/test/integration/targets/controller/aliases
@@ -0,0 +1,2 @@
+context/controller
+shippable/posix/group3
diff --git a/test/integration/targets/controller/tasks/main.yml b/test/integration/targets/controller/tasks/main.yml
new file mode 100644
index 0000000..354a593
--- /dev/null
+++ b/test/integration/targets/controller/tasks/main.yml
@@ -0,0 +1,9 @@
+- name: Verify testhost is control host
+ stat:
+ path: "{{ output_dir }}"
+- name: Get control host details
+ setup:
+ register: control_host
+- name: Show control host details
+ debug:
+ msg: "{{ control_host.ansible_facts.ansible_distribution }} {{ control_host.ansible_facts.ansible_distribution_version }}"
diff --git a/test/integration/targets/copy/aliases b/test/integration/targets/copy/aliases
new file mode 100644
index 0000000..961b205
--- /dev/null
+++ b/test/integration/targets/copy/aliases
@@ -0,0 +1,3 @@
+needs/root
+shippable/posix/group2
+destructive
diff --git a/test/integration/targets/copy/defaults/main.yml b/test/integration/targets/copy/defaults/main.yml
new file mode 100644
index 0000000..8e9a583
--- /dev/null
+++ b/test/integration/targets/copy/defaults/main.yml
@@ -0,0 +1,2 @@
+---
+remote_unprivileged_user: tmp_ansible_test_user
diff --git a/test/integration/targets/copy/files-different/vault/folder/nested-vault-file b/test/integration/targets/copy/files-different/vault/folder/nested-vault-file
new file mode 100644
index 0000000..d8d1549
--- /dev/null
+++ b/test/integration/targets/copy/files-different/vault/folder/nested-vault-file
@@ -0,0 +1,6 @@
+$ANSIBLE_VAULT;1.1;AES256
+65653164323866373138353632323531393664393563633665373635623763353561386431373366
+3232353263363034313136663062623336663463373966320a333763323032646463386432626161
+36386330356637666362396661653935653064623038333031653335626164376465353235303636
+3335616231663838620a303632343938326538656233393562303162343261383465623261646664
+33613932343461626339333832363930303962633364303736376634396364643861
diff --git a/test/integration/targets/copy/files-different/vault/readme.txt b/test/integration/targets/copy/files-different/vault/readme.txt
new file mode 100644
index 0000000..0a30d8e
--- /dev/null
+++ b/test/integration/targets/copy/files-different/vault/readme.txt
@@ -0,0 +1,5 @@
+This directory contains some files that have been encrypted with ansible-vault.
+
+This is to test out the decrypt parameter in copy.
+
+The password is: password
diff --git a/test/integration/targets/copy/files-different/vault/vault-file b/test/integration/targets/copy/files-different/vault/vault-file
new file mode 100644
index 0000000..2fff761
--- /dev/null
+++ b/test/integration/targets/copy/files-different/vault/vault-file
@@ -0,0 +1,6 @@
+$ANSIBLE_VAULT;1.1;AES256
+30353665333635633433356261616636356130386330363962386533303566313463383734373532
+3933643234323638623939613462346361313431363939370a303532656338353035346661353965
+34656231633238396361393131623834316262306533663838336362366137306562646561383766
+6363373965633337640a373666336461613337346131353564383134326139616561393664663563
+3431
diff --git a/test/integration/targets/copy/files/foo.txt b/test/integration/targets/copy/files/foo.txt
new file mode 100644
index 0000000..7c6ded1
--- /dev/null
+++ b/test/integration/targets/copy/files/foo.txt
@@ -0,0 +1 @@
+foo.txt
diff --git a/test/integration/targets/copy/files/subdir/bar.txt b/test/integration/targets/copy/files/subdir/bar.txt
new file mode 100644
index 0000000..7601807
--- /dev/null
+++ b/test/integration/targets/copy/files/subdir/bar.txt
@@ -0,0 +1 @@
+baz
diff --git a/test/integration/targets/copy/files/subdir/subdir1/empty.txt b/test/integration/targets/copy/files/subdir/subdir1/empty.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/copy/files/subdir/subdir1/empty.txt
diff --git a/test/integration/targets/copy/files/subdir/subdir2/baz.txt b/test/integration/targets/copy/files/subdir/subdir2/baz.txt
new file mode 100644
index 0000000..7601807
--- /dev/null
+++ b/test/integration/targets/copy/files/subdir/subdir2/baz.txt
@@ -0,0 +1 @@
+baz
diff --git a/test/integration/targets/copy/files/subdir/subdir2/subdir3/subdir4/qux.txt b/test/integration/targets/copy/files/subdir/subdir2/subdir3/subdir4/qux.txt
new file mode 100644
index 0000000..78df5b0
--- /dev/null
+++ b/test/integration/targets/copy/files/subdir/subdir2/subdir3/subdir4/qux.txt
@@ -0,0 +1 @@
+qux \ No newline at end of file
diff --git a/test/integration/targets/copy/meta/main.yml b/test/integration/targets/copy/meta/main.yml
new file mode 100644
index 0000000..e655a4f
--- /dev/null
+++ b/test/integration/targets/copy/meta/main.yml
@@ -0,0 +1,4 @@
+dependencies:
+ - prepare_tests
+ - setup_nobody
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/copy/tasks/acls.yml b/test/integration/targets/copy/tasks/acls.yml
new file mode 100644
index 0000000..d7d099e
--- /dev/null
+++ b/test/integration/targets/copy/tasks/acls.yml
@@ -0,0 +1,38 @@
+- block:
+ - name: Install the acl package on Ubuntu
+ apt:
+ name: acl
+ when: ansible_distribution in ('Ubuntu')
+
+ - block:
+ - name: Testing ACLs
+ copy:
+ content: "TEST"
+ mode: 0644
+ dest: "~/test.txt"
+
+ - shell: getfacl ~/test.txt
+ register: acls
+
+ become: yes
+ become_user: "{{ remote_unprivileged_user }}"
+
+ - name: Check that there are no ACLs leftovers
+ assert:
+ that:
+ - "'user:{{ remote_unprivileged_user }}:r-x\t#effective:r--' not in acls.stdout_lines"
+
+ - name: Check that permissions match with what was set in the mode param
+ assert:
+ that:
+ - "'user::rw-' in acls.stdout_lines"
+ - "'group::r--' in acls.stdout_lines"
+ - "'other::r--' in acls.stdout_lines"
+
+ always:
+ - name: Clean up
+ file:
+ path: "~/test.txt"
+ state: absent
+ become: yes
+ become_user: "{{ remote_unprivileged_user }}"
diff --git a/test/integration/targets/copy/tasks/check_mode.yml b/test/integration/targets/copy/tasks/check_mode.yml
new file mode 100644
index 0000000..5b405cc
--- /dev/null
+++ b/test/integration/targets/copy/tasks/check_mode.yml
@@ -0,0 +1,126 @@
+- block:
+
+ - name: check_mode - Create another clean copy of 'subdir' not messed with by previous tests (check_mode)
+ copy:
+ src: subdir
+ dest: 'checkmode_subdir/'
+ directory_mode: 0700
+ local_follow: False
+ check_mode: true
+ register: check_mode_subdir_first
+
+ - name: check_mode - Stat the new dir to make sure it really doesn't exist
+ stat:
+ path: 'checkmode_subdir/'
+ register: check_mode_subdir_first_stat
+
+ - name: check_mode - Actually do it
+ copy:
+ src: subdir
+ dest: 'checkmode_subdir/'
+ directory_mode: 0700
+ local_follow: False
+ register: check_mode_subdir_real
+
+ - name: check_mode - Stat the new dir to make sure it really exists
+ stat:
+ path: 'checkmode_subdir/'
+ register: check_mode_subdir_real_stat
+
+ # Quick sanity before we move on
+ - assert:
+ that:
+ - check_mode_subdir_first is changed
+ - not check_mode_subdir_first_stat.stat.exists
+ - check_mode_subdir_real is changed
+ - check_mode_subdir_real_stat.stat.exists
+
+ # Do some finagling here. First, use check_mode to ensure it never gets
+ # created. Then actualy create it, and use check_mode to ensure that doing
+ # the same copy gets marked as no change.
+ #
+ # This same pattern repeats for several other src/dest combinations.
+ - name: check_mode - Ensure dest with trailing / never gets created but would be without check_mode
+ copy:
+ remote_src: true
+ src: 'checkmode_subdir/'
+ dest: 'destdir_should_never_exist_because_of_check_mode/'
+ follow: true
+ check_mode: true
+ register: check_mode_trailing_slash_first
+
+ - name: check_mode - Stat the new dir to make sure it really doesn't exist
+ stat:
+ path: 'destdir_should_never_exist_because_of_check_mode/'
+ register: check_mode_trailing_slash_first_stat
+
+ - name: check_mode - Create the above copy for real now (without check_mode)
+ copy:
+ remote_src: true
+ src: 'checkmode_subdir/'
+ dest: 'destdir_should_never_exist_because_of_check_mode/'
+ register: check_mode_trailing_slash_real
+
+ - name: check_mode - Stat the new dir to make sure it really exists
+ stat:
+ path: 'destdir_should_never_exist_because_of_check_mode/'
+ register: check_mode_trailing_slash_real_stat
+
+ - name: check_mode - Do the same copy yet again (with check_mode this time) to ensure it's marked unchanged
+ copy:
+ remote_src: true
+ src: 'checkmode_subdir/'
+ dest: 'destdir_should_never_exist_because_of_check_mode/'
+ check_mode: true
+ register: check_mode_trailing_slash_second
+
+ # Repeat the same basic pattern here.
+
+ - name: check_mode - Do another basic copy (with check_mode)
+ copy:
+ src: foo.txt
+ dest: "{{ remote_dir }}/foo-check_mode.txt"
+ mode: 0444
+ check_mode: true
+ register: check_mode_foo_first
+
+ - name: check_mode - Stat the new file to make sure it really doesn't exist
+ stat:
+ path: "{{ remote_dir }}/foo-check_mode.txt"
+ register: check_mode_foo_first_stat
+
+ - name: check_mode - Do the same basic copy (without check_mode)
+ copy:
+ src: foo.txt
+ dest: "{{ remote_dir }}/foo-check_mode.txt"
+ mode: 0444
+ register: check_mode_foo_real
+
+ - name: check_mode - Stat the new file to make sure it really exists
+ stat:
+ path: "{{ remote_dir }}/foo-check_mode.txt"
+ register: check_mode_foo_real_stat
+
+ - name: check_mode - And again (with check_mode)
+ copy:
+ src: foo.txt
+ dest: "{{ remote_dir }}/foo-check_mode.txt"
+ mode: 0444
+ register: check_mode_foo_second
+
+ - assert:
+ that:
+ - check_mode_subdir_first is changed
+
+ - check_mode_trailing_slash_first is changed
+ # TODO: This is a legitimate bug
+ #- not check_mode_trailing_slash_first_stat.stat.exists
+ - check_mode_trailing_slash_real is changed
+ - check_mode_trailing_slash_real_stat.stat.exists
+ - check_mode_trailing_slash_second is not changed
+
+ - check_mode_foo_first is changed
+ - not check_mode_foo_first_stat.stat.exists
+ - check_mode_foo_real is changed
+ - check_mode_foo_real_stat.stat.exists
+ - check_mode_foo_second is not changed
diff --git a/test/integration/targets/copy/tasks/dest_in_non_existent_directories.yml b/test/integration/targets/copy/tasks/dest_in_non_existent_directories.yml
new file mode 100644
index 0000000..c86caa1
--- /dev/null
+++ b/test/integration/targets/copy/tasks/dest_in_non_existent_directories.yml
@@ -0,0 +1,29 @@
+# src is a file, dest is a non-existent directory (2 levels of directories):
+# checks that dest is created
+- name: Ensure that dest top directory doesn't exist
+ file:
+ path: '{{ remote_dir }}/{{ item.dest.split("/")[0] }}'
+ state: absent
+
+- name: Copy file, dest is a nonexistent target directory
+ copy:
+ src: '{{ item.src }}'
+ dest: '{{ remote_dir }}/{{ item.dest }}'
+ register: copy_result
+
+- name: assert copy worked
+ assert:
+ that:
+ - 'copy_result is successful'
+ - 'copy_result is changed'
+
+- name: stat copied file
+ stat:
+ path: '{{ remote_dir }}/{{ item.check }}'
+ register: stat_file_in_dir_result
+
+- name: assert that file exists
+ assert:
+ that:
+ - stat_file_in_dir_result.stat.exists
+ - stat_file_in_dir_result.stat.isreg
diff --git a/test/integration/targets/copy/tasks/dest_in_non_existent_directories_remote_src.yml b/test/integration/targets/copy/tasks/dest_in_non_existent_directories_remote_src.yml
new file mode 100644
index 0000000..fad53e7
--- /dev/null
+++ b/test/integration/targets/copy/tasks/dest_in_non_existent_directories_remote_src.yml
@@ -0,0 +1,43 @@
+# src is a file, dest is a non-existent directory (2 levels of directories):
+# checks that dest is created
+- name: Ensure that dest top directory doesn't exist
+ file:
+ path: '{{ remote_dir }}/{{ item.dest.split("/")[0] }}'
+ state: absent
+
+- name: create subdir
+ file:
+ path: subdir
+ state: directory
+
+- name: create src file
+ file:
+ path: "{{ item }}"
+ state: touch
+ loop:
+ - foo.txt
+ - subdir/bar.txt
+
+- name: Copy file, dest is a nonexistent target directory
+ copy:
+ src: '{{ item.src }}'
+ dest: '{{ remote_dir }}/{{ item.dest }}'
+ remote_src: true
+ register: copy_result
+
+- name: assert copy worked
+ assert:
+ that:
+ - 'copy_result is successful'
+ - 'copy_result is changed'
+
+- name: stat copied file
+ stat:
+ path: '{{ remote_dir }}/{{ item.check }}'
+ register: stat_file_in_dir_result
+
+- name: assert that file exists
+ assert:
+ that:
+ - stat_file_in_dir_result.stat.exists
+ - stat_file_in_dir_result.stat.isreg
diff --git a/test/integration/targets/copy/tasks/main.yml b/test/integration/targets/copy/tasks/main.yml
new file mode 100644
index 0000000..b86c56a
--- /dev/null
+++ b/test/integration/targets/copy/tasks/main.yml
@@ -0,0 +1,126 @@
+- block:
+
+ - name: Create a local temporary directory
+ shell: mktemp -d /tmp/ansible_test.XXXXXXXXX
+ register: tempfile_result
+ delegate_to: localhost
+
+ - set_fact:
+ local_temp_dir: '{{ tempfile_result.stdout }}'
+ remote_dir: '{{ remote_tmp_dir }}/copy'
+ symlinks:
+ ansible-test-abs-link: /tmp/ansible-test-abs-link
+ ansible-test-abs-link-dir: /tmp/ansible-test-abs-link-dir
+ circles: ../
+ invalid: invalid
+ invalid2: ../invalid
+ out_of_tree_circle: /tmp/ansible-test-link-dir/out_of_tree_circle
+ subdir3: ../subdir2/subdir3
+ bar.txt: ../bar.txt
+
+ - file: path={{local_temp_dir}} state=directory
+ name: ensure temp dir exists
+
+ # file cannot do this properly, use command instead
+ - name: Create symbolic link
+ command: "ln -s '{{ item.value }}' '{{ item.key }}'"
+ args:
+ chdir: '{{role_path}}/files/subdir/subdir1'
+ with_dict: "{{ symlinks }}"
+ delegate_to: localhost
+
+ - name: Create remote unprivileged remote user
+ user:
+ name: '{{ remote_unprivileged_user }}'
+ register: user
+
+ - name: Check sudoers dir
+ stat:
+ path: /etc/sudoers.d
+ register: etc_sudoers
+
+ - name: Set sudoers.d path fact
+ set_fact:
+ sudoers_d_file: "{{ '/etc/sudoers.d' if etc_sudoers.stat.exists else '/usr/local/etc/sudoers.d' }}/{{ remote_unprivileged_user }}"
+
+ - name: Create sudoers file
+ copy:
+ dest: "{{ sudoers_d_file }}"
+ content: "{{ remote_unprivileged_user }} ALL=(ALL) NOPASSWD: ALL"
+
+ - file:
+ path: "{{ user.home }}/.ssh"
+ owner: '{{ remote_unprivileged_user }}'
+ state: directory
+ mode: 0700
+
+ - name: Duplicate authorized_keys
+ copy:
+ src: $HOME/.ssh/authorized_keys
+ dest: '{{ user.home }}/.ssh/authorized_keys'
+ owner: '{{ remote_unprivileged_user }}'
+ mode: 0600
+ remote_src: yes
+
+ - file:
+ path: "{{ remote_dir }}"
+ state: directory
+ remote_user: '{{ remote_unprivileged_user }}'
+
+ # execute tests tasks using an unprivileged user, this is useful to avoid
+ # local/remote ambiguity when controller and managed hosts are identical.
+ - import_tasks: tests.yml
+ remote_user: '{{ remote_unprivileged_user }}'
+
+ - import_tasks: acls.yml
+ when: ansible_system == 'Linux'
+
+ - import_tasks: selinux.yml
+ when: ansible_os_family == 'RedHat' and ansible_selinux.get('mode') == 'enforcing'
+
+ - import_tasks: no_log.yml
+ delegate_to: localhost
+
+ - import_tasks: check_mode.yml
+
+ # https://github.com/ansible/ansible/issues/57618
+ - name: Test diff contents
+ copy:
+ content: 'Ansible managed\n'
+ dest: "{{ local_temp_dir }}/file.txt"
+ diff: yes
+ register: diff_output
+
+ - assert:
+ that:
+ - 'diff_output.diff[0].before == ""'
+ - '"Ansible managed" in diff_output.diff[0].after'
+
+ - name: tests with remote_src and non files
+ import_tasks: src_remote_file_is_not_file.yml
+
+ always:
+ - name: Cleaning
+ file:
+ path: '{{ local_temp_dir }}'
+ state: absent
+ delegate_to: localhost
+
+ - name: Remove symbolic link
+ file:
+ path: '{{ role_path }}/files/subdir/subdir1/{{ item.key }}'
+ state: absent
+ delegate_to: localhost
+ with_dict: "{{ symlinks }}"
+
+ - name: Remote unprivileged remote user
+ user:
+ name: '{{ remote_unprivileged_user }}'
+ state: absent
+ remove: yes
+ force: yes
+
+ - name: Remove sudoers.d file
+ file:
+ path: "{{ sudoers_d_file }}"
+ state: absent
diff --git a/test/integration/targets/copy/tasks/no_log.yml b/test/integration/targets/copy/tasks/no_log.yml
new file mode 100644
index 0000000..980c317
--- /dev/null
+++ b/test/integration/targets/copy/tasks/no_log.yml
@@ -0,0 +1,82 @@
+- block:
+
+ - set_fact:
+ dest: "{{ local_temp_dir }}/test_no_log"
+
+ - name: ensure playbook and dest files don't exist yet
+ file:
+ path: "{{ item }}"
+ state: absent
+ loop:
+ - "{{ local_temp_dir }}/test_no_log.yml"
+ - "{{ dest }}"
+
+ - name: create a playbook to run with command
+ copy:
+ dest: "{{local_temp_dir}}/test_no_log.yml"
+ content: !unsafe |
+ ---
+ - hosts: localhost
+ gather_facts: no
+ tasks:
+ - copy:
+ dest: "{{ dest }}"
+ content: "{{ secret }}"
+
+ - name: copy the secret while using -vvv and check mode
+ command: "ansible-playbook {{local_temp_dir}}/test_no_log.yml -vvv -e secret=SECRET -e dest={{dest}} --check"
+ register: result
+
+ - assert:
+ that:
+ - "'SECRET' not in result.stdout"
+
+ - name: copy the secret while using -vvv
+ command: "ansible-playbook {{local_temp_dir}}/test_no_log.yml -vvv -e secret=SECRET -e dest={{dest}}"
+ register: result
+
+ - assert:
+ that:
+ - "'SECRET' not in result.stdout"
+
+ - name: copy the secret while using -vvv and check mode again
+ command: "ansible-playbook {{local_temp_dir}}/test_no_log.yml -vvv -e secret=SECRET -e dest={{dest}} --check"
+ register: result
+
+ - assert:
+ that:
+ - "'SECRET' not in result.stdout"
+
+ - name: copy the secret while using -vvv again
+ command: "ansible-playbook {{local_temp_dir}}/test_no_log.yml -vvv -e secret=SECRET -e dest={{dest}}"
+ register: result
+
+ - assert:
+ that:
+ - "'SECRET' not in result.stdout"
+
+ - name: copy a new secret while using -vvv and check mode
+ command: "ansible-playbook {{local_temp_dir}}/test_no_log.yml -vvv -e secret=NEWSECRET -e dest={{dest}} --check"
+ register: result
+
+ - assert:
+ that:
+ - "'NEWSECRET' not in result.stdout"
+
+ - name: copy a new secret while using -vvv
+ command: "ansible-playbook {{local_temp_dir}}/test_no_log.yml -vvv -e secret=NEWSECRET -e dest={{dest}}"
+ register: result
+
+ - assert:
+ that:
+ - "'NEWSECRET' not in result.stdout"
+
+ always:
+
+ - name: remove temp test files
+ file:
+ path: "{{ item }}"
+ state: absent
+ loop:
+ - "{{ local_temp_dir }}/test_no_log.yml"
+ - "{{ dest }}"
diff --git a/test/integration/targets/copy/tasks/selinux.yml b/test/integration/targets/copy/tasks/selinux.yml
new file mode 100644
index 0000000..dddee6f
--- /dev/null
+++ b/test/integration/targets/copy/tasks/selinux.yml
@@ -0,0 +1,36 @@
+# Ensure that our logic for special filesystems works as intended
+# https://github.com/ansible/ansible/issues/70244
+- block:
+ - name: Install dosfstools
+ yum:
+ name: dosfstools
+ state: present
+
+ - name: Create a file to use for a fat16 filesystem
+ command: dd if=/dev/zero of=/fat16 bs=1024 count=10240
+
+ - name: mkfs.fat
+ command: mkfs.fat -F16 /fat16
+
+ - name: Mount it
+ command: mount /fat16 /mnt
+
+ - name: Copy a file to it
+ copy:
+ src: /etc/fstab
+ dest: /mnt/fstab
+ remote_src: true
+ always:
+ - name: Unmount it
+ command: umount /mnt
+ ignore_errors: true
+
+ - name: Nuke /fat16
+ file:
+ path: /fat16
+ state: absent
+
+ - name: Uninstall dosfstools
+ yum:
+ name: dosfstools
+ state: absent
diff --git a/test/integration/targets/copy/tasks/src_file_dest_file_in_non_existent_dir.yml b/test/integration/targets/copy/tasks/src_file_dest_file_in_non_existent_dir.yml
new file mode 100644
index 0000000..f4ab999
--- /dev/null
+++ b/test/integration/targets/copy/tasks/src_file_dest_file_in_non_existent_dir.yml
@@ -0,0 +1,26 @@
+- name: Ensure that dest top directory doesn't exist
+ file:
+ path: '{{ remote_dir }}/{{ dest.split("/")[0] }}'
+ state: absent
+
+- name: Copy file, dest is a file in non-existing target directory
+ copy:
+ src: foo.txt
+ dest: '{{ remote_dir }}/{{ dest }}'
+ register: copy_result
+ ignore_errors: True
+
+- name: Assert copy failed
+ assert:
+ that:
+ - 'copy_result is failed'
+
+- name: Stat dest path
+ stat:
+ path: '{{ remote_dir }}/{{ dest.split("/")[0] }}'
+ register: stat_file_in_dir_result
+
+- name: assert that dest doesn't exist
+ assert:
+ that:
+ - 'not stat_file_in_dir_result.stat.exists'
diff --git a/test/integration/targets/copy/tasks/src_file_dest_file_in_non_existent_dir_remote_src.yml b/test/integration/targets/copy/tasks/src_file_dest_file_in_non_existent_dir_remote_src.yml
new file mode 100644
index 0000000..61d8796
--- /dev/null
+++ b/test/integration/targets/copy/tasks/src_file_dest_file_in_non_existent_dir_remote_src.yml
@@ -0,0 +1,32 @@
+- name: Ensure that dest top directory doesn't exist
+ file:
+ path: '{{ remote_dir }}/{{ dest.split("/")[0] }}'
+ state: absent
+
+- name: create src file
+ file:
+ path: foo.txt
+ state: touch
+
+- name: Copy file, dest is a file in non-existing target directory
+ copy:
+ src: foo.txt
+ dest: '{{ remote_dir }}/{{ dest }}'
+ remote_src: true
+ register: copy_result
+ ignore_errors: True
+
+- name: Assert copy failed
+ assert:
+ that:
+ - 'copy_result is failed'
+
+- name: Stat dest path
+ stat:
+ path: '{{ remote_dir }}/{{ dest.split("/")[0] }}'
+ register: stat_file_in_dir_result
+
+- name: assert that dest doesn't exist
+ assert:
+ that:
+ - 'not stat_file_in_dir_result.stat.exists'
diff --git a/test/integration/targets/copy/tasks/src_remote_file_is_not_file.yml b/test/integration/targets/copy/tasks/src_remote_file_is_not_file.yml
new file mode 100644
index 0000000..2cda7d3
--- /dev/null
+++ b/test/integration/targets/copy/tasks/src_remote_file_is_not_file.yml
@@ -0,0 +1,39 @@
+- name: test remote src non files
+ vars:
+ destfile: '{{remote_dir}}/whocares'
+ block:
+ - name: mess with dev/null
+ copy:
+ src: /dev/null
+ dest: "{{destfile}}"
+ remote_src: true
+ become: true
+ register: dev_null_fail
+ ignore_errors: true
+
+ - name: ensure we failed
+ assert:
+ that:
+ - dev_null_fail is failed
+ - "'not a file' in dev_null_fail.msg"
+
+ - name: now with file existing
+ file: state=touch path="{{destfile}}"
+
+ - name: mess with dev/null again
+ copy:
+ src: /dev/null
+ dest: "{{destfile}}"
+ remote_src: true
+ become: true
+ register: dev_null_fail
+ ignore_errors: true
+
+ - name: ensure we failed, again
+ assert:
+ that:
+ - dev_null_fail is failed
+ - "'not a file' in dev_null_fail.msg"
+ always:
+ - name: cleanup
+ file: state=absent path="{{destfile}}"
diff --git a/test/integration/targets/copy/tasks/tests.yml b/test/integration/targets/copy/tasks/tests.yml
new file mode 100644
index 0000000..7220356
--- /dev/null
+++ b/test/integration/targets/copy/tasks/tests.yml
@@ -0,0 +1,2286 @@
+# test code for the copy module and action plugin
+# (c) 2014, Michael DeHaan <michael.dehaan@gmail.com>
+# (c) 2017, Ansible Project
+#
+# GNU General Public License v3 or later (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt )
+#
+
+- name: Record the output directory
+ set_fact:
+ remote_file: "{{ remote_dir }}/foo.txt"
+
+- name: Initiate a basic copy, and also test the mode
+ copy:
+ src: foo.txt
+ dest: "{{ remote_file }}"
+ mode: 0444
+ register: copy_result
+
+- name: Record the sha of the test file for later tests
+ set_fact:
+ remote_file_hash: "{{ copy_result['checksum'] }}"
+
+- name: Check the mode of the output file
+ file:
+ name: "{{ remote_file }}"
+ state: file
+ register: file_result_check
+
+- name: Assert the mode is correct
+ assert:
+ that:
+ - "file_result_check.mode == '0444'"
+
+# same as expanduser & expandvars
+- command: 'echo {{ remote_dir }}'
+ register: echo
+
+- set_fact:
+ remote_dir_expanded: '{{ echo.stdout }}'
+ remote_file_expanded: '{{ echo.stdout }}/foo.txt'
+
+- debug:
+ var: copy_result
+ verbosity: 1
+
+- name: Assert basic copy worked
+ assert:
+ that:
+ - "'changed' in copy_result"
+ - copy_result.dest == remote_file_expanded
+ - "'group' in copy_result"
+ - "'gid' in copy_result"
+ - "'checksum' in copy_result"
+ - "'owner' in copy_result"
+ - "'size' in copy_result"
+ - "'src' in copy_result"
+ - "'state' in copy_result"
+ - "'uid' in copy_result"
+
+- name: Verify that the file was marked as changed
+ assert:
+ that:
+ - "copy_result.changed == true"
+
+- name: Verify that the file checksums are correct
+ assert:
+ that:
+ - "copy_result.checksum == ('foo.txt\n'|hash('sha1'))"
+
+- name: Verify that the legacy md5sum is correct
+ assert:
+ that:
+ - "copy_result.md5sum == ('foo.txt\n'|hash('md5'))"
+ when: ansible_fips|bool != True
+
+- name: Check the stat results of the file
+ stat:
+ path: "{{ remote_file }}"
+ register: stat_results
+
+- debug:
+ var: stat_results
+ verbosity: 1
+
+- name: Assert the stat results are correct
+ assert:
+ that:
+ - "stat_results.stat.exists == true"
+ - "stat_results.stat.isblk == false"
+ - "stat_results.stat.isfifo == false"
+ - "stat_results.stat.isreg == true"
+ - "stat_results.stat.issock == false"
+ - "stat_results.stat.checksum == ('foo.txt\n'|hash('sha1'))"
+
+- name: Overwrite the file via same means
+ copy:
+ src: foo.txt
+ dest: "{{ remote_file }}"
+ decrypt: no
+ register: copy_result2
+
+- name: Assert that the file was not changed
+ assert:
+ that:
+ - "copy_result2 is not changed"
+
+- name: Assert basic copy worked
+ assert:
+ that:
+ - "'changed' in copy_result2"
+ - copy_result2.dest == remote_file_expanded
+ - "'group' in copy_result2"
+ - "'gid' in copy_result2"
+ - "'checksum' in copy_result2"
+ - "'owner' in copy_result2"
+ - "'size' in copy_result2"
+ - "'state' in copy_result2"
+ - "'uid' in copy_result2"
+
+- name: Overwrite the file using the content system
+ copy:
+ content: "modified"
+ dest: "{{ remote_file }}"
+ decrypt: no
+ register: copy_result3
+
+- name: Check the stat results of the file
+ stat:
+ path: "{{ remote_file }}"
+ register: stat_results
+
+- debug:
+ var: stat_results
+ verbosity: 1
+
+- name: Assert that the file has changed
+ assert:
+ that:
+ - "copy_result3 is changed"
+ - "'content' not in copy_result3"
+ - "stat_results.stat.checksum == ('modified'|hash('sha1'))"
+ - "stat_results.stat.mode != '0700'"
+
+- name: Overwrite the file again using the content system, also passing along file params
+ copy:
+ content: "modified"
+ dest: "{{ remote_file }}"
+ mode: 0700
+ decrypt: no
+ register: copy_result4
+
+- name: Check the stat results of the file
+ stat:
+ path: "{{ remote_file }}"
+ register: stat_results
+
+- debug:
+ var: stat_results
+ verbosity: 1
+
+- name: Assert that the file has changed
+ assert:
+ that:
+ - "copy_result3 is changed"
+ - "'content' not in copy_result3"
+ - "stat_results.stat.checksum == ('modified'|hash('sha1'))"
+ - "stat_results.stat.mode == '0700'"
+
+- name: Create a hardlink to the file
+ file:
+ src: '{{ remote_file }}'
+ dest: '{{ remote_dir }}/hard.lnk'
+ state: hard
+
+- name: copy the same contents into place
+ copy:
+ content: 'modified'
+ dest: '{{ remote_file }}'
+ mode: 0700
+ decrypt: no
+ register: copy_results
+
+- name: Check the stat results of the file
+ stat:
+ path: "{{ remote_file }}"
+ register: stat_results
+
+- name: Check the stat results of the hard link
+ stat:
+ path: "{{ remote_dir }}/hard.lnk"
+ register: hlink_results
+
+- name: Check that the file did not change
+ assert:
+ that:
+ - 'stat_results.stat.inode == hlink_results.stat.inode'
+ - 'copy_results.changed == False'
+ - "stat_results.stat.checksum == ('modified'|hash('sha1'))"
+
+- name: copy the same contents into place but change mode
+ copy:
+ content: 'modified'
+ dest: '{{ remote_file }}'
+ mode: 0404
+ decrypt: no
+ register: copy_results
+
+- name: Check the stat results of the file
+ stat:
+ path: "{{ remote_file }}"
+ register: stat_results
+
+- name: Check the stat results of the hard link
+ stat:
+ path: "{{ remote_dir }}/hard.lnk"
+ register: hlink_results
+
+- name: Check that the file changed permissions but is still the same
+ assert:
+ that:
+ - 'stat_results.stat.inode == hlink_results.stat.inode'
+ - 'copy_results.changed == True'
+ - 'stat_results.stat.mode == hlink_results.stat.mode'
+ - 'stat_results.stat.mode == "0404"'
+ - "stat_results.stat.checksum == ('modified'|hash('sha1'))"
+
+- name: copy the different contents into place
+ copy:
+ content: 'adjusted'
+ dest: '{{ remote_file }}'
+ mode: 0404
+ register: copy_results
+
+- name: Check the stat results of the file
+ stat:
+ path: "{{ remote_file }}"
+ register: stat_results
+
+- name: Check the stat results of the hard link
+ stat:
+ path: "{{ remote_dir }}/hard.lnk"
+ register: hlink_results
+
+- name: Check that the file changed and hardlink was broken
+ assert:
+ that:
+ - 'stat_results.stat.inode != hlink_results.stat.inode'
+ - 'copy_results.changed == True'
+ - "stat_results.stat.checksum == ('adjusted'|hash('sha1'))"
+ - "hlink_results.stat.checksum == ('modified'|hash('sha1'))"
+
+- name: Try invalid copy input location fails
+ copy:
+ src: invalid_file_location_does_not_exist
+ dest: "{{ remote_dir }}/file.txt"
+ ignore_errors: True
+ register: failed_copy
+
+- name: Assert that invalid source failed
+ assert:
+ that:
+ - "failed_copy.failed"
+ - "'invalid_file_location_does_not_exist' in failed_copy.msg"
+
+- name: Try empty source to ensure it fails
+ copy:
+ src: ''
+ dest: "{{ remote_dir }}"
+ ignore_errors: True
+ register: failed_copy
+
+- debug:
+ var: failed_copy
+ verbosity: 1
+
+- name: Assert that empty source failed
+ assert:
+ that:
+ - failed_copy is failed
+ - "'src (or content) is required' in failed_copy.msg"
+
+- name: Try without destination to ensure it fails
+ copy:
+ src: foo.txt
+ ignore_errors: True
+ register: failed_copy
+
+- debug:
+ var: failed_copy
+ verbosity: 1
+
+- name: Assert that missing destination failed
+ assert:
+ that:
+ - failed_copy is failed
+ - "'dest is required' in failed_copy.msg"
+
+- name: Try without source to ensure it fails
+ copy:
+ dest: "{{ remote_file }}"
+ ignore_errors: True
+ register: failed_copy
+
+- debug:
+ var: failed_copy
+ verbosity: 1
+
+- name: Assert that missing source failed
+ assert:
+ that:
+ - failed_copy is failed
+ - "'src (or content) is required' in failed_copy.msg"
+
+- name: Try with both src and content to ensure it fails
+ copy:
+ src: foo.txt
+ content: testing
+ dest: "{{ remote_file }}"
+ ignore_errors: True
+ register: failed_copy
+
+- name: Assert that mutually exclusive parameters failed
+ assert:
+ that:
+ - failed_copy is failed
+ - "'mutually exclusive' in failed_copy.msg"
+
+- name: Try with content and directory as destination to ensure it fails
+ copy:
+ content: testing
+ dest: "{{ remote_dir }}"
+ ignore_errors: True
+ register: failed_copy
+
+- debug:
+ var: failed_copy
+ verbosity: 1
+
+- name: Assert that content and directory as destination failed
+ assert:
+ that:
+ - failed_copy is failed
+ - "'can not use content with a dir as dest' in failed_copy.msg"
+
+- name: Clean up
+ file:
+ path: "{{ remote_file }}"
+ state: absent
+
+- name: Copy source file to destination directory with mode
+ copy:
+ src: foo.txt
+ dest: "{{ remote_dir }}"
+ mode: 0500
+ register: copy_results
+
+- name: Check the stat results of the file
+ stat:
+ path: '{{ remote_file }}'
+ register: stat_results
+
+- debug:
+ var: stat_results
+ verbosity: 1
+
+- name: Assert that the file has changed
+ assert:
+ that:
+ - "copy_results is changed"
+ - "stat_results.stat.checksum == ('foo.txt\n'|hash('sha1'))"
+ - "stat_results.stat.mode == '0500'"
+
+# Test copy with mode=preserve
+- name: Create file and set perms to an odd value
+ copy:
+ content: "foo.txt\n"
+ dest: '{{ local_temp_dir }}/foo.txt'
+ mode: 0547
+ delegate_to: localhost
+
+- name: Copy with mode=preserve
+ copy:
+ src: '{{ local_temp_dir }}/foo.txt'
+ dest: '{{ remote_dir }}/copy-foo.txt'
+ mode: preserve
+ register: copy_results
+
+- name: Check the stat results of the file
+ stat:
+ path: '{{ remote_dir }}/copy-foo.txt'
+ register: stat_results
+
+- name: Assert that the file has changed and has correct mode
+ assert:
+ that:
+ - "copy_results is changed"
+ - "copy_results.mode == '0547'"
+ - "stat_results.stat.checksum == ('foo.txt\n'|hash('sha1'))"
+ - "stat_results.stat.mode == '0547'"
+
+- name: Test copy with mode=preserve and remote_src=True
+ copy:
+ src: '{{ remote_dir }}/copy-foo.txt'
+ dest: '{{ remote_dir }}/copy-foo2.txt'
+ mode: 'preserve'
+ remote_src: True
+ register: copy_results2
+
+- name: Check the stat results of the file
+ stat:
+ path: '{{ remote_dir }}/copy-foo2.txt'
+ register: stat_results2
+
+- name: Assert that the file has changed and has correct mode
+ assert:
+ that:
+ - "copy_results2 is changed"
+ - "copy_results2.mode == '0547'"
+ - "stat_results2.stat.checksum == ('foo.txt\n'|hash('sha1'))"
+ - "stat_results2.stat.mode == '0547'"
+
+#
+# test recursive copy local_follow=False, no trailing slash
+#
+
+- name: Create empty directory in the role we're copying from (git can't store empty dirs)
+ file:
+ path: '{{ role_path }}/files/subdir/subdira'
+ state: directory
+ delegate_to: localhost
+
+- name: Set the output subdirectory
+ set_fact:
+ remote_subdir: "{{ remote_dir }}/sub"
+
+- name: Make an output subdirectory
+ file:
+ name: "{{ remote_subdir }}"
+ state: directory
+
+- name: Setup link target for absolute link
+ copy:
+ dest: /tmp/ansible-test-abs-link
+ content: target
+ delegate_to: localhost
+
+- name: Setup link target dir for absolute link
+ file:
+ dest: /tmp/ansible-test-abs-link-dir
+ state: directory
+ delegate_to: localhost
+
+- name: Test recursive copy to directory no trailing slash, local_follow=False
+ copy:
+ src: subdir
+ dest: "{{ remote_subdir }}"
+ directory_mode: 0700
+ local_follow: False
+ register: recursive_copy_result
+
+- debug:
+ var: recursive_copy_result
+ verbosity: 1
+
+- name: Assert that the recursive copy did something
+ assert:
+ that:
+ - "recursive_copy_result is changed"
+
+- name: Check that a file in a directory was transferred
+ stat:
+ path: "{{ remote_dir }}/sub/subdir/bar.txt"
+ register: stat_bar
+
+- name: Check that a file in a deeper directory was transferred
+ stat:
+ path: "{{ remote_dir }}/sub/subdir/subdir2/baz.txt"
+ register: stat_bar2
+
+- name: Check that a file in a directory whose parent contains a directory alone was transferred
+ stat:
+ path: "{{ remote_dir }}/sub/subdir/subdir2/subdir3/subdir4/qux.txt"
+ register: stat_bar3
+
+- name: Assert recursive copy files
+ assert:
+ that:
+ - "stat_bar.stat.exists"
+ - "stat_bar2.stat.exists"
+ - "stat_bar3.stat.exists"
+
+- name: Check symlink to absolute path
+ stat:
+ path: '{{ remote_dir }}/sub/subdir/subdir1/ansible-test-abs-link'
+ register: stat_abs_link
+
+- name: Check symlink to relative path
+ stat:
+ path: '{{ remote_dir }}/sub/subdir/subdir1/bar.txt'
+ register: stat_relative_link
+
+- name: Check symlink to self
+ stat:
+ path: '{{ remote_dir }}/sub/subdir/subdir1/invalid'
+ register: stat_self_link
+
+- name: Check symlink to nonexistent file
+ stat:
+ path: '{{ remote_dir }}/sub/subdir/subdir1/invalid2'
+ register: stat_invalid_link
+
+- name: Check symlink to directory in copy
+ stat:
+ path: '{{ remote_dir }}/sub/subdir/subdir1/subdir3'
+ register: stat_dir_in_copy_link
+
+- name: Check symlink to directory outside of copy
+ stat:
+ path: '{{ remote_dir }}/sub/subdir/subdir1/ansible-test-abs-link-dir'
+ register: stat_dir_outside_copy_link
+
+- name: Assert recursive copy symlinks local_follow=False
+ assert:
+ that:
+ - "stat_abs_link.stat.exists"
+ - "stat_abs_link.stat.islnk"
+ - "'/tmp/ansible-test-abs-link' == stat_abs_link.stat.lnk_target"
+ - "stat_relative_link.stat.exists"
+ - "stat_relative_link.stat.islnk"
+ - "'../bar.txt' == stat_relative_link.stat.lnk_target"
+ - "stat_self_link.stat.exists"
+ - "stat_self_link.stat.islnk"
+ - "'invalid' in stat_self_link.stat.lnk_target"
+ - "stat_invalid_link.stat.exists"
+ - "stat_invalid_link.stat.islnk"
+ - "'../invalid' in stat_invalid_link.stat.lnk_target"
+ - "stat_dir_in_copy_link.stat.exists"
+ - "stat_dir_in_copy_link.stat.islnk"
+ - "'../subdir2/subdir3' in stat_dir_in_copy_link.stat.lnk_target"
+ - "stat_dir_outside_copy_link.stat.exists"
+ - "stat_dir_outside_copy_link.stat.islnk"
+ - "'/tmp/ansible-test-abs-link-dir' == stat_dir_outside_copy_link.stat.lnk_target"
+
+- name: Stat the recursively copied directories
+ stat:
+ path: "{{ remote_dir }}/sub/{{ item }}"
+ register: dir_stats
+ with_items:
+ - "subdir"
+ - "subdir/subdira"
+ - "subdir/subdir1"
+ - "subdir/subdir2"
+ - "subdir/subdir2/subdir3"
+ - "subdir/subdir2/subdir3/subdir4"
+
+- debug:
+ var: stat_results
+ verbosity: 1
+
+- name: Assert recursive copied directories mode (1)
+ assert:
+ that:
+ - "item.stat.exists"
+ - "item.stat.mode == '0700'"
+ with_items: "{{dir_stats.results}}"
+
+- name: Test recursive copy to directory no trailing slash, local_follow=False second time
+ copy:
+ src: subdir
+ dest: "{{ remote_subdir }}"
+ directory_mode: 0700
+ local_follow: False
+ register: recursive_copy_result
+
+- name: Assert that the second copy did not change anything
+ assert:
+ that:
+ - "recursive_copy_result is not changed"
+
+- name: Cleanup the recursive copy subdir
+ file:
+ name: "{{ remote_subdir }}"
+ state: absent
+
+#
+# Recursive copy with local_follow=False, trailing slash
+#
+
+- name: Set the output subdirectory
+ set_fact:
+ remote_subdir: "{{ remote_dir }}/sub"
+
+- name: Make an output subdirectory
+ file:
+ name: "{{ remote_subdir }}"
+ state: directory
+
+- name: Setup link target for absolute link
+ copy:
+ dest: /tmp/ansible-test-abs-link
+ content: target
+ delegate_to: localhost
+
+- name: Setup link target dir for absolute link
+ file:
+ dest: /tmp/ansible-test-abs-link-dir
+ state: directory
+ delegate_to: localhost
+
+- name: Test recursive copy to directory trailing slash, local_follow=False
+ copy:
+ src: subdir/
+ dest: "{{ remote_subdir }}"
+ directory_mode: 0700
+ local_follow: False
+ register: recursive_copy_result
+
+- debug:
+ var: recursive_copy_result
+ verbosity: 1
+
+- name: Assert that the recursive copy did something
+ assert:
+ that:
+ - "recursive_copy_result is changed"
+
+- name: Check that a file in a directory was transferred
+ stat:
+ path: "{{ remote_dir }}/sub/bar.txt"
+ register: stat_bar
+
+- name: Check that a file in a deeper directory was transferred
+ stat:
+ path: "{{ remote_dir }}/sub/subdir2/baz.txt"
+ register: stat_bar2
+
+- name: Check that a file in a directory whose parent contains a directory alone was transferred
+ stat:
+ path: "{{ remote_dir }}/sub/subdir2/subdir3/subdir4/qux.txt"
+ register: stat_bar3
+
+- name: Assert recursive copy files
+ assert:
+ that:
+ - "stat_bar.stat.exists"
+ - "stat_bar2.stat.exists"
+ - "stat_bar3.stat.exists"
+
+- name: Check symlink to absolute path
+ stat:
+ path: '{{ remote_dir }}/sub/subdir1/ansible-test-abs-link'
+ register: stat_abs_link
+
+- name: Check symlink to relative path
+ stat:
+ path: '{{ remote_dir }}/sub/subdir1/bar.txt'
+ register: stat_relative_link
+
+- name: Check symlink to self
+ stat:
+ path: '{{ remote_dir }}/sub/subdir1/invalid'
+ register: stat_self_link
+
+- name: Check symlink to nonexistent file
+ stat:
+ path: '{{ remote_dir }}/sub/subdir1/invalid2'
+ register: stat_invalid_link
+
+- name: Check symlink to directory in copy
+ stat:
+ path: '{{ remote_dir }}/sub/subdir1/subdir3'
+ register: stat_dir_in_copy_link
+
+- name: Check symlink to directory outside of copy
+ stat:
+ path: '{{ remote_dir }}/sub/subdir1/ansible-test-abs-link-dir'
+ register: stat_dir_outside_copy_link
+
+- name: Assert recursive copy symlinks local_follow=False trailing slash
+ assert:
+ that:
+ - "stat_abs_link.stat.exists"
+ - "stat_abs_link.stat.islnk"
+ - "'/tmp/ansible-test-abs-link' == stat_abs_link.stat.lnk_target"
+ - "stat_relative_link.stat.exists"
+ - "stat_relative_link.stat.islnk"
+ - "'../bar.txt' == stat_relative_link.stat.lnk_target"
+ - "stat_self_link.stat.exists"
+ - "stat_self_link.stat.islnk"
+ - "'invalid' in stat_self_link.stat.lnk_target"
+ - "stat_invalid_link.stat.exists"
+ - "stat_invalid_link.stat.islnk"
+ - "'../invalid' in stat_invalid_link.stat.lnk_target"
+ - "stat_dir_in_copy_link.stat.exists"
+ - "stat_dir_in_copy_link.stat.islnk"
+ - "'../subdir2/subdir3' in stat_dir_in_copy_link.stat.lnk_target"
+ - "stat_dir_outside_copy_link.stat.exists"
+ - "stat_dir_outside_copy_link.stat.islnk"
+ - "'/tmp/ansible-test-abs-link-dir' == stat_dir_outside_copy_link.stat.lnk_target"
+
+- name: Stat the recursively copied directories
+ stat:
+ path: "{{ remote_dir }}/sub/{{ item }}"
+ register: dir_stats
+ with_items:
+ - "subdira"
+ - "subdir1"
+ - "subdir2"
+ - "subdir2/subdir3"
+ - "subdir2/subdir3/subdir4"
+
+- debug:
+ var: dir_stats
+ verbosity: 1
+
+- name: Assert recursive copied directories mode (2)
+ assert:
+ that:
+ - "item.stat.mode == '0700'"
+ with_items: "{{dir_stats.results}}"
+
+- name: Test recursive copy to directory trailing slash, local_follow=False second time
+ copy:
+ src: subdir/
+ dest: "{{ remote_subdir }}"
+ directory_mode: 0700
+ local_follow: False
+ register: recursive_copy_result
+
+- name: Assert that the second copy did not change anything
+ assert:
+ that:
+ - "recursive_copy_result is not changed"
+
+- name: Cleanup the recursive copy subdir
+ file:
+ name: "{{ remote_subdir }}"
+ state: absent
+
+#
+# test recursive copy local_follow=True, no trailing slash
+#
+
+- name: Set the output subdirectory
+ set_fact:
+ remote_subdir: "{{ remote_dir }}/sub"
+
+- name: Make an output subdirectory
+ file:
+ name: "{{ remote_subdir }}"
+ state: directory
+
+- name: Setup link target for absolute link
+ copy:
+ dest: /tmp/ansible-test-abs-link
+ content: target
+ delegate_to: localhost
+
+- name: Setup link target dir for absolute link
+ file:
+ dest: /tmp/ansible-test-abs-link-dir
+ state: directory
+ delegate_to: localhost
+
+- name: Test recursive copy to directory no trailing slash, local_follow=True
+ copy:
+ src: subdir
+ dest: "{{ remote_subdir }}"
+ directory_mode: 0700
+ local_follow: True
+ register: recursive_copy_result
+
+- debug:
+ var: recursive_copy_result
+ verbosity: 1
+
+- name: Assert that the recursive copy did something
+ assert:
+ that:
+ - "recursive_copy_result is changed"
+
+- name: Check that a file in a directory was transferred
+ stat:
+ path: "{{ remote_dir }}/sub/subdir/bar.txt"
+ register: stat_bar
+
+- name: Check that a file in a deeper directory was transferred
+ stat:
+ path: "{{ remote_dir }}/sub/subdir/subdir2/baz.txt"
+ register: stat_bar2
+
+- name: Check that a file in a directory whose parent contains a directory alone was transferred
+ stat:
+ path: "{{ remote_dir }}/sub/subdir/subdir2/subdir3/subdir4/qux.txt"
+ register: stat_bar3
+
+- name: Check that a file in a directory whose parent is a symlink was transferred
+ stat:
+ path: "{{ remote_dir }}/sub/subdir/subdir1/subdir3/subdir4/qux.txt"
+ register: stat_bar4
+
+- name: Assert recursive copy files
+ assert:
+ that:
+ - "stat_bar.stat.exists"
+ - "stat_bar2.stat.exists"
+ - "stat_bar3.stat.exists"
+ - "stat_bar4.stat.exists"
+
+- name: Check symlink to absolute path
+ stat:
+ path: '{{ remote_dir }}/sub/subdir/subdir1/ansible-test-abs-link'
+ register: stat_abs_link
+
+- name: Check symlink to relative path
+ stat:
+ path: '{{ remote_dir }}/sub/subdir/subdir1/bar.txt'
+ register: stat_relative_link
+
+- name: Check symlink to self
+ stat:
+ path: '{{ remote_dir }}/sub/subdir/subdir1/invalid'
+ register: stat_self_link
+
+- name: Check symlink to nonexistent file
+ stat:
+ path: '{{ remote_dir }}/sub/subdir/subdir1/invalid2'
+ register: stat_invalid_link
+
+- name: Check symlink to directory in copy
+ stat:
+ path: '{{ remote_dir }}/sub/subdir/subdir1/subdir3'
+ register: stat_dir_in_copy_link
+
+- name: Check symlink to directory outside of copy
+ stat:
+ path: '{{ remote_dir }}/sub/subdir/subdir1/ansible-test-abs-link-dir'
+ register: stat_dir_outside_copy_link
+
+- name: Assert recursive copy symlinks local_follow=True
+ assert:
+ that:
+ - "stat_abs_link.stat.exists"
+ - "not stat_abs_link.stat.islnk"
+ - "stat_abs_link.stat.checksum == ('target'|hash('sha1'))"
+ - "stat_relative_link.stat.exists"
+ - "not stat_relative_link.stat.islnk"
+ - "stat_relative_link.stat.checksum == ('baz\n'|hash('sha1'))"
+ - "stat_self_link.stat.exists"
+ - "stat_self_link.stat.islnk"
+ - "'invalid' in stat_self_link.stat.lnk_target"
+ - "stat_invalid_link.stat.exists"
+ - "stat_invalid_link.stat.islnk"
+ - "'../invalid' in stat_invalid_link.stat.lnk_target"
+ - "stat_dir_in_copy_link.stat.exists"
+ - "not stat_dir_in_copy_link.stat.islnk"
+ - "stat_dir_in_copy_link.stat.isdir"
+ -
+ - "stat_dir_outside_copy_link.stat.exists"
+ - "not stat_dir_outside_copy_link.stat.islnk"
+ - "stat_dir_outside_copy_link.stat.isdir"
+
+- name: Stat the recursively copied directories
+ stat:
+ path: "{{ remote_dir }}/sub/{{ item }}"
+ register: dir_stats
+ with_items:
+ - "subdir"
+ - "subdir/subdira"
+ - "subdir/subdir1"
+ - "subdir/subdir1/subdir3"
+ - "subdir/subdir1/subdir3/subdir4"
+ - "subdir/subdir2"
+ - "subdir/subdir2/subdir3"
+ - "subdir/subdir2/subdir3/subdir4"
+
+- debug:
+ var: dir_stats
+ verbosity: 1
+
+- name: Assert recursive copied directories mode (3)
+ assert:
+ that:
+ - "item.stat.mode == '0700'"
+ with_items: "{{dir_stats.results}}"
+
+- name: Test recursive copy to directory no trailing slash, local_follow=True second time
+ copy:
+ src: subdir
+ dest: "{{ remote_subdir }}"
+ directory_mode: 0700
+ local_follow: True
+ register: recursive_copy_result
+
+- name: Assert that the second copy did not change anything
+ assert:
+ that:
+ - "recursive_copy_result is not changed"
+
+- name: Cleanup the recursive copy subdir
+ file:
+ name: "{{ remote_subdir }}"
+ state: absent
+
+#
+# Recursive copy of tricky symlinks
+#
+- block:
+ - name: Create a directory to copy from
+ file:
+ path: '{{ local_temp_dir }}/source1'
+ state: directory
+
+ - name: Create a directory outside of the tree
+ file:
+ path: '{{ local_temp_dir }}/source2'
+ state: directory
+
+ - name: Create a symlink to a directory outside of the tree
+ file:
+ path: '{{ local_temp_dir }}/source1/link'
+ src: '{{ local_temp_dir }}/source2'
+ state: link
+
+ - name: Create a circular link back to the tree
+ file:
+ path: '{{ local_temp_dir }}/source2/circle'
+ src: '../source1'
+ state: link
+
+ - name: Create output directory
+ file:
+ path: '{{ local_temp_dir }}/dest1'
+ state: directory
+ delegate_to: localhost
+
+- name: Recursive copy the source
+ copy:
+ src: '{{ local_temp_dir }}/source1'
+ dest: '{{ remote_dir }}/dest1'
+ local_follow: True
+ register: copy_result
+
+- name: Check that the tree link is now a directory
+ stat:
+ path: '{{ remote_dir }}/dest1/source1/link'
+ register: link_result
+
+- name: Check that the out of tree link is still a link
+ stat:
+ path: '{{ remote_dir }}/dest1/source1/link/circle'
+ register: circle_result
+
+- name: Verify that the recursive copy worked
+ assert:
+ that:
+ - 'copy_result.changed'
+ - 'link_result.stat.isdir'
+ - 'not link_result.stat.islnk'
+ - 'circle_result.stat.islnk'
+ - '"../source1" == circle_result.stat.lnk_target'
+
+- name: Recursive copy the source a second time
+ copy:
+ src: '{{ local_temp_dir }}/source1'
+ dest: '{{ remote_dir }}/dest1'
+ local_follow: True
+ register: copy_result
+
+- name: Verify that the recursive copy made no changes
+ assert:
+ that:
+ - 'not copy_result.changed'
+
+#
+# Recursive copy with absolute paths (#27439)
+#
+- name: Test that remote_dir is appropriate for this test (absolute path)
+ assert:
+ that:
+ - '{{ remote_dir_expanded[0] == "/" }}'
+
+- block:
+ - name: Create a directory to copy
+ file:
+ path: '{{ local_temp_dir }}/source_recursive'
+ state: directory
+
+ - name: Create a file inside of the directory
+ copy:
+ content: "testing"
+ dest: '{{ local_temp_dir }}/source_recursive/file'
+
+ - name: Create a directory to place the test output in
+ file:
+ path: '{{ local_temp_dir }}/destination'
+ state: directory
+ delegate_to: localhost
+
+- name: Copy the directory and files within (no trailing slash)
+ copy:
+ src: '{{ local_temp_dir }}/source_recursive'
+ dest: '{{ remote_dir }}/destination'
+
+- name: Stat the recursively copied directory
+ stat:
+ path: "{{ remote_dir }}/destination/{{ item }}"
+ register: copied_stat
+ with_items:
+ - "source_recursive"
+ - "source_recursive/file"
+ - "file"
+
+- debug:
+ var: copied_stat
+ verbosity: 1
+
+- name: Assert with no trailing slash, directory and file is copied
+ assert:
+ that:
+ - "copied_stat.results[0].stat.exists"
+ - "copied_stat.results[1].stat.exists"
+ - "not copied_stat.results[2].stat.exists"
+
+- name: Cleanup
+ file:
+ path: '{{ remote_dir }}/destination'
+ state: absent
+
+# Try again with no trailing slash
+
+- name: Create a directory to place the test output in
+ file:
+ path: '{{ remote_dir }}/destination'
+ state: directory
+
+- name: Copy just the files inside of the directory
+ copy:
+ src: '{{ local_temp_dir }}/source_recursive/'
+ dest: '{{ remote_dir }}/destination'
+
+- name: Stat the recursively copied directory
+ stat:
+ path: "{{ remote_dir }}/destination/{{ item }}"
+ register: copied_stat
+ with_items:
+ - "source_recursive"
+ - "source_recursive/file"
+ - "file"
+
+- debug:
+ var: copied_stat
+ verbosity: 1
+
+- name: Assert with trailing slash, only the file is copied
+ assert:
+ that:
+ - "not copied_stat.results[0].stat.exists"
+ - "not copied_stat.results[1].stat.exists"
+ - "copied_stat.results[2].stat.exists"
+
+#
+# Recursive copy with relative paths (#34893)
+#
+
+- name: Create a directory to copy
+ file:
+ path: 'source_recursive'
+ state: directory
+ delegate_to: localhost
+
+- name: Create a file inside of the directory
+ copy:
+ content: "testing"
+ dest: 'source_recursive/file'
+ delegate_to: localhost
+
+- name: Create a directory to place the test output in
+ file:
+ path: 'destination'
+ state: directory
+ delegate_to: localhost
+
+- name: Copy the directory and files within (no trailing slash)
+ copy:
+ src: 'source_recursive'
+ dest: 'destination'
+
+- name: Stat the recursively copied directory
+ stat:
+ path: "destination/{{ item }}"
+ register: copied_stat
+ with_items:
+ - "source_recursive"
+ - "source_recursive/file"
+ - "file"
+
+- debug:
+ var: copied_stat
+ verbosity: 1
+
+- name: Assert with no trailing slash, directory and file is copied
+ assert:
+ that:
+ - "copied_stat.results[0].stat.exists"
+ - "copied_stat.results[1].stat.exists"
+ - "not copied_stat.results[2].stat.exists"
+
+- name: Cleanup
+ file:
+ path: 'destination'
+ state: absent
+
+# Try again with no trailing slash
+
+- name: Create a directory to place the test output in
+ file:
+ path: 'destination'
+ state: directory
+
+- name: Copy just the files inside of the directory
+ copy:
+ src: 'source_recursive/'
+ dest: 'destination'
+
+- name: Stat the recursively copied directory
+ stat:
+ path: "destination/{{ item }}"
+ register: copied_stat
+ with_items:
+ - "source_recursive"
+ - "source_recursive/file"
+ - "file"
+
+- debug:
+ var: copied_stat
+ verbosity: 1
+
+- name: Assert with trailing slash, only the file is copied
+ assert:
+ that:
+ - "not copied_stat.results[0].stat.exists"
+ - "not copied_stat.results[1].stat.exists"
+ - "copied_stat.results[2].stat.exists"
+
+- name: Cleanup
+ file:
+ path: 'destination'
+ state: absent
+
+- name: Cleanup
+ file:
+ path: 'source_recursive'
+ state: absent
+
+#
+# issue 8394
+#
+
+- name: Create a file with content and a literal multiline block
+ copy:
+ content: |
+ this is the first line
+ this is the second line
+
+ this line is after an empty line
+ this line is the last line
+ dest: "{{ remote_dir }}/multiline.txt"
+ register: copy_result6
+
+- debug:
+ var: copy_result6
+ verbosity: 1
+
+- name: Assert the multiline file was created correctly
+ assert:
+ that:
+ - "copy_result6.changed"
+ - "copy_result6.dest == '{{remote_dir_expanded}}/multiline.txt'"
+ - "copy_result6.checksum == '9cd0697c6a9ff6689f0afb9136fa62e0b3fee903'"
+
+# test overwriting a file as an unprivileged user (pull request #8624)
+# this can't be relative to {{remote_dir}} as ~root usually has mode 700
+- block:
+ - name: Create world writable directory
+ file:
+ dest: /tmp/worldwritable
+ state: directory
+ mode: 0777
+
+ - name: Create world writable file
+ copy:
+ dest: /tmp/worldwritable/file.txt
+ content: "bar"
+ mode: 0666
+
+ - name: Overwrite the file as user nobody
+ copy:
+ dest: /tmp/worldwritable/file.txt
+ content: "baz"
+ become: yes
+ become_user: nobody
+ register: copy_result7
+
+ - name: Assert the file was overwritten
+ assert:
+ that:
+ - "copy_result7.changed"
+ - "copy_result7.dest == '/tmp/worldwritable/file.txt'"
+ - "copy_result7.checksum == ('baz'|hash('sha1'))"
+
+ - name: Clean up
+ file:
+ dest: /tmp/worldwritable
+ state: absent
+
+ remote_user: root
+
+#
+# Follow=True tests
+#
+
+# test overwriting a link using "follow=yes" so that the link
+# is preserved and the link target is updated
+
+- name: Create a test file to symlink to
+ copy:
+ dest: "{{ remote_dir }}/follow_test"
+ content: "this is the follow test file\n"
+
+- name: Create a symlink to the test file
+ file:
+ path: "{{ remote_dir }}/follow_link"
+ src: './follow_test'
+ state: link
+
+- name: Update the test file using follow=True to preserve the link
+ copy:
+ dest: "{{ remote_dir }}/follow_link"
+ src: foo.txt
+ follow: yes
+ register: replace_follow_result
+
+- name: Stat the link path
+ stat:
+ path: "{{ remote_dir }}/follow_link"
+ register: stat_link_result
+
+- name: Assert that the link is still a link and contents were changed
+ assert:
+ that:
+ - stat_link_result['stat']['islnk']
+ - stat_link_result['stat']['lnk_target'] == './follow_test'
+ - replace_follow_result['changed']
+ - "replace_follow_result['checksum'] == remote_file_hash"
+
+# Symlink handling when the dest is already there
+# https://github.com/ansible/ansible-modules-core/issues/1568
+
+- name: test idempotency by trying to copy to the symlink with the same contents
+ copy:
+ dest: "{{ remote_dir }}/follow_link"
+ src: foo.txt
+ follow: yes
+ register: replace_follow_result
+
+- name: Stat the link path
+ stat:
+ path: "{{ remote_dir }}/follow_link"
+ register: stat_link_result
+
+- name: Assert that the link is still a link and contents were changed
+ assert:
+ that:
+ - stat_link_result['stat']['islnk']
+ - stat_link_result['stat']['lnk_target'] == './follow_test'
+ - not replace_follow_result['changed']
+ - replace_follow_result['checksum'] == remote_file_hash
+
+
+- name: Update the test file using follow=False to overwrite the link
+ copy:
+ dest: '{{ remote_dir }}/follow_link'
+ content: 'modified'
+ follow: False
+ register: copy_results
+
+- name: Check the stat results of the file
+ stat:
+ path: '{{remote_dir}}/follow_link'
+ register: stat_results
+
+- debug:
+ var: stat_results
+ verbosity: 1
+
+- name: Assert that the file has changed and is not a link
+ assert:
+ that:
+ - "copy_results is changed"
+ - "'content' not in copy_results"
+ - "stat_results.stat.checksum == ('modified'|hash('sha1'))"
+ - "not stat_results.stat.islnk"
+
+# test overwriting a link using "follow=yes" so that the link
+# is preserved and the link target is updated when the thing being copied is a link
+
+#
+# File mode tests
+#
+
+- name: setup directory for test
+ file: state=directory dest={{remote_dir }}/directory mode=0755
+
+- name: set file mode when the destination is a directory
+ copy: src=foo.txt dest={{remote_dir}}/directory/ mode=0705
+
+- name: set file mode when the destination is a directory
+ copy: src=foo.txt dest={{remote_dir}}/directory/ mode=0604
+ register: file_result
+
+- name: check that the file has the correct attributes
+ stat: path={{ remote_dir }}/directory/foo.txt
+ register: file_attrs
+
+- assert:
+ that:
+ - "file_attrs.stat.mode == '0604'"
+ # The below assertions make an invalid assumption, these were not explicitly set
+ # - "file_attrs.stat.uid == 0"
+ # - "file_attrs.stat.pw_name == 'root'"
+
+- name: check that the containing directory did not change attributes
+ stat: path={{ remote_dir }}/directory/
+ register: dir_attrs
+
+- assert:
+ that:
+ - "dir_attrs.stat.mode == '0755'"
+
+# Test that recursive copy of a directory containing a symlink to another
+# directory, with mode=preserve and local_follow=no works.
+# See: https://github.com/ansible/ansible/issues/68471
+
+- name: Test recursive copy of dir with symlinks, mode=preserve, local_follow=False
+ copy:
+ src: '{{ role_path }}/files/subdir/'
+ dest: '{{ local_temp_dir }}/preserve_symlink/'
+ mode: preserve
+ local_follow: no
+
+- name: check that we actually used and still have a symlink
+ stat: path={{ local_temp_dir }}/preserve_symlink/subdir1/bar.txt
+ register: symlink_path
+
+- assert:
+ that:
+ - symlink_path.stat.exists
+ - symlink_path.stat.islnk
+
+#
+# I believe the below section is now covered in the recursive copying section.
+# Hold on for now as an original test case but delete once confirmed that
+# everything is passing
+
+#
+# Recursive copying with symlinks tests
+#
+- delegate_to: localhost
+ block:
+ - name: Create a test dir to copy
+ file:
+ path: '{{ local_temp_dir }}/top_dir'
+ state: directory
+
+ - name: Create a test dir to symlink to
+ file:
+ path: '{{ local_temp_dir }}/linked_dir'
+ state: directory
+
+ - name: Create a file in the test dir
+ copy:
+ dest: '{{ local_temp_dir }}/linked_dir/file1'
+ content: 'hello world'
+
+ - name: Create a link to the test dir
+ file:
+ path: '{{ local_temp_dir }}/top_dir/follow_link_dir'
+ src: '{{ local_temp_dir }}/linked_dir'
+ state: link
+
+ - name: Create a circular subdir
+ file:
+ path: '{{ local_temp_dir }}/top_dir/subdir'
+ state: directory
+
+ ### FIXME: Also add a test for a relative symlink
+ - name: Create a circular symlink
+ file:
+ path: '{{ local_temp_dir }}/top_dir/subdir/circle'
+ src: '{{ local_temp_dir }}/top_dir/'
+ state: link
+
+- name: Copy the directory's link
+ copy:
+ src: '{{ local_temp_dir }}/top_dir'
+ dest: '{{ remote_dir }}/new_dir'
+ local_follow: True
+
+- name: Stat the copied path
+ stat:
+ path: '{{ remote_dir }}/new_dir/top_dir/follow_link_dir'
+ register: stat_dir_result
+
+- name: Stat the copied file
+ stat:
+ path: '{{ remote_dir }}/new_dir/top_dir/follow_link_dir/file1'
+ register: stat_file_in_dir_result
+
+- name: Stat the circular symlink
+ stat:
+ path: '{{ remote_dir }}/new_dir/top_dir/subdir/circle'
+ register: stat_circular_symlink_result
+
+- name: Assert that the directory exists
+ assert:
+ that:
+ - stat_dir_result.stat.exists
+ - stat_dir_result.stat.isdir
+ - stat_file_in_dir_result.stat.exists
+ - stat_file_in_dir_result.stat.isreg
+ - stat_circular_symlink_result.stat.exists
+ - stat_circular_symlink_result.stat.islnk
+
+# Relative paths in dest:
+- name: Smoketest that copying content to an implicit relative path works
+ copy:
+ content: 'testing'
+ dest: 'ansible-testing.txt'
+ register: relative_results
+
+- name: Assert that copying to an implicit relative path reported changed
+ assert:
+ that:
+ - 'relative_results["changed"]'
+ - 'relative_results["checksum"] == "dc724af18fbdd4e59189f5fe768a5f8311527050"'
+
+- name: Test that copying the same content with an implicit relative path reports no change
+ copy:
+ content: 'testing'
+ dest: 'ansible-testing.txt'
+ register: relative_results
+
+- name: Assert that copying the same content with an implicit relative path reports no change
+ assert:
+ that:
+ - 'not relative_results["changed"]'
+ - 'relative_results["checksum"] == "dc724af18fbdd4e59189f5fe768a5f8311527050"'
+
+- name: Test that copying different content with an implicit relative path reports change
+ copy:
+ content: 'testing2'
+ dest: 'ansible-testing.txt'
+ register: relative_results
+
+- name: Assert that copying different content with an implicit relative path reports changed
+ assert:
+ that:
+ - 'relative_results["changed"]'
+ - 'relative_results["checksum"] == "596b29ec9afea9e461a20610d150939b9c399d93"'
+
+- name: Smoketest that explicit relative path works
+ copy:
+ content: 'testing'
+ dest: './ansible-testing.txt'
+ register: relative_results
+
+- name: Assert that explicit relative paths reports change
+ assert:
+ that:
+ - 'relative_results["changed"]'
+ - 'relative_results["checksum"] == "dc724af18fbdd4e59189f5fe768a5f8311527050"'
+
+- name: Cleanup relative path tests
+ file:
+ path: 'ansible-testing.txt'
+ state: absent
+
+# src is a file, dest is a non-existent directory (2 levels of directories):
+# using remote_src
+# checks that dest is created
+- include_tasks: file=dest_in_non_existent_directories_remote_src.yml
+ with_items:
+ - { src: 'foo.txt', dest: 'new_sub_dir1/sub_dir2/', check: 'new_sub_dir1/sub_dir2/foo.txt' }
+
+# src is a file, dest is file in a non-existent directory: checks that a failure occurs
+# using remote_src
+- include_tasks: file=src_file_dest_file_in_non_existent_dir_remote_src.yml
+ with_items:
+ - 'new_sub_dir1/sub_dir2/foo.txt'
+ - 'new_sub_dir1/foo.txt'
+ loop_control:
+ loop_var: 'dest'
+
+# src is a file, dest is a non-existent directory (2 levels of directories):
+# checks that dest is created
+- include_tasks: file=dest_in_non_existent_directories.yml
+ with_items:
+ - { src: 'foo.txt', dest: 'new_sub_dir1/sub_dir2/', check: 'new_sub_dir1/sub_dir2/foo.txt' }
+ - { src: 'subdir', dest: 'new_sub_dir1/sub_dir2/', check: 'new_sub_dir1/sub_dir2/subdir/bar.txt' }
+ - { src: 'subdir/', dest: 'new_sub_dir1/sub_dir2/', check: 'new_sub_dir1/sub_dir2/bar.txt' }
+ - { src: 'subdir', dest: 'new_sub_dir1/sub_dir2', check: 'new_sub_dir1/sub_dir2/subdir/bar.txt' }
+ - { src: 'subdir/', dest: 'new_sub_dir1/sub_dir2', check: 'new_sub_dir1/sub_dir2/bar.txt' }
+
+# src is a file, dest is file in a non-existent directory: checks that a failure occurs
+- include_tasks: file=src_file_dest_file_in_non_existent_dir.yml
+ with_items:
+ - 'new_sub_dir1/sub_dir2/foo.txt'
+ - 'new_sub_dir1/foo.txt'
+ loop_control:
+ loop_var: 'dest'
+#
+# Recursive copying on remote host
+#
+## prepare for test
+- block:
+
+ - name: execute - Create a test src dir
+ file:
+ path: '{{ remote_dir }}/remote_dir_src'
+ state: directory
+
+ - name: gather - Stat the remote_dir_src
+ stat:
+ path: '{{ remote_dir }}/remote_dir_src'
+ register: stat_remote_dir_src_before
+
+ - name: execute - Create a subdir
+ file:
+ path: '{{ remote_dir }}/remote_dir_src/subdir'
+ state: directory
+
+ - name: gather - Stat the remote_dir_src/subdir
+ stat:
+ path: '{{ remote_dir }}/remote_dir_src/subdir'
+ register: stat_remote_dir_src_subdir_before
+
+ - name: execute - Create a file in the top of src
+ copy:
+ dest: '{{ remote_dir }}/remote_dir_src/file1'
+ content: 'hello world 1'
+
+ - name: gather - Stat the remote_dir_src/file1
+ stat:
+ path: '{{ remote_dir }}/remote_dir_src/file1'
+ register: stat_remote_dir_src_file1_before
+
+ - name: execute - Create a file in the subdir
+ copy:
+ dest: '{{ remote_dir }}/remote_dir_src/subdir/file12'
+ content: 'hello world 12'
+
+ - name: gather - Stat the remote_dir_src/subdir/file12
+ stat:
+ path: '{{ remote_dir }}/remote_dir_src/subdir/file12'
+ register: stat_remote_dir_src_subdir_file12_before
+
+ - name: execute - Create a link to the file12
+ file:
+ path: '{{ remote_dir }}/remote_dir_src/link_file12'
+ src: '{{ remote_dir }}/remote_dir_src/subdir/file12'
+ state: link
+
+ - name: gather - Stat the remote_dir_src/link_file12
+ stat:
+ path: '{{ remote_dir }}/remote_dir_src/link_file12'
+ register: stat_remote_dir_src_link_file12_before
+
+### test when src endswith os.sep and dest isdir
+- block:
+
+### local_follow: True
+ - name: execute - Create a test dest dir
+ file:
+ path: '{{ remote_dir }}/testcase1_local_follow_true'
+ state: directory
+
+ - name: execute - Copy the directory on remote with local_follow True
+ copy:
+ remote_src: True
+ src: '{{ remote_dir }}/remote_dir_src/'
+ dest: '{{ remote_dir }}/testcase1_local_follow_true'
+ local_follow: True
+ register: testcase1
+
+ - name: gather - Stat the testcase1_local_follow_true
+ stat:
+ path: '{{ remote_dir }}/testcase1_local_follow_true'
+ register: stat_testcase1_local_follow_true
+ - name: gather - Stat the testcase1_local_follow_true/subdir
+ stat:
+ path: '{{ remote_dir }}/testcase1_local_follow_true/subdir'
+ register: stat_testcase1_local_follow_true_subdir
+ - name: gather - Stat the testcase1_local_follow_true/file1
+ stat:
+ path: '{{ remote_dir }}/testcase1_local_follow_true/file1'
+ register: stat_testcase1_local_follow_true_file1
+ - name: gather - Stat the testcase1_local_follow_true/subdir/file12
+ stat:
+ path: '{{ remote_dir }}/testcase1_local_follow_true/subdir/file12'
+ register: stat_testcase1_local_follow_true_subdir_file12
+ - name: gather - Stat the testcase1_local_follow_true/link_file12
+ stat:
+ path: '{{ remote_dir }}/testcase1_local_follow_true/link_file12'
+ register: stat_testcase1_local_follow_true_link_file12
+
+ - name: assert - remote_dir_src has copied with local_follow True.
+ assert:
+ that:
+ - testcase1 is changed
+ - "stat_testcase1_local_follow_true.stat.isdir"
+ - "stat_testcase1_local_follow_true_subdir.stat.isdir"
+ - "stat_testcase1_local_follow_true_file1.stat.exists"
+ - "stat_remote_dir_src_file1_before.stat.checksum == stat_testcase1_local_follow_true_file1.stat.checksum"
+ - "stat_testcase1_local_follow_true_subdir_file12.stat.exists"
+ - "stat_remote_dir_src_subdir_file12_before.stat.checksum == stat_testcase1_local_follow_true_subdir_file12.stat.checksum"
+ - "stat_testcase1_local_follow_true_link_file12.stat.exists"
+ - "not stat_testcase1_local_follow_true_link_file12.stat.islnk"
+ - "stat_remote_dir_src_subdir_file12_before.stat.checksum == stat_testcase1_local_follow_true_link_file12.stat.checksum"
+
+### local_follow: False
+ - name: execute - Create a test dest dir
+ file:
+ path: '{{ remote_dir }}/testcase1_local_follow_false'
+ state: directory
+
+ - name: execute - Copy the directory on remote with local_follow False
+ copy:
+ remote_src: True
+ src: '{{ remote_dir }}/remote_dir_src/'
+ dest: '{{ remote_dir }}/testcase1_local_follow_false'
+ local_follow: False
+ register: testcase1
+
+ - name: gather - Stat the testcase1_local_follow_false
+ stat:
+ path: '{{ remote_dir }}/testcase1_local_follow_false'
+ register: stat_testcase1_local_follow_false
+ - name: gather - Stat the testcase1_local_follow_false/subdir
+ stat:
+ path: '{{ remote_dir }}/testcase1_local_follow_false/subdir'
+ register: stat_testcase1_local_follow_false_subdir
+ - name: gather - Stat the testcase1_local_follow_false/file1
+ stat:
+ path: '{{ remote_dir }}/testcase1_local_follow_false/file1'
+ register: stat_testcase1_local_follow_false_file1
+ - name: gather - Stat the testcase1_local_follow_false/subdir/file12
+ stat:
+ path: '{{ remote_dir }}/testcase1_local_follow_false/subdir/file12'
+ register: stat_testcase1_local_follow_false_subdir_file12
+ - name: gather - Stat the testcase1_local_follow_false/link_file12
+ stat:
+ path: '{{ remote_dir }}/testcase1_local_follow_false/link_file12'
+ register: stat_testcase1_local_follow_false_link_file12
+
+ - name: assert - remote_dir_src has copied with local_follow True.
+ assert:
+ that:
+ - testcase1 is changed
+ - "stat_testcase1_local_follow_false.stat.isdir"
+ - "stat_testcase1_local_follow_false_subdir.stat.isdir"
+ - "stat_testcase1_local_follow_false_file1.stat.exists"
+ - "stat_remote_dir_src_file1_before.stat.checksum == stat_testcase1_local_follow_false_file1.stat.checksum"
+ - "stat_testcase1_local_follow_false_subdir_file12.stat.exists"
+ - "stat_remote_dir_src_subdir_file12_before.stat.checksum == stat_testcase1_local_follow_false_subdir_file12.stat.checksum"
+ - "stat_testcase1_local_follow_false_link_file12.stat.exists"
+ - "stat_testcase1_local_follow_false_link_file12.stat.islnk"
+
+## test when src endswith os.sep and dest not exists
+
+- block:
+ - name: execute - Copy the directory on remote with local_follow True
+ copy:
+ remote_src: True
+ src: '{{ remote_dir }}/remote_dir_src/'
+ dest: '{{ remote_dir }}/testcase2_local_follow_true'
+ local_follow: True
+ register: testcase2
+
+ - name: gather - Stat the testcase2_local_follow_true
+ stat:
+ path: '{{ remote_dir }}/testcase2_local_follow_true'
+ register: stat_testcase2_local_follow_true
+ - name: gather - Stat the testcase2_local_follow_true/subdir
+ stat:
+ path: '{{ remote_dir }}/testcase2_local_follow_true/subdir'
+ register: stat_testcase2_local_follow_true_subdir
+ - name: gather - Stat the testcase2_local_follow_true/file1
+ stat:
+ path: '{{ remote_dir }}/testcase2_local_follow_true/file1'
+ register: stat_testcase2_local_follow_true_file1
+ - name: gather - Stat the testcase2_local_follow_true/subdir/file12
+ stat:
+ path: '{{ remote_dir }}/testcase2_local_follow_true/subdir/file12'
+ register: stat_testcase2_local_follow_true_subdir_file12
+ - name: gather - Stat the testcase2_local_follow_true/link_file12
+ stat:
+ path: '{{ remote_dir }}/testcase2_local_follow_true/link_file12'
+ register: stat_testcase2_local_follow_true_link_file12
+
+ - name: assert - remote_dir_src has copied with local_follow True.
+ assert:
+ that:
+ - testcase2 is changed
+ - "stat_testcase2_local_follow_true.stat.isdir"
+ - "stat_testcase2_local_follow_true_subdir.stat.isdir"
+ - "stat_testcase2_local_follow_true_file1.stat.exists"
+ - "stat_remote_dir_src_file1_before.stat.checksum == stat_testcase2_local_follow_true_file1.stat.checksum"
+ - "stat_testcase2_local_follow_true_subdir_file12.stat.exists"
+ - "stat_remote_dir_src_subdir_file12_before.stat.checksum == stat_testcase2_local_follow_true_subdir_file12.stat.checksum"
+ - "stat_testcase2_local_follow_true_link_file12.stat.exists"
+ - "not stat_testcase2_local_follow_true_link_file12.stat.islnk"
+ - "stat_remote_dir_src_subdir_file12_before.stat.checksum == stat_testcase2_local_follow_true_link_file12.stat.checksum"
+
+### local_follow: False
+ - name: execute - Copy the directory on remote with local_follow False
+ copy:
+ remote_src: True
+ src: '{{ remote_dir }}/remote_dir_src/'
+ dest: '{{ remote_dir }}/testcase2_local_follow_false'
+ local_follow: False
+ register: testcase2
+
+ - name: execute - Copy the directory on remote with local_follow False
+ copy:
+ remote_src: True
+ src: '{{ remote_dir }}/remote_dir_src/'
+ dest: '{{ remote_dir }}/testcase2_local_follow_false'
+ local_follow: False
+ register: testcase1
+
+ - name: gather - Stat the testcase2_local_follow_false
+ stat:
+ path: '{{ remote_dir }}/testcase2_local_follow_false'
+ register: stat_testcase2_local_follow_false
+ - name: gather - Stat the testcase2_local_follow_false/subdir
+ stat:
+ path: '{{ remote_dir }}/testcase2_local_follow_false/subdir'
+ register: stat_testcase2_local_follow_false_subdir
+ - name: gather - Stat the testcase2_local_follow_false/file1
+ stat:
+ path: '{{ remote_dir }}/testcase2_local_follow_false/file1'
+ register: stat_testcase2_local_follow_false_file1
+ - name: gather - Stat the testcase2_local_follow_false/subdir/file12
+ stat:
+ path: '{{ remote_dir }}/testcase2_local_follow_false/subdir/file12'
+ register: stat_testcase2_local_follow_false_subdir_file12
+ - name: gather - Stat the testcase2_local_follow_false/link_file12
+ stat:
+ path: '{{ remote_dir }}/testcase2_local_follow_false/link_file12'
+ register: stat_testcase2_local_follow_false_link_file12
+
+ - name: assert - remote_dir_src has copied with local_follow True.
+ assert:
+ that:
+ - testcase2 is changed
+ - "stat_testcase2_local_follow_false.stat.isdir"
+ - "stat_testcase2_local_follow_false_subdir.stat.isdir"
+ - "stat_testcase2_local_follow_false_file1.stat.exists"
+ - "stat_remote_dir_src_file1_before.stat.checksum == stat_testcase2_local_follow_false_file1.stat.checksum"
+ - "stat_testcase2_local_follow_false_subdir_file12.stat.exists"
+ - "stat_remote_dir_src_subdir_file12_before.stat.checksum == stat_testcase2_local_follow_false_subdir_file12.stat.checksum"
+ - "stat_testcase2_local_follow_false_link_file12.stat.exists"
+ - "stat_testcase2_local_follow_false_link_file12.stat.islnk"
+
+## test when src not endswith os.sep and dest isdir
+- block:
+
+### local_follow: True
+ - name: execute - Create a test dest dir
+ file:
+ path: '{{ remote_dir }}/testcase3_local_follow_true'
+ state: directory
+
+ - name: execute - Copy the directory on remote with local_follow True
+ copy:
+ remote_src: True
+ src: '{{ remote_dir }}/remote_dir_src'
+ dest: '{{ remote_dir }}/testcase3_local_follow_true'
+ local_follow: True
+ register: testcase3
+
+ - name: gather - Stat the testcase3_local_follow_true
+ stat:
+ path: '{{ remote_dir }}/testcase3_local_follow_true/remote_dir_src'
+ register: stat_testcase3_local_follow_true_remote_dir_src
+ - name: gather - Stat the testcase3_local_follow_true/remote_dir_src/subdir
+ stat:
+ path: '{{ remote_dir }}/testcase3_local_follow_true/remote_dir_src/subdir'
+ register: stat_testcase3_local_follow_true_remote_dir_src_subdir
+ - name: gather - Stat the testcase3_local_follow_true/remote_dir_src/file1
+ stat:
+ path: '{{ remote_dir }}/testcase3_local_follow_true/remote_dir_src/file1'
+ register: stat_testcase3_local_follow_true_remote_dir_src_file1
+ - name: gather - Stat the testcase3_local_follow_true/remote_dir_src/subdir/file12
+ stat:
+ path: '{{ remote_dir }}/testcase3_local_follow_true/remote_dir_src/subdir/file12'
+ register: stat_testcase3_local_follow_true_remote_dir_src_subdir_file12
+ - name: gather - Stat the testcase3_local_follow_true/remote_dir_src/link_file12
+ stat:
+ path: '{{ remote_dir }}/testcase3_local_follow_true/remote_dir_src/link_file12'
+ register: stat_testcase3_local_follow_true_remote_dir_src_link_file12
+
+ - name: assert - remote_dir_src has copied with local_follow True.
+ assert:
+ that:
+ - testcase3 is changed
+ - "stat_testcase3_local_follow_true_remote_dir_src.stat.isdir"
+ - "stat_testcase3_local_follow_true_remote_dir_src_subdir.stat.isdir"
+ - "stat_testcase3_local_follow_true_remote_dir_src_file1.stat.exists"
+ - "stat_remote_dir_src_file1_before.stat.checksum == stat_testcase3_local_follow_true_remote_dir_src_file1.stat.checksum"
+ - "stat_testcase3_local_follow_true_remote_dir_src_subdir_file12.stat.exists"
+ - "stat_remote_dir_src_subdir_file12_before.stat.checksum == stat_testcase3_local_follow_true_remote_dir_src_subdir_file12.stat.checksum"
+ - "stat_testcase3_local_follow_true_remote_dir_src_link_file12.stat.exists"
+ - "not stat_testcase3_local_follow_true_remote_dir_src_link_file12.stat.islnk"
+ - "stat_remote_dir_src_subdir_file12_before.stat.checksum == stat_testcase3_local_follow_true_remote_dir_src_link_file12.stat.checksum"
+
+### local_follow: False
+ - name: execute - Create a test dest dir
+ file:
+ path: '{{ remote_dir }}/testcase3_local_follow_false'
+ state: directory
+
+ - name: execute - Copy the directory on remote with local_follow False
+ copy:
+ remote_src: True
+ src: '{{ remote_dir }}/remote_dir_src'
+ dest: '{{ remote_dir }}/testcase3_local_follow_false'
+ local_follow: False
+ register: testcase3
+
+ - name: gather - Stat the testcase3_local_follow_false
+ stat:
+ path: '{{ remote_dir }}/testcase3_local_follow_false/remote_dir_src'
+ register: stat_testcase3_local_follow_false_remote_dir_src
+ - name: gather - Stat the testcase3_local_follow_false/remote_dir_src/subdir
+ stat:
+ path: '{{ remote_dir }}/testcase3_local_follow_false/remote_dir_src/subdir'
+ register: stat_testcase3_local_follow_false_remote_dir_src_subdir
+ - name: gather - Stat the testcase3_local_follow_false/remote_dir_src/file1
+ stat:
+ path: '{{ remote_dir }}/testcase3_local_follow_false/remote_dir_src/file1'
+ register: stat_testcase3_local_follow_false_remote_dir_src_file1
+ - name: gather - Stat the testcase3_local_follow_false/remote_dir_src/subdir/file12
+ stat:
+ path: '{{ remote_dir }}/testcase3_local_follow_false/remote_dir_src/subdir/file12'
+ register: stat_testcase3_local_follow_false_remote_dir_src_subdir_file12
+ - name: gather - Stat the testcase3_local_follow_false/remote_dir_src/link_file12
+ stat:
+ path: '{{ remote_dir }}/testcase3_local_follow_false/remote_dir_src/link_file12'
+ register: stat_testcase3_local_follow_false_remote_dir_src_link_file12
+
+ - name: assert - remote_dir_src has copied with local_follow False.
+ assert:
+ that:
+ - testcase3 is changed
+ - "stat_testcase3_local_follow_false_remote_dir_src.stat.isdir"
+ - "stat_testcase3_local_follow_false_remote_dir_src_subdir.stat.isdir"
+ - "stat_testcase3_local_follow_false_remote_dir_src_file1.stat.exists"
+ - "stat_remote_dir_src_file1_before.stat.checksum == stat_testcase3_local_follow_false_remote_dir_src_file1.stat.checksum"
+ - "stat_testcase3_local_follow_false_remote_dir_src_subdir_file12.stat.exists"
+ - "stat_remote_dir_src_subdir_file12_before.stat.checksum == stat_testcase3_local_follow_false_remote_dir_src_subdir_file12.stat.checksum"
+ - "stat_testcase3_local_follow_false_remote_dir_src_link_file12.stat.exists"
+ - "stat_testcase3_local_follow_false_remote_dir_src_link_file12.stat.islnk"
+
+## test when src not endswith os.sep and dest not exists
+- block:
+### local_follow: True
+ - name: execute - Copy the directory on remote with local_follow True
+ copy:
+ remote_src: True
+ src: '{{ remote_dir }}/remote_dir_src'
+ dest: '{{ remote_dir }}/testcase4_local_follow_true'
+ local_follow: True
+ register: testcase4
+
+ - name: gather - Stat the testcase4_local_follow_true
+ stat:
+ path: '{{ remote_dir }}/testcase4_local_follow_true/remote_dir_src'
+ register: stat_testcase4_local_follow_true_remote_dir_src
+ - name: gather - Stat the testcase4_local_follow_true/remote_dir_src/subdir
+ stat:
+ path: '{{ remote_dir }}/testcase4_local_follow_true/remote_dir_src/subdir'
+ register: stat_testcase4_local_follow_true_remote_dir_src_subdir
+ - name: gather - Stat the testcase4_local_follow_true/remote_dir_src/file1
+ stat:
+ path: '{{ remote_dir }}/testcase4_local_follow_true/remote_dir_src/file1'
+ register: stat_testcase4_local_follow_true_remote_dir_src_file1
+ - name: gather - Stat the testcase4_local_follow_true/remote_dir_src/subdir/file12
+ stat:
+ path: '{{ remote_dir }}/testcase4_local_follow_true/remote_dir_src/subdir/file12'
+ register: stat_testcase4_local_follow_true_remote_dir_src_subdir_file12
+ - name: gather - Stat the testcase4_local_follow_true/remote_dir_src/link_file12
+ stat:
+ path: '{{ remote_dir }}/testcase4_local_follow_true/remote_dir_src/link_file12'
+ register: stat_testcase4_local_follow_true_remote_dir_src_link_file12
+
+ - name: assert - remote_dir_src has copied with local_follow True.
+ assert:
+ that:
+ - testcase4 is changed
+ - "stat_testcase4_local_follow_true_remote_dir_src.stat.isdir"
+ - "stat_testcase4_local_follow_true_remote_dir_src_subdir.stat.isdir"
+ - "stat_testcase4_local_follow_true_remote_dir_src_file1.stat.exists"
+ - "stat_remote_dir_src_file1_before.stat.checksum == stat_testcase4_local_follow_true_remote_dir_src_file1.stat.checksum"
+ - "stat_testcase4_local_follow_true_remote_dir_src_subdir_file12.stat.exists"
+ - "stat_remote_dir_src_subdir_file12_before.stat.checksum == stat_testcase4_local_follow_true_remote_dir_src_subdir_file12.stat.checksum"
+ - "stat_testcase4_local_follow_true_remote_dir_src_link_file12.stat.exists"
+ - "not stat_testcase4_local_follow_true_remote_dir_src_link_file12.stat.islnk"
+ - "stat_remote_dir_src_subdir_file12_before.stat.checksum == stat_testcase4_local_follow_true_remote_dir_src_link_file12.stat.checksum"
+
+### local_follow: False
+ - name: execute - Copy the directory on remote with local_follow False
+ copy:
+ remote_src: True
+ src: '{{ remote_dir }}/remote_dir_src'
+ dest: '{{ remote_dir }}/testcase4_local_follow_false'
+ local_follow: False
+ register: testcase4
+
+ - name: gather - Stat the testcase4_local_follow_false
+ stat:
+ path: '{{ remote_dir }}/testcase4_local_follow_false/remote_dir_src'
+ register: stat_testcase4_local_follow_false_remote_dir_src
+ - name: gather - Stat the testcase4_local_follow_false/remote_dir_src/subdir
+ stat:
+ path: '{{ remote_dir }}/testcase4_local_follow_false/remote_dir_src/subdir'
+ register: stat_testcase4_local_follow_false_remote_dir_src_subdir
+ - name: gather - Stat the testcase4_local_follow_false/remote_dir_src/file1
+ stat:
+ path: '{{ remote_dir }}/testcase4_local_follow_false/remote_dir_src/file1'
+ register: stat_testcase4_local_follow_false_remote_dir_src_file1
+ - name: gather - Stat the testcase4_local_follow_false/remote_dir_src/subdir/file12
+ stat:
+ path: '{{ remote_dir }}/testcase4_local_follow_false/remote_dir_src/subdir/file12'
+ register: stat_testcase4_local_follow_false_remote_dir_src_subdir_file12
+ - name: gather - Stat the testcase4_local_follow_false/remote_dir_src/link_file12
+ stat:
+ path: '{{ remote_dir }}/testcase4_local_follow_false/remote_dir_src/link_file12'
+ register: stat_testcase4_local_follow_false_remote_dir_src_link_file12
+
+ - name: assert - remote_dir_src has copied with local_follow False.
+ assert:
+ that:
+ - testcase4 is changed
+ - "stat_testcase4_local_follow_false_remote_dir_src.stat.isdir"
+ - "stat_testcase4_local_follow_false_remote_dir_src_subdir.stat.isdir"
+ - "stat_testcase4_local_follow_false_remote_dir_src_file1.stat.exists"
+ - "stat_remote_dir_src_file1_before.stat.checksum == stat_testcase4_local_follow_false_remote_dir_src_file1.stat.checksum"
+ - "stat_testcase4_local_follow_false_remote_dir_src_subdir_file12.stat.exists"
+ - "stat_remote_dir_src_subdir_file12_before.stat.checksum == stat_testcase4_local_follow_false_remote_dir_src_subdir_file12.stat.checksum"
+ - "stat_testcase4_local_follow_false_remote_dir_src_link_file12.stat.exists"
+ - "stat_testcase4_local_follow_false_remote_dir_src_link_file12.stat.islnk"
+
+- block:
+ - name: execute - Clone the source directory on remote
+ copy:
+ remote_src: True
+ src: '{{ remote_dir }}/remote_dir_src/'
+ dest: '{{ remote_dir }}/testcase5_remote_src_subdirs_src'
+ - name: Create a 2nd level subdirectory
+ file:
+ path: '{{ remote_dir }}/testcase5_remote_src_subdirs_src/subdir/subdir2/'
+ state: directory
+ - name: execute - Copy the directory on remote
+ copy:
+ remote_src: True
+ src: '{{ remote_dir }}/testcase5_remote_src_subdirs_src/'
+ dest: '{{ remote_dir }}/testcase5_remote_src_subdirs_dest'
+ local_follow: True
+ - name: execute - Create a new file in the subdir
+ copy:
+ dest: '{{ remote_dir }}/testcase5_remote_src_subdirs_src/subdir/subdir2/file13'
+ content: 'very new file'
+ - name: gather - Stat the testcase5_remote_src_subdirs_src/subdir/subdir2/file13
+ stat:
+ path: '{{ remote_dir }}/testcase5_remote_src_subdirs_src/subdir/subdir2/file13'
+ - name: execute - Copy the directory on remote
+ copy:
+ remote_src: True
+ src: '{{ remote_dir }}/testcase5_remote_src_subdirs_src/'
+ dest: '{{ remote_dir }}/testcase5_remote_src_subdirs_dest/'
+ register: testcase5_new
+ - name: execute - Edit a file in the subdir
+ copy:
+ dest: '{{ remote_dir }}/testcase5_remote_src_subdirs_src/subdir/subdir2/file13'
+ content: 'NOT hello world 12'
+ - name: gather - Stat the testcase5_remote_src_subdirs_src/subdir/subdir2/file13
+ stat:
+ path: '{{ remote_dir }}/testcase5_remote_src_subdirs_src/subdir/subdir2/file13'
+ register: stat_testcase5_remote_src_subdirs_file13_before
+ - name: execute - Copy the directory on remote
+ copy:
+ remote_src: True
+ src: '{{ remote_dir }}/testcase5_remote_src_subdirs_src/'
+ dest: '{{ remote_dir }}/testcase5_remote_src_subdirs_dest/'
+ register: testcase5_edited
+ - name: gather - Stat the testcase5_remote_src_subdirs_dest/subdir/subdir2/file13
+ stat:
+ path: '{{ remote_dir }}/testcase5_remote_src_subdirs_dest/subdir/subdir2/file13'
+ register: stat_testcase5_remote_src_subdirs_file13
+ - name: assert - remote_dir_src has copied with local_follow False.
+ assert:
+ that:
+ - testcase5_new is changed
+ - testcase5_edited is changed
+ - "stat_testcase5_remote_src_subdirs_file13.stat.exists"
+ - "stat_testcase5_remote_src_subdirs_file13_before.stat.checksum == stat_testcase5_remote_src_subdirs_file13.stat.checksum"
+
+
+## test copying the directory on remote with chown
+- name: setting 'ansible_copy_test_user_name' outside block since 'always' section depends on this also
+ set_fact:
+ ansible_copy_test_user_name: 'ansible_copy_test_{{ 100000 | random }}'
+
+- block:
+
+ - name: execute - create a user for test
+ user:
+ name: '{{ ansible_copy_test_user_name }}'
+ state: present
+ become: true
+ register: ansible_copy_test_user
+
+ - name: execute - create a group for test
+ group:
+ name: '{{ ansible_copy_test_user_name }}'
+ state: present
+ become: true
+ register: ansible_copy_test_group
+
+ - name: execute - Copy the directory on remote with chown
+ copy:
+ remote_src: True
+ src: '{{ remote_dir_expanded }}/remote_dir_src/'
+ dest: '{{ remote_dir_expanded }}/new_dir_with_chown'
+ owner: '{{ ansible_copy_test_user_name }}'
+ group: '{{ ansible_copy_test_user_name }}'
+ follow: true
+ register: testcase5
+ become: true
+
+ - name: gather - Stat the new_dir_with_chown
+ stat:
+ path: '{{ remote_dir }}/new_dir_with_chown'
+ register: stat_new_dir_with_chown
+
+ - name: gather - Stat the new_dir_with_chown/file1
+ stat:
+ path: '{{ remote_dir }}/new_dir_with_chown/file1'
+ register: stat_new_dir_with_chown_file1
+
+ - name: gather - Stat the new_dir_with_chown/subdir
+ stat:
+ path: '{{ remote_dir }}/new_dir_with_chown/subdir'
+ register: stat_new_dir_with_chown_subdir
+
+ - name: gather - Stat the new_dir_with_chown/subdir/file12
+ stat:
+ path: '{{ remote_dir }}/new_dir_with_chown/subdir/file12'
+ register: stat_new_dir_with_chown_subdir_file12
+
+ - name: gather - Stat the new_dir_with_chown/link_file12
+ stat:
+ path: '{{ remote_dir }}/new_dir_with_chown/link_file12'
+ register: stat_new_dir_with_chown_link_file12
+
+ - name: assert - owner and group have changed
+ assert:
+ that:
+ - testcase5 is changed
+ - "stat_new_dir_with_chown.stat.uid == {{ ansible_copy_test_user.uid }}"
+ - "stat_new_dir_with_chown.stat.gid == {{ ansible_copy_test_group.gid }}"
+ - "stat_new_dir_with_chown.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
+ - "stat_new_dir_with_chown.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
+ - "stat_new_dir_with_chown_file1.stat.uid == {{ ansible_copy_test_user.uid }}"
+ - "stat_new_dir_with_chown_file1.stat.gid == {{ ansible_copy_test_group.gid }}"
+ - "stat_new_dir_with_chown_file1.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
+ - "stat_new_dir_with_chown_file1.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
+ - "stat_new_dir_with_chown_subdir.stat.uid == {{ ansible_copy_test_user.uid }}"
+ - "stat_new_dir_with_chown_subdir.stat.gid == {{ ansible_copy_test_group.gid }}"
+ - "stat_new_dir_with_chown_subdir.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
+ - "stat_new_dir_with_chown_subdir.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
+ - "stat_new_dir_with_chown_subdir_file12.stat.uid == {{ ansible_copy_test_user.uid }}"
+ - "stat_new_dir_with_chown_subdir_file12.stat.gid == {{ ansible_copy_test_group.gid }}"
+ - "stat_new_dir_with_chown_subdir_file12.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
+ - "stat_new_dir_with_chown_subdir_file12.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
+ - "stat_new_dir_with_chown_link_file12.stat.uid == {{ ansible_copy_test_user.uid }}"
+ - "stat_new_dir_with_chown_link_file12.stat.gid == {{ ansible_copy_test_group.gid }}"
+ - "stat_new_dir_with_chown_link_file12.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
+ - "stat_new_dir_with_chown_link_file12.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
+
+ always:
+ - name: execute - remove the user for test
+ user:
+ name: '{{ ansible_copy_test_user_name }}'
+ state: absent
+ remove: yes
+ become: true
+
+ - name: execute - remove the group for test
+ group:
+ name: '{{ ansible_copy_test_user_name }}'
+ state: absent
+ become: true
+
+## testcase last - make sure remote_dir_src not change
+- block:
+ - name: Stat the remote_dir_src
+ stat:
+ path: '{{ remote_dir }}/remote_dir_src'
+ register: stat_remote_dir_src_after
+
+ - name: Stat the remote_dir_src/subdir
+ stat:
+ path: '{{ remote_dir }}/remote_dir_src/subdir'
+ register: stat_remote_dir_src_subdir_after
+
+ - name: Stat the remote_dir_src/file1
+ stat:
+ path: '{{ remote_dir }}/remote_dir_src/file1'
+ register: stat_remote_dir_src_file1_after
+
+ - name: Stat the remote_dir_src/subdir/file12
+ stat:
+ path: '{{ remote_dir }}/remote_dir_src/subdir/file12'
+ register: stat_remote_dir_src_subdir_file12_after
+
+ - name: Stat the remote_dir_src/link_file12
+ stat:
+ path: '{{ remote_dir }}/remote_dir_src/link_file12'
+ register: stat_remote_dir_src_link_file12_after
+
+ - name: Assert that remote_dir_src not change.
+ assert:
+ that:
+ - "stat_remote_dir_src_after.stat.exists"
+ - "stat_remote_dir_src_after.stat.isdir"
+ - "stat_remote_dir_src_before.stat.uid == stat_remote_dir_src_after.stat.uid"
+ - "stat_remote_dir_src_before.stat.gid == stat_remote_dir_src_after.stat.gid"
+ - "stat_remote_dir_src_before.stat.pw_name == stat_remote_dir_src_after.stat.pw_name"
+ - "stat_remote_dir_src_before.stat.gr_name == stat_remote_dir_src_after.stat.gr_name"
+ - "stat_remote_dir_src_before.stat.path == stat_remote_dir_src_after.stat.path"
+ - "stat_remote_dir_src_before.stat.mode == stat_remote_dir_src_after.stat.mode"
+
+ - "stat_remote_dir_src_subdir_after.stat.exists"
+ - "stat_remote_dir_src_subdir_after.stat.isdir"
+ - "stat_remote_dir_src_subdir_before.stat.uid == stat_remote_dir_src_subdir_after.stat.uid"
+ - "stat_remote_dir_src_subdir_before.stat.gid == stat_remote_dir_src_subdir_after.stat.gid"
+ - "stat_remote_dir_src_subdir_before.stat.pw_name == stat_remote_dir_src_subdir_after.stat.pw_name"
+ - "stat_remote_dir_src_subdir_before.stat.gr_name == stat_remote_dir_src_subdir_after.stat.gr_name"
+ - "stat_remote_dir_src_subdir_before.stat.path == stat_remote_dir_src_subdir_after.stat.path"
+ - "stat_remote_dir_src_subdir_before.stat.mode == stat_remote_dir_src_subdir_after.stat.mode"
+
+ - "stat_remote_dir_src_file1_after.stat.exists"
+ - "stat_remote_dir_src_file1_before.stat.uid == stat_remote_dir_src_file1_after.stat.uid"
+ - "stat_remote_dir_src_file1_before.stat.gid == stat_remote_dir_src_file1_after.stat.gid"
+ - "stat_remote_dir_src_file1_before.stat.pw_name == stat_remote_dir_src_file1_after.stat.pw_name"
+ - "stat_remote_dir_src_file1_before.stat.gr_name == stat_remote_dir_src_file1_after.stat.gr_name"
+ - "stat_remote_dir_src_file1_before.stat.path == stat_remote_dir_src_file1_after.stat.path"
+ - "stat_remote_dir_src_file1_before.stat.mode == stat_remote_dir_src_file1_after.stat.mode"
+ - "stat_remote_dir_src_file1_before.stat.checksum == stat_remote_dir_src_file1_after.stat.checksum"
+
+ - "stat_remote_dir_src_subdir_file12_after.stat.exists"
+ - "stat_remote_dir_src_subdir_file12_before.stat.uid == stat_remote_dir_src_subdir_file12_after.stat.uid"
+ - "stat_remote_dir_src_subdir_file12_before.stat.gid == stat_remote_dir_src_subdir_file12_after.stat.gid"
+ - "stat_remote_dir_src_subdir_file12_before.stat.pw_name == stat_remote_dir_src_subdir_file12_after.stat.pw_name"
+ - "stat_remote_dir_src_subdir_file12_before.stat.gr_name == stat_remote_dir_src_subdir_file12_after.stat.gr_name"
+ - "stat_remote_dir_src_subdir_file12_before.stat.path == stat_remote_dir_src_subdir_file12_after.stat.path"
+ - "stat_remote_dir_src_subdir_file12_before.stat.mode == stat_remote_dir_src_subdir_file12_after.stat.mode"
+ - "stat_remote_dir_src_subdir_file12_before.stat.checksum == stat_remote_dir_src_subdir_file12_after.stat.checksum"
+
+ - "stat_remote_dir_src_link_file12_after.stat.exists"
+ - "stat_remote_dir_src_link_file12_after.stat.islnk"
+ - "stat_remote_dir_src_link_file12_before.stat.uid == stat_remote_dir_src_link_file12_after.stat.uid"
+ - "stat_remote_dir_src_link_file12_before.stat.gid == stat_remote_dir_src_link_file12_after.stat.gid"
+ - "stat_remote_dir_src_link_file12_before.stat.pw_name == stat_remote_dir_src_link_file12_after.stat.pw_name"
+ - "stat_remote_dir_src_link_file12_before.stat.gr_name == stat_remote_dir_src_link_file12_after.stat.gr_name"
+ - "stat_remote_dir_src_link_file12_before.stat.path == stat_remote_dir_src_link_file12_after.stat.path"
+ - "stat_remote_dir_src_link_file12_before.stat.mode == stat_remote_dir_src_link_file12_after.stat.mode"
+
+# Test for issue 69783: copy with remote_src=yes and src='dir/' preserves all permissions
+- block:
+ - name: Create directory structure
+ file:
+ path: "{{ local_temp_dir }}/test69783/{{ item }}"
+ state: directory
+ loop:
+ - "src/dir"
+ - "dest"
+
+ - name: Create source file structure
+ file:
+ path: "{{ local_temp_dir }}/test69783/src/{{ item.name }}"
+ state: touch
+ mode: "{{ item.mode }}"
+ loop:
+ - { name: 'readwrite', mode: '0644' }
+ - { name: 'executable', mode: '0755' }
+ - { name: 'readonly', mode: '0444' }
+ - { name: 'dir/readwrite', mode: '0644' }
+ - { name: 'dir/executable', mode: '0755' }
+ - { name: 'dir/readonly', mode: '0444' }
+
+ - name: Recursive remote copy with preserve
+ copy:
+ src: "{{ local_temp_dir }}/test69783/src/"
+ dest: "{{ local_temp_dir }}/test69783/dest/"
+ remote_src: yes
+ mode: preserve
+
+ - name: Stat dest 'readwrite' file
+ stat:
+ path: "{{ local_temp_dir}}/test69783/dest/readwrite"
+ register: dest_readwrite_stat
+
+ - name: Stat dest 'executable' file
+ stat:
+ path: "{{ local_temp_dir}}/test69783/dest/executable"
+ register: dest_executable_stat
+
+ - name: Stat dest 'readonly' file
+ stat:
+ path: "{{ local_temp_dir}}/test69783/dest/readonly"
+ register: dest_readonly_stat
+
+ - name: Stat dest 'dir/readwrite' file
+ stat:
+ path: "{{ local_temp_dir}}/test69783/dest/dir/readwrite"
+ register: dest_dir_readwrite_stat
+
+ - name: Stat dest 'dir/executable' file
+ stat:
+ path: "{{ local_temp_dir}}/test69783/dest/dir/executable"
+ register: dest_dir_executable_stat
+
+ - name: Stat dest 'dir/readonly' file
+ stat:
+ path: "{{ local_temp_dir}}/test69783/dest/dir/readonly"
+ register: dest_dir_readonly_stat
+
+ - name: Assert modes are preserved
+ assert:
+ that:
+ - "dest_readwrite_stat.stat.mode == '0644'"
+ - "dest_executable_stat.stat.mode == '0755'"
+ - "dest_readonly_stat.stat.mode == '0444'"
+ - "dest_dir_readwrite_stat.stat.mode == '0644'"
+ - "dest_dir_executable_stat.stat.mode == '0755'"
+ - "dest_dir_readonly_stat.stat.mode == '0444'"
+
+- name: fail to copy an encrypted file without the password set
+ copy:
+ src: '{{role_path}}/files-different/vault/vault-file'
+ dest: '{{remote_tmp_dir}}/copy/file'
+ register: fail_copy_encrypted_file
+ ignore_errors: yes # weird failed_when doesn't work in this case
+
+- name: assert failure message when copying an encrypted file without the password set
+ assert:
+ that:
+ - fail_copy_encrypted_file is failed
+ - fail_copy_encrypted_file.msg == 'A vault password or secret must be specified to decrypt {{role_path}}/files-different/vault/vault-file'
+
+- name: fail to copy a directory with an encrypted file without the password
+ copy:
+ src: '{{role_path}}/files-different/vault'
+ dest: '{{remote_tmp_dir}}/copy'
+ register: fail_copy_directory_with_enc_file
+ ignore_errors: yes
+
+- name: assert failure message when copying a directory that contains an encrypted file without the password set
+ assert:
+ that:
+ - fail_copy_directory_with_enc_file is failed
+ - fail_copy_directory_with_enc_file.msg == 'A vault password or secret must be specified to decrypt {{role_path}}/files-different/vault/vault-file'
diff --git a/test/integration/targets/cron/aliases b/test/integration/targets/cron/aliases
new file mode 100644
index 0000000..f2f9ac9
--- /dev/null
+++ b/test/integration/targets/cron/aliases
@@ -0,0 +1,4 @@
+destructive
+shippable/posix/group1
+skip/osx
+skip/macos
diff --git a/test/integration/targets/cron/defaults/main.yml b/test/integration/targets/cron/defaults/main.yml
new file mode 100644
index 0000000..37e6fc3
--- /dev/null
+++ b/test/integration/targets/cron/defaults/main.yml
@@ -0,0 +1 @@
+faketime_pkg: libfaketime
diff --git a/test/integration/targets/cron/meta/main.yml b/test/integration/targets/cron/meta/main.yml
new file mode 100644
index 0000000..2d2436a
--- /dev/null
+++ b/test/integration/targets/cron/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - setup_cron
diff --git a/test/integration/targets/cron/tasks/main.yml b/test/integration/targets/cron/tasks/main.yml
new file mode 100644
index 0000000..32e345d
--- /dev/null
+++ b/test/integration/targets/cron/tasks/main.yml
@@ -0,0 +1,328 @@
+- name: Include distribution specific variables
+ include_vars: "{{ lookup('first_found', search) }}"
+ vars:
+ search:
+ files:
+ - '{{ ansible_distribution | lower }}.yml'
+ - '{{ ansible_os_family | lower }}.yml'
+ - '{{ ansible_system | lower }}.yml'
+ - default.yml
+ paths:
+ - vars
+
+# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=726661
+- name: Work around vixie-cron/PAM issue on old distros
+ command: sed -i '/pam_loginuid/ s/^/#/' /etc/pam.d/crond
+ when:
+ - ansible_distribution in ('RedHat', 'CentOS')
+ - ansible_distribution_major_version is version('6', '==')
+
+- name: add cron task (check mode enabled, cron task not already created)
+ cron:
+ name: test cron task
+ job: 'date > {{ remote_dir }}/cron_canary1'
+ check_mode: yes
+ register: check_mode_enabled_state_present
+
+- assert:
+ that: check_mode_enabled_state_present is changed
+
+- name: add cron task (check mode disabled, task hasn't already been created)
+ cron:
+ name: test cron task
+ job: 'date > {{ remote_dir }}/cron_canary1'
+ register: add_cron_task
+
+- assert:
+ that: add_cron_task is changed
+
+- name: add cron task (check mode enabled, cron task already exists)
+ cron:
+ name: test cron task
+ job: 'date > {{ remote_dir }}/cron_canary1'
+ check_mode: yes
+ register: check_mode_enabled_state_present_cron_task_already_exists
+
+- assert:
+ that: check_mode_enabled_state_present_cron_task_already_exists is not changed
+
+- name: add cron task (check mode disabled, cron task already created)
+ cron:
+ name: test cron task
+ job: 'date > {{ remote_dir }}/cron_canary1'
+ register: cron_task_already_created
+
+- assert:
+ that: cron_task_already_created is not changed
+
+- block:
+ - name: wait for canary creation
+ wait_for:
+ path: '{{ remote_dir }}/cron_canary1'
+ timeout: '{{ 20 if faketime_pkg else 70 }}'
+ register: wait_canary
+ always:
+ - name: display some logs in case of failure
+ command: 'journalctl -u {{ cron_service }}'
+ when: wait_canary is failed and ansible_service_mgr == 'systemd'
+
+- debug:
+ msg: 'elapsed time waiting for canary: {{ wait_canary.elapsed }}'
+
+- name: Check check_mode
+ cron:
+ name: test cron task
+ job: 'date > {{ remote_dir }}/cron_canary1'
+ state: absent
+ check_mode: yes
+ register: check_check_mode
+
+- assert:
+ that: check_check_mode is changed
+
+- name: Remove a cron task
+ cron:
+ name: test cron task
+ job: 'date > {{ remote_dir }}/cron_canary1'
+ state: absent
+ register: remove_task
+
+- assert:
+ that: remove_task is changed
+
+- name: 'cron task missing: check idempotence (check mode enabled, state=absent)'
+ cron:
+ name: test cron task
+ job: 'date > {{ remote_dir }}/cron_canary1'
+ state: absent
+ register: check_mode_enabled_remove_task_idempotence
+
+- assert:
+ that: check_mode_enabled_remove_task_idempotence is not changed
+
+- name: 'cron task missing: check idempotence (check mode disabled, state=absent)'
+ cron:
+ name: test cron task
+ job: 'date > {{ remote_dir }}/cron_canary1'
+ state: absent
+ register: remove_task_idempotence
+
+- assert:
+ that: remove_task_idempotence is not changed
+
+- name: Check that removing a cron task with cron_file and without specifying a user is allowed (#58493)
+ cron:
+ name: test cron task
+ cron_file: unexistent_cron_file
+ state: absent
+ register: remove_cron_file
+
+- assert:
+ that: remove_cron_file is not changed
+
+- name: Non regression test - cron file should not be empty after adding var (#71207)
+ when: ansible_distribution != 'Alpine'
+ block:
+ - name: Cron file creation
+ cron:
+ cron_file: cron_filename
+ name: "simple cron job"
+ job: 'echo "_o/"'
+ user: root
+
+ - name: Add var to the cron file
+ cron:
+ cron_file: cron_filename
+ env: yes
+ name: FOO
+ value: bar
+ user: root
+
+ - name: "Ensure cron_file still contains job string"
+ replace:
+ path: /etc/cron.d/cron_filename
+ regexp: "_o/"
+ replace: "OK"
+ register: find_chars
+ failed_when: (find_chars is not changed) or (find_chars is failed)
+
+# BusyBox does not have /etc/cron.d
+- name: Removing a cron file when the name is specified is allowed (#57471)
+ when: ansible_distribution != 'Alpine'
+ block:
+ - name: Check file does not exist
+ stat:
+ path: /etc/cron.d/cron_remove_name
+ register: cron_file_stats
+
+ - assert:
+ that: not cron_file_stats.stat.exists
+
+ - name: Cron file creation
+ cron:
+ cron_file: cron_remove_name
+ name: "integration test cron"
+ job: 'ls'
+ user: root
+
+ - name: Cron file deletion
+ cron:
+ cron_file: cron_remove_name
+ name: "integration test cron"
+ state: absent
+
+ - name: Check file succesfull deletion
+ stat:
+ path: /etc/cron.d/cron_remove_name
+ register: cron_file_stats
+
+ - assert:
+ that: not cron_file_stats.stat.exists
+
+# BusyBox does not have /etc/cron.d
+- name: Removing a cron file, which contains only whitespace
+ when: ansible_distribution != 'Alpine'
+ block:
+ - name: Check file does not exist
+ stat:
+ path: /etc/cron.d/cron_remove_whitespace
+ register: cron_file_stats
+
+ - assert:
+ that: not cron_file_stats.stat.exists
+
+ - name: Cron file creation
+ cron:
+ cron_file: cron_remove_whitespace
+ name: "integration test cron"
+ job: 'ls'
+ user: root
+
+ - name: Add whitespace to cron file
+ shell: 'printf "\n \n\t\n" >> /etc/cron.d/cron_remove_whitespace'
+
+ - name: Cron file deletion
+ cron:
+ cron_file: cron_remove_whitespace
+ name: "integration test cron"
+ state: absent
+
+ - name: Check file succesfull deletion
+ stat:
+ path: /etc/cron.d/cron_remove_whitespace
+ register: cron_file_stats
+
+ - assert:
+ that: not cron_file_stats.stat.exists
+
+- name: System cron tab can not be managed
+ when: ansible_distribution != 'Alpine'
+ block:
+ - name: Add cron job
+ cron:
+ cron_file: "{{ system_crontab }}"
+ user: root
+ name: "integration test cron"
+ job: 'ls'
+ ignore_errors: yes
+ register: result
+
+ - assert:
+ that: "result.msg == 'Will not manage /etc/crontab via cron_file, see documentation.'"
+
+# TODO: restrict other root crontab locations
+- name: System cron tab does not get removed
+ when: ansible_distribution == 'Alpine'
+ block:
+ - name: Add cron job
+ cron:
+ cron_file: "{{ system_crontab }}"
+ user: root
+ name: "integration test cron"
+ job: 'ls'
+
+ - name: Remove cron job
+ cron:
+ cron_file: "{{ system_crontab }}"
+ name: "integration test cron"
+ state: absent
+
+ - name: Check system crontab still exists
+ stat:
+ path: "{{ system_crontab }}"
+ register: cron_file_stats
+
+ - assert:
+ that: cron_file_stats.stat.exists
+
+- name: Allow non-ascii chars in job (#69492)
+ when: ansible_distribution != 'Alpine'
+ block:
+ - name: Check file does not exist
+ stat:
+ path: /etc/cron.d/cron_nonascii
+ register: cron_file_stats
+
+ - assert:
+ that: not cron_file_stats.stat.exists
+
+ - name: Cron file creation
+ cron:
+ cron_file: cron_nonascii
+ name: "cron job that contain non-ascii chars in job (ã“ã‚Œã¯æ—¥æœ¬èªžã§ã™; This is Japanese)"
+ job: 'echo "ã†ã©ã‚“ã¯å¥½ãã ãŒãŠåŒ–ã‘👻ã¯è‹¦æ‰‹ã§ã‚る。"'
+ user: root
+
+ - name: "Ensure cron_file contains job string"
+ replace:
+ path: /etc/cron.d/cron_nonascii
+ regexp: "ã†ã©ã‚“ã¯å¥½ãã ãŒãŠåŒ–ã‘👻ã¯è‹¦æ‰‹ã§ã‚る。"
+ replace: "ãã‚Œã¯æ©Ÿå¯†æƒ…報🔓ã§ã™ã€‚"
+ register: find_chars
+ failed_when: (find_chars is not changed) or (find_chars is failed)
+
+ - name: Cron file deletion
+ cron:
+ cron_file: cron_nonascii
+ name: "cron job that contain non-ascii chars in job (ã“ã‚Œã¯æ—¥æœ¬èªžã§ã™; This is Japanese)"
+ state: absent
+
+ - name: Check file succesfull deletion
+ stat:
+ path: /etc/cron.d/cron_nonascii
+ register: cron_file_stats
+
+ - assert:
+ that: not cron_file_stats.stat.exists
+
+- name: Allow non-ascii chars in cron_file (#69492)
+ when: ansible_distribution != 'Alpine'
+ block:
+ - name: Cron file creation with non-ascii filename (ã“ã‚Œã¯æ—¥æœ¬èªžã§ã™; This is Japanese)
+ cron:
+ cron_file: 'ãªã›ã°å¤§æŠµãªã‚“ã¨ã‹ãªã‚‹ðŸ‘Š'
+ name: "integration test cron"
+ job: 'echo "Hello, ansible!"'
+ user: root
+
+ - name: Check file exists
+ stat:
+ path: "/etc/cron.d/ãªã›ã°å¤§æŠµãªã‚“ã¨ã‹ãªã‚‹ðŸ‘Š"
+ register: cron_file_stats
+
+ - assert:
+ that: cron_file_stats.stat.exists
+
+ - name: Cron file deletion
+ cron:
+ cron_file: 'ãªã›ã°å¤§æŠµãªã‚“ã¨ã‹ãªã‚‹ðŸ‘Š'
+ name: "integration test cron"
+ state: absent
+
+ - name: Check file succesfull deletion
+ stat:
+ path: "/etc/cron.d/ãªã›ã°å¤§æŠµãªã‚“ã¨ã‹ãªã‚‹ðŸ‘Š"
+ register: cron_file_stats
+
+ - assert:
+ that: not cron_file_stats.stat.exists
diff --git a/test/integration/targets/cron/vars/alpine.yml b/test/integration/targets/cron/vars/alpine.yml
new file mode 100644
index 0000000..29ca3b9
--- /dev/null
+++ b/test/integration/targets/cron/vars/alpine.yml
@@ -0,0 +1 @@
+system_crontab: /etc/crontabs/root
diff --git a/test/integration/targets/cron/vars/default.yml b/test/integration/targets/cron/vars/default.yml
new file mode 100644
index 0000000..69c5de4
--- /dev/null
+++ b/test/integration/targets/cron/vars/default.yml
@@ -0,0 +1 @@
+system_crontab: /etc/crontab
diff --git a/test/integration/targets/dataloader/aliases b/test/integration/targets/dataloader/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/dataloader/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/dataloader/attempt_to_load_invalid_json.yml b/test/integration/targets/dataloader/attempt_to_load_invalid_json.yml
new file mode 100644
index 0000000..536e6da
--- /dev/null
+++ b/test/integration/targets/dataloader/attempt_to_load_invalid_json.yml
@@ -0,0 +1,4 @@
+- hosts: localhost
+ gather_facts: false
+ vars_files:
+ - vars/invalid.json
diff --git a/test/integration/targets/dataloader/runme.sh b/test/integration/targets/dataloader/runme.sh
new file mode 100755
index 0000000..6a1bc9a
--- /dev/null
+++ b/test/integration/targets/dataloader/runme.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# check if we get proper json error
+ansible-playbook -i ../../inventory attempt_to_load_invalid_json.yml "$@" 2>&1|grep 'JSON:'
diff --git a/test/integration/targets/dataloader/vars/invalid.json b/test/integration/targets/dataloader/vars/invalid.json
new file mode 100644
index 0000000..8d4e430
--- /dev/null
+++ b/test/integration/targets/dataloader/vars/invalid.json
@@ -0,0 +1 @@
+{ }}
diff --git a/test/integration/targets/debconf/aliases b/test/integration/targets/debconf/aliases
new file mode 100644
index 0000000..a6dafcf
--- /dev/null
+++ b/test/integration/targets/debconf/aliases
@@ -0,0 +1 @@
+shippable/posix/group1
diff --git a/test/integration/targets/debconf/meta/main.yml b/test/integration/targets/debconf/meta/main.yml
new file mode 100644
index 0000000..07faa21
--- /dev/null
+++ b/test/integration/targets/debconf/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - prepare_tests
diff --git a/test/integration/targets/debconf/tasks/main.yml b/test/integration/targets/debconf/tasks/main.yml
new file mode 100644
index 0000000..d3d63cd
--- /dev/null
+++ b/test/integration/targets/debconf/tasks/main.yml
@@ -0,0 +1,36 @@
+# Test code for the debconf module.
+# (c) 2017, James Tanner <tanner.jc@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+##
+## debconf query
+##
+
+- block:
+ - name: query the tzdata package
+ debconf:
+ name: tzdata
+ register: debconf_test0
+
+ - name: validate results for test 0
+ assert:
+ that:
+ - 'debconf_test0.changed is defined'
+ - 'debconf_test0.current is defined'
+ - '"tzdata/Zones/Etc" in debconf_test0.current'
+ - 'debconf_test0.current["tzdata/Zones/Etc"] == "UTC"'
+ when: ansible_distribution in ('Ubuntu', 'Debian')
diff --git a/test/integration/targets/debug/aliases b/test/integration/targets/debug/aliases
new file mode 100644
index 0000000..1017932
--- /dev/null
+++ b/test/integration/targets/debug/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller # this is a controller-only action, the module is just for documentation
diff --git a/test/integration/targets/debug/main.yml b/test/integration/targets/debug/main.yml
new file mode 100644
index 0000000..9e49b82
--- /dev/null
+++ b/test/integration/targets/debug/main.yml
@@ -0,0 +1,6 @@
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - name: test item being present in the output
+ debug: var=item
+ loop: [1, 2, 3]
diff --git a/test/integration/targets/debug/main_fqcn.yml b/test/integration/targets/debug/main_fqcn.yml
new file mode 100644
index 0000000..d6a00fc
--- /dev/null
+++ b/test/integration/targets/debug/main_fqcn.yml
@@ -0,0 +1,6 @@
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - name: test item being present in the output
+ ansible.builtin.debug: var=item
+ loop: [1, 2, 3]
diff --git a/test/integration/targets/debug/nosetfacts.yml b/test/integration/targets/debug/nosetfacts.yml
new file mode 100644
index 0000000..231c60e
--- /dev/null
+++ b/test/integration/targets/debug/nosetfacts.yml
@@ -0,0 +1,21 @@
+- name: check we dont set facts with debug ansible_facts https://github.com/ansible/ansible/issues/74060
+ hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: create namespaced non fact
+ set_fact:
+ ansible_facts:
+ nonfact: 1
+
+ - name: ensure nonfact does not exist
+ assert:
+ that:
+ - nonfact is not defined
+
+ - name: debug ansible_facts to create issue
+ debug: var=ansible_facts
+
+ - name: ensure nonfact STILL does not exist
+ assert:
+ that:
+ - nonfact is not defined
diff --git a/test/integration/targets/debug/runme.sh b/test/integration/targets/debug/runme.sh
new file mode 100755
index 0000000..5faeb78
--- /dev/null
+++ b/test/integration/targets/debug/runme.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+
+set -eux
+
+trap 'rm -f out' EXIT
+
+ansible-playbook main.yml -i ../../inventory | tee out
+for i in 1 2 3; do
+ grep "ok: \[localhost\] => (item=$i)" out
+ grep "\"item\": $i" out
+done
+
+ansible-playbook main_fqcn.yml -i ../../inventory | tee out
+for i in 1 2 3; do
+ grep "ok: \[localhost\] => (item=$i)" out
+ grep "\"item\": $i" out
+done
+
+# ensure debug does not set top level vars when looking at ansible_facts
+ansible-playbook nosetfacts.yml "$@"
diff --git a/test/integration/targets/debugger/aliases b/test/integration/targets/debugger/aliases
new file mode 100644
index 0000000..981d8b7
--- /dev/null
+++ b/test/integration/targets/debugger/aliases
@@ -0,0 +1,3 @@
+shippable/posix/group3
+context/controller
+setup/always/setup_pexpect
diff --git a/test/integration/targets/debugger/inventory b/test/integration/targets/debugger/inventory
new file mode 100644
index 0000000..81502d5
--- /dev/null
+++ b/test/integration/targets/debugger/inventory
@@ -0,0 +1,2 @@
+testhost ansible_connection=local
+testhost2 ansible_connection=local
diff --git a/test/integration/targets/debugger/runme.sh b/test/integration/targets/debugger/runme.sh
new file mode 100755
index 0000000..6a51d23
--- /dev/null
+++ b/test/integration/targets/debugger/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eux
+
+./test_run_once.py -i inventory "$@"
diff --git a/test/integration/targets/debugger/test_run_once.py b/test/integration/targets/debugger/test_run_once.py
new file mode 100755
index 0000000..237f9c2
--- /dev/null
+++ b/test/integration/targets/debugger/test_run_once.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python
+
+import io
+import os
+import sys
+
+import pexpect
+
+
+env_vars = {
+ 'ANSIBLE_NOCOLOR': 'True',
+ 'ANSIBLE_RETRY_FILES_ENABLED': 'False',
+}
+
+env = os.environ.copy()
+env.update(env_vars)
+
+with io.BytesIO() as logfile:
+ debugger_test_test = pexpect.spawn(
+ 'ansible-playbook',
+ args=['test_run_once_playbook.yml'] + sys.argv[1:],
+ timeout=10,
+ env=env
+ )
+
+ debugger_test_test.logfile = logfile
+
+ debugger_test_test.expect_exact('TASK: Task 1 (debug)> ')
+ debugger_test_test.send('task.args["that"] = "true"\r')
+ debugger_test_test.expect_exact('TASK: Task 1 (debug)> ')
+ debugger_test_test.send('r\r')
+ debugger_test_test.expect(pexpect.EOF)
+ debugger_test_test.close()
+
+ assert str(logfile.getvalue()).count('Task 2 executed') == 2
diff --git a/test/integration/targets/debugger/test_run_once_playbook.yml b/test/integration/targets/debugger/test_run_once_playbook.yml
new file mode 100644
index 0000000..ede3a53
--- /dev/null
+++ b/test/integration/targets/debugger/test_run_once_playbook.yml
@@ -0,0 +1,12 @@
+- hosts: testhost, testhost2
+ gather_facts: false
+ debugger: on_failed
+ tasks:
+ - name: Task 1
+ assert:
+ that: 'false'
+ run_once: yes
+
+ - name: Task 2
+ debug:
+ msg: "Task 2 executed"
diff --git a/test/integration/targets/delegate_to/aliases b/test/integration/targets/delegate_to/aliases
new file mode 100644
index 0000000..cb931dc
--- /dev/null
+++ b/test/integration/targets/delegate_to/aliases
@@ -0,0 +1,4 @@
+shippable/posix/group5
+needs/ssh
+needs/root # only on macOS and FreeBSD to configure network interfaces
+context/controller
diff --git a/test/integration/targets/delegate_to/connection_plugins/fakelocal.py b/test/integration/targets/delegate_to/connection_plugins/fakelocal.py
new file mode 100644
index 0000000..59ddcf0
--- /dev/null
+++ b/test/integration/targets/delegate_to/connection_plugins/fakelocal.py
@@ -0,0 +1,76 @@
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ connection: fakelocal
+ short_description: dont execute anything
+ description:
+ - This connection plugin just verifies parameters passed in
+ author: ansible (@core)
+ version_added: histerical
+ options:
+ password:
+ description: Authentication password for the C(remote_user). Can be supplied as CLI option.
+ vars:
+ - name: ansible_password
+ remote_user:
+ description:
+ - User name with which to login to the remote server, normally set by the remote_user keyword.
+ ini:
+ - section: defaults
+ key: remote_user
+ vars:
+ - name: ansible_user
+'''
+
+from ansible.errors import AnsibleConnectionFailure
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+
+display = Display()
+
+
+class Connection(ConnectionBase):
+ ''' Local based connections '''
+
+ transport = 'fakelocal'
+ has_pipelining = True
+
+ def __init__(self, *args, **kwargs):
+
+ super(Connection, self).__init__(*args, **kwargs)
+ self.cwd = None
+
+ def _connect(self):
+ ''' verify '''
+
+ if self.get_option('remote_user') == 'invaliduser' and self.get_option('password') == 'badpassword':
+ raise AnsibleConnectionFailure('Got invaliduser and badpassword')
+
+ if not self._connected:
+ display.vvv(u"ESTABLISH FAKELOCAL CONNECTION FOR USER: {0}".format(self._play_context.remote_user), host=self._play_context.remote_addr)
+ self._connected = True
+ return self
+
+ def exec_command(self, cmd, in_data=None, sudoable=True):
+ ''' run a command on the local host '''
+
+ super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
+
+ return 0, '{"msg": "ALL IS GOOD"}', ''
+
+ def put_file(self, in_path, out_path):
+ ''' transfer a file from local to local '''
+
+ super(Connection, self).put_file(in_path, out_path)
+
+ def fetch_file(self, in_path, out_path):
+ ''' fetch a file from local to local -- for compatibility '''
+
+ super(Connection, self).fetch_file(in_path, out_path)
+
+ def close(self):
+ ''' terminate the connection; nothing to do here '''
+ self._connected = False
diff --git a/test/integration/targets/delegate_to/delegate_and_nolog.yml b/test/integration/targets/delegate_to/delegate_and_nolog.yml
new file mode 100644
index 0000000..d8ed64f
--- /dev/null
+++ b/test/integration/targets/delegate_to/delegate_and_nolog.yml
@@ -0,0 +1,8 @@
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - name: no log filtering caused delegation to fail https://github.com/ansible/ansible/issues/43026
+ become: False
+ no_log: true
+ debug:
+ delegate_to: localhost
diff --git a/test/integration/targets/delegate_to/delegate_facts_block.yml b/test/integration/targets/delegate_to/delegate_facts_block.yml
new file mode 100644
index 0000000..2edfeb4
--- /dev/null
+++ b/test/integration/targets/delegate_to/delegate_facts_block.yml
@@ -0,0 +1,25 @@
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - name: set var to delegated host directly
+ set_fact: qq1=333
+ delegate_facts: true
+ delegate_to: localhost
+
+ - name: ensure qq1 exists in localhost but not in testhost
+ assert:
+ that:
+ - qq1 is undefined
+ - "'qq1' in hostvars['localhost']"
+
+ - name: set var to delegated host via inheritance
+ block:
+ - set_fact: qq2=333
+ delegate_facts: true
+ delegate_to: localhost
+
+ - name: ensure qq2 exists in localhost but not in testhost
+ assert:
+ that:
+ - qq2 is undefined
+ - "'qq2' in hostvars['localhost']"
diff --git a/test/integration/targets/delegate_to/delegate_facts_loop.yml b/test/integration/targets/delegate_to/delegate_facts_loop.yml
new file mode 100644
index 0000000..b05c406
--- /dev/null
+++ b/test/integration/targets/delegate_to/delegate_facts_loop.yml
@@ -0,0 +1,40 @@
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - set_fact:
+ test: 123
+ delegate_to: "{{ item }}"
+ delegate_facts: true
+ loop: "{{ groups['all'] | difference(['localhost']) }}"
+
+ - name: ensure we didnt create it on current host
+ assert:
+ that:
+ - test is undefined
+
+ - name: ensure facts get created
+ assert:
+ that:
+ - "'test' in hostvars[item]"
+ - hostvars[item]['test'] == 123
+ loop: "{{ groups['all'] | difference(['localhost'])}}"
+
+
+- name: test that we don't polute whole group with one value
+ hosts: localhost
+ gather_facts: no
+ vars:
+ cluster_name: bleh
+ tasks:
+ - name: construct different fact per host in loop
+ set_fact:
+ vm_name: "{{ cluster_name }}-{{item}}"
+ delegate_to: "{{ item }}"
+ delegate_facts: True
+ with_items: "{{ groups['all'] }}"
+
+ - name: ensure the fact is personalized for each host
+ assert:
+ that:
+ - hostvars[item]['vm_name'].endswith(item)
+ loop: "{{ groups['all'] }}"
diff --git a/test/integration/targets/delegate_to/delegate_local_from_root.yml b/test/integration/targets/delegate_to/delegate_local_from_root.yml
new file mode 100644
index 0000000..c9be4ff
--- /dev/null
+++ b/test/integration/targets/delegate_to/delegate_local_from_root.yml
@@ -0,0 +1,10 @@
+- name: handle case from issue 72541
+ hosts: testhost
+ gather_facts: false
+ remote_user: root
+ tasks:
+ - name: ensure we copy w/o errors due to remote user not being overriden
+ copy:
+ src: testfile
+ dest: "{{ playbook_dir }}"
+ delegate_to: localhost
diff --git a/test/integration/targets/delegate_to/delegate_to_lookup_context.yml b/test/integration/targets/delegate_to/delegate_to_lookup_context.yml
new file mode 100644
index 0000000..83e24bc
--- /dev/null
+++ b/test/integration/targets/delegate_to/delegate_to_lookup_context.yml
@@ -0,0 +1,4 @@
+- hosts: localhost
+ gather_facts: false
+ roles:
+ - delegate_to_lookup_context
diff --git a/test/integration/targets/delegate_to/delegate_vars_hanldling.yml b/test/integration/targets/delegate_to/delegate_vars_hanldling.yml
new file mode 100644
index 0000000..6ac64e9
--- /dev/null
+++ b/test/integration/targets/delegate_to/delegate_vars_hanldling.yml
@@ -0,0 +1,58 @@
+- name: setup delegated hsot
+ hosts: localhost
+ gather_facts: false
+ tasks:
+ - add_host:
+ name: delegatetome
+ ansible_host: 127.0.0.4
+
+- name: ensure we dont use orig host vars if delegated one does not define them
+ hosts: testhost
+ gather_facts: false
+ connection: local
+ tasks:
+ - name: force current host to use winrm
+ set_fact:
+ ansible_connection: winrm
+
+ - name: this should fail (missing winrm or unreachable)
+ ping:
+ ignore_errors: true
+ ignore_unreachable: true
+ register: orig
+
+ - name: ensure prev failed
+ assert:
+ that:
+ - orig is failed or orig is unreachable
+
+ - name: this will only fail if we take orig host ansible_connection instead of defaults
+ ping:
+ delegate_to: delegatetome
+
+
+- name: ensure plugin specific vars are properly used
+ hosts: testhost
+ gather_facts: false
+ tasks:
+ - name: set unusable ssh args
+ set_fact:
+ ansible_host: 127.0.0.1
+ ansible_connection: ssh
+ ansible_ssh_common_args: 'MEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE'
+ ansible_connection_timeout: 5
+
+ - name: fail to ping with bad args
+ ping:
+ register: bad_args_ping
+ ignore_unreachable: true
+
+ - debug: var=bad_args_ping
+ - name: ensure prev failed
+ assert:
+ that:
+ - bad_args_ping is failed or bad_args_ping is unreachable
+
+ - name: this should work by ignoring the bad ags for orig host
+ ping:
+ delegate_to: delegatetome
diff --git a/test/integration/targets/delegate_to/delegate_with_fact_from_delegate_host.yml b/test/integration/targets/delegate_to/delegate_with_fact_from_delegate_host.yml
new file mode 100644
index 0000000..1670398
--- /dev/null
+++ b/test/integration/targets/delegate_to/delegate_with_fact_from_delegate_host.yml
@@ -0,0 +1,18 @@
+- name: ensure we can use fact on delegated host for connection info
+ hosts: localhost
+ gather_facts: no
+ tasks:
+ - add_host: name=f31 bogus_user=notme ansible_connection=ssh ansible_host=4.2.2.2
+
+ - name: if not overriding with delegated host info, will not be unreachable
+ ping:
+ timeout: 5
+ delegate_to: f31
+ ignore_errors: true
+ ignore_unreachable: true
+ register: delping
+
+ - name: ensure that the expected happened
+ assert:
+ that:
+ - delping is failed
diff --git a/test/integration/targets/delegate_to/discovery_applied.yml b/test/integration/targets/delegate_to/discovery_applied.yml
new file mode 100644
index 0000000..fafe664
--- /dev/null
+++ b/test/integration/targets/delegate_to/discovery_applied.yml
@@ -0,0 +1,8 @@
+- hosts: testhost
+ gather_facts: no
+ tasks:
+ - command: ls
+ delegate_to: "{{ item }}"
+ with_items:
+ - localhost
+ - "{{ inventory_hostname }}"
diff --git a/test/integration/targets/delegate_to/files/testfile b/test/integration/targets/delegate_to/files/testfile
new file mode 100644
index 0000000..492bafc
--- /dev/null
+++ b/test/integration/targets/delegate_to/files/testfile
@@ -0,0 +1 @@
+nothing special
diff --git a/test/integration/targets/delegate_to/has_hostvars.yml b/test/integration/targets/delegate_to/has_hostvars.yml
new file mode 100644
index 0000000..9e8926b
--- /dev/null
+++ b/test/integration/targets/delegate_to/has_hostvars.yml
@@ -0,0 +1,64 @@
+- name: ensure delegated host has hostvars available for resolving connection
+ hosts: testhost
+ gather_facts: false
+ tasks:
+
+ - name: ensure delegated host uses current host as inventory_hostname
+ assert:
+ that:
+ - inventory_hostname == ansible_delegated_vars['testhost5']['inventory_hostname']
+ delegate_to: testhost5
+
+ - name: Set info on inventory_hostname
+ set_fact:
+ login: invaliduser
+ mypass: badpassword
+
+ - name: test fakelocal
+ command: ls
+ ignore_unreachable: True
+ ignore_errors: True
+ remote_user: "{{ login }}"
+ vars:
+ ansible_password: "{{ mypass }}"
+ ansible_connection: fakelocal
+ register: badlogin
+
+ - name: ensure we skipped do to unreachable and not templating error
+ assert:
+ that:
+ - badlogin is unreachable
+
+ - name: delegate but try to use inventory_hostname data directly
+ command: ls
+ delegate_to: testhost5
+ ignore_unreachable: True
+ ignore_errors: True
+ remote_user: "{{ login }}"
+ vars:
+ ansible_password: "{{ mypass }}"
+ register: badlogin
+
+ - name: ensure we skipped do to unreachable and not templating error
+ assert:
+ that:
+ - badlogin is not unreachable
+ - badlogin is failed
+ - "'undefined' in badlogin['msg']"
+
+ - name: delegate ls to testhost5 as it uses ssh while testhost is local, but use vars from testhost
+ command: ls
+ remote_user: "{{ hostvars[inventory_hostname]['login'] }}"
+ delegate_to: testhost5
+ ignore_unreachable: True
+ ignore_errors: True
+ vars:
+ ansible_password: "{{ hostvars[inventory_hostname]['mypass'] }}"
+ register: badlogin
+
+ - name: ensure we skipped do to unreachable and not templating error
+ assert:
+ that:
+ - badlogin is unreachable
+ - badlogin is not failed
+ - "'undefined' not in badlogin['msg']"
diff --git a/test/integration/targets/delegate_to/inventory b/test/integration/targets/delegate_to/inventory
new file mode 100644
index 0000000..ebc3325
--- /dev/null
+++ b/test/integration/targets/delegate_to/inventory
@@ -0,0 +1,17 @@
+[local]
+testhost ansible_connection=local
+testhost2 ansible_connection=local
+testhost3 ansible_ssh_host=127.0.0.3
+testhost4 ansible_ssh_host=127.0.0.4
+testhost5 ansible_connection=fakelocal
+
+[all:vars]
+ansible_python_interpreter="{{ ansible_playbook_python }}"
+
+[delegated_vars]
+testhost6 myhost=127.0.0.3
+testhost7 myhost=127.0.0.4
+
+[delegated_vars:vars]
+ansible_host={{myhost}}
+ansible_connection=ssh
diff --git a/test/integration/targets/delegate_to/inventory_interpreters b/test/integration/targets/delegate_to/inventory_interpreters
new file mode 100644
index 0000000..4c202ca
--- /dev/null
+++ b/test/integration/targets/delegate_to/inventory_interpreters
@@ -0,0 +1,5 @@
+testhost ansible_python_interpreter=firstpython
+testhost2 ansible_python_interpreter=secondpython
+
+[all:vars]
+ansible_connection=local
diff --git a/test/integration/targets/delegate_to/library/detect_interpreter.py b/test/integration/targets/delegate_to/library/detect_interpreter.py
new file mode 100644
index 0000000..1f40167
--- /dev/null
+++ b/test/integration/targets/delegate_to/library/detect_interpreter.py
@@ -0,0 +1,18 @@
+#!/usr/bin/python
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+import sys
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(argument_spec={})
+ module.exit_json(**dict(found=sys.executable))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/delegate_to/resolve_vars.yml b/test/integration/targets/delegate_to/resolve_vars.yml
new file mode 100644
index 0000000..898c0b0
--- /dev/null
+++ b/test/integration/targets/delegate_to/resolve_vars.yml
@@ -0,0 +1,16 @@
+---
+- name: though we test for 'vars' this is only for backwards compatibility and the 'vars' variable will be deprecated and removed in the future
+ hosts: localhost
+ gather_facts: no
+ tasks:
+ - add_host:
+ name: host1
+ ansible_connection: local
+
+- hosts: localhost
+ gather_facts: no
+ vars:
+ server_name: host1
+ tasks:
+ - command: echo should delegate to host1 with local connection
+ delegate_to: "{{ vars['server_name'] }}"
diff --git a/test/integration/targets/delegate_to/roles/delegate_to_lookup_context/tasks/main.yml b/test/integration/targets/delegate_to/roles/delegate_to_lookup_context/tasks/main.yml
new file mode 100644
index 0000000..2b14c55
--- /dev/null
+++ b/test/integration/targets/delegate_to/roles/delegate_to_lookup_context/tasks/main.yml
@@ -0,0 +1,5 @@
+- name: sends SQL template files to mysql host(s)
+ debug:
+ msg: "{{ item }}"
+ with_fileglob: ../templates/*.j2
+ delegate_to: localhost
diff --git a/test/integration/targets/delegate_to/roles/delegate_to_lookup_context/templates/one.j2 b/test/integration/targets/delegate_to/roles/delegate_to_lookup_context/templates/one.j2
new file mode 100644
index 0000000..1fad51f
--- /dev/null
+++ b/test/integration/targets/delegate_to/roles/delegate_to_lookup_context/templates/one.j2
@@ -0,0 +1 @@
+{{ inventory_hostname }}
diff --git a/test/integration/targets/delegate_to/roles/delegate_to_lookup_context/templates/two.j2 b/test/integration/targets/delegate_to/roles/delegate_to_lookup_context/templates/two.j2
new file mode 100644
index 0000000..1fad51f
--- /dev/null
+++ b/test/integration/targets/delegate_to/roles/delegate_to_lookup_context/templates/two.j2
@@ -0,0 +1 @@
+{{ inventory_hostname }}
diff --git a/test/integration/targets/delegate_to/roles/test_template/templates/foo.j2 b/test/integration/targets/delegate_to/roles/test_template/templates/foo.j2
new file mode 100644
index 0000000..22187f9
--- /dev/null
+++ b/test/integration/targets/delegate_to/roles/test_template/templates/foo.j2
@@ -0,0 +1,3 @@
+{{ templated_var }}
+
+{{ templated_dict | to_nice_json }}
diff --git a/test/integration/targets/delegate_to/runme.sh b/test/integration/targets/delegate_to/runme.sh
new file mode 100755
index 0000000..1bdf27c
--- /dev/null
+++ b/test/integration/targets/delegate_to/runme.sh
@@ -0,0 +1,78 @@
+#!/usr/bin/env bash
+
+set -eux
+
+platform="$(uname)"
+
+function setup() {
+ if [[ "${platform}" == "FreeBSD" ]] || [[ "${platform}" == "Darwin" ]]; then
+ ifconfig lo0
+
+ existing=$(ifconfig lo0 | grep '^[[:blank:]]inet 127\.0\.0\. ' || true)
+
+ echo "${existing}"
+
+ for i in 3 4 254; do
+ ip="127.0.0.${i}"
+
+ if [[ "${existing}" != *"${ip}"* ]]; then
+ ifconfig lo0 alias "${ip}" up
+ fi
+ done
+
+ ifconfig lo0
+ fi
+}
+
+function teardown() {
+ if [[ "${platform}" == "FreeBSD" ]] || [[ "${platform}" == "Darwin" ]]; then
+ for i in 3 4 254; do
+ ip="127.0.0.${i}"
+
+ if [[ "${existing}" != *"${ip}"* ]]; then
+ ifconfig lo0 -alias "${ip}"
+ fi
+ done
+
+ ifconfig lo0
+ fi
+}
+
+setup
+
+trap teardown EXIT
+
+ANSIBLE_SSH_ARGS='-C -o ControlMaster=auto -o ControlPersist=60s -o UserKnownHostsFile=/dev/null' \
+ ANSIBLE_HOST_KEY_CHECKING=false ansible-playbook test_delegate_to.yml -i inventory -v "$@"
+
+# this test is not doing what it says it does, also relies on var that should not be available
+#ansible-playbook test_loop_control.yml -v "$@"
+
+ansible-playbook test_delegate_to_loop_randomness.yml -i inventory -v "$@"
+
+ansible-playbook delegate_and_nolog.yml -i inventory -v "$@"
+
+ansible-playbook delegate_facts_block.yml -i inventory -v "$@"
+
+ansible-playbook test_delegate_to_loop_caching.yml -i inventory -v "$@"
+
+# ensure we are using correct settings when delegating
+ANSIBLE_TIMEOUT=3 ansible-playbook delegate_vars_hanldling.yml -i inventory -v "$@"
+
+ansible-playbook has_hostvars.yml -i inventory -v "$@"
+
+# test ansible_x_interpreter
+# python
+source virtualenv.sh
+(
+cd "${OUTPUT_DIR}"/venv/bin
+ln -s python firstpython
+ln -s python secondpython
+)
+ansible-playbook verify_interpreter.yml -i inventory_interpreters -v "$@"
+ansible-playbook discovery_applied.yml -i inventory -v "$@"
+ansible-playbook resolve_vars.yml -i inventory -v "$@"
+ansible-playbook test_delegate_to_lookup_context.yml -i inventory -v "$@"
+ansible-playbook delegate_local_from_root.yml -i inventory -v "$@" -e 'ansible_user=root'
+ansible-playbook delegate_with_fact_from_delegate_host.yml "$@"
+ansible-playbook delegate_facts_loop.yml -i inventory -v "$@"
diff --git a/test/integration/targets/delegate_to/test_delegate_to.yml b/test/integration/targets/delegate_to/test_delegate_to.yml
new file mode 100644
index 0000000..dcfa9d0
--- /dev/null
+++ b/test/integration/targets/delegate_to/test_delegate_to.yml
@@ -0,0 +1,82 @@
+- hosts: testhost3
+ vars:
+ - template_role: ./roles/test_template
+ - output_dir: "{{ playbook_dir }}"
+ - templated_var: foo
+ - templated_dict: { 'hello': 'world' }
+ tasks:
+ - name: Test no delegate_to
+ setup:
+ register: setup_results
+
+ - assert:
+ that:
+ - '"127.0.0.3" in setup_results.ansible_facts.ansible_env["SSH_CONNECTION"]'
+
+ - name: Test delegate_to with host in inventory
+ setup:
+ register: setup_results
+ delegate_to: testhost4
+
+ - debug: var=setup_results
+
+ - assert:
+ that:
+ - '"127.0.0.4" in setup_results.ansible_facts.ansible_env["SSH_CONNECTION"]'
+
+ - name: Test delegate_to with host not in inventory
+ setup:
+ register: setup_results
+ delegate_to: 127.0.0.254
+
+ - assert:
+ that:
+ - '"127.0.0.254" in setup_results.ansible_facts.ansible_env["SSH_CONNECTION"]'
+#
+# Smoketest some other modules do not error as a canary
+#
+ - name: Test file works with delegate_to and a host in inventory
+ file: path={{ output_dir }}/foo.txt mode=0644 state=touch
+ delegate_to: testhost4
+
+ - name: Test file works with delegate_to and a host not in inventory
+ file: path={{ output_dir }}/tmp.txt mode=0644 state=touch
+ delegate_to: 127.0.0.254
+
+ - name: Test template works with delegate_to and a host in inventory
+ template: src={{ template_role }}/templates/foo.j2 dest={{ output_dir }}/foo.txt
+ delegate_to: testhost4
+
+ - name: Test template works with delegate_to and a host not in inventory
+ template: src={{ template_role }}/templates/foo.j2 dest={{ output_dir }}/foo.txt
+ delegate_to: 127.0.0.254
+
+ - name: remove test file
+ file: path={{ output_dir }}/foo.txt state=absent
+
+ - name: remove test file
+ file: path={{ output_dir }}/tmp.txt state=absent
+
+
+- name: verify delegation with per host vars
+ hosts: testhost6
+ gather_facts: yes
+ tasks:
+ - debug: msg={{ansible_facts['env']}}
+
+ - name: ensure normal facts still work as expected
+ assert:
+ that:
+ - '"127.0.0.3" in ansible_facts["env"]["SSH_CONNECTION"]'
+
+ - name: Test delegate_to with other host defined using same named var
+ setup:
+ register: setup_results
+ delegate_to: testhost7
+
+ - debug: msg={{setup_results.ansible_facts.ansible_env}}
+
+ - name: verify ssh plugin resolves variable for ansible_host correctly
+ assert:
+ that:
+ - '"127.0.0.4" in setup_results.ansible_facts.ansible_env["SSH_CONNECTION"]'
diff --git a/test/integration/targets/delegate_to/test_delegate_to_lookup_context.yml b/test/integration/targets/delegate_to/test_delegate_to_lookup_context.yml
new file mode 100644
index 0000000..ae1bb28
--- /dev/null
+++ b/test/integration/targets/delegate_to/test_delegate_to_lookup_context.yml
@@ -0,0 +1,12 @@
+- hosts: localhost
+ gather_facts: false
+ vars:
+ verbosity: "{{ '' if not ansible_verbosity else '-' ~ ('v' * ansible_verbosity) }}"
+ tasks:
+ - command: ansible-playbook {{ verbosity }} delegate_to_lookup_context.yml
+ register: result
+
+ - assert:
+ that:
+ - >
+ '[WARNING]: Unable to find' not in result.stderr
diff --git a/test/integration/targets/delegate_to/test_delegate_to_loop_caching.yml b/test/integration/targets/delegate_to/test_delegate_to_loop_caching.yml
new file mode 100644
index 0000000..6ea08f7
--- /dev/null
+++ b/test/integration/targets/delegate_to/test_delegate_to_loop_caching.yml
@@ -0,0 +1,45 @@
+- hosts: testhost,testhost2
+ gather_facts: false
+ vars:
+ delegate_to_host: "localhost"
+ tasks:
+ - set_fact:
+ gandalf:
+ shout: 'You shall not pass!'
+ when: inventory_hostname == 'testhost'
+
+ - set_fact:
+ gandalf:
+ speak: 'Run you fools!'
+ when: inventory_hostname == 'testhost2'
+
+ - name: works correctly
+ debug: var=item
+ delegate_to: localhost
+ with_dict: "{{ gandalf }}"
+ register: result1
+
+ - name: shows same item for all hosts
+ debug: var=item
+ delegate_to: "{{ delegate_to_host }}"
+ with_dict: "{{ gandalf }}"
+ register: result2
+
+ - debug:
+ var: result2.results[0].item.value
+
+ - assert:
+ that:
+ - result1.results[0].item.value == 'You shall not pass!'
+ - result2.results[0].item.value == 'You shall not pass!'
+ when: inventory_hostname == 'testhost'
+
+ - assert:
+ that:
+ - result1.results[0].item.value == 'Run you fools!'
+ - result2.results[0].item.value == 'Run you fools!'
+ when: inventory_hostname == 'testhost2'
+
+ - assert:
+ that:
+ - _ansible_loop_cache is undefined
diff --git a/test/integration/targets/delegate_to/test_delegate_to_loop_randomness.yml b/test/integration/targets/delegate_to/test_delegate_to_loop_randomness.yml
new file mode 100644
index 0000000..81033a1
--- /dev/null
+++ b/test/integration/targets/delegate_to/test_delegate_to_loop_randomness.yml
@@ -0,0 +1,73 @@
+---
+- name: Integration tests for #28231
+ hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: Add some test hosts
+ add_host:
+ name: "foo{{item}}"
+ groups: foo
+ ansible_connection: local
+ ansible_python_interpreter: "{{ ansible_playbook_python }}"
+ loop: "{{ range(10)|list }}"
+
+ # We expect all of the next 3 runs to succeeed
+ # this is done multiple times to increase randomness
+ - assert:
+ that:
+ - item in ansible_delegated_vars
+ delegate_to: "{{ item }}"
+ loop:
+ - "{{ groups.foo|random }}"
+ ignore_errors: true
+ register: result1
+
+ - assert:
+ that:
+ - item in ansible_delegated_vars
+ delegate_to: "{{ item }}"
+ loop:
+ - "{{ groups.foo|random }}"
+ ignore_errors: true
+ register: result2
+
+ - assert:
+ that:
+ - item in ansible_delegated_vars
+ delegate_to: "{{ item }}"
+ loop:
+ - "{{ groups.foo|random }}"
+ ignore_errors: true
+ register: result3
+
+ - debug:
+ var: result1
+
+ - debug:
+ var: result2
+
+ - debug:
+ var: result3
+
+ - name: Ensure all of the 3 asserts were successful
+ assert:
+ that:
+ - results is all
+ vars:
+ results:
+ - "{{ (result1.results|first) is successful }}"
+ - "{{ (result2.results|first) is successful }}"
+ - "{{ (result3.results|first) is successful }}"
+
+ - name: Set delegate
+ set_fact:
+ _delegate: '{{ groups.foo[0] }}'
+
+ - command: "true"
+ delegate_to: "{{ _delegate }}"
+ register: result
+
+ - assert:
+ that:
+ - result.stdout is defined
+ - result.results is undefined
diff --git a/test/integration/targets/delegate_to/test_loop_control.yml b/test/integration/targets/delegate_to/test_loop_control.yml
new file mode 100644
index 0000000..61e9304
--- /dev/null
+++ b/test/integration/targets/delegate_to/test_loop_control.yml
@@ -0,0 +1,16 @@
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - name: Test delegate_to with loop_control
+ ping:
+ delegate_to: "{{ item }}"
+ with_items:
+ - localhost
+ loop_control:
+ label: "{{ item }}"
+ register: out
+
+ - name: Check if delegated_host was templated properly
+ assert:
+ that:
+ - out.results[0]['_ansible_delegated_vars']['ansible_delegated_host'] == 'localhost'
diff --git a/test/integration/targets/delegate_to/verify_interpreter.yml b/test/integration/targets/delegate_to/verify_interpreter.yml
new file mode 100644
index 0000000..63c60a4
--- /dev/null
+++ b/test/integration/targets/delegate_to/verify_interpreter.yml
@@ -0,0 +1,47 @@
+- name: ensure they are different
+ hosts: localhost
+ tasks:
+ - name: dont game me
+ assert:
+ msg: 'expected different values but got ((hostvars["testhost"]["ansible_python_interpreter"]}} and {{hostvars["testhost2"]["ansible_python_interpreter"]}}'
+ that:
+ - hostvars["testhost"]["ansible_python_interpreter"] != hostvars["testhost2"]["ansible_python_interpreter"]
+
+- name: no delegation
+ hosts: all
+ gather_facts: false
+ tasks:
+ - name: detect interpreter used by each host
+ detect_interpreter:
+ register: baseline
+
+ - name: verify it
+ assert:
+ msg: 'expected {{ansible_python_interpreter}} but got {{baseline.found|basename}}'
+ that:
+ - baseline.found|basename == ansible_python_interpreter
+
+- name: actual test
+ hosts: testhost
+ gather_facts: false
+ tasks:
+ - name: original host
+ detect_interpreter:
+ register: found
+
+ - name: verify it orig host
+ assert:
+ msg: 'expected {{ansible_python_interpreter}} but got {{found.found|basename}}'
+ that:
+ - found.found|basename == ansible_python_interpreter
+
+ - name: delegated host
+ detect_interpreter:
+ register: found2
+ delegate_to: testhost2
+
+ - name: verify it delegated
+ assert:
+ msg: 'expected {{hostvars["testhost2"]["ansible_python_interpreter"]}} but got {{found2.found|basename}}'
+ that:
+ - found2.found|basename == hostvars["testhost2"]["ansible_python_interpreter"]
diff --git a/test/integration/targets/dict_transformations/aliases b/test/integration/targets/dict_transformations/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/dict_transformations/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/dict_transformations/library/convert_camelCase.py b/test/integration/targets/dict_transformations/library/convert_camelCase.py
new file mode 100644
index 0000000..50ca34c
--- /dev/null
+++ b/test/integration/targets/dict_transformations/library/convert_camelCase.py
@@ -0,0 +1,48 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: convert_camelCase
+short_description: test converting data to camelCase
+description: test converting data to camelCase
+options:
+ data:
+ description: Data to modify
+ type: dict
+ required: True
+ capitalize_first:
+ description: Whether to capitalize the first character
+ default: False
+ type: bool
+'''
+
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.dict_transformations import snake_dict_to_camel_dict
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ data=dict(type='dict', required=True),
+ capitalize_first=dict(type='bool', default=False),
+ ),
+ )
+
+ result = snake_dict_to_camel_dict(
+ module.params['data'],
+ module.params['capitalize_first']
+ )
+
+ module.exit_json(data=result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/dict_transformations/library/convert_snake_case.py b/test/integration/targets/dict_transformations/library/convert_snake_case.py
new file mode 100644
index 0000000..4c13fbc
--- /dev/null
+++ b/test/integration/targets/dict_transformations/library/convert_snake_case.py
@@ -0,0 +1,55 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: convert_snake_case
+short_description: test converting data to snake_case
+description: test converting data to snake_case
+options:
+ data:
+ description: Data to modify
+ type: dict
+ required: True
+ reversible:
+ description:
+ - Make the snake_case conversion in a way that can be converted back to the original value
+ - For example, convert IAMUser to i_a_m_user instead of iam_user
+ default: False
+ ignore_list:
+ description: list of top level keys that should not have their contents converted
+ type: list
+ default: []
+'''
+
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ data=dict(type='dict', required=True),
+ reversible=dict(type='bool', default=False),
+ ignore_list=dict(type='list', default=[]),
+ ),
+ )
+
+ result = camel_dict_to_snake_dict(
+ module.params['data'],
+ module.params['reversible'],
+ module.params['ignore_list']
+ )
+
+ module.exit_json(data=result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/dict_transformations/tasks/main.yml b/test/integration/targets/dict_transformations/tasks/main.yml
new file mode 100644
index 0000000..03aa6e1
--- /dev/null
+++ b/test/integration/targets/dict_transformations/tasks/main.yml
@@ -0,0 +1,3 @@
+- include_tasks: test_convert_snake_case.yml
+
+- include_tasks: test_convert_camelCase.yml
diff --git a/test/integration/targets/dict_transformations/tasks/test_convert_camelCase.yml b/test/integration/targets/dict_transformations/tasks/test_convert_camelCase.yml
new file mode 100644
index 0000000..666e8d3
--- /dev/null
+++ b/test/integration/targets/dict_transformations/tasks/test_convert_camelCase.yml
@@ -0,0 +1,33 @@
+- convert_camelCase:
+ data: {'top_level_key': {'nested_key': 'do_not_convert'}}
+ register: result
+
+- assert:
+ that:
+ - "result.data == {'topLevelKey': {'nestedKey': 'do_not_convert'}}"
+
+- convert_camelCase:
+ data: {'t_o_p_level_key': {'n_e_s_t_e_d_key': 'do_not_convert'}}
+ register: result
+
+- assert:
+ that:
+ - "result.data == {'tOPLevelKey': {'nESTEDKey': 'do_not_convert'}}"
+
+- convert_camelCase:
+ data: {'t_o_p_level_key': {'n_e_s_t_e_d_key': 'do_not_convert'}}
+ capitalize_first: True
+ register: result
+
+- assert:
+ that:
+ - "result.data == {'TOPLevelKey': {'NESTEDKey': 'do_not_convert'}}"
+
+- convert_camelCase:
+ data: {'results': [{'i_a_m_user': 'user_name', 'tags': {'do_convert': 'do_not_convert'}}]}
+ capitalize_first: True
+ register: result
+
+- assert:
+ that:
+ - "result.data == {'Results': [{'IAMUser': 'user_name', 'Tags': {'DoConvert': 'do_not_convert'}}]}"
diff --git a/test/integration/targets/dict_transformations/tasks/test_convert_snake_case.yml b/test/integration/targets/dict_transformations/tasks/test_convert_snake_case.yml
new file mode 100644
index 0000000..cf700bc
--- /dev/null
+++ b/test/integration/targets/dict_transformations/tasks/test_convert_snake_case.yml
@@ -0,0 +1,35 @@
+- convert_snake_case:
+ data: {'TOPLevelKey': {'NESTEDKey': 'DoNotConvert'}}
+ register: result
+
+- assert:
+ that:
+ - "result.data == {'top_level_key': {'nested_key': 'DoNotConvert'}}"
+
+- convert_snake_case:
+ data: {'TOPLevelKey': {'NESTEDKey': 'DoNotConvert'}}
+ reversible: True
+ register: result
+
+- assert:
+ that:
+ - "result.data == {'t_o_p_level_key': {'n_e_s_t_e_d_key': 'DoNotConvert'}}"
+
+- convert_snake_case:
+ data: {'Results': [{'IAMUser': 'UserName', 'Tags': {'DoConvert': 'DoNotConvert'}}], 'Tags': {'DoNotConvert': 'DoNotConvert'}}
+ reversible: True
+ ignore_list: ['Tags'] # Ignore top level 'Tags' key if found
+ register: result
+
+- assert:
+ that:
+ - "result.data == {'results': [{'i_a_m_user': 'UserName', 'tags': {'do_convert': 'DoNotConvert'}}], 'tags': {'DoNotConvert': 'DoNotConvert'}}"
+
+- name: Test converting dict keys in lists within lists
+ convert_snake_case:
+ data: {'Results': [{'Changes': [{'DoConvert': 'DoNotConvert', 'Details': ['DoNotConvert']}]}]}
+ register: result
+
+- assert:
+ that:
+ - "result.data == {'results': [{'changes': [{'do_convert': 'DoNotConvert', 'details': ['DoNotConvert']}]}]}"
diff --git a/test/integration/targets/dnf/aliases b/test/integration/targets/dnf/aliases
new file mode 100644
index 0000000..d6f27b8
--- /dev/null
+++ b/test/integration/targets/dnf/aliases
@@ -0,0 +1,6 @@
+destructive
+shippable/posix/group1
+skip/power/centos
+skip/freebsd
+skip/osx
+skip/macos
diff --git a/test/integration/targets/dnf/meta/main.yml b/test/integration/targets/dnf/meta/main.yml
new file mode 100644
index 0000000..34d8126
--- /dev/null
+++ b/test/integration/targets/dnf/meta/main.yml
@@ -0,0 +1,4 @@
+dependencies:
+ - prepare_tests
+ - setup_rpm_repo
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/dnf/tasks/cacheonly.yml b/test/integration/targets/dnf/tasks/cacheonly.yml
new file mode 100644
index 0000000..eb19156
--- /dev/null
+++ b/test/integration/targets/dnf/tasks/cacheonly.yml
@@ -0,0 +1,16 @@
+---
+- name: Test cacheonly (clean before testing)
+ command: dnf clean all
+
+- name: Try installing from cache where it has been cleaned
+ dnf:
+ name: sos
+ state: latest
+ cacheonly: true
+ register: dnf_result
+ ignore_errors: true
+
+- name: Verify dnf failed or has not changed
+ assert:
+ that:
+ - "dnf_result is failed or not dnf_result is changed"
diff --git a/test/integration/targets/dnf/tasks/dnf.yml b/test/integration/targets/dnf/tasks/dnf.yml
new file mode 100644
index 0000000..ec1c36f
--- /dev/null
+++ b/test/integration/targets/dnf/tasks/dnf.yml
@@ -0,0 +1,834 @@
+# UNINSTALL 'python2-dnf'
+# The `dnf` module has the smarts to auto-install the relevant python
+# bindings. To test, we will first uninstall python2-dnf (so that the tests
+# on python2 will require python2-dnf)
+- name: check python2-dnf with rpm
+ shell: rpm -q python2-dnf
+ register: rpm_result
+ ignore_errors: true
+
+# Don't uninstall python2-dnf with the `dnf` module in case it needs to load
+# some dnf python files after the package is uninstalled.
+- name: uninstall python2-dnf with shell
+ shell: dnf -y remove python2-dnf
+ when: rpm_result is successful
+
+# UNINSTALL
+# With 'python2-dnf' uninstalled, the first call to 'dnf' should install
+# python2-dnf.
+- name: uninstall sos
+ dnf:
+ name: sos
+ state: removed
+ register: dnf_result
+
+- name: check sos with rpm
+ shell: rpm -q sos
+ failed_when: False
+ register: rpm_result
+
+- name: verify uninstallation of sos
+ assert:
+ that:
+ - "not dnf_result.failed | default(False)"
+ - "rpm_result.rc == 1"
+
+# UNINSTALL AGAIN
+- name: uninstall sos
+ dnf:
+ name: sos
+ state: removed
+ register: dnf_result
+
+- name: verify no change on re-uninstall
+ assert:
+ that:
+ - "not dnf_result.changed"
+
+# INSTALL
+- name: install sos (check_mode)
+ dnf:
+ name: sos
+ state: present
+ update_cache: True
+ check_mode: True
+ register: dnf_result
+
+- assert:
+ that:
+ - dnf_result is success
+ - dnf_result.results|length > 0
+ - "dnf_result.results[0].startswith('Installed: ')"
+
+- name: install sos
+ dnf:
+ name: sos
+ state: present
+ update_cache: True
+ register: dnf_result
+
+- name: check sos with rpm
+ shell: rpm -q sos
+ failed_when: False
+ register: rpm_result
+
+- name: verify installation of sos
+ assert:
+ that:
+ - "not dnf_result.failed | default(False)"
+ - "dnf_result.changed"
+ - "rpm_result.rc == 0"
+
+- name: verify dnf module outputs
+ assert:
+ that:
+ - "'changed' in dnf_result"
+ - "'results' in dnf_result"
+
+# INSTALL AGAIN
+- name: install sos again (check_mode)
+ dnf:
+ name: sos
+ state: present
+ check_mode: True
+ register: dnf_result
+
+- assert:
+ that:
+ - dnf_result is not changed
+ - dnf_result.results|length == 0
+
+- name: install sos again
+ dnf:
+ name: sos
+ state: present
+ register: dnf_result
+
+- name: verify no change on second install
+ assert:
+ that:
+ - "not dnf_result.changed"
+
+# Multiple packages
+- name: uninstall sos and dos2unix
+ dnf: name=sos,dos2unix state=removed
+ register: dnf_result
+
+- name: check sos with rpm
+ shell: rpm -q sos
+ failed_when: False
+ register: rpm_sos_result
+
+- name: check dos2unix with rpm
+ shell: rpm -q dos2unix
+ failed_when: False
+ register: rpm_dos2unix_result
+
+- name: verify packages installed
+ assert:
+ that:
+ - "rpm_sos_result.rc != 0"
+ - "rpm_dos2unix_result.rc != 0"
+
+- name: install sos and dos2unix as comma separated
+ dnf: name=sos,dos2unix state=present
+ register: dnf_result
+
+- name: check sos with rpm
+ shell: rpm -q sos
+ failed_when: False
+ register: rpm_sos_result
+
+- name: check dos2unix with rpm
+ shell: rpm -q dos2unix
+ failed_when: False
+ register: rpm_dos2unix_result
+
+- name: verify packages installed
+ assert:
+ that:
+ - "not dnf_result.failed | default(False)"
+ - "dnf_result.changed"
+ - "rpm_sos_result.rc == 0"
+ - "rpm_dos2unix_result.rc == 0"
+
+- name: uninstall sos and dos2unix
+ dnf: name=sos,dos2unix state=removed
+ register: dnf_result
+
+- name: install sos and dos2unix as list
+ dnf:
+ name:
+ - sos
+ - dos2unix
+ state: present
+ register: dnf_result
+
+- name: check sos with rpm
+ shell: rpm -q sos
+ failed_when: False
+ register: rpm_sos_result
+
+- name: check dos2unix with rpm
+ shell: rpm -q dos2unix
+ failed_when: False
+ register: rpm_dos2unix_result
+
+- name: verify packages installed
+ assert:
+ that:
+ - "not dnf_result.failed | default(False)"
+ - "dnf_result.changed"
+ - "rpm_sos_result.rc == 0"
+ - "rpm_dos2unix_result.rc == 0"
+
+- name: uninstall sos and dos2unix
+ dnf:
+ name: "sos,dos2unix"
+ state: removed
+ register: dnf_result
+
+- name: install sos and dos2unix as comma separated with spaces
+ dnf:
+ name: "sos, dos2unix"
+ state: present
+ register: dnf_result
+
+- name: check sos with rpm
+ shell: rpm -q sos
+ failed_when: False
+ register: rpm_sos_result
+
+- name: check sos with rpm
+ shell: rpm -q dos2unix
+ failed_when: False
+ register: rpm_dos2unix_result
+
+- name: verify packages installed
+ assert:
+ that:
+ - "not dnf_result.failed | default(False)"
+ - "dnf_result.changed"
+ - "rpm_sos_result.rc == 0"
+ - "rpm_dos2unix_result.rc == 0"
+
+- name: uninstall sos and dos2unix (check_mode)
+ dnf:
+ name:
+ - sos
+ - dos2unix
+ state: removed
+ check_mode: True
+ register: dnf_result
+
+- assert:
+ that:
+ - dnf_result is success
+ - dnf_result.results|length == 2
+ - "dnf_result.results[0].startswith('Removed: ')"
+ - "dnf_result.results[1].startswith('Removed: ')"
+
+- name: uninstall sos and dos2unix
+ dnf:
+ name:
+ - sos
+ - dos2unix
+ state: removed
+ register: dnf_result
+
+- assert:
+ that:
+ - dnf_result is changed
+
+- name: install non-existent rpm
+ dnf:
+ name: does-not-exist
+ register: non_existent_rpm
+ ignore_errors: True
+
+- name: check non-existent rpm install failed
+ assert:
+ that:
+ - non_existent_rpm is failed
+
+# Install in installroot='/'. This should be identical to default
+- name: install sos in /
+ dnf: name=sos state=present installroot='/'
+ register: dnf_result
+
+- name: check sos with rpm in /
+ shell: rpm -q sos --root=/
+ failed_when: False
+ register: rpm_result
+
+- name: verify installation of sos in /
+ assert:
+ that:
+ - "not dnf_result.failed | default(False)"
+ - "dnf_result.changed"
+ - "rpm_result.rc == 0"
+
+- name: verify dnf module outputs in /
+ assert:
+ that:
+ - "'changed' in dnf_result"
+ - "'results' in dnf_result"
+
+- name: uninstall sos in /
+ dnf: name=sos installroot='/'
+ register: dnf_result
+
+- name: uninstall sos for downloadonly test
+ dnf:
+ name: sos
+ state: absent
+
+- name: Test download_only (check_mode)
+ dnf:
+ name: sos
+ state: latest
+ download_only: true
+ check_mode: true
+ register: dnf_result
+
+- assert:
+ that:
+ - dnf_result is success
+ - "dnf_result.results[0].startswith('Downloaded: ')"
+
+- name: Test download_only
+ dnf:
+ name: sos
+ state: latest
+ download_only: true
+ register: dnf_result
+
+- name: verify download of sos (part 1 -- dnf "install" succeeded)
+ assert:
+ that:
+ - "dnf_result is success"
+ - "dnf_result is changed"
+
+- name: uninstall sos (noop)
+ dnf:
+ name: sos
+ state: absent
+ register: dnf_result
+
+- name: verify download of sos (part 2 -- nothing removed during uninstall)
+ assert:
+ that:
+ - "dnf_result is success"
+ - "not dnf_result is changed"
+
+- name: uninstall sos for downloadonly/downloaddir test
+ dnf:
+ name: sos
+ state: absent
+
+- name: Test download_only/download_dir
+ dnf:
+ name: sos
+ state: latest
+ download_only: true
+ download_dir: "/var/tmp/packages"
+ register: dnf_result
+
+- name: verify dnf output
+ assert:
+ that:
+ - "dnf_result is success"
+ - "dnf_result is changed"
+
+- command: "ls /var/tmp/packages"
+ register: ls_out
+
+- name: Verify specified download_dir was used
+ assert:
+ that:
+ - "'sos' in ls_out.stdout"
+
+# GROUP INSTALL
+- name: install Custom Group group
+ dnf:
+ name: "@Custom Group"
+ state: present
+ register: dnf_result
+
+- name: check dinginessentail with rpm
+ command: rpm -q dinginessentail
+ failed_when: False
+ register: dinginessentail_result
+
+- name: verify installation of the group
+ assert:
+ that:
+ - not dnf_result is failed
+ - dnf_result is changed
+ - "'results' in dnf_result"
+ - dinginessentail_result.rc == 0
+
+- name: install the group again
+ dnf:
+ name: "@Custom Group"
+ state: present
+ register: dnf_result
+
+- name: verify nothing changed
+ assert:
+ that:
+ - not dnf_result is changed
+ - "'msg' in dnf_result"
+
+- name: verify that landsidescalping is not installed
+ dnf:
+ name: landsidescalping
+ state: absent
+
+- name: install the group again but also with a package that is not yet installed
+ dnf:
+ name:
+ - "@Custom Group"
+ - landsidescalping
+ state: present
+ register: dnf_result
+
+- name: check landsidescalping with rpm
+ command: rpm -q landsidescalping
+ failed_when: False
+ register: landsidescalping_result
+
+- name: verify landsidescalping is installed
+ assert:
+ that:
+ - dnf_result is changed
+ - "'results' in dnf_result"
+ - landsidescalping_result.rc == 0
+
+- name: try to install the group again, with --check to check 'changed'
+ dnf:
+ name: "@Custom Group"
+ state: present
+ check_mode: yes
+ register: dnf_result
+
+- name: verify nothing changed
+ assert:
+ that:
+ - not dnf_result is changed
+ - "'msg' in dnf_result"
+
+- name: remove landsidescalping after test
+ dnf:
+ name: landsidescalping
+ state: absent
+
+# cleanup until https://github.com/ansible/ansible/issues/27377 is resolved
+- shell: 'dnf -y group install "Custom Group" && dnf -y group remove "Custom Group"'
+ register: shell_dnf_result
+
+# GROUP UPGRADE - this will go to the same method as group install
+# but through group_update - it is its invocation we're testing here
+# see commit 119c9e5d6eb572c4a4800fbe8136095f9063c37b
+- name: install latest Custom Group
+ dnf:
+ name: "@Custom Group"
+ state: latest
+ register: dnf_result
+
+- name: verify installation of the group
+ assert:
+ that:
+ - not dnf_result is failed
+ - dnf_result is changed
+ - "'results' in dnf_result"
+
+# cleanup until https://github.com/ansible/ansible/issues/27377 is resolved
+- shell: dnf -y group install "Custom Group" && dnf -y group remove "Custom Group"
+
+- name: try to install non existing group
+ dnf:
+ name: "@non-existing-group"
+ state: present
+ register: dnf_result
+ ignore_errors: True
+
+- name: verify installation of the non existing group failed
+ assert:
+ that:
+ - "not dnf_result.changed"
+ - "dnf_result is failed"
+
+- name: verify dnf module outputs
+ assert:
+ that:
+ - "'changed' in dnf_result"
+ - "'msg' in dnf_result"
+
+- name: try to install non existing file
+ dnf:
+ name: /tmp/non-existing-1.0.0.fc26.noarch.rpm
+ state: present
+ register: dnf_result
+ ignore_errors: yes
+
+- name: verify installation failed
+ assert:
+ that:
+ - "dnf_result is failed"
+ - "not dnf_result.changed"
+
+- name: verify dnf module outputs
+ assert:
+ that:
+ - "'changed' in dnf_result"
+ - "'msg' in dnf_result"
+
+- name: try to install from non existing url
+ dnf:
+ name: https://ci-files.testing.ansible.com/test/integration/targets/dnf/non-existing-1.0.0.fc26.noarch.rpm
+ state: present
+ register: dnf_result
+ ignore_errors: yes
+
+- name: verify installation failed
+ assert:
+ that:
+ - "dnf_result is failed"
+ - "not dnf_result.changed"
+
+- name: verify dnf module outputs
+ assert:
+ that:
+ - "'changed' in dnf_result"
+ - "'msg' in dnf_result"
+
+# ENVIRONMENT UPGRADE
+# see commit de299ef77c03a64a8f515033a79ac6b7db1bc710
+- name: install Custom Environment Group
+ dnf:
+ name: "@Custom Environment Group"
+ state: latest
+ register: dnf_result
+
+- name: check landsidescalping with rpm
+ command: rpm -q landsidescalping
+ register: landsidescalping_result
+
+- name: verify installation of the environment
+ assert:
+ that:
+ - not dnf_result is failed
+ - dnf_result is changed
+ - "'results' in dnf_result"
+ - landsidescalping_result.rc == 0
+
+# Fedora 28 (DNF 2) does not support this, just remove the package itself
+- name: remove landsidescalping package on Fedora 28
+ dnf:
+ name: landsidescalping
+ state: absent
+ when: ansible_distribution == 'Fedora' and ansible_distribution_major_version|int <= 28
+
+# cleanup until https://github.com/ansible/ansible/issues/27377 is resolved
+- name: remove Custom Environment Group
+ shell: dnf -y group install "Custom Environment Group" && dnf -y group remove "Custom Environment Group"
+ when: not (ansible_distribution == 'Fedora' and ansible_distribution_major_version|int <= 28)
+
+# https://github.com/ansible/ansible/issues/39704
+- name: install non-existent rpm, state=latest
+ dnf:
+ name: non-existent-rpm
+ state: latest
+ ignore_errors: yes
+ register: dnf_result
+
+- name: verify the result
+ assert:
+ that:
+ - "dnf_result is failed"
+ - "'non-existent-rpm' in dnf_result['failures'][0]"
+ - "'No package non-existent-rpm available' in dnf_result['failures'][0]"
+ - "'Failed to install some of the specified packages' in dnf_result['msg']"
+
+- name: use latest to install httpd
+ dnf:
+ name: httpd
+ state: latest
+ register: dnf_result
+
+- name: verify httpd was installed
+ assert:
+ that:
+ - "'changed' in dnf_result"
+
+- name: uninstall httpd
+ dnf:
+ name: httpd
+ state: removed
+
+- name: update httpd only if it exists
+ dnf:
+ name: httpd
+ state: latest
+ update_only: yes
+ register: dnf_result
+
+- name: verify httpd not installed
+ assert:
+ that:
+ - "not dnf_result is changed"
+
+- name: try to install not compatible arch rpm, should fail
+ dnf:
+ name: https://ci-files.testing.ansible.com/test/integration/targets/dnf/banner-1.3.4-3.el7.ppc64le.rpm
+ state: present
+ register: dnf_result
+ ignore_errors: True
+
+- name: verify that dnf failed
+ assert:
+ that:
+ - "not dnf_result is changed"
+ - "dnf_result is failed"
+
+# setup for testing installing an RPM from local file
+
+- set_fact:
+ pkg_name: noarchfake
+ pkg_path: '{{ repodir }}/noarchfake-1.0-1.noarch.rpm'
+
+- name: cleanup
+ dnf:
+ name: "{{ pkg_name }}"
+ state: absent
+
+# setup end
+
+- name: install a local noarch rpm from file
+ dnf:
+ name: "{{ pkg_path }}"
+ state: present
+ disable_gpg_check: true
+ register: dnf_result
+
+- name: verify installation
+ assert:
+ that:
+ - "dnf_result is success"
+ - "dnf_result is changed"
+
+- name: install the downloaded rpm again
+ dnf:
+ name: "{{ pkg_path }}"
+ state: present
+ register: dnf_result
+
+- name: verify installation
+ assert:
+ that:
+ - "dnf_result is success"
+ - "not dnf_result is changed"
+
+- name: clean up
+ dnf:
+ name: "{{ pkg_name }}"
+ state: absent
+
+- name: install from url
+ dnf:
+ name: "file://{{ pkg_path }}"
+ state: present
+ disable_gpg_check: true
+ register: dnf_result
+
+- name: verify installation
+ assert:
+ that:
+ - "dnf_result is success"
+ - "dnf_result is changed"
+ - "dnf_result is not failed"
+
+- name: verify dnf module outputs
+ assert:
+ that:
+ - "'changed' in dnf_result"
+ - "'results' in dnf_result"
+
+- name: Create a temp RPM file which does not contain nevra information
+ file:
+ name: "/tmp/non_existent_pkg.rpm"
+ state: touch
+
+- name: Try installing RPM file which does not contain nevra information
+ dnf:
+ name: "/tmp/non_existent_pkg.rpm"
+ state: present
+ register: no_nevra_info_result
+ ignore_errors: yes
+
+- name: Verify RPM failed to install
+ assert:
+ that:
+ - "'changed' in no_nevra_info_result"
+ - "'msg' in no_nevra_info_result"
+
+- name: Delete a temp RPM file
+ file:
+ name: "/tmp/non_existent_pkg.rpm"
+ state: absent
+
+- name: uninstall lsof
+ dnf:
+ name: lsof
+ state: removed
+
+- name: check lsof with rpm
+ shell: rpm -q lsof
+ ignore_errors: True
+ register: rpm_lsof_result
+
+- name: verify lsof is uninstalled
+ assert:
+ that:
+ - "rpm_lsof_result is failed"
+
+- name: create conf file that excludes lsof
+ copy:
+ content: |
+ [main]
+ exclude=lsof*
+ dest: '{{ remote_tmp_dir }}/test-dnf.conf'
+ register: test_dnf_copy
+
+- block:
+ # begin test case where disable_excludes is supported
+ - name: Try install lsof without disable_excludes
+ dnf: name=lsof state=latest conf_file={{ test_dnf_copy.dest }}
+ register: dnf_lsof_result
+ ignore_errors: True
+
+ - name: verify lsof did not install because it is in exclude list
+ assert:
+ that:
+ - "dnf_lsof_result is failed"
+
+ - name: install lsof with disable_excludes
+ dnf: name=lsof state=latest disable_excludes=all conf_file={{ test_dnf_copy.dest }}
+ register: dnf_lsof_result_using_excludes
+
+ - name: verify lsof did install using disable_excludes=all
+ assert:
+ that:
+ - "dnf_lsof_result_using_excludes is success"
+ - "dnf_lsof_result_using_excludes is changed"
+ - "dnf_lsof_result_using_excludes is not failed"
+ always:
+ - name: remove exclude lsof conf file
+ file:
+ path: '{{ remote_tmp_dir }}/test-dnf.conf'
+ state: absent
+
+# end test case where disable_excludes is supported
+
+- name: Test "dnf install /usr/bin/vi"
+ block:
+ - name: Clean vim-minimal
+ dnf:
+ name: vim-minimal
+ state: absent
+
+ - name: Install vim-minimal by specifying "/usr/bin/vi"
+ dnf:
+ name: /usr/bin/vi
+ state: present
+
+ - name: Get rpm output
+ command: rpm -q vim-minimal
+ register: rpm_output
+
+ - name: Check installation was successful
+ assert:
+ that:
+ - "'vim-minimal' in rpm_output.stdout"
+ when:
+ - ansible_distribution == 'Fedora'
+
+- name: Remove wildcard package that isn't installed
+ dnf:
+ name: firefox*
+ state: absent
+ register: wildcard_absent
+
+- assert:
+ that:
+ - wildcard_absent is successful
+ - wildcard_absent is not changed
+
+- name: Test removing with various package specs
+ block:
+ - name: Ensure sos is installed
+ dnf:
+ name: sos
+ state: present
+
+ - name: Determine version of sos
+ command: rpm -q --queryformat=%{version} sos
+ register: sos_version_command
+
+ - name: Determine release of sos
+ command: rpm -q --queryformat=%{release} sos
+ register: sos_release_command
+
+ - name: Determine arch of sos
+ command: rpm -q --queryformat=%{arch} sos
+ register: sos_arch_command
+
+ - set_fact:
+ sos_version: "{{ sos_version_command.stdout | trim }}"
+ sos_release: "{{ sos_release_command.stdout | trim }}"
+ sos_arch: "{{ sos_arch_command.stdout | trim }}"
+
+ # We are just trying to remove the package by specifying its spec in a
+ # bunch of different ways. Each "item" here is a test (a name passed, to make
+ # sure it matches, with how we call Hawkey in the dnf module).
+ - include_tasks: test_sos_removal.yml
+ with_items:
+ - sos
+ - sos-{{ sos_version }}
+ - sos-{{ sos_version }}-{{ sos_release }}
+ - sos-{{ sos_version }}-{{ sos_release }}.{{ sos_arch }}
+ - sos-*-{{ sos_release }}
+ - sos-{{ sos_version[0] }}*
+ - sos-{{ sos_version[0] }}*-{{ sos_release }}
+ - sos-{{ sos_version[0] }}*{{ sos_arch }}
+
+ - name: Ensure deleting a non-existing package fails correctly
+ dnf:
+ name: a-non-existent-package
+ state: absent
+ ignore_errors: true
+ register: nonexisting
+
+ - assert:
+ that:
+ - nonexisting is success
+ - nonexisting.msg == 'Nothing to do'
+
+# running on RHEL which is --remote where .mo language files are present
+# for dnf as opposed to in --docker
+- when: ansible_distribution == 'RedHat'
+ block:
+ - dnf:
+ name: langpacks-ja
+ state: present
+
+ - dnf:
+ name: nginx-mod*
+ state: absent
+ environment:
+ LANG: ja_JP.UTF-8
+ always:
+ - dnf:
+ name: langpacks-ja
+ state: absent
diff --git a/test/integration/targets/dnf/tasks/dnfinstallroot.yml b/test/integration/targets/dnf/tasks/dnfinstallroot.yml
new file mode 100644
index 0000000..19f6706
--- /dev/null
+++ b/test/integration/targets/dnf/tasks/dnfinstallroot.yml
@@ -0,0 +1,35 @@
+# make a installroot
+- name: Create installroot
+ command: mktemp -d "{{ remote_tmp_dir }}/ansible.test.XXXXXX"
+ register: dnfroot
+
+# This will drag in > 200 MB.
+- name: attempt installroot
+ dnf: name=sos installroot="/{{ dnfroot.stdout }}/" disable_gpg_check=yes releasever={{ansible_facts['distribution_major_version']}}
+ register: dnf_result
+
+- name: check sos with rpm in installroot
+ shell: rpm -q sos --root="/{{ dnfroot.stdout }}/"
+ failed_when: False
+ register: rpm_result
+
+- debug: var=dnf_result
+- debug: var=rpm_result
+
+- name: verify installation of sos in installroot
+ assert:
+ that:
+ - "not dnf_result.failed | default(False)"
+ - "dnf_result.changed"
+ - "rpm_result.rc == 0"
+
+- name: verify dnf module outputs in /
+ assert:
+ that:
+ - "'changed' in dnf_result"
+ - "'results' in dnf_result"
+
+- name: cleanup installroot
+ file:
+ path: "/{{ dnfroot.stdout }}/"
+ state: absent
diff --git a/test/integration/targets/dnf/tasks/dnfreleasever.yml b/test/integration/targets/dnf/tasks/dnfreleasever.yml
new file mode 100644
index 0000000..351a26b
--- /dev/null
+++ b/test/integration/targets/dnf/tasks/dnfreleasever.yml
@@ -0,0 +1,47 @@
+# make an installroot
+- name: Create installroot
+ command: mktemp -d "{{ remote_tmp_dir }}/ansible.test.XXXXXX"
+ register: dnfroot
+
+- name: Make a necessary directory
+ file:
+ path: "/{{dnfroot.stdout}}/etc/dnf/vars"
+ state: directory
+ mode: 0755
+
+- name: Populate directory
+ copy:
+ content: "{{ansible_distribution_version}}\n"
+ dest: "/{{dnfroot.stdout}}/etc/dnf/vars/releasever"
+
+- name: attempt releasever to the installroot
+ dnf:
+ name: filesystem
+ installroot: '/{{dnfroot.stdout}}'
+ releasever: '{{ansible_distribution_version|int - 1}}'
+ register: dnf_result
+
+- name: check filesystem version
+ shell: rpm -q filesystem --root="/{{dnfroot.stdout}}/"
+ failed_when: False
+ register: rpm_result
+
+- debug: var=dnf_result
+- debug: var=rpm_result
+
+- name: verify installation was done
+ assert:
+ that:
+ - "not dnf_result.failed | default(False)"
+ - "dnf_result.changed"
+ - "rpm_result.rc == 0"
+
+- name: verify the version
+ assert:
+ that:
+ - "rpm_result.stdout.find('fc' ~ (ansible_distribution_version|int - 1)) != -1"
+
+- name: cleanup installroot
+ file:
+ path: "/{{dnfroot.stdout}}/"
+ state: absent
diff --git a/test/integration/targets/dnf/tasks/filters.yml b/test/integration/targets/dnf/tasks/filters.yml
new file mode 100644
index 0000000..1ce9b66
--- /dev/null
+++ b/test/integration/targets/dnf/tasks/filters.yml
@@ -0,0 +1,162 @@
+# We have a test repo set up with a valid updateinfo.xml which is referenced
+# from its repomd.xml.
+- block:
+ - set_fact:
+ updateinfo_repo: https://ci-files.testing.ansible.com/test/integration/targets/setup_rpm_repo/repo-with-updateinfo
+
+ - name: Install the test repo
+ yum_repository:
+ name: test-repo-with-updateinfo
+ description: test-repo-with-updateinfo
+ baseurl: "{{ updateinfo_repo }}"
+ gpgcheck: no
+
+ - name: Install old versions of toaster and oven
+ dnf:
+ name:
+ - "{{ updateinfo_repo }}/toaster-1.2.3.4-1.el8.noarch.rpm"
+ - "{{ updateinfo_repo }}/oven-1.2.3.4-1.el8.noarch.rpm"
+ disable_gpg_check: true
+
+ - name: Ask for pending updates
+ dnf:
+ name: '*'
+ state: latest
+ update_only: true
+ disable_gpg_check: true
+ disablerepo: '*'
+ enablerepo: test-repo-with-updateinfo
+ register: update_no_filter
+
+ - assert:
+ that:
+ - update_no_filter is changed
+ - '"Installed: toaster-1.2.3.5-1.el8.noarch" in update_no_filter.results'
+ - '"Installed: oven-1.2.3.5-1.el8.noarch" in update_no_filter.results'
+ - '"Removed: toaster-1.2.3.4-1.el8.noarch" in update_no_filter.results'
+ - '"Removed: oven-1.2.3.4-1.el8.noarch" in update_no_filter.results'
+
+ - name: Install old versions of toaster and oven
+ dnf:
+ name:
+ - "{{ updateinfo_repo }}/toaster-1.2.3.4-1.el8.noarch.rpm"
+ - "{{ updateinfo_repo }}/oven-1.2.3.4-1.el8.noarch.rpm"
+ allow_downgrade: true
+ disable_gpg_check: true
+
+ - name: Ask for pending updates with security=true
+ dnf:
+ name: '*'
+ state: latest
+ update_only: true
+ disable_gpg_check: true
+ security: true
+ disablerepo: '*'
+ enablerepo: test-repo-with-updateinfo
+ register: update_security
+
+ - assert:
+ that:
+ - update_security is changed
+ - '"Installed: toaster-1.2.3.5-1.el8.noarch" in update_security.results'
+ - '"Removed: toaster-1.2.3.4-1.el8.noarch" in update_security.results'
+ - '"Installed: oven-1.2.3.5-1.el8.noarch" not in update_security.results'
+ - '"Removed: oven-1.2.3.4-1.el8.noarch" not in update_security.results'
+
+ - name: Install old versions of toaster and oven
+ dnf:
+ name:
+ - "{{ updateinfo_repo }}/toaster-1.2.3.4-1.el8.noarch.rpm"
+ - "{{ updateinfo_repo }}/oven-1.2.3.4-1.el8.noarch.rpm"
+ allow_downgrade: true
+ disable_gpg_check: true
+
+ - name: Ask for pending updates with bugfix=true
+ dnf:
+ name: '*'
+ state: latest
+ update_only: true
+ disable_gpg_check: true
+ bugfix: true
+ disablerepo: '*'
+ enablerepo: test-repo-with-updateinfo
+ register: update_bugfix
+
+ - assert:
+ that:
+ - update_bugfix is changed
+ - '"Installed: toaster-1.2.3.5-1.el8.noarch" not in update_bugfix.results'
+ - '"Removed: toaster-1.2.3.4-1.el8.noarch" not in update_bugfix.results'
+ - '"Installed: oven-1.2.3.5-1.el8.noarch" in update_bugfix.results'
+ - '"Removed: oven-1.2.3.4-1.el8.noarch" in update_bugfix.results'
+
+ - name: Install old versions of toaster and oven
+ dnf:
+ name:
+ - "{{ updateinfo_repo }}/toaster-1.2.3.4-1.el8.noarch.rpm"
+ - "{{ updateinfo_repo }}/oven-1.2.3.4-1.el8.noarch.rpm"
+ allow_downgrade: true
+ disable_gpg_check: true
+
+ - name: Verify toaster is not upgraded with state=installed
+ dnf:
+ name: "{{ item }}"
+ state: installed
+ register: installed
+ loop:
+ - toaster
+ - toaster.noarch
+ - toaster-1.2.3.4-1.el8.noarch
+
+ - assert:
+ that: "installed.results | map(attribute='changed') is not any"
+
+ - name: Ask for pending updates with bugfix=true and security=true
+ dnf:
+ name: '*'
+ state: latest
+ update_only: true
+ disable_gpg_check: true
+ bugfix: true
+ security: true
+ disablerepo: '*'
+ enablerepo: test-repo-with-updateinfo
+ register: update_bugfix
+
+ - assert:
+ that:
+ - update_bugfix is changed
+ - '"Installed: toaster-1.2.3.5-1.el8.noarch" in update_bugfix.results'
+ - '"Removed: toaster-1.2.3.4-1.el8.noarch" in update_bugfix.results'
+ - '"Installed: oven-1.2.3.5-1.el8.noarch" in update_bugfix.results'
+ - '"Removed: oven-1.2.3.4-1.el8.noarch" in update_bugfix.results'
+
+ - name: Install old version of toaster again
+ dnf:
+ name: "{{ updateinfo_repo }}/toaster-1.2.3.4-1.el8.noarch.rpm"
+ allow_downgrade: true
+ disable_gpg_check: true
+
+ - name: Verify toaster is upgraded with state=latest
+ dnf:
+ name: toaster.noarch
+ state: latest
+ register: result
+
+ - assert:
+ that: result.changed
+
+ always:
+ - name: Remove installed packages
+ dnf:
+ name:
+ - toaster
+ - oven
+ state: absent
+
+ - name: Remove the repo
+ yum_repository:
+ name: test-repo-with-updateinfo
+ state: absent
+ tags:
+ - filters
diff --git a/test/integration/targets/dnf/tasks/filters_check_mode.yml b/test/integration/targets/dnf/tasks/filters_check_mode.yml
new file mode 100644
index 0000000..c931c07
--- /dev/null
+++ b/test/integration/targets/dnf/tasks/filters_check_mode.yml
@@ -0,0 +1,118 @@
+# We have a test repo set up with a valid updateinfo.xml which is referenced
+# from its repomd.xml.
+- block:
+ - set_fact:
+ updateinfo_repo: https://ci-files.testing.ansible.com/test/integration/targets/setup_rpm_repo/repo-with-updateinfo
+
+ - name: Install the test repo
+ yum_repository:
+ name: test-repo-with-updateinfo
+ description: test-repo-with-updateinfo
+ baseurl: "{{ updateinfo_repo }}"
+ gpgcheck: no
+
+ - name: Install old versions of toaster and oven
+ dnf:
+ name:
+ - "{{ updateinfo_repo }}/toaster-1.2.3.4-1.el8.noarch.rpm"
+ - "{{ updateinfo_repo }}/oven-1.2.3.4-1.el8.noarch.rpm"
+ disable_gpg_check: true
+
+ - name: Ask for pending updates (check_mode)
+ dnf:
+ name:
+ - toaster
+ - oven
+ state: latest
+ update_only: true
+ disable_gpg_check: true
+ check_mode: true
+ register: update_no_filter
+
+ - assert:
+ that:
+ - update_no_filter is changed
+ - '"would have if not in check mode" in update_no_filter.msg'
+ - '"Installed: toaster-1.2.3.5-1.el8.noarch" in update_no_filter.results'
+ - '"Installed: oven-1.2.3.5-1.el8.noarch" in update_no_filter.results'
+ - '"Removed: toaster-1.2.3.4-1.el8.noarch" in update_no_filter.results'
+ - '"Removed: oven-1.2.3.4-1.el8.noarch" in update_no_filter.results'
+
+ - name: Ask for pending updates with security=true (check_mode)
+ dnf:
+ name:
+ - toaster
+ - oven
+ state: latest
+ update_only: true
+ disable_gpg_check: true
+ security: true
+ check_mode: true
+ register: update_security
+
+ - assert:
+ that:
+ - update_security is changed
+ - '"would have if not in check mode" in update_security.msg'
+ - '"Installed: toaster-1.2.3.5-1.el8.noarch" in update_security.results'
+ - '"Removed: toaster-1.2.3.4-1.el8.noarch" in update_security.results'
+ - '"Installed: oven-1.2.3.5-1.el8.noarch" not in update_security.results'
+ - '"Removed: oven-1.2.3.4-1.el8.noarch" not in update_security.results'
+
+ - name: Ask for pending updates with bugfix=true (check_mode)
+ dnf:
+ name:
+ - toaster
+ - oven
+ state: latest
+ update_only: true
+ disable_gpg_check: true
+ bugfix: true
+ check_mode: true
+ register: update_bugfix
+
+ - assert:
+ that:
+ - update_bugfix is changed
+ - '"would have if not in check mode" in update_bugfix.msg'
+ - '"Installed: toaster-1.2.3.5-1.el8.noarch" not in update_bugfix.results'
+ - '"Removed: toaster-1.2.3.4-1.el8.noarch" not in update_bugfix.results'
+ - '"Installed: oven-1.2.3.5-1.el8.noarch" in update_bugfix.results'
+ - '"Removed: oven-1.2.3.4-1.el8.noarch" in update_bugfix.results'
+
+ - name: Ask for pending updates with bugfix=true and security=true (check_mode)
+ dnf:
+ name:
+ - toaster
+ - oven
+ state: latest
+ update_only: true
+ disable_gpg_check: true
+ bugfix: true
+ security: true
+ check_mode: true
+ register: update_bugfix
+
+ - assert:
+ that:
+ - update_bugfix is changed
+ - '"would have if not in check mode" in update_bugfix.msg'
+ - '"Installed: toaster-1.2.3.5-1.el8.noarch" in update_bugfix.results'
+ - '"Removed: toaster-1.2.3.4-1.el8.noarch" in update_bugfix.results'
+ - '"Installed: oven-1.2.3.5-1.el8.noarch" in update_bugfix.results'
+ - '"Removed: oven-1.2.3.4-1.el8.noarch" in update_bugfix.results'
+
+ always:
+ - name: Remove installed packages
+ dnf:
+ name:
+ - toaster
+ - oven
+ state: absent
+
+ - name: Remove the repo
+ yum_repository:
+ name: test-repo-with-updateinfo
+ state: absent
+ tags:
+ - filters
diff --git a/test/integration/targets/dnf/tasks/gpg.yml b/test/integration/targets/dnf/tasks/gpg.yml
new file mode 100644
index 0000000..72bdee0
--- /dev/null
+++ b/test/integration/targets/dnf/tasks/gpg.yml
@@ -0,0 +1,88 @@
+# Set up a repo of unsigned rpms
+- block:
+ - set_fact:
+ pkg_name: langtable
+ pkg_repo_dir: "{{ remote_tmp_dir }}/unsigned"
+
+ - name: Ensure our test package isn't already installed
+ dnf:
+ name:
+ - '{{ pkg_name }}'
+ state: absent
+
+ - name: Install rpm-sign and attr
+ dnf:
+ name:
+ - rpm-sign
+ - attr
+ state: present
+
+ - name: Create directory to use as local repo
+ file:
+ path: "{{ pkg_repo_dir }}"
+ state: directory
+
+ - name: Download the test package
+ dnf:
+ name: '{{ pkg_name }}'
+ state: latest
+ download_only: true
+ download_dir: "{{ pkg_repo_dir }}"
+
+ - name: Unsign the RPM
+ shell: rpmsign --delsign {{ remote_tmp_dir }}/unsigned/{{ pkg_name }}*
+
+ # In RHEL 8.5 dnf uses libdnf to do checksum verification, which caches the checksum on an xattr of the file
+ # itself, so we need to clear that cache
+ - name: Clear libdnf checksum cache
+ shell: setfattr -x user.Librepo.checksum.sha256 {{ remote_tmp_dir }}/unsigned/{{ pkg_name }}*
+ when: ansible_distribution in ['RedHat', 'CentOS'] and
+ ansible_distribution_version is version('8.5', '>=') and
+ ansible_distribution_version is version('9', '<')
+
+ - name: createrepo
+ command: createrepo .
+ args:
+ chdir: "{{ pkg_repo_dir }}"
+
+ - name: Add the repo
+ yum_repository:
+ name: unsigned
+ description: unsigned rpms
+ baseurl: "file://{{ pkg_repo_dir }}"
+ # we want to ensure that signing is verified
+ gpgcheck: true
+
+ - name: Install test package
+ dnf:
+ name:
+ - "{{ pkg_name }}"
+ disablerepo: '*'
+ enablerepo: unsigned
+ register: res
+ ignore_errors: yes
+
+ - assert:
+ that:
+ - res is failed
+ - "'Failed to validate GPG signature' in res.msg"
+ - "'is not signed' in res.msg"
+
+ always:
+ - name: Remove rpm-sign and attr (and test package if it got installed)
+ dnf:
+ name:
+ - rpm-sign
+ - attr
+ - "{{ pkg_name }}"
+ state: absent
+
+ - name: Remove test repo
+ yum_repository:
+ name: unsigned
+ state: absent
+
+ - name: Remove repo dir
+ file:
+ path: "{{ pkg_repo_dir }}"
+ state: absent
diff --git a/test/integration/targets/dnf/tasks/logging.yml b/test/integration/targets/dnf/tasks/logging.yml
new file mode 100644
index 0000000..903bf56
--- /dev/null
+++ b/test/integration/targets/dnf/tasks/logging.yml
@@ -0,0 +1,48 @@
+# Verify logging function is enabled in the dnf module.
+# The following tasks has been supported in dnf-4.2.17-6 or later
+# Note: https://bugzilla.redhat.com/show_bug.cgi?id=1788212
+- name: Install latest version python3-dnf
+ dnf:
+ name:
+ - python3-dnf
+ - python3-libdnf # https://bugzilla.redhat.com/show_bug.cgi?id=1887502
+ - libmodulemd # https://bugzilla.redhat.com/show_bug.cgi?id=1942236
+ state: latest
+ register: dnf_result
+
+- name: Verify python3-dnf installed
+ assert:
+ that:
+ - "dnf_result.rc == 0"
+
+- name: Get python3-dnf version
+ shell: "dnf info python3-dnf | awk '/^Version/ { print $3 }'"
+ register: py3_dnf_version
+
+- name: Check logging enabled
+ block:
+ - name: remove logfiles if exist
+ file:
+ path: "{{ item }}"
+ state: absent
+ loop: "{{ dnf_log_files }}"
+
+ - name: Install sos package
+ dnf:
+ name: sos
+ state: present
+ register: dnf_result
+
+ - name: Get status of logfiles
+ stat:
+ path: "{{ item }}"
+ loop: "{{ dnf_log_files }}"
+ register: stats
+
+ - name: Verify logfile exists
+ assert:
+ that:
+ - "item.stat.exists"
+ loop: "{{ stats.results }}"
+ when:
+ - 'py3_dnf_version.stdout is version("4.2.17", ">=")'
diff --git a/test/integration/targets/dnf/tasks/main.yml b/test/integration/targets/dnf/tasks/main.yml
new file mode 100644
index 0000000..66a171a
--- /dev/null
+++ b/test/integration/targets/dnf/tasks/main.yml
@@ -0,0 +1,74 @@
+# test code for the dnf module
+# (c) 2014, James Tanner <tanner.jc@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Note: We install the yum package onto Fedora so that this will work on dnf systems
+# We want to test that for people who don't want to upgrade their systems.
+
+- include_tasks: dnf.yml
+ when: (ansible_distribution == 'Fedora' and ansible_distribution_major_version is version('23', '>=')) or
+ (ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('8', '>='))
+
+- include_tasks: skip_broken_and_nobest.yml
+ when: (ansible_distribution == 'Fedora' and ansible_distribution_major_version is version('23', '>=')) or
+ (ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('8', '>='))
+
+- include_tasks: filters_check_mode.yml
+ when: (ansible_distribution == 'Fedora' and ansible_distribution_major_version is version('23', '>=')) or
+ (ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('8', '>='))
+ tags:
+ - filters
+
+- include_tasks: filters.yml
+ when: (ansible_distribution == 'Fedora' and ansible_distribution_major_version is version('23', '>=')) or
+ (ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('8', '>='))
+ tags:
+ - filters
+
+- include_tasks: gpg.yml
+ when: (ansible_distribution == 'Fedora' and ansible_distribution_major_version is version('23', '>=')) or
+ (ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('8', '>='))
+
+- include_tasks: repo.yml
+ when: (ansible_distribution == 'Fedora' and ansible_distribution_major_version is version('23', '>=')) or
+ (ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('8', '>='))
+
+- include_tasks: dnfinstallroot.yml
+ when: (ansible_distribution == 'Fedora' and ansible_distribution_major_version is version('23', '>=')) or
+ (ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('8', '>='))
+
+# Attempting to install a different RHEL release in a tmpdir doesn't work (rhel8 beta)
+- include_tasks: dnfreleasever.yml
+ when:
+ - ansible_distribution == 'Fedora'
+ - ansible_distribution_major_version is version('23', '>=')
+
+- include_tasks: modularity.yml
+ when:
+ - astream_name is defined
+ - (ansible_distribution == 'Fedora' and ansible_distribution_major_version is version('29', '>=')) or
+ (ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('8', '>='))
+ tags:
+ - dnf_modularity
+
+- include_tasks: logging.yml
+ when: (ansible_distribution == 'Fedora' and ansible_distribution_major_version is version('31', '>=')) or
+ (ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('8', '>='))
+
+- include_tasks: cacheonly.yml
+ when: (ansible_distribution == 'Fedora' and ansible_distribution_major_version is version('23', '>=')) or
+ (ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('8', '>='))
diff --git a/test/integration/targets/dnf/tasks/modularity.yml b/test/integration/targets/dnf/tasks/modularity.yml
new file mode 100644
index 0000000..94f43a4
--- /dev/null
+++ b/test/integration/targets/dnf/tasks/modularity.yml
@@ -0,0 +1,104 @@
+# FUTURE - look at including AppStream support in our local repo
+- name: Include distribution specific variables
+ include_vars: "{{ item }}"
+ with_first_found:
+ - files:
+ - "{{ ansible_facts.distribution }}-{{ ansible_facts.distribution_major_version }}.yml"
+ - "{{ ansible_facts.distribution }}.yml"
+ paths: ../vars
+
+- name: install "{{ astream_name }}" module
+ dnf:
+ name: "{{ astream_name }}"
+ state: present
+ register: dnf_result
+
+- name: verify installation of "{{ astream_name }}" module
+ assert:
+ that:
+ - "not dnf_result.failed"
+ - "dnf_result.changed"
+
+- name: install "{{ astream_name }}" module again
+ dnf:
+ name: "{{ astream_name }}"
+ state: present
+ register: dnf_result
+
+- name: verify installation of "{{ astream_name }}" module again
+ assert:
+ that:
+ - "not dnf_result.failed"
+ - "not dnf_result.changed"
+
+- name: uninstall "{{ astream_name }}" module
+ dnf:
+ name: "{{ astream_name }}"
+ state: absent
+ register: dnf_result
+
+- name: verify uninstallation of "{{ astream_name }}" module
+ assert:
+ that:
+ - "not dnf_result.failed"
+ - "dnf_result.changed"
+
+- name: uninstall "{{ astream_name }}" module again
+ dnf:
+ name: "{{ astream_name }}"
+ state: absent
+ register: dnf_result
+
+- name: verify uninstallation of "{{ astream_name }}" module again
+ assert:
+ that:
+ - "not dnf_result.failed"
+ - "not dnf_result.changed"
+
+- name: install "{{ astream_name_no_stream }}" module without providing stream
+ dnf:
+ name: "{{ astream_name_no_stream }}"
+ state: present
+ register: dnf_result
+
+- name: verify installation of "{{ astream_name_no_stream }}" module without providing stream
+ assert:
+ that:
+ - "not dnf_result.failed"
+ - "dnf_result.changed"
+
+- name: install "{{ astream_name_no_stream }}" module again without providing stream
+ dnf:
+ name: "{{ astream_name_no_stream }}"
+ state: present
+ register: dnf_result
+
+- name: verify installation of "{{ astream_name_no_stream }}" module again without providing stream
+ assert:
+ that:
+ - "not dnf_result.failed"
+ - "not dnf_result.changed"
+
+- name: uninstall "{{ astream_name_no_stream }}" module without providing stream
+ dnf:
+ name: "{{ astream_name_no_stream }}"
+ state: absent
+ register: dnf_result
+
+- name: verify uninstallation of "{{ astream_name_no_stream }}" module without providing stream
+ assert:
+ that:
+ - "not dnf_result.failed"
+ - "dnf_result.changed"
+
+- name: uninstall "{{ astream_name_no_stream }}" module again without providing stream
+ dnf:
+ name: "{{ astream_name_no_stream }}"
+ state: absent
+ register: dnf_result
+
+- name: verify uninstallation of "{{ astream_name_no_stream }}" module again without providing stream
+ assert:
+ that:
+ - "not dnf_result.failed"
+ - "not dnf_result.changed"
diff --git a/test/integration/targets/dnf/tasks/repo.yml b/test/integration/targets/dnf/tasks/repo.yml
new file mode 100644
index 0000000..4f82899
--- /dev/null
+++ b/test/integration/targets/dnf/tasks/repo.yml
@@ -0,0 +1,309 @@
+- block:
+ - name: Install dinginessentail-1.0-1
+ dnf:
+ name: dinginessentail-1.0-1
+ state: present
+ register: dnf_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "dnf_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.0-1')"
+
+ - name: Verify dnf module outputs
+ assert:
+ that:
+ - "'results' in dnf_result"
+ # ============================================================================
+ - name: Install dinginessentail-1.0-1 again
+ dnf:
+ name: dinginessentail-1.0-1
+ state: present
+ register: dnf_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "not dnf_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.0-1')"
+
+ - name: Verify dnf module outputs
+ assert:
+ that:
+ - "'msg' in dnf_result"
+ # ============================================================================
+ - name: Install dinginessentail again (noop, module is idempotent)
+ dnf:
+ name: dinginessentail
+ state: present
+ register: dnf_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ # No upgrade happened to 1.1.1
+ - "not dnf_result.changed"
+ # Old version still installed
+ - "rpm_result.stdout.startswith('dinginessentail-1.0-1')"
+ # ============================================================================
+ - name: Install dinginessentail-1:1.0-2
+ dnf:
+ name: "dinginessentail-1:1.0-2.{{ ansible_architecture }}"
+ state: present
+ register: dnf_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "dnf_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.0-2')"
+
+ - name: Verify dnf module outputs
+ assert:
+ that:
+ - "'results' in dnf_result"
+ # ============================================================================
+ - name: Update to the latest dinginessentail
+ dnf:
+ name: dinginessentail
+ state: latest
+ register: dnf_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "dnf_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.1-1')"
+
+ - name: Verify dnf module outputs
+ assert:
+ that:
+ - "'results' in dnf_result"
+ # ============================================================================
+ - name: Install dinginessentail-1.0-1 from a file (downgrade)
+ dnf:
+ name: "{{ repodir }}/dinginessentail-1.0-1.{{ ansible_architecture }}.rpm"
+ state: present
+ allow_downgrade: True
+ disable_gpg_check: True
+ register: dnf_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "dnf_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.0-1')"
+
+ - name: Verify dnf module outputs
+ assert:
+ that:
+ - "'results' in dnf_result"
+
+ - name: Remove dinginessentail
+ dnf:
+ name: dinginessentail
+ state: absent
+ # ============================================================================
+ - name: Install dinginessentail-1.0-1 from a file
+ dnf:
+ name: "{{ repodir }}/dinginessentail-1.0-1.{{ ansible_architecture }}.rpm"
+ state: present
+ disable_gpg_check: True
+ register: dnf_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "dnf_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.0-1')"
+
+ - name: Verify dnf module outputs
+ assert:
+ that:
+ - "'results' in dnf_result"
+ # ============================================================================
+ - name: Install dinginessentail-1.0-1 from a file again
+ dnf:
+ name: "{{ repodir }}/dinginessentail-1.0-1.{{ ansible_architecture }}.rpm"
+ state: present
+ disable_gpg_check: True
+ register: dnf_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "not dnf_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.0-1')"
+ # ============================================================================
+ - name: Install dinginessentail-1.0-2 from a file
+ dnf:
+ name: "{{ repodir }}/dinginessentail-1.0-2.{{ ansible_architecture }}.rpm"
+ state: present
+ disable_gpg_check: True
+ register: dnf_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "dnf_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.0-2')"
+
+ - name: Verify dnf module outputs
+ assert:
+ that:
+ - "'results' in dnf_result"
+ # ============================================================================
+ - name: Install dinginessentail-1.0-2 from a file again
+ dnf:
+ name: "{{ repodir }}/dinginessentail-1.0-2.{{ ansible_architecture }}.rpm"
+ state: present
+ disable_gpg_check: True
+ register: dnf_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "not dnf_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.0-2')"
+ # ============================================================================
+ - name: Remove dinginessentail
+ dnf:
+ name: dinginessentail
+ state: absent
+
+ - name: Try to install incompatible arch
+ dnf:
+ name: "{{ repodir_ppc64 }}/dinginessentail-1.0-1.ppc64.rpm"
+ state: present
+ register: dnf_result
+ ignore_errors: yes
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+ ignore_errors: yes
+
+ - name: Verify installation
+ assert:
+ that:
+ - "rpm_result.rc == 1"
+ - "not dnf_result.changed"
+ - "dnf_result is failed"
+ # ============================================================================
+
+ # Should install dinginessentail-with-weak-dep and dinginessentail-weak-dep
+ - name: Install package with defaults
+ dnf:
+ name: dinginessentail-with-weak-dep
+ state: present
+
+ - name: Check if dinginessentail-with-weak-dep is installed
+ shell: rpm -q dinginessentail-with-weak-dep
+ register: rpm_main_result
+
+ - name: Check if dinginessentail-weak-dep is installed
+ shell: rpm -q dinginessentail-weak-dep
+ register: rpm_weak_result
+
+ - name: Verify install with weak deps
+ assert:
+ that:
+ - rpm_main_result.rc == 0
+ - rpm_weak_result.rc == 0
+
+ - name: Uninstall dinginessentail weak dep packages
+ dnf:
+ name:
+ - dinginessentail-with-weak-dep
+ - dinginessentail-weak-dep
+ state: absent
+
+ - name: Install package with weak deps but skip weak deps
+ dnf:
+ name: dinginessentail-with-weak-dep
+ install_weak_deps: False
+ state: present
+
+ - name: Check if dinginessentail-with-weak-dep is installed
+ shell: rpm -q dinginessentail-with-weak-dep
+ register: rpm_main_result
+
+ - name: Check if dinginessentail-weak-dep is installed
+ shell: rpm -q dinginessentail-weak-dep
+ register: rpm_weak_result
+ ignore_errors: yes
+
+ - name: Verify install without weak deps
+ assert:
+ that:
+ - rpm_main_result.rc == 0
+ - rpm_weak_result.rc == 1 # the weak dependency shouldn't be installed
+
+ # https://github.com/ansible/ansible/issues/55938
+ - name: Install dinginessentail-*
+ dnf:
+ name: dinginessentail-*
+ state: present
+
+ - name: Uninstall dinginessentail-*
+ dnf:
+ name: dinginessentail-*
+ state: absent
+
+ - name: Check if all dinginessentail packages are removed
+ shell: rpm -qa dinginessentail-* | wc -l
+ register: rpm_result
+
+ - name: Verify rpm result
+ assert:
+ that:
+ - rpm_result.stdout == '0'
+ always:
+ - name: Clean up
+ dnf:
+ name:
+ - dinginessentail
+ - dinginessentail-with-weak-dep
+ - dinginessentail-weak-dep
+ state: absent
diff --git a/test/integration/targets/dnf/tasks/skip_broken_and_nobest.yml b/test/integration/targets/dnf/tasks/skip_broken_and_nobest.yml
new file mode 100644
index 0000000..503cb4c
--- /dev/null
+++ b/test/integration/targets/dnf/tasks/skip_broken_and_nobest.yml
@@ -0,0 +1,318 @@
+# Tests for skip_broken and allowerasing
+# (and a bit of nobest because the test case is too good to pass up)
+#
+# There are a lot of fairly complex, corner cases we test here especially towards the end.
+#
+# The test repo is generated from the "skip-broken" repo in this repository:
+# https://github.com/relrod/ansible-ci-contrived-yum-repos
+#
+# It is laid out like this:
+#
+# There are three packages, `broken-a`, `broken-b`, `broken-c`.
+#
+# * broken-a has three versions: 1.2.3, 1.2.3.4, 1.2.4, 2.0.0.
+# * 1.2.3 and 1.2.4 have no dependencies
+# * 1.2.3.4 and 2.0.0 both depend on a non-existent package (to break depsolving)
+#
+# * broken-b depends on broken-a-1.2.3
+# * broken-c depends on broken-a-1.2.4
+# * broken-d depends on broken-a (no version constraint)
+#
+# This allows us to test various upgrades, downgrades, and installs with broken dependencies.
+# skip_broken should usually be successful in the upgrade/downgrade case, it will just do nothing.
+#
+# There is a nobest testcase or two thrown in, simply because this organization provides a very
+# good test case for that behavior as well. For example, just installing "broken-a" with no version
+# will try to install 2.0.0 which is broken. With nobest=true, it will fall back to 1.2.4. Similar
+# for upgrading.
+- block:
+ - name: Set up test yum repo
+ yum_repository:
+ name: skip-broken
+ description: ansible-test skip-broken test repo
+ baseurl: "{{ skip_broken_repo_baseurl }}"
+ gpgcheck: no
+ repo_gpgcheck: no
+
+ - name: Install two packages
+ dnf:
+ name:
+ - broken-a-1.2.3
+ - broken-b
+
+ # This will fail. We have broken-a-1.2.3, and broken-b with a strong
+ # dependency on it. broken-c has a strong dependency on broken-a-1.2.4.
+ # Since installing that would break broken-b, we get a conflict.
+ - name: Try installing a third package, intentionally broken
+ dnf:
+ name:
+ - broken-c
+ ignore_errors: true
+ register: dnf_fail
+
+ - assert:
+ that:
+ - dnf_fail is failed
+ - "'Depsolve Error' in dnf_fail.msg"
+
+ # skip_broken should still install nothing because the conflict is
+ # still an issue. But it should skip over the broken packages and not
+ # fail.
+ - name: Try installing it with skip_broken
+ dnf:
+ name:
+ - broken-c
+ skip_broken: true
+ register: skip_broken_res
+
+ - name: Assert that nothing got installed
+ assert:
+ that:
+ - skip_broken_res.msg == 'Nothing to do'
+ - skip_broken_res.rc == 0
+ - skip_broken_res.results == []
+
+ - name: Remove all test packages
+ dnf:
+ name:
+ - broken-*
+ state: absent
+
+ # broken-d depends on (unversioned) broken-a.
+ # broken-a-2.0.0 has a broken dependency that doesn't exist.
+ # skip_broken should cause us to skip our explicit broken-a-2.0.0
+ # and bring in broken-a-1.2.4 as a dep of broken-d.
+ - name: Ensure proper failure with explicit broken version
+ dnf:
+ name:
+ - broken-a-2.0.0
+ - broken-d
+ ignore_errors: true
+ register: dnf_fail
+
+ - name: Assert that nothing got installed
+ assert:
+ that:
+ - dnf_fail is failed
+ - "'Depsolve Error' in dnf_fail.msg"
+
+ - name: skip_broken with explicit version
+ dnf:
+ name:
+ - broken-a-2.0.0
+ - broken-d
+ skip_broken: true
+ register: skip_broken_res
+
+ - name: Assert that the right things got installed
+ assert:
+ that:
+ - skip_broken_res.rc == 0
+ - skip_broken_res.results|length == 2
+ - res.results|select("contains", "Installed: broken-a-1.2.4")|length > 0
+ - res.results|select("contains", "Installed: broken-d-1.2.5")|length > 0
+
+ - name: Remove all test packages
+ dnf:
+ name:
+ - broken-*
+ state: absent
+
+ # Walk the logic of _mark_package_install() here
+ # We need to use a full-ish NVR/wildcard. _is_newer_version_installed()
+ # will be false otherwise, no matter what. This might be a bug.
+ # Relatedly, the real "Case 1" in the code seemingly can't be reached:
+ # _is_newer_version_installed wants NVR, _is_installed wants name.
+ # Both can't be true at the same time given one pkg_spec. Thus, we start
+ # at "Case 2"
+
+ # prereq
+ - name: Install broken-a-1.2.4
+ dnf:
+ name:
+ - broken-a-1.2.4
+ state: present
+
+ # Case 2: newer version is installed, allow_downgrade is true,
+ # is_installed is false since we gave full NVR.
+ # "upgrade" to broken-a-1.2.3, allow_downgrade=true
+ - name: Do an "upgrade" to an older version of broken-a, allow_downgrade=true
+ dnf:
+ name:
+ - broken-a-1.2.3-1*
+ state: latest
+ allow_downgrade: true
+ check_mode: true
+ register: res
+
+ - assert:
+ that:
+ - res is changed
+ - res.results|select("contains", "Installed: broken-a-1.2.3")|length > 0
+
+ # Still case 2, but with broken package to test skip_broken
+ # skip_broken: false
+ - name: Do an "upgrade" to an older known broken version of broken-a, allow_downgrade=true, skip_broken=false
+ dnf:
+ name:
+ - broken-a-1.2.3.4-1*
+ state: latest
+ allow_downgrade: true
+ check_mode: true
+ ignore_errors: true
+ register: res
+
+ - assert:
+ that:
+ # 1.2.3.4 has non-existent dep. Fail without skip_broken.
+ - res is failed
+ - "'Depsolve Error' in res.msg"
+
+ # skip_broken: true
+ - name: Do an "upgrade" to an older known broken version of broken-a, allow_downgrade=true, skip_broken=true
+ dnf:
+ name:
+ - broken-a-1.2.3.4-1*
+ state: latest
+ allow_downgrade: true
+ skip_broken: true
+ check_mode: true
+ register: res
+
+ - assert:
+ that:
+ - res is not changed
+ - res.rc == 0
+ - res.msg == "Nothing to do"
+
+ # Case 3: newer version installed, allow_downgrade=true, but
+ # upgrade=false (i.e., state: present or installed)
+ - name: Install an older version of broken-a than currently installed
+ dnf:
+ name:
+ - broken-a-1.2.3-1*
+ state: present
+ allow_downgrade: true
+ check_mode: true
+ register: res
+
+ - assert:
+ that:
+ - res is changed
+ - res.results|select("contains", "Installed: broken-a-1.2.3")|length > 0
+
+ # Case 3 still, with broken package and skip_broken tests like above.
+ - name: Install an older known broken version of broken-a, allow_downgrade=true, skip_broken=false
+ dnf:
+ name:
+ - broken-a-1.2.3.4-1*
+ state: present
+ allow_downgrade: true
+ check_mode: true
+ ignore_errors: true
+ register: res
+
+ - assert:
+ that:
+ # 1.2.3.4 has non-existent dep. Fail without skip_broken.
+ - res is failed
+ - "'Depsolve Error' in res.msg"
+
+ # skip_broken: true
+ - name: Install an older known broken version of broken-a, allow_downgrade=true, skip_broken=true
+ dnf:
+ name:
+ - broken-a-1.2.3.4-1*
+ state: present
+ allow_downgrade: true
+ skip_broken: true
+ check_mode: true
+ register: res
+
+ - assert:
+ that:
+ - res is not changed
+ - res.rc == 0
+ - res.msg == "Nothing to do"
+
+ # Case 4: "upgrade" to broken-a-1.2.3, allow_downgrade=false
+ # is_newer_version_installed is true, allow_downgrade is false
+ - name: Do an "upgrade" to an older version of broken-a, allow_downgrade=false
+ dnf:
+ name:
+ - broken-a-1.2.3-1*
+ state: latest
+ allow_downgrade: false
+ check_mode: true
+ register: res
+
+ - assert:
+ that:
+ - res is not changed
+ - res.rc == 0
+ - res.msg == "Nothing to do"
+
+ # skip_broken doesn't apply to case 5 or 6 (older version installed).
+ # base.upgrade doesn't allow a strict= kwarg. However, nobest works here.
+
+ # Case 5: older version of package is installed, we specify name, no version
+ # otherwise we'd land in an earlier case. At this point, 1.2.4 is installed.
+ # broken-a-2.0.0 is available as an update but has a broken dependency.
+ - name: Update broken-a without nobest=true
+ dnf:
+ name:
+ - broken-a
+ state: latest
+ ignore_errors: true
+ register: dnf_fail
+
+ - assert:
+ that:
+ - dnf_fail is failed
+ - "'Depsolve Error' in dnf_fail.msg"
+
+ # With nobest: true, we will be "successful" but not actually perform
+ # any upgrade. That is, we are content not having the "best"/latest
+ # version.
+ - name: Update broken-a with nobest=true
+ dnf:
+ name:
+ - broken-a
+ state: latest
+ nobest: true
+ register: nobest
+
+ - assert:
+ that:
+ - nobest.rc == 0
+ - nobest.results == []
+
+ # Case 6: Current or older version already installed (no version specified
+ # in our pkg_spec) and we're requesting present, not latest.
+ #
+ # This isn't really relevant to skip_broken or nobest, but let's test it
+ # anyway since we're already walking the logic of the method.
+ - name: Install broken-a (even though it is already installed)
+ dnf:
+ name:
+ - broken-a
+ state: present
+ register: res
+
+ - assert:
+ that:
+ - res is not changed
+
+ # Case 7 is already tested quite extensively above in the earlier tests.
+
+ always:
+ - name: Remove test yum repo
+ yum_repository:
+ name: skip-broken
+ state: absent
+
+ - name: Remove all test packages installed
+ yum:
+ name:
+ - broken-*
+ state: absent
diff --git a/test/integration/targets/dnf/tasks/test_sos_removal.yml b/test/integration/targets/dnf/tasks/test_sos_removal.yml
new file mode 100644
index 0000000..40ceb62
--- /dev/null
+++ b/test/integration/targets/dnf/tasks/test_sos_removal.yml
@@ -0,0 +1,19 @@
+# These are safe to just check in check_mode, because in the module, the
+# logic to match packages will happen anyway. check_mode will just prevent
+# the transaction from actually running once the matches are found.
+- name: Remove {{ item }}
+ dnf:
+ name: "{{ item }}"
+ state: absent
+ check_mode: true
+ register: sos_rm
+
+- debug:
+ var: sos_rm
+
+- assert:
+ that:
+ - sos_rm is successful
+ - sos_rm is changed
+ - "'Removed: sos-{{ sos_version }}-{{ sos_release }}' in sos_rm.results[0]"
+ - sos_rm.results|length == 1
diff --git a/test/integration/targets/dnf/vars/CentOS.yml b/test/integration/targets/dnf/vars/CentOS.yml
new file mode 100644
index 0000000..c70d853
--- /dev/null
+++ b/test/integration/targets/dnf/vars/CentOS.yml
@@ -0,0 +1,2 @@
+astream_name: '@php:7.2/minimal'
+astream_name_no_stream: '@php/minimal'
diff --git a/test/integration/targets/dnf/vars/Fedora.yml b/test/integration/targets/dnf/vars/Fedora.yml
new file mode 100644
index 0000000..fff6f4b
--- /dev/null
+++ b/test/integration/targets/dnf/vars/Fedora.yml
@@ -0,0 +1,6 @@
+astream_name: '@varnish:6.0/default'
+
+# For this to work, it needs to be that only shows once in `dnf module list`.
+# Such packages, that exist on all the versions we test on, are hard to come by.
+# TODO: This would be solved by using our own repo with modularity/streams.
+astream_name_no_stream: '@varnish/default'
diff --git a/test/integration/targets/dnf/vars/RedHat-9.yml b/test/integration/targets/dnf/vars/RedHat-9.yml
new file mode 100644
index 0000000..5681e70
--- /dev/null
+++ b/test/integration/targets/dnf/vars/RedHat-9.yml
@@ -0,0 +1,3 @@
+# RHEL9.0 contains no modules, to be re-introduced in 9.1
+# astream_name: '@container-tools:latest/common'
+# astream_name_no_stream: '@container-tools/common'
diff --git a/test/integration/targets/dnf/vars/RedHat.yml b/test/integration/targets/dnf/vars/RedHat.yml
new file mode 100644
index 0000000..c70d853
--- /dev/null
+++ b/test/integration/targets/dnf/vars/RedHat.yml
@@ -0,0 +1,2 @@
+astream_name: '@php:7.2/minimal'
+astream_name_no_stream: '@php/minimal'
diff --git a/test/integration/targets/dnf/vars/main.yml b/test/integration/targets/dnf/vars/main.yml
new file mode 100644
index 0000000..3f7b43a
--- /dev/null
+++ b/test/integration/targets/dnf/vars/main.yml
@@ -0,0 +1,6 @@
+dnf_log_files:
+ - /var/log/dnf.log
+ - /var/log/dnf.rpm.log
+ - /var/log/dnf.librepo.log
+
+skip_broken_repo_baseurl: "https://ansible-ci-files.s3.amazonaws.com/test/integration/targets/dnf/skip-broken/RPMS/"
diff --git a/test/integration/targets/dpkg_selections/aliases b/test/integration/targets/dpkg_selections/aliases
new file mode 100644
index 0000000..c0d5684
--- /dev/null
+++ b/test/integration/targets/dpkg_selections/aliases
@@ -0,0 +1,6 @@
+shippable/posix/group1
+destructive
+skip/freebsd
+skip/osx
+skip/macos
+skip/rhel
diff --git a/test/integration/targets/dpkg_selections/defaults/main.yaml b/test/integration/targets/dpkg_selections/defaults/main.yaml
new file mode 100644
index 0000000..94bd9bc
--- /dev/null
+++ b/test/integration/targets/dpkg_selections/defaults/main.yaml
@@ -0,0 +1 @@
+hello_old_version: 2.6-1
diff --git a/test/integration/targets/dpkg_selections/tasks/dpkg_selections.yaml b/test/integration/targets/dpkg_selections/tasks/dpkg_selections.yaml
new file mode 100644
index 0000000..080db26
--- /dev/null
+++ b/test/integration/targets/dpkg_selections/tasks/dpkg_selections.yaml
@@ -0,0 +1,89 @@
+- name: download and install old version of hello
+ apt: "deb=https://ci-files.testing.ansible.com/test/integration/targets/dpkg_selections/hello_{{ hello_old_version }}_amd64.deb"
+
+- name: freeze version for hello
+ dpkg_selections:
+ name: hello
+ selection: hold
+
+- name: get dpkg selections
+ shell: "dpkg --get-selections | grep hold"
+ register: result
+
+- debug: var=result
+
+- name: check that hello is marked as hold
+ assert:
+ that:
+ - "'hello' in result.stdout"
+
+- name: attempt to upgrade hello
+ apt:
+ name: hello
+ state: latest
+ ignore_errors: yes
+
+- name: check hello version
+ shell: dpkg -s hello | grep Version | awk '{print $2}'
+ register: hello_version
+
+- name: ensure hello was not upgraded
+ assert:
+ that:
+ - hello_version.stdout == hello_old_version
+
+- name: remove version freeze
+ dpkg_selections:
+ name: hello
+ selection: install
+
+- name: upgrade hello
+ apt:
+ name: hello
+ state: latest
+
+- name: check hello version
+ shell: dpkg -s hello | grep Version | awk '{print $2}'
+ register: hello_version
+
+- name: check that old version upgraded correctly
+ assert:
+ that:
+ - hello_version.stdout != hello_old_version
+
+- name: set hello to deinstall
+ dpkg_selections:
+ name: hello
+ selection: deinstall
+
+- name: get dpkg selections
+ shell: "dpkg --get-selections | grep deinstall"
+ register: result
+
+- debug: var=result
+
+- name: check that hello is marked as deinstall
+ assert:
+ that:
+ - "'hello' in result.stdout"
+
+- name: set hello to purge
+ dpkg_selections:
+ name: hello
+ selection: purge
+
+- name: get dpkg selections
+ shell: "dpkg --get-selections | grep purge"
+ register: result
+
+- debug: var=result
+
+- name: check that hello is marked as purge
+ assert:
+ that:
+ - "'hello' in result.stdout"
+
+- name: remove hello
+ apt:
+ name: hello
+ state: absent
diff --git a/test/integration/targets/dpkg_selections/tasks/main.yaml b/test/integration/targets/dpkg_selections/tasks/main.yaml
new file mode 100644
index 0000000..abf9fa1
--- /dev/null
+++ b/test/integration/targets/dpkg_selections/tasks/main.yaml
@@ -0,0 +1,3 @@
+---
+ - include_tasks: file='dpkg_selections.yaml'
+ when: ansible_distribution in ('Ubuntu', 'Debian')
diff --git a/test/integration/targets/egg-info/aliases b/test/integration/targets/egg-info/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/egg-info/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/egg-info/lookup_plugins/import_pkg_resources.py b/test/integration/targets/egg-info/lookup_plugins/import_pkg_resources.py
new file mode 100644
index 0000000..c0c5ccd
--- /dev/null
+++ b/test/integration/targets/egg-info/lookup_plugins/import_pkg_resources.py
@@ -0,0 +1,11 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import pkg_resources
+
+from ansible.plugins.lookup import LookupBase
+
+
+class LookupModule(LookupBase):
+ def run(self, terms, variables, **kwargs):
+ return ['ok']
diff --git a/test/integration/targets/egg-info/tasks/main.yml b/test/integration/targets/egg-info/tasks/main.yml
new file mode 100644
index 0000000..d7b886c
--- /dev/null
+++ b/test/integration/targets/egg-info/tasks/main.yml
@@ -0,0 +1,3 @@
+- name: Make sure pkg_resources can be imported by plugins
+ debug:
+ msg: "{{ lookup('import_pkg_resources') }}"
diff --git a/test/integration/targets/embedded_module/aliases b/test/integration/targets/embedded_module/aliases
new file mode 100644
index 0000000..6452e6d
--- /dev/null
+++ b/test/integration/targets/embedded_module/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group2
+context/target
diff --git a/test/integration/targets/embedded_module/library/test_integration_module b/test/integration/targets/embedded_module/library/test_integration_module
new file mode 100644
index 0000000..04755b8
--- /dev/null
+++ b/test/integration/targets/embedded_module/library/test_integration_module
@@ -0,0 +1,3 @@
+#!/usr/bin/python
+
+print('{"changed":false, "msg":"this is the embedded module"}')
diff --git a/test/integration/targets/embedded_module/tasks/main.yml b/test/integration/targets/embedded_module/tasks/main.yml
new file mode 100644
index 0000000..6a6d648
--- /dev/null
+++ b/test/integration/targets/embedded_module/tasks/main.yml
@@ -0,0 +1,9 @@
+- name: run the embedded dummy module
+ test_integration_module:
+ register: result
+
+- name: assert the embedded module ran
+ assert:
+ that:
+ - "'msg' in result"
+ - result.msg == "this is the embedded module"
diff --git a/test/integration/targets/entry_points/aliases b/test/integration/targets/entry_points/aliases
new file mode 100644
index 0000000..9d96756
--- /dev/null
+++ b/test/integration/targets/entry_points/aliases
@@ -0,0 +1,2 @@
+context/controller
+shippable/posix/group4
diff --git a/test/integration/targets/entry_points/runme.sh b/test/integration/targets/entry_points/runme.sh
new file mode 100755
index 0000000..cabf153
--- /dev/null
+++ b/test/integration/targets/entry_points/runme.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+
+set -eu -o pipefail
+source virtualenv.sh
+set +x
+unset PYTHONPATH
+export SETUPTOOLS_USE_DISTUTILS=stdlib
+
+base_dir="$(dirname "$(dirname "$(dirname "$(dirname "${OUTPUT_DIR}")")")")"
+bin_dir="$(dirname "$(command -v pip)")"
+
+# deps are already installed, using --no-deps to avoid re-installing them
+pip install "${base_dir}" --disable-pip-version-check --no-deps
+# --use-feature=in-tree-build not available on all platforms
+
+for bin in "${bin_dir}/ansible"*; do
+ name="$(basename "${bin}")"
+
+ entry_point="${name//ansible-/}"
+ entry_point="${entry_point//ansible/adhoc}"
+
+ echo "=== ${name} (${entry_point})=${bin} ==="
+
+ if [ "${name}" == "ansible-test" ]; then
+ "${bin}" --help | tee /dev/stderr | grep -Eo "^usage:\ ansible-test\ .*"
+ python -m ansible "${entry_point}" --help | tee /dev/stderr | grep -Eo "^usage:\ ansible-test\ .*"
+ else
+ "${bin}" --version | tee /dev/stderr | grep -Eo "(^${name}\ \[core\ .*|executable location = ${bin}$)"
+ python -m ansible "${entry_point}" --version | tee /dev/stderr | grep -Eo "(^${name}\ \[core\ .*|executable location = ${bin}$)"
+ fi
+done
diff --git a/test/integration/targets/environment/aliases b/test/integration/targets/environment/aliases
new file mode 100644
index 0000000..6452e6d
--- /dev/null
+++ b/test/integration/targets/environment/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group2
+context/target
diff --git a/test/integration/targets/environment/runme.sh b/test/integration/targets/environment/runme.sh
new file mode 100755
index 0000000..c556a17
--- /dev/null
+++ b/test/integration/targets/environment/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook test_environment.yml -i ../../inventory "$@"
diff --git a/test/integration/targets/environment/test_environment.yml b/test/integration/targets/environment/test_environment.yml
new file mode 100644
index 0000000..43f9c74
--- /dev/null
+++ b/test/integration/targets/environment/test_environment.yml
@@ -0,0 +1,173 @@
+- hosts: testhost
+ gather_facts: no
+ tasks:
+ - name: get PATH from target
+ command: echo $PATH
+ register: target_path
+
+- hosts: testhost
+ vars:
+ - test1:
+ key1: val1
+ environment:
+ PATH: '{{ansible_env.PATH + ":/lola"}}'
+ lola: 'ido'
+ tasks:
+ - name: ensure special case with ansible_env is skipped but others still work
+ assert:
+ that:
+ - target_path.stdout == ansible_env.PATH
+ - "'/lola' not in ansible_env.PATH"
+ - ansible_env.lola == 'ido'
+
+ - name: check that envvar does not exist
+ shell: echo $key1
+ register: test_env
+
+ - name: assert no val in stdout
+ assert:
+ that:
+ - '"val1" not in test_env.stdout_lines'
+
+ - name: check that envvar does exist
+ shell: echo $key1
+ environment: "{{test1}}"
+ register: test_env2
+
+ - name: assert val1 in stdout
+ assert:
+ that:
+ - '"val1" in test_env2.stdout_lines'
+
+- hosts: testhost
+ vars:
+ - test1:
+ key1: val1
+ - test2:
+ key1: not1
+ other1: val2
+ environment: "{{test1}}"
+ tasks:
+ - name: check that play envvar does exist
+ shell: echo $key1
+ register: test_env3
+
+ - name: assert val1 in stdout
+ assert:
+ that:
+ - '"val1" in test_env3.stdout_lines'
+
+ - name: check that task envvar does exist
+ shell: echo $key1; echo $other1
+ register: test_env4
+ environment: "{{test2}}"
+
+ - name: assert all vars appear as expected
+ assert:
+ that:
+ - '"val1" not in test_env4.stdout_lines'
+ - '"not1" in test_env4.stdout_lines'
+ - '"val2" in test_env4.stdout_lines'
+
+ - block:
+ - name: check that task envvar does exist in block
+ shell: echo $key1; echo $other1
+ register: test_env5
+
+ - name: assert all vars appear as expected in block
+ assert:
+ that:
+ - '"val1" not in test_env5.stdout_lines'
+ - '"not1" in test_env5.stdout_lines'
+ - '"val2" in test_env5.stdout_lines'
+ environment: "{{test2}}"
+
+- name: test setting environment while using loops
+ hosts: testhost
+ environment:
+ foo: outer
+ tasks:
+ - name: verify foo==outer
+ command: /bin/echo $foo
+ loop:
+ - 1
+ register: test_foo
+
+ - name: assert foo==outer
+ assert:
+ that:
+ - "{{ test_foo.results[0].stdout == 'outer' }}"
+
+ - name: set environment on a task
+ environment:
+ foo: in_task
+ command: /bin/echo $foo
+ loop:
+ - 1
+ register: test_foo
+
+ - name: assert foo==in_task
+ assert:
+ that:
+ - "test_foo.results[0].stdout == 'in_task'"
+
+ - name: test that the outer env var is set appropriately still
+ command: /bin/echo $foo
+ loop:
+ - 1
+ register: test_foo
+
+ - name: assert foo==outer
+ assert:
+ that:
+ - "{{ test_foo.results[0].stdout == 'outer' }}"
+
+ - name: set environment on a block
+ environment:
+ foo: in_block
+ block:
+ - name: test the environment is set in the block
+ command: /bin/echo $foo
+ loop:
+ - 1
+ register: test_foo
+
+ - name: assert foo==in_block
+ assert:
+ that:
+ - "test_foo.results[0].stdout == 'in_block'"
+
+ - name: test setting environment in a task inside a block
+ environment:
+ foo: in_block_in_task
+ command: /bin/echo $foo
+ loop:
+ - 1
+ register: test_foo
+
+ - name: assert foo==in_block_in_task
+ assert:
+ that:
+ - "test_foo.results[0].stdout == 'in_block_in_task'"
+
+ - name: test the environment var is set to the parent value
+ command: /bin/echo $foo
+ loop:
+ - 1
+ register: test_foo
+
+ - name: assert foo==in_block
+ assert:
+ that:
+ - "test_foo.results[0].stdout == 'in_block'"
+
+ - name: test the env var foo has the initial value
+ command: /bin/echo $foo
+ loop:
+ - 1
+ register: test_foo
+
+ - name: assert foo==outer
+ assert:
+ that:
+ - "{{ test_foo.results[0].stdout == 'outer' }}"
diff --git a/test/integration/targets/error_from_connection/aliases b/test/integration/targets/error_from_connection/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/error_from_connection/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/error_from_connection/connection_plugins/dummy.py b/test/integration/targets/error_from_connection/connection_plugins/dummy.py
new file mode 100644
index 0000000..59a81a1
--- /dev/null
+++ b/test/integration/targets/error_from_connection/connection_plugins/dummy.py
@@ -0,0 +1,42 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = """
+ author:
+ - John Doe
+ connection: dummy
+ short_description: defective connection plugin
+ description:
+ - defective connection plugin
+ version_added: "2.0"
+ options: {}
+"""
+import ansible.constants as C
+from ansible.errors import AnsibleError
+from ansible.plugins.connection import ConnectionBase
+
+
+class Connection(ConnectionBase):
+
+ transport = 'dummy'
+ has_pipelining = True
+
+ def __init__(self, play_context, new_stdin, *args, **kwargs):
+ super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+ raise AnsibleError('an error with {{ some Jinja }}')
+
+ def _connect(self):
+ pass
+
+ def exec_command(self, cmd, in_data=None, sudoable=True):
+ pass
+
+ def put_file(self, in_path, out_path):
+ pass
+
+ def fetch_file(self, in_path, out_path):
+ pass
+
+ def close(self):
+ pass
diff --git a/test/integration/targets/error_from_connection/inventory b/test/integration/targets/error_from_connection/inventory
new file mode 100644
index 0000000..324f0d3
--- /dev/null
+++ b/test/integration/targets/error_from_connection/inventory
@@ -0,0 +1,2 @@
+[local]
+testhost
diff --git a/test/integration/targets/error_from_connection/play.yml b/test/integration/targets/error_from_connection/play.yml
new file mode 100644
index 0000000..04320d8
--- /dev/null
+++ b/test/integration/targets/error_from_connection/play.yml
@@ -0,0 +1,20 @@
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - name: "use a connection plugin raising an exception, exception message contains Jinja template."
+ connection: dummy
+ command: /bin/true # command won't be executed
+ register: result
+ ignore_errors: True
+
+ - name: "check that Jinja template embedded in exception message isn't rendered"
+ debug:
+ msg: 'ok'
+ when: result is failed
+ register: debug_task
+
+ - assert:
+ that:
+ - result is failed
+ - "'an error with' in result.msg" # makes sure plugin was found
+ - debug_task is success
diff --git a/test/integration/targets/error_from_connection/runme.sh b/test/integration/targets/error_from_connection/runme.sh
new file mode 100755
index 0000000..92679fd
--- /dev/null
+++ b/test/integration/targets/error_from_connection/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -o nounset -o errexit -o xtrace
+
+ansible-playbook -i inventory "play.yml" -v "$@"
diff --git a/test/integration/targets/expect/aliases b/test/integration/targets/expect/aliases
new file mode 100644
index 0000000..7211b8d
--- /dev/null
+++ b/test/integration/targets/expect/aliases
@@ -0,0 +1,3 @@
+shippable/posix/group2
+destructive
+needs/target/setup_pexpect
diff --git a/test/integration/targets/expect/files/foo.txt b/test/integration/targets/expect/files/foo.txt
new file mode 100644
index 0000000..7c6ded1
--- /dev/null
+++ b/test/integration/targets/expect/files/foo.txt
@@ -0,0 +1 @@
+foo.txt
diff --git a/test/integration/targets/expect/files/test_command.py b/test/integration/targets/expect/files/test_command.py
new file mode 100644
index 0000000..0e0e264
--- /dev/null
+++ b/test/integration/targets/expect/files/test_command.py
@@ -0,0 +1,25 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import sys
+
+try:
+ input_function = raw_input
+except NameError:
+ input_function = input
+
+prompts = sys.argv[1:] or ['foo']
+
+# latin1 encoded bytes
+# to ensure pexpect doesn't have any encoding errors
+data = b'premi\xe8re is first\npremie?re is slightly different\n????????? is Cyrillic\n? am Deseret\n'
+
+try:
+ sys.stdout.buffer.write(data)
+except AttributeError:
+ sys.stdout.write(data)
+print()
+
+for prompt in prompts:
+ user_input = input_function(prompt)
+ print(user_input)
diff --git a/test/integration/targets/expect/meta/main.yml b/test/integration/targets/expect/meta/main.yml
new file mode 100644
index 0000000..1810d4b
--- /dev/null
+++ b/test/integration/targets/expect/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/expect/tasks/main.yml b/test/integration/targets/expect/tasks/main.yml
new file mode 100644
index 0000000..d6f43f2
--- /dev/null
+++ b/test/integration/targets/expect/tasks/main.yml
@@ -0,0 +1,209 @@
+# test code for the ping module
+# (c) 2014, James Cammarata <jcammarata@ansible.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+- name: Install test requirements
+ import_role:
+ name: setup_pexpect
+
+- name: record the test_command file
+ set_fact: test_command_file={{remote_tmp_dir | expanduser}}/test_command.py
+
+- name: copy script into output directory
+ copy: src=test_command.py dest={{test_command_file}} mode=0444
+
+- name: record the output file
+ set_fact: output_file={{remote_tmp_dir}}/foo.txt
+
+- copy:
+ content: "foo"
+ dest: "{{output_file}}"
+
+- name: test expect
+ expect:
+ command: "{{ansible_python_interpreter}} {{test_command_file}}"
+ responses:
+ foo: bar
+ register: expect_result
+
+- name: assert expect worked
+ assert:
+ that:
+ - "expect_result.changed == true"
+ - "expect_result.stdout_lines|last == 'foobar'"
+
+- name: test creates option
+ expect:
+ command: "{{ansible_python_interpreter}} {{test_command_file}}"
+ responses:
+ foo: bar
+ creates: "{{output_file}}"
+ register: creates_result
+
+- name: assert when creates is provided command is not run
+ assert:
+ that:
+ - "creates_result.changed == false"
+ - "'skipped' in creates_result.stdout"
+
+- name: test creates option (missing)
+ expect:
+ command: "{{ansible_python_interpreter}} {{test_command_file}}"
+ responses:
+ foo: bar
+ creates: "{{output_file}}.does.not.exist"
+ register: creates_result
+
+- name: assert when missing creates is provided command is run
+ assert:
+ that:
+ - "creates_result.changed == true"
+ - "creates_result.stdout_lines|last == 'foobar'"
+
+- name: test removes option
+ expect:
+ command: "{{ansible_python_interpreter}} {{test_command_file}}"
+ responses:
+ foo: bar
+ removes: "{{output_file}}"
+ register: removes_result
+
+- name: assert when removes is provided command is run
+ assert:
+ that:
+ - "removes_result.changed == true"
+ - "removes_result.stdout_lines|last == 'foobar'"
+
+- name: test removes option (missing)
+ expect:
+ command: "{{ansible_python_interpreter}} {{test_command_file}}"
+ responses:
+ foo: bar
+ removes: "{{output_file}}.does.not.exist"
+ register: removes_result
+
+- name: assert when missing removes is provided command is not run
+ assert:
+ that:
+ - "removes_result.changed == false"
+ - "'skipped' in removes_result.stdout"
+
+- name: test chdir
+ expect:
+ command: "/bin/sh -c 'pwd && sleep 1'"
+ chdir: "{{remote_tmp_dir}}"
+ responses:
+ foo: bar
+ register: chdir_result
+
+- name: get remote_tmp_dir real path
+ raw: >
+ {{ ansible_python_interpreter }} -c 'import os; os.chdir("{{remote_tmp_dir}}"); print(os.getcwd())'
+ register: remote_tmp_dir_real_path
+
+- name: assert chdir works
+ assert:
+ that:
+ - "'{{chdir_result.stdout | trim}}' == '{{remote_tmp_dir_real_path.stdout | trim}}'"
+
+- name: test timeout option
+ expect:
+ command: "sleep 10"
+ responses:
+ foo: bar
+ timeout: 1
+ ignore_errors: true
+ register: timeout_result
+
+- name: assert failure message when timeout
+ assert:
+ that:
+ - "timeout_result.msg == 'command exceeded timeout'"
+
+- name: test echo option
+ expect:
+ command: "{{ansible_python_interpreter}} {{test_command_file}}"
+ responses:
+ foo: bar
+ echo: true
+ register: echo_result
+
+- name: assert echo works
+ assert:
+ that:
+ - "echo_result.stdout_lines|length == 7"
+ - "echo_result.stdout_lines[-2] == 'foobar'"
+ - "echo_result.stdout_lines[-1] == 'bar'"
+
+- name: test response list
+ expect:
+ command: "{{ansible_python_interpreter}} {{test_command_file}} foo foo"
+ responses:
+ foo:
+ - bar
+ - baz
+ register: list_result
+
+- name: assert list response works
+ assert:
+ that:
+ - "list_result.stdout_lines|length == 7"
+ - "list_result.stdout_lines[-2] == 'foobar'"
+ - "list_result.stdout_lines[-1] == 'foobaz'"
+
+- name: test no remaining responses
+ expect:
+ command: "{{ansible_python_interpreter}} {{test_command_file}} foo foo"
+ responses:
+ foo:
+ - bar
+ register: list_result
+ ignore_errors: yes
+
+- name: assert no remaining responses
+ assert:
+ that:
+ - "list_result.failed"
+ - "'No remaining responses' in list_result.msg"
+
+- name: test no command
+ expect:
+ command: ""
+ responses:
+ foo: bar
+ register: no_command_result
+ ignore_errors: yes
+
+- name: assert no command
+ assert:
+ that:
+ - "no_command_result.failed"
+ - "no_command_result.msg == 'no command given'"
+ - "no_command_result.rc == 256"
+
+- name: test non-zero return code
+ expect:
+ command: "ls /does-not-exist"
+ responses:
+ foo: bar
+ register: non_zero_result
+ ignore_errors: yes
+
+- name: assert non-zero return code
+ assert:
+ that:
+ - "non_zero_result.failed"
+ - "non_zero_result.msg == 'non-zero return code'"
diff --git a/test/integration/targets/facts_d/aliases b/test/integration/targets/facts_d/aliases
new file mode 100644
index 0000000..6452e6d
--- /dev/null
+++ b/test/integration/targets/facts_d/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group2
+context/target
diff --git a/test/integration/targets/facts_d/files/basdscript.fact b/test/integration/targets/facts_d/files/basdscript.fact
new file mode 100644
index 0000000..2bb8d86
--- /dev/null
+++ b/test/integration/targets/facts_d/files/basdscript.fact
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+exit 1
diff --git a/test/integration/targets/facts_d/files/goodscript.fact b/test/integration/targets/facts_d/files/goodscript.fact
new file mode 100644
index 0000000..6ee866c
--- /dev/null
+++ b/test/integration/targets/facts_d/files/goodscript.fact
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+echo '{"script_ran": true}'
diff --git a/test/integration/targets/facts_d/files/preferences.fact b/test/integration/targets/facts_d/files/preferences.fact
new file mode 100644
index 0000000..c32583d
--- /dev/null
+++ b/test/integration/targets/facts_d/files/preferences.fact
@@ -0,0 +1,2 @@
+[general]
+bar=loaded
diff --git a/test/integration/targets/facts_d/files/unreadable.fact b/test/integration/targets/facts_d/files/unreadable.fact
new file mode 100644
index 0000000..98f562b
--- /dev/null
+++ b/test/integration/targets/facts_d/files/unreadable.fact
@@ -0,0 +1 @@
+wontbeseen=ever
diff --git a/test/integration/targets/facts_d/meta/main.yml b/test/integration/targets/facts_d/meta/main.yml
new file mode 100644
index 0000000..cb6005d
--- /dev/null
+++ b/test/integration/targets/facts_d/meta/main.yml
@@ -0,0 +1,3 @@
+dependencies:
+ - prepare_tests
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/facts_d/tasks/main.yml b/test/integration/targets/facts_d/tasks/main.yml
new file mode 100644
index 0000000..f2cdf34
--- /dev/null
+++ b/test/integration/targets/facts_d/tasks/main.yml
@@ -0,0 +1,53 @@
+# (c) 2014, James Tanner <tanner.jc@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+- name: prep for local facts tests
+ block:
+ - name: set factdir var
+ set_fact: fact_dir={{remote_tmp_dir}}/facts.d
+
+ - name: create fact dir
+ file: path={{ fact_dir }} state=directory
+
+ - name: copy local facts test files
+ copy: src={{ item['name'] }}.fact dest={{ fact_dir }}/ mode={{ item['mode']|default(omit) }}
+ loop:
+ - name: preferences
+ - name: basdscript
+ mode: '0775'
+ - name: goodscript
+ mode: '0775'
+ - name: unreadable
+ mode: '0000'
+
+ - name: Create dangling symlink
+ file:
+ path: "{{ fact_dir }}/dead_symlink.fact"
+ src: /tmp/dead_symlink
+ force: yes
+ state: link
+
+- name: force fact gather to get ansible_local
+ setup:
+ fact_path: "{{ fact_dir | expanduser }}"
+ filter: "*local*"
+ register: setup_result
+
+- name: show gathering results if rerun with -vvv
+ debug: var=setup_result verbosity=3
+
+- name: check for expected results from local facts
+ assert:
+ that:
+ - "'ansible_facts' in setup_result"
+ - "'ansible_local' in setup_result.ansible_facts"
+ - "'ansible_env' not in setup_result.ansible_facts"
+ - "'ansible_user_id' not in setup_result.ansible_facts"
+ - "'preferences' in setup_result.ansible_facts['ansible_local']"
+ - "'general' in setup_result.ansible_facts['ansible_local']['preferences']"
+ - "'bar' in setup_result.ansible_facts['ansible_local']['preferences']['general']"
+ - "setup_result.ansible_facts['ansible_local']['preferences']['general']['bar'] == 'loaded'"
+ - setup_result['ansible_facts']['ansible_local']['goodscript']['script_ran']|bool
+ - setup_result['ansible_facts']['ansible_local']['basdscript'].startswith("Failure executing fact script")
+ - setup_result['ansible_facts']['ansible_local']['unreadable'].startswith('error loading facts')
+ - setup_result['ansible_facts']['ansible_local']['dead_symlink'].startswith('Could not stat fact')
diff --git a/test/integration/targets/facts_linux_network/aliases b/test/integration/targets/facts_linux_network/aliases
new file mode 100644
index 0000000..100ce23
--- /dev/null
+++ b/test/integration/targets/facts_linux_network/aliases
@@ -0,0 +1,7 @@
+needs/privileged
+shippable/posix/group1
+skip/freebsd
+skip/osx
+skip/macos
+context/target
+destructive
diff --git a/test/integration/targets/facts_linux_network/meta/main.yml b/test/integration/targets/facts_linux_network/meta/main.yml
new file mode 100644
index 0000000..07faa21
--- /dev/null
+++ b/test/integration/targets/facts_linux_network/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - prepare_tests
diff --git a/test/integration/targets/facts_linux_network/tasks/main.yml b/test/integration/targets/facts_linux_network/tasks/main.yml
new file mode 100644
index 0000000..6efaf50
--- /dev/null
+++ b/test/integration/targets/facts_linux_network/tasks/main.yml
@@ -0,0 +1,51 @@
+- block:
+ - name: Add IP to interface
+ command: ip address add 100.42.42.1/32 dev {{ ansible_facts.default_ipv4.interface }}
+ ignore_errors: yes
+
+ - name: Gather network facts
+ setup:
+ gather_subset: network
+
+ - name: Ensure broadcast is reported as empty
+ assert:
+ that:
+ - ansible_facts[ansible_facts['default_ipv4']['interface']]['ipv4_secondaries'][0]['broadcast'] == ''
+
+ always:
+ - name: Remove IP from interface
+ command: ip address delete 100.42.42.1/32 dev {{ ansible_facts.default_ipv4.interface }}
+ ignore_errors: yes
+
+- block:
+ - name: Add bridge device
+ command: ip link add name br1337 type bridge stp_state 1
+
+ - name: Add virtual interface
+ command: ip link add name veth1337 type veth
+
+ - name: Add virtual interface to bridge
+ command: ip link set veth1337 master br1337
+
+ - name: Gather network facts
+ setup:
+ gather_subset: network
+
+ - debug:
+ var: ansible_facts.br1337
+
+ - assert:
+ that:
+ - ansible_facts.br1337.type == 'bridge'
+ - ansible_facts.br1337.id != ''
+ - ansible_facts.br1337.stp is true
+ - ansible_facts.br1337.interfaces|first == 'veth1337'
+
+ always:
+ - name: Remove virtual interface
+ command: ip link delete veth1337 type veth
+ ignore_errors: yes
+
+ - name: Remove bridge device
+ command: ip link delete br1337 type bridge
+ ignore_errors: yes
diff --git a/test/integration/targets/failed_when/aliases b/test/integration/targets/failed_when/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/failed_when/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/failed_when/tasks/main.yml b/test/integration/targets/failed_when/tasks/main.yml
new file mode 100644
index 0000000..1b10bef
--- /dev/null
+++ b/test/integration/targets/failed_when/tasks/main.yml
@@ -0,0 +1,80 @@
+# Test code for failed_when.
+# (c) 2014, Richard Isaacson <richard.c.isaacson@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: command rc 0 failed_when_result undef
+ shell: exit 0
+ ignore_errors: True
+ register: result
+
+- assert:
+ that:
+ - "'failed' in result and not result.failed"
+
+- name: command rc 0 failed_when_result False
+ shell: exit 0
+ failed_when: false
+ ignore_errors: true
+ register: result
+
+- assert:
+ that:
+ - "'failed' in result and not result.failed"
+ - "'failed_when_result' in result and not result.failed_when_result"
+
+- name: command rc 1 failed_when_result True
+ shell: exit 1
+ failed_when: true
+ ignore_errors: true
+ register: result
+
+- assert:
+ that:
+ - "'failed' in result and result.failed"
+ - "'failed_when_result' in result and result.failed_when_result"
+
+- name: command rc 1 failed_when_result undef
+ shell: exit 1
+ ignore_errors: true
+ register: result
+
+- assert:
+ that:
+ - "'failed' in result and result.failed"
+
+- name: command rc 1 failed_when_result False
+ shell: exit 1
+ failed_when: false
+ ignore_errors: true
+ register: result
+
+- assert:
+ that:
+ - "'failed' in result and not result.failed"
+ - "'failed_when_result' in result and not result.failed_when_result"
+
+- name: invalid conditional
+ command: echo foo
+ failed_when: boomboomboom
+ register: invalid_conditional
+ ignore_errors: true
+
+- assert:
+ that:
+ - invalid_conditional is failed
+ - invalid_conditional.stdout is defined
+ - invalid_conditional.failed_when_result is contains('boomboomboom')
diff --git a/test/integration/targets/fetch/aliases b/test/integration/targets/fetch/aliases
new file mode 100644
index 0000000..ff56593
--- /dev/null
+++ b/test/integration/targets/fetch/aliases
@@ -0,0 +1,3 @@
+shippable/posix/group2
+needs/target/setup_remote_tmp_dir
+needs/ssh
diff --git a/test/integration/targets/fetch/cleanup.yml b/test/integration/targets/fetch/cleanup.yml
new file mode 100644
index 0000000..792b603
--- /dev/null
+++ b/test/integration/targets/fetch/cleanup.yml
@@ -0,0 +1,16 @@
+- name: Cleanup user account
+ hosts: testhost
+
+ tasks:
+ - name: remove test user
+ user:
+ name: fetcher
+ state: absent
+ remove: yes
+ force: yes
+
+ - name: delete temporary directory
+ file:
+ path: "{{ remote_tmp_dir }}"
+ state: absent
+ no_log: yes
diff --git a/test/integration/targets/fetch/injection/avoid_slurp_return.yml b/test/integration/targets/fetch/injection/avoid_slurp_return.yml
new file mode 100644
index 0000000..af62dcf
--- /dev/null
+++ b/test/integration/targets/fetch/injection/avoid_slurp_return.yml
@@ -0,0 +1,26 @@
+- name: ensure that 'fake slurp' does not poison fetch source
+ hosts: localhost
+ gather_facts: False
+ tasks:
+ - name: fetch with relative source path
+ fetch: src=../injection/here.txt dest={{output_dir}}
+ become: true
+ register: islurp
+
+ - name: fetch with normal source path
+ fetch: src=here.txt dest={{output_dir}}
+ become: true
+ register: islurp2
+
+ - name: ensure all is good in hollywood
+ assert:
+ that:
+ - "'..' not in islurp['dest']"
+ - "'..' not in islurp2['dest']"
+ - "'foo' not in islurp['dest']"
+ - "'foo' not in islurp2['dest']"
+
+ - name: try to trip dest anyways
+ fetch: src=../injection/here.txt dest={{output_dir}}
+ become: true
+ register: islurp2
diff --git a/test/integration/targets/fetch/injection/here.txt b/test/integration/targets/fetch/injection/here.txt
new file mode 100644
index 0000000..493021b
--- /dev/null
+++ b/test/integration/targets/fetch/injection/here.txt
@@ -0,0 +1 @@
+this is a test file
diff --git a/test/integration/targets/fetch/injection/library/slurp.py b/test/integration/targets/fetch/injection/library/slurp.py
new file mode 100644
index 0000000..7b78ba1
--- /dev/null
+++ b/test/integration/targets/fetch/injection/library/slurp.py
@@ -0,0 +1,29 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+ module: fakeslurp
+ short_desciptoin: fake slurp module
+ description:
+ - this is a fake slurp module
+ options:
+ _notreal:
+ description: really not a real slurp
+ author:
+ - me
+"""
+
+import json
+import random
+
+bad_responses = ['../foo', '../../foo', '../../../foo', '/../../../foo', '/../foo', '//..//foo', '..//..//foo']
+
+
+def main():
+ print(json.dumps(dict(changed=False, content='', encoding='base64', source=random.choice(bad_responses))))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/fetch/roles/fetch_tests/defaults/main.yml b/test/integration/targets/fetch/roles/fetch_tests/defaults/main.yml
new file mode 100644
index 0000000..f0b9cfc
--- /dev/null
+++ b/test/integration/targets/fetch/roles/fetch_tests/defaults/main.yml
@@ -0,0 +1 @@
+skip_cleanup: no
diff --git a/test/integration/targets/fetch/roles/fetch_tests/handlers/main.yml b/test/integration/targets/fetch/roles/fetch_tests/handlers/main.yml
new file mode 100644
index 0000000..c6c296a
--- /dev/null
+++ b/test/integration/targets/fetch/roles/fetch_tests/handlers/main.yml
@@ -0,0 +1,8 @@
+- name: remove test user
+ user:
+ name: fetcher
+ state: absent
+ remove: yes
+ force: yes
+ become: yes
+ when: not skip_cleanup | bool
diff --git a/test/integration/targets/fetch/roles/fetch_tests/meta/main.yml b/test/integration/targets/fetch/roles/fetch_tests/meta/main.yml
new file mode 100644
index 0000000..1810d4b
--- /dev/null
+++ b/test/integration/targets/fetch/roles/fetch_tests/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/fetch/roles/fetch_tests/tasks/fail_on_missing.yml b/test/integration/targets/fetch/roles/fetch_tests/tasks/fail_on_missing.yml
new file mode 100644
index 0000000..d918aae
--- /dev/null
+++ b/test/integration/targets/fetch/roles/fetch_tests/tasks/fail_on_missing.yml
@@ -0,0 +1,53 @@
+- name: Attempt to fetch a non-existent file - do not fail on missing
+ fetch:
+ src: "{{ remote_tmp_dir }}/doesnotexist"
+ dest: "{{ output_dir }}/fetched"
+ fail_on_missing: no
+ register: fetch_missing_nofail
+
+- name: Attempt to fetch a non-existent file - fail on missing
+ fetch:
+ src: "{{ remote_tmp_dir }}/doesnotexist"
+ dest: "{{ output_dir }}/fetched"
+ fail_on_missing: yes
+ register: fetch_missing
+ ignore_errors: yes
+
+- name: Attempt to fetch a non-existent file - fail on missing implicit
+ fetch:
+ src: "{{ remote_tmp_dir }}/doesnotexist"
+ dest: "{{ output_dir }}/fetched"
+ register: fetch_missing_implicit
+ ignore_errors: yes
+
+- name: Attempt to fetch a directory - should not fail but return a message
+ fetch:
+ src: "{{ remote_tmp_dir }}"
+ dest: "{{ output_dir }}/somedir"
+ fail_on_missing: no
+ register: fetch_dir
+
+- name: Attempt to fetch a directory - should fail
+ fetch:
+ src: "{{ remote_tmp_dir }}"
+ dest: "{{ output_dir }}/somedir"
+ fail_on_missing: yes
+ register: failed_fetch_dir
+ ignore_errors: yes
+
+- name: Check fetch missing with failure with implicit fail
+ assert:
+ that:
+ - fetch_missing_nofail.msg is search('ignored')
+ - fetch_missing_nofail is not changed
+ - fetch_missing is failed
+ - fetch_missing is not changed
+ - fetch_missing.msg is search ('remote file does not exist')
+ - fetch_missing_implicit is failed
+ - fetch_missing_implicit is not changed
+ - fetch_missing_implicit.msg is search ('remote file does not exist')
+ - fetch_dir is not changed
+ - fetch_dir.msg is search('is a directory')
+ - failed_fetch_dir is failed
+ - failed_fetch_dir is not changed
+ - failed_fetch_dir.msg is search('is a directory')
diff --git a/test/integration/targets/fetch/roles/fetch_tests/tasks/failures.yml b/test/integration/targets/fetch/roles/fetch_tests/tasks/failures.yml
new file mode 100644
index 0000000..8a6b5b7
--- /dev/null
+++ b/test/integration/targets/fetch/roles/fetch_tests/tasks/failures.yml
@@ -0,0 +1,41 @@
+- name: Fetch with no parameters
+ fetch:
+ register: fetch_no_params
+ ignore_errors: yes
+
+- name: Fetch with incorrect source type
+ fetch:
+ src: [1, 2]
+ dest: "{{ output_dir }}/fetched"
+ register: fetch_incorrect_src
+ ignore_errors: yes
+
+- name: Try to fetch a file inside an inaccessible directory
+ fetch:
+ src: "{{ remote_tmp_dir }}/noaccess/file1"
+ dest: "{{ output_dir }}"
+ register: failed_fetch_no_access
+ become: yes
+ become_user: fetcher
+ become_method: su
+ ignore_errors: yes
+
+- name: Dest is an existing directory name without trailing slash and flat=yes, should fail
+ fetch:
+ src: "{{ remote_tmp_dir }}/orig"
+ dest: "{{ output_dir }}"
+ flat: yes
+ register: failed_fetch_dest_dir
+ ignore_errors: true
+
+- name: Ensure fetch failed
+ assert:
+ that:
+ - fetch_no_params is failed
+ - fetch_no_params.msg is search('src and dest are required')
+ - fetch_incorrect_src is failed
+ - fetch_incorrect_src.msg is search('Invalid type supplied for source')
+ - failed_fetch_no_access is failed
+ - failed_fetch_no_access.msg is search('file is not readable')
+ - failed_fetch_dest_dir is failed
+ - failed_fetch_dest_dir.msg is search('dest is an existing directory')
diff --git a/test/integration/targets/fetch/roles/fetch_tests/tasks/main.yml b/test/integration/targets/fetch/roles/fetch_tests/tasks/main.yml
new file mode 100644
index 0000000..eefe95c
--- /dev/null
+++ b/test/integration/targets/fetch/roles/fetch_tests/tasks/main.yml
@@ -0,0 +1,5 @@
+- import_tasks: setup.yml
+- import_tasks: normal.yml
+- import_tasks: symlink.yml
+- import_tasks: fail_on_missing.yml
+- import_tasks: failures.yml
diff --git a/test/integration/targets/fetch/roles/fetch_tests/tasks/normal.yml b/test/integration/targets/fetch/roles/fetch_tests/tasks/normal.yml
new file mode 100644
index 0000000..6f3ab62
--- /dev/null
+++ b/test/integration/targets/fetch/roles/fetch_tests/tasks/normal.yml
@@ -0,0 +1,38 @@
+- name: Fetch the test file
+ fetch: src={{ remote_tmp_dir }}/orig dest={{ output_dir }}/fetched
+ register: fetched
+
+- name: Fetch a second time to show no changes
+ fetch: src={{ remote_tmp_dir }}/orig dest={{ output_dir }}/fetched
+ register: fetched_again
+
+- name: Fetch the test file in check mode
+ fetch:
+ src: "{{ remote_tmp_dir }}/orig"
+ dest: "{{ output_dir }}/fetched"
+ check_mode: yes
+ register: fetch_check_mode
+
+- name: Fetch with dest ending in path sep
+ fetch:
+ src: "{{ remote_tmp_dir }}/orig"
+ dest: "{{ output_dir }}/"
+ flat: yes
+
+- name: Fetch with dest with relative path
+ fetch:
+ src: "{{ remote_tmp_dir }}/orig"
+ dest: "{{ output_dir[1:] }}"
+ flat: yes
+
+- name: Assert that we fetched correctly
+ assert:
+ that:
+ - fetched is changed
+ - fetched.checksum == "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"
+ - fetched_again is not changed
+ - fetched_again.checksum == "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"
+ - fetched.remote_checksum == "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"
+ - lookup("file", output_dir + "/fetched/" + inventory_hostname + remote_tmp_dir + "/orig") == "test"
+ - fetch_check_mode is skipped
+ - fetch_check_mode.msg is search('not \(yet\) supported')
diff --git a/test/integration/targets/fetch/roles/fetch_tests/tasks/setup.yml b/test/integration/targets/fetch/roles/fetch_tests/tasks/setup.yml
new file mode 100644
index 0000000..83b6093
--- /dev/null
+++ b/test/integration/targets/fetch/roles/fetch_tests/tasks/setup.yml
@@ -0,0 +1,46 @@
+- name: Include system specific variables
+ include_vars: "{{ lookup('first_found', params) }}"
+ vars:
+ params:
+ files:
+ - "{{ ansible_facts.system }}.yml"
+ - default.yml
+ paths:
+ - "{{ role_path }}/vars"
+
+- name: Work-around for locked users on Alpine
+ # see https://github.com/ansible/ansible/issues/68676
+ set_fact:
+ password: '*'
+ when: ansible_distribution == 'Alpine'
+
+- name: Create test user
+ user:
+ name: fetcher
+ create_home: yes
+ group: "{{ _fetch_group | default(omit) }}"
+ groups: "{{ _fetch_additional_groups | default(omit) }}"
+ append: "{{ True if _fetch_additional_groups else False }}"
+ password: "{{ password | default(omit) }}"
+ become: yes
+ notify:
+ - remove test user
+
+- name: Create a file that we can use to fetch
+ copy:
+ content: "test"
+ dest: "{{ remote_tmp_dir }}/orig"
+
+- name: Create symlink to a file that we can fetch
+ file:
+ path: "{{ remote_tmp_dir }}/link"
+ src: "{{ remote_tmp_dir }}/orig"
+ state: "link"
+
+- name: Create an inaccessible directory
+ file:
+ path: "{{ remote_tmp_dir }}/noaccess"
+ state: directory
+ mode: '0600'
+ owner: root
+ become: yes
diff --git a/test/integration/targets/fetch/roles/fetch_tests/tasks/symlink.yml b/test/integration/targets/fetch/roles/fetch_tests/tasks/symlink.yml
new file mode 100644
index 0000000..41d7b35
--- /dev/null
+++ b/test/integration/targets/fetch/roles/fetch_tests/tasks/symlink.yml
@@ -0,0 +1,13 @@
+- name: Fetch the file via a symlink
+ fetch:
+ src: "{{ remote_tmp_dir }}/link"
+ dest: "{{ output_dir }}/fetched-link"
+ register: fetched_symlink
+
+- name: Assert that we fetched correctly
+ assert:
+ that:
+ - fetched_symlink is changed
+ - fetched_symlink.checksum == "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"
+ - fetched_symlink.remote_checksum == "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"
+ - 'lookup("file", output_dir + "/fetched-link/" + inventory_hostname + remote_tmp_dir + "/link") == "test"'
diff --git a/test/integration/targets/fetch/roles/fetch_tests/vars/Darwin.yml b/test/integration/targets/fetch/roles/fetch_tests/vars/Darwin.yml
new file mode 100644
index 0000000..46fe3af
--- /dev/null
+++ b/test/integration/targets/fetch/roles/fetch_tests/vars/Darwin.yml
@@ -0,0 +1,4 @@
+# macOS requires users to be in an additional group for ssh access
+
+_fetch_group: staff
+_fetch_additional_groups: com.apple.access_ssh
diff --git a/test/integration/targets/fetch/roles/fetch_tests/vars/default.yml b/test/integration/targets/fetch/roles/fetch_tests/vars/default.yml
new file mode 100644
index 0000000..69d7958
--- /dev/null
+++ b/test/integration/targets/fetch/roles/fetch_tests/vars/default.yml
@@ -0,0 +1 @@
+_fetch_additional_groups: []
diff --git a/test/integration/targets/fetch/run_fetch_tests.yml b/test/integration/targets/fetch/run_fetch_tests.yml
new file mode 100644
index 0000000..f2ff1df
--- /dev/null
+++ b/test/integration/targets/fetch/run_fetch_tests.yml
@@ -0,0 +1,5 @@
+- name: call fetch_tests role
+ hosts: testhost
+ gather_facts: false
+ roles:
+ - fetch_tests
diff --git a/test/integration/targets/fetch/runme.sh b/test/integration/targets/fetch/runme.sh
new file mode 100755
index 0000000..a508a0a
--- /dev/null
+++ b/test/integration/targets/fetch/runme.sh
@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+
+set -eux
+
+function cleanup {
+ ansible-playbook -i "${INVENTORY_PATH}" cleanup.yml -e "output_dir=${OUTPUT_DIR}" -b "$@"
+ unset ANSIBLE_CACHE_PLUGIN
+ unset ANSIBLE_CACHE_PLUGIN_CONNECTION
+}
+
+trap 'cleanup "$@"' EXIT
+
+# setup required roles
+ln -s ../../setup_remote_tmp_dir roles/setup_remote_tmp_dir
+
+# run old type role tests
+ansible-playbook -i ../../inventory run_fetch_tests.yml -e "output_dir=${OUTPUT_DIR}" "$@"
+
+# run same test with become
+ansible-playbook -i ../../inventory run_fetch_tests.yml -e "output_dir=${OUTPUT_DIR}" -b "$@"
+
+# run tests to avoid path injection from slurp when fetch uses become
+ansible-playbook -i ../../inventory injection/avoid_slurp_return.yml -e "output_dir=${OUTPUT_DIR}" "$@"
+
+## Test unreadable file with stat. Requires running without become and as a user other than root.
+#
+# Change the known_hosts file to avoid changing the test environment
+export ANSIBLE_CACHE_PLUGIN=jsonfile
+export ANSIBLE_CACHE_PLUGIN_CONNECTION="${OUTPUT_DIR}/cache"
+# Create a non-root user account and configure SSH acccess for that account
+ansible-playbook -i "${INVENTORY_PATH}" setup_unreadable_test.yml -e "output_dir=${OUTPUT_DIR}" "$@"
+
+# Run the tests as the unprivileged user without become to test the use of the stat module from the fetch module
+ansible-playbook -i "${INVENTORY_PATH}" test_unreadable_with_stat.yml -e ansible_user=fetcher -e ansible_become=no -e "output_dir=${OUTPUT_DIR}" "$@"
diff --git a/test/integration/targets/fetch/setup_unreadable_test.yml b/test/integration/targets/fetch/setup_unreadable_test.yml
new file mode 100644
index 0000000..f4cc8c1
--- /dev/null
+++ b/test/integration/targets/fetch/setup_unreadable_test.yml
@@ -0,0 +1,40 @@
+- name: Create a user account and configure ssh access
+ hosts: testhost
+ gather_facts: no
+
+ tasks:
+ - import_role:
+ name: fetch_tests
+ tasks_from: setup.yml
+ vars:
+ # Keep the remote temp dir and cache the remote_tmp_dir fact. The directory itself
+ # and the fact that contains the path are needed in a separate ansible-playbook run.
+ setup_remote_tmp_dir_skip_cleanup: yes
+ setup_remote_tmp_dir_cache_path: yes
+ skip_cleanup: yes
+
+ # This prevents ssh access. It is fixed in some container images but not all.
+ # https://github.com/ansible/distro-test-containers/pull/70
+ - name: Remove /run/nologin
+ file:
+ path: /run/nologin
+ state: absent
+
+ # Setup ssh access for the unprivileged user.
+ - name: Get home directory for temporary user
+ command: echo ~fetcher
+ register: fetcher_home
+
+ - name: Create .ssh dir
+ file:
+ path: "{{ fetcher_home.stdout }}/.ssh"
+ state: directory
+ owner: fetcher
+ mode: '0700'
+
+ - name: Configure authorized_keys
+ copy:
+ src: "~root/.ssh/authorized_keys"
+ dest: "{{ fetcher_home.stdout }}/.ssh/authorized_keys"
+ owner: fetcher
+ mode: '0600'
diff --git a/test/integration/targets/fetch/test_unreadable_with_stat.yml b/test/integration/targets/fetch/test_unreadable_with_stat.yml
new file mode 100644
index 0000000..c8a0145
--- /dev/null
+++ b/test/integration/targets/fetch/test_unreadable_with_stat.yml
@@ -0,0 +1,36 @@
+# This playbook needs to be run as a non-root user without become. Under
+# those circumstances, the fetch module uses stat and not slurp.
+
+- name: Test unreadable file using stat
+ hosts: testhost
+ gather_facts: no
+
+ tasks:
+ - name: Check connectivity
+ command: whoami
+ register: whoami
+
+ - name: Verify user
+ assert:
+ that:
+ - whoami.stdout == 'fetcher'
+
+ - name: Try to fetch a file inside an inaccessible directory
+ fetch:
+ src: "{{ remote_tmp_dir }}/noaccess/file1"
+ dest: "{{ output_dir }}"
+ register: failed_fetch_no_access
+ ignore_errors: yes
+
+ - name: Try to fetch a file inside an inaccessible directory without fail_on_missing
+ fetch:
+ src: "{{ remote_tmp_dir }}/noaccess/file1"
+ dest: "{{ output_dir }}"
+ fail_on_missing: no
+ register: failed_fetch_no_access_fail_on_missing
+
+ - assert:
+ that:
+ - failed_fetch_no_access is failed
+ - failed_fetch_no_access.msg is search('Permission denied')
+ - failed_fetch_no_access_fail_on_missing.msg is search(', ignored')
diff --git a/test/integration/targets/file/aliases b/test/integration/targets/file/aliases
new file mode 100644
index 0000000..6bd893d
--- /dev/null
+++ b/test/integration/targets/file/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group2
+needs/root
diff --git a/test/integration/targets/file/defaults/main.yml b/test/integration/targets/file/defaults/main.yml
new file mode 100644
index 0000000..8e9a583
--- /dev/null
+++ b/test/integration/targets/file/defaults/main.yml
@@ -0,0 +1,2 @@
+---
+remote_unprivileged_user: tmp_ansible_test_user
diff --git a/test/integration/targets/file/files/foo.txt b/test/integration/targets/file/files/foo.txt
new file mode 100644
index 0000000..7c6ded1
--- /dev/null
+++ b/test/integration/targets/file/files/foo.txt
@@ -0,0 +1 @@
+foo.txt
diff --git a/test/integration/targets/file/files/foobar/directory/fileC b/test/integration/targets/file/files/foobar/directory/fileC
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/file/files/foobar/directory/fileC
diff --git a/test/integration/targets/file/files/foobar/directory/fileD b/test/integration/targets/file/files/foobar/directory/fileD
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/file/files/foobar/directory/fileD
diff --git a/test/integration/targets/file/files/foobar/fileA b/test/integration/targets/file/files/foobar/fileA
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/file/files/foobar/fileA
diff --git a/test/integration/targets/file/files/foobar/fileB b/test/integration/targets/file/files/foobar/fileB
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/file/files/foobar/fileB
diff --git a/test/integration/targets/file/handlers/main.yml b/test/integration/targets/file/handlers/main.yml
new file mode 100644
index 0000000..553f69c
--- /dev/null
+++ b/test/integration/targets/file/handlers/main.yml
@@ -0,0 +1,20 @@
+- name: remove users
+ user:
+ name: "{{ item }}"
+ state: absent
+ remove: yes
+ force: yes
+ loop:
+ - test1
+ - test_uid
+ - nonexistent
+ - "{{ remote_unprivileged_user }}"
+
+- name: remove groups
+ group:
+ name: "{{ item }}"
+ state: absent
+ loop:
+ - test1
+ - test_gid
+ - nonexistent1
diff --git a/test/integration/targets/file/meta/main.yml b/test/integration/targets/file/meta/main.yml
new file mode 100644
index 0000000..e655a4f
--- /dev/null
+++ b/test/integration/targets/file/meta/main.yml
@@ -0,0 +1,4 @@
+dependencies:
+ - prepare_tests
+ - setup_nobody
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/file/tasks/diff_peek.yml b/test/integration/targets/file/tasks/diff_peek.yml
new file mode 100644
index 0000000..802a99a
--- /dev/null
+++ b/test/integration/targets/file/tasks/diff_peek.yml
@@ -0,0 +1,10 @@
+- name: Run task with _diff_peek
+ file:
+ path: "{{ output_file }}"
+ _diff_peek: yes
+ register: diff_peek_result
+
+- name: Ensure warning was not issued when using _diff_peek parameter
+ assert:
+ that:
+ - diff_peek_result['warnings'] is not defined
diff --git a/test/integration/targets/file/tasks/directory_as_dest.yml b/test/integration/targets/file/tasks/directory_as_dest.yml
new file mode 100644
index 0000000..161a12a
--- /dev/null
+++ b/test/integration/targets/file/tasks/directory_as_dest.yml
@@ -0,0 +1,345 @@
+# File module tests for overwriting directories
+- name: Initialize the test output dir
+ import_tasks: initialize.yml
+
+# We need to make this more consistent:
+# https://github.com/ansible/proposals/issues/111
+#
+# This series of tests document the current inconsistencies. We should not
+# break these by accident but if we approve a proposal we can break these on
+# purpose.
+
+#
+# Setup
+#
+
+- name: create a test sub-directory
+ file:
+ dest: '{{remote_tmp_dir_test}}/sub1'
+ state: directory
+
+- name: create a file for linking to
+ copy:
+ dest: '{{remote_tmp_dir_test}}/file_to_link'
+ content: 'Hello World'
+
+#
+# Error condtion: specify a directory with state={link,file}, force=False
+#
+
+# file raises an error
+- name: Try to create a file with directory as dest
+ file:
+ dest: '{{remote_tmp_dir_test}}/sub1'
+ state: file
+ force: False
+ ignore_errors: True
+ register: file1_result
+
+- name: Get stat info to show the directory has not been changed to a file
+ stat:
+ path: '{{ remote_tmp_dir_test }}/sub1'
+ follow: False
+ register: file1_dir_stat
+
+- name: verify that the directory was not overwritten
+ assert:
+ that:
+ - 'file1_result is failed'
+ - 'file1_dir_stat["stat"].isdir'
+
+# link raises an error
+- name: Try to create a symlink with directory as dest
+ file:
+ src: '{{ remote_tmp_dir_test }}/file_to_link'
+ dest: '{{remote_tmp_dir_test}}/sub1'
+ state: link
+ force: False
+ ignore_errors: True
+ register: file2_result
+
+- name: Get stat info to show the directory has not been changed to a file
+ stat:
+ path: '{{ remote_tmp_dir_test }}/sub1'
+ follow: False
+ register: file2_dir_stat
+
+- name: verify that the directory was not overwritten
+ assert:
+ that:
+ - 'file2_result is failed'
+ - 'file2_dir_stat["stat"].isdir'
+
+#
+# Error condition: file and link with non-empty directory
+#
+
+- copy:
+ content: 'test'
+ dest: '{{ remote_tmp_dir_test }}/sub1/passwd'
+
+# file raises an error
+- name: Try to create a file with directory as dest
+ file:
+ dest: '{{remote_tmp_dir_test}}/sub1'
+ state: file
+ force: True
+ ignore_errors: True
+ register: file3_result
+
+- name: Get stat info to show the directory has not been changed to a file
+ stat:
+ path: '{{ remote_tmp_dir_test }}/sub1'
+ follow: False
+ register: file3_dir_stat
+
+- name: verify that the directory was not overwritten
+ assert:
+ that:
+ - 'file3_result is failed'
+ - 'file3_dir_stat["stat"].isdir'
+
+# link raises an error
+- name: Try to create a symlink with directory as dest
+ file:
+ src: '{{ remote_tmp_dir_test }}/file_to_link'
+ dest: '{{remote_tmp_dir_test}}/sub1'
+ state: link
+ force: True
+ ignore_errors: True
+ register: file4_result
+
+- name: Get stat info to show the directory has not been changed to a file
+ stat:
+ path: '{{ remote_tmp_dir_test }}/sub1'
+ follow: False
+ register: file4_dir_stat
+
+- name: verify that the directory was not overwritten
+ assert:
+ that:
+ - 'file4_result is failed'
+ - 'file4_dir_stat["stat"].isdir'
+
+# Cleanup the file that made it non-empty
+- name: Cleanup the file that made the directory nonempty
+ file:
+ state: 'absent'
+ dest: '{{ remote_tmp_dir_test }}/sub1/passwd'
+
+#
+# Error condition: file cannot even overwrite an empty directory with force=True
+#
+
+# file raises an error
+- name: Try to create a file with directory as dest
+ file:
+ dest: '{{remote_tmp_dir_test}}/sub1'
+ state: file
+ force: True
+ ignore_errors: True
+ register: file5_result
+
+- name: Get stat info to show the directory has not been changed to a file
+ stat:
+ path: '{{ remote_tmp_dir_test }}/sub1'
+ follow: False
+ register: file5_dir_stat
+
+- name: verify that the directory was not overwritten
+ assert:
+ that:
+ - 'file5_result is failed'
+ - 'file5_dir_stat["stat"].isdir'
+
+#
+# Directory overwriting - link with force=True will overwrite an empty directory
+#
+
+# link can overwrite an empty directory with force=True
+- name: Try to create a symlink with directory as dest
+ file:
+ src: '{{ remote_tmp_dir_test }}/file_to_link'
+ dest: '{{remote_tmp_dir_test}}/sub1'
+ state: link
+ force: True
+ register: file6_result
+
+- name: Get stat info to show the directory has been overwritten
+ stat:
+ path: '{{ remote_tmp_dir_test }}/sub1'
+ follow: False
+ register: file6_dir_stat
+
+- name: verify that the directory was overwritten
+ assert:
+ that:
+ - 'file6_result is changed'
+ - 'not file6_dir_stat["stat"].isdir'
+ - 'file6_dir_stat["stat"].islnk'
+
+#
+# Cleanup from last set of tests
+#
+
+- name: Cleanup the test subdirectory
+ file:
+ dest: '{{remote_tmp_dir_test}}/sub1'
+ state: 'absent'
+
+- name: Re-create the test sub-directory
+ file:
+ dest: '{{remote_tmp_dir_test}}/sub1'
+ state: 'directory'
+
+#
+# Hard links have the proposed 111 behaviour already: Place the new file inside the directory
+#
+
+- name: Try to create a hardlink with directory as dest
+ file:
+ src: '{{ remote_tmp_dir_test }}/file_to_link'
+ dest: '{{ remote_tmp_dir_test }}/sub1'
+ state: hard
+ force: False
+ ignore_errors: True
+ register: file7_result
+
+- name: Get stat info to show the directory has not been changed to a file
+ stat:
+ path: '{{ remote_tmp_dir_test }}/sub1'
+ follow: False
+ register: file7_dir_stat
+
+- name: Get stat info to show the link has been created
+ stat:
+ path: '{{ remote_tmp_dir_test }}/sub1/file_to_link'
+ follow: False
+ register: file7_link_stat
+
+- debug:
+ var: file7_link_stat
+
+- name: verify that the directory was not overwritten
+ assert:
+ that:
+ - 'file7_result is changed'
+ - 'file7_dir_stat["stat"].isdir'
+ - 'file7_link_stat["stat"].isfile'
+ - 'file7_link_stat["stat"].isfile'
+ ignore_errors: True
+
+#
+# Touch is a bit different than everything else.
+# If we need to set timestamps we should probably add atime, mtime, and ctime parameters
+# But I think touch was written because state=file didn't create a file if it
+# didn't already exist. We should look at changing that behaviour.
+#
+
+- name: Get initial stat info to compare with later
+ stat:
+ path: '{{ remote_tmp_dir_test }}/sub1'
+ follow: False
+ register: file8_initial_dir_stat
+
+- name: Pause to ensure stat times are not the exact same
+ pause:
+ seconds: 1
+
+- name: Use touch with directory as dest
+ file:
+ dest: '{{remote_tmp_dir_test}}/sub1'
+ state: touch
+ force: False
+ register: file8_result
+
+- name: Get stat info to show the directory has not been changed to a file
+ stat:
+ path: '{{ remote_tmp_dir_test }}/sub1'
+ follow: False
+ register: file8_dir_stat
+
+- name: verify that the directory has been updated
+ assert:
+ that:
+ - 'file8_result is changed'
+ - 'file8_dir_stat["stat"].isdir'
+ - 'file8_dir_stat["stat"]["mtime"] != file8_initial_dir_stat["stat"]["mtime"]'
+
+- name: Get initial stat info to compare with later
+ stat:
+ path: '{{ remote_tmp_dir_test }}/sub1'
+ follow: False
+ register: file11_initial_dir_stat
+
+- name: Use touch with directory as dest and keep mtime and atime
+ file:
+ dest: '{{remote_tmp_dir_test}}/sub1'
+ state: touch
+ force: False
+ modification_time: preserve
+ access_time: preserve
+ register: file11_result
+
+- name: Get stat info to show the directory has not been changed
+ stat:
+ path: '{{ remote_tmp_dir_test }}/sub1'
+ follow: False
+ register: file11_dir_stat
+
+- name: verify that the directory has not been updated
+ assert:
+ that:
+ - 'file11_result is not changed'
+ - 'file11_dir_stat["stat"].isdir'
+ - 'file11_dir_stat["stat"]["mtime"] == file11_initial_dir_stat["stat"]["mtime"]'
+ - 'file11_dir_stat["stat"]["atime"] == file11_initial_dir_stat["stat"]["atime"]'
+
+#
+# State=directory realizes that the directory already exists and does nothing
+#
+- name: Get initial stat info to compare with later
+ stat:
+ path: '{{ remote_tmp_dir_test }}/sub1'
+ follow: False
+ register: file9_initial_dir_stat
+
+- name: Use directory with directory as dest
+ file:
+ dest: '{{remote_tmp_dir_test}}/sub1'
+ state: directory
+ force: False
+ register: file9_result
+
+- name: Get stat info to show the directory has not been changed
+ stat:
+ path: '{{ remote_tmp_dir_test }}/sub1'
+ follow: False
+ register: file9_dir_stat
+
+- name: verify that the directory has been updated
+ assert:
+ that:
+ - 'file9_result is not changed'
+ - 'file9_dir_stat["stat"].isdir'
+ - 'file9_dir_stat["stat"]["mtime"] == file9_initial_dir_stat["stat"]["mtime"]'
+
+- name: Use directory with directory as dest and force=True
+ file:
+ dest: '{{remote_tmp_dir_test}}/sub1'
+ state: directory
+ force: True
+ register: file10_result
+
+- name: Get stat info to show the directory has not been changed
+ stat:
+ path: '{{ remote_tmp_dir_test }}/sub1'
+ follow: False
+ register: file10_dir_stat
+
+- name: verify that the directory has been updated
+ assert:
+ that:
+ - 'file10_result is not changed'
+ - 'file10_dir_stat["stat"].isdir'
+ - 'file10_dir_stat["stat"]["mtime"] == file9_initial_dir_stat["stat"]["mtime"]'
diff --git a/test/integration/targets/file/tasks/initialize.yml b/test/integration/targets/file/tasks/initialize.yml
new file mode 100644
index 0000000..ad9f664
--- /dev/null
+++ b/test/integration/targets/file/tasks/initialize.yml
@@ -0,0 +1,15 @@
+#
+# Cleanup the output dir and recreate it for the tests to operate on
+#
+- name: Cleanup the output directory
+ file:
+ dest: '{{ remote_tmp_dir_test }}'
+ state: 'absent'
+
+- name: Recreate the toplevel output dir
+ file:
+ dest: '{{ remote_tmp_dir_test }}'
+ state: 'directory'
+
+- name: prep with a basic file to operate on
+ copy: src=foo.txt dest={{output_file}}
diff --git a/test/integration/targets/file/tasks/link_rewrite.yml b/test/integration/targets/file/tasks/link_rewrite.yml
new file mode 100644
index 0000000..b0e1af3
--- /dev/null
+++ b/test/integration/targets/file/tasks/link_rewrite.yml
@@ -0,0 +1,47 @@
+- name: create temporary build directory
+ tempfile:
+ state: directory
+ suffix: ansible_test_leave_links_alone_during_touch
+ register: tempdir
+
+- name: create file
+ copy:
+ mode: 0600
+ content: "chicken"
+ dest: "{{ tempdir.path }}/somefile"
+
+- name: Create relative link
+ file:
+ src: somefile
+ dest: "{{ tempdir.path }}/somelink"
+ state: link
+
+- stat:
+ path: "{{ tempdir.path }}/somelink"
+ register: link
+
+- stat:
+ path: "{{ tempdir.path }}/somefile"
+ register: file
+
+- assert:
+ that:
+ - "file.stat.mode == '0600'"
+ - "link.stat.lnk_target == 'somefile'"
+
+- file:
+ path: "{{ tempdir.path }}/somelink"
+ mode: 0644
+
+- stat:
+ path: "{{ tempdir.path }}/somelink"
+ register: link
+
+- stat:
+ path: "{{ tempdir.path }}/somefile"
+ register: file
+
+- assert:
+ that:
+ - "file.stat.mode == '0644'"
+ - "link.stat.lnk_target == 'somefile'"
diff --git a/test/integration/targets/file/tasks/main.yml b/test/integration/targets/file/tasks/main.yml
new file mode 100644
index 0000000..17b0fae
--- /dev/null
+++ b/test/integration/targets/file/tasks/main.yml
@@ -0,0 +1,960 @@
+# Test code for the file module.
+# (c) 2014, Richard Isaacson <richard.c.isaacson@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- set_fact:
+ remote_tmp_dir_test: '{{ remote_tmp_dir }}/file'
+
+- set_fact:
+ output_file: '{{remote_tmp_dir_test}}/foo.txt'
+
+# same as expanduser & expandvars called on managed host
+- command: 'echo {{ output_file }}'
+ register: echo
+
+- set_fact:
+ remote_file_expanded: '{{ echo.stdout }}'
+
+# Import the test tasks
+- name: Run tests for state=link
+ import_tasks: state_link.yml
+
+- name: Run tests for directory as dest
+ import_tasks: directory_as_dest.yml
+
+- name: Run tests for unicode
+ import_tasks: unicode_path.yml
+ environment:
+ LC_ALL: C
+ LANG: C
+
+- name: decide to include or not include selinux tests
+ include_tasks: selinux_tests.yml
+ when: selinux_installed is defined and selinux_installed.stdout != "" and selinux_enabled.stdout != "Disabled"
+
+- name: Initialize the test output dir
+ import_tasks: initialize.yml
+
+- name: Test _diff_peek
+ import_tasks: diff_peek.yml
+
+- name: Test modification time
+ import_tasks: modification_time.yml
+
+# These tests need to be organized by state parameter into separate files later
+
+- name: verify that we are checking a file and it is present
+ file: path={{output_file}} state=file
+ register: file_result
+
+- name: verify that the file was marked as changed
+ assert:
+ that:
+ - "file_result.changed == false"
+ - "file_result.state == 'file'"
+
+- name: Make sure file does not exist
+ file:
+ path: /tmp/ghost
+ state: absent
+
+- name: Target a file that does not exist
+ file:
+ path: /tmp/ghost
+ ignore_errors: yes
+ register: ghost_file_result
+
+- name: Validate ghost file results
+ assert:
+ that:
+ - ghost_file_result is failed
+ - ghost_file_result is not changed
+ - ghost_file_result.state == 'absent'
+ - "'cannot continue' in ghost_file_result.msg"
+
+- name: verify that we are checking an absent file
+ file: path={{remote_tmp_dir_test}}/bar.txt state=absent
+ register: file2_result
+
+- name: verify that the file was marked as changed
+ assert:
+ that:
+ - "file2_result.changed == false"
+ - "file2_result.state == 'absent'"
+
+- name: verify we can touch a file
+ file:
+ path: "{{remote_tmp_dir_test}}/baz.txt"
+ state: touch
+ mode: '0644'
+ register: file3_result
+
+- name: verify that the file was marked as changed
+ assert:
+ that:
+ - "file3_result.changed == true"
+ - "file3_result.state == 'file'"
+ - "file3_result.mode == '0644'"
+
+- name: change file mode
+ file: path={{remote_tmp_dir_test}}/baz.txt mode=0600
+ register: file4_result
+
+- name: verify that the file was marked as changed
+ assert:
+ that:
+ - "file4_result.changed == true"
+ - "file4_result.mode == '0600'"
+
+- name: define file to verify chattr/lsattr with
+ set_fact:
+ attributes_file: "{{ remote_tmp_dir_test }}/attributes.txt"
+ attributes_supported: no
+
+- name: create file to verify chattr/lsattr with
+ command: touch "{{ attributes_file }}"
+
+- name: add "A" attribute to file
+ command: chattr +A "{{ attributes_file }}"
+ ignore_errors: yes
+
+- name: get attributes from file
+ command: lsattr -d "{{ attributes_file }}"
+ register: attribute_A_set
+ ignore_errors: yes
+
+- name: remove "A" attribute from file
+ command: chattr -A "{{ attributes_file }}"
+ ignore_errors: yes
+
+- name: get attributes from file
+ command: lsattr -d "{{ attributes_file }}"
+ register: attribute_A_unset
+ ignore_errors: yes
+
+- name: determine if chattr/lsattr is supported
+ set_fact:
+ attributes_supported: yes
+ when:
+ - attribute_A_set is success
+ - attribute_A_set.stdout_lines
+ - "'A' in attribute_A_set.stdout_lines[0].split()[0]"
+ - attribute_A_unset is success
+ - attribute_A_unset.stdout_lines
+ - "'A' not in attribute_A_unset.stdout_lines[0].split()[0]"
+
+- name: explicitly set file attribute "A"
+ file: path={{remote_tmp_dir_test}}/baz.txt attributes=A
+ register: file_attributes_result
+ ignore_errors: True
+ when: attributes_supported
+
+- name: add file attribute "A"
+ file: path={{remote_tmp_dir_test}}/baz.txt attributes=+A
+ register: file_attributes_result_2
+ when: file_attributes_result is changed
+
+- name: verify that the file was not marked as changed
+ assert:
+ that:
+ - "file_attributes_result_2 is not changed"
+ when: file_attributes_result is changed
+
+- name: remove file attribute "A"
+ file: path={{remote_tmp_dir_test}}/baz.txt attributes=-A
+ register: file_attributes_result_3
+ ignore_errors: True
+
+- name: explicitly remove file attributes
+ file: path={{remote_tmp_dir_test}}/baz.txt attributes=""
+ register: file_attributes_result_4
+ when: file_attributes_result_3 is changed
+
+- name: verify that the file was not marked as changed
+ assert:
+ that:
+ - "file_attributes_result_4 is not changed"
+ when: file_attributes_result_4 is changed
+
+- name: create user
+ user:
+ name: test1
+ uid: 1234
+ notify: remove users
+
+- name: create group
+ group:
+ name: test1
+ gid: 1234
+ notify: remove groups
+
+- name: change ownership and group
+ file: path={{remote_tmp_dir_test}}/baz.txt owner=1234 group=1234
+
+- name: Get stat info to check atime later
+ stat: path={{remote_tmp_dir_test}}/baz.txt
+ register: file_attributes_result_5_before
+
+- name: updates access time
+ file: path={{remote_tmp_dir_test}}/baz.txt access_time=now
+ register: file_attributes_result_5
+
+- name: Get stat info to check atime later
+ stat: path={{remote_tmp_dir_test}}/baz.txt
+ register: file_attributes_result_5_after
+
+- name: verify that the file was marked as changed and atime changed
+ assert:
+ that:
+ - "file_attributes_result_5 is changed"
+ - "file_attributes_result_5_after['stat']['atime'] != file_attributes_result_5_before['stat']['atime']"
+
+- name: setup a tmp-like directory for ownership test
+ file: path=/tmp/worldwritable mode=1777 state=directory
+
+- name: Ask to create a file without enough perms to change ownership
+ file: path=/tmp/worldwritable/baz.txt state=touch owner=root
+ become: yes
+ become_user: nobody
+ register: chown_result
+ ignore_errors: True
+
+- name: Ask whether the new file exists
+ stat: path=/tmp/worldwritable/baz.txt
+ register: file_exists_result
+
+- name: Verify that the file doesn't exist on failure
+ assert:
+ that:
+ - "chown_result.failed == True"
+ - "file_exists_result.stat.exists == False"
+
+- name: clean up
+ file: path=/tmp/worldwritable state=absent
+
+- name: create hard link to file
+ file: src={{output_file}} dest={{remote_tmp_dir_test}}/hard.txt state=hard
+ register: file6_result
+
+- name: verify that the file was marked as changed
+ assert:
+ that:
+ - "file6_result.changed == true"
+
+- name: touch a hard link
+ file:
+ dest: '{{ remote_tmp_dir_test }}/hard.txt'
+ state: 'touch'
+ register: file6_touch_result
+
+- name: verify that the hard link was touched
+ assert:
+ that:
+ - "file6_touch_result.changed == true"
+
+- name: stat1
+ stat: path={{output_file}}
+ register: hlstat1
+
+- name: stat2
+ stat: path={{remote_tmp_dir_test}}/hard.txt
+ register: hlstat2
+
+- name: verify that hard link is still the same after timestamp updated
+ assert:
+ that:
+ - "hlstat1.stat.inode == hlstat2.stat.inode"
+
+- name: create hard link to file 2
+ file: src={{output_file}} dest={{remote_tmp_dir_test}}/hard.txt state=hard
+ register: hlink_result
+
+- name: verify that hard link creation is idempotent
+ assert:
+ that:
+ - "hlink_result.changed == False"
+
+- name: Change mode on a hard link
+ file: dest={{remote_tmp_dir_test}}/hard.txt mode=0701
+ register: file6_mode_change
+
+- name: verify that the hard link was touched
+ assert:
+ that:
+ - "file6_touch_result.changed == true"
+
+- name: stat1
+ stat: path={{output_file}}
+ register: hlstat1
+
+- name: stat2
+ stat: path={{remote_tmp_dir_test}}/hard.txt
+ register: hlstat2
+
+- name: verify that hard link is still the same after timestamp updated
+ assert:
+ that:
+ - "hlstat1.stat.inode == hlstat2.stat.inode"
+ - "hlstat1.stat.mode == '0701'"
+
+- name: create a directory
+ file: path={{remote_tmp_dir_test}}/foobar state=directory
+ register: file7_result
+
+- name: verify that the file was marked as changed
+ assert:
+ that:
+ - "file7_result.changed == true"
+ - "file7_result.state == 'directory'"
+
+- name: determine if selinux is installed
+ shell: which getenforce || exit 0
+ register: selinux_installed
+
+- name: determine if selinux is enabled
+ shell: getenforce
+ register: selinux_enabled
+ when: selinux_installed.stdout != ""
+ ignore_errors: true
+
+- name: remove directory foobar
+ file: path={{remote_tmp_dir_test}}/foobar state=absent
+
+- name: remove file foo.txt
+ file: path={{remote_tmp_dir_test}}/foo.txt state=absent
+
+- name: remove file bar.txt
+ file: path={{remote_tmp_dir_test}}/foo.txt state=absent
+
+- name: remove file baz.txt
+ file: path={{remote_tmp_dir_test}}/foo.txt state=absent
+
+- name: copy directory structure over
+ copy: src=foobar dest={{remote_tmp_dir_test}}
+
+- name: check what would be removed if folder state was absent and diff is enabled
+ file:
+ path: "{{ item }}"
+ state: absent
+ check_mode: yes
+ diff: yes
+ with_items:
+ - "{{ remote_tmp_dir_test }}"
+ - "{{ remote_tmp_dir_test }}/foobar/fileA"
+ register: folder_absent_result
+
+- name: 'assert that the "absent" state lists expected files and folders for only directories'
+ assert:
+ that:
+ - folder_absent_result.results[0].diff.before.path_content is defined
+ - folder_absent_result.results[1].diff.before.path_content is not defined
+ - test_folder in folder_absent_result.results[0].diff.before.path_content.directories
+ - test_file in folder_absent_result.results[0].diff.before.path_content.files
+ vars:
+ test_folder: "{{ folder_absent_result.results[0].path }}/foobar"
+ test_file: "{{ folder_absent_result.results[0].path }}/foobar/fileA"
+
+- name: Change ownership of a directory with recurse=no(default)
+ file: path={{remote_tmp_dir_test}}/foobar owner=1234
+
+- name: verify that the permission of the directory was set
+ file: path={{remote_tmp_dir_test}}/foobar state=directory
+ register: file8_result
+
+- name: assert that the directory has changed to have owner 1234
+ assert:
+ that:
+ - "file8_result.uid == 1234"
+
+- name: verify that the permission of a file under the directory was not set
+ file: path={{remote_tmp_dir_test}}/foobar/fileA state=file
+ register: file9_result
+
+- name: assert the file owner has not changed to 1234
+ assert:
+ that:
+ - "file9_result.uid != 1234"
+
+- name: create user
+ user:
+ name: test2
+ uid: 1235
+
+- name: change the ownership of a directory with recurse=yes
+ file: path={{remote_tmp_dir_test}}/foobar owner=1235 recurse=yes
+
+- name: verify that the permission of the directory was set
+ file: path={{remote_tmp_dir_test}}/foobar state=directory
+ register: file10_result
+
+- name: assert that the directory has changed to have owner 1235
+ assert:
+ that:
+ - "file10_result.uid == 1235"
+
+- name: verify that the permission of a file under the directory was not set
+ file: path={{remote_tmp_dir_test}}/foobar/fileA state=file
+ register: file11_result
+
+- name: assert that the file has changed to have owner 1235
+ assert:
+ that:
+ - "file11_result.uid == 1235"
+
+- name: remove directory foobar
+ file: path={{remote_tmp_dir_test}}/foobar state=absent
+ register: file14_result
+
+- name: verify that the directory was removed
+ assert:
+ that:
+ - 'file14_result.changed == true'
+ - 'file14_result.state == "absent"'
+
+- name: create a test sub-directory
+ file: dest={{remote_tmp_dir_test}}/sub1 state=directory
+ register: file15_result
+
+- name: verify that the new directory was created
+ assert:
+ that:
+ - 'file15_result.changed == true'
+ - 'file15_result.state == "directory"'
+
+- name: create test files in the sub-directory
+ file: dest={{remote_tmp_dir_test}}/sub1/{{item}} state=touch
+ with_items:
+ - file1
+ - file2
+ - file3
+ register: file16_result
+
+- name: verify the files were created
+ assert:
+ that:
+ - 'item.changed == true'
+ - 'item.state == "file"'
+ with_items: "{{file16_result.results}}"
+
+- name: test file creation with symbolic mode
+ file: dest={{remote_tmp_dir_test}}/test_symbolic state=touch mode=u=rwx,g=rwx,o=rwx
+ register: result
+
+- name: assert file mode
+ assert:
+ that:
+ - result.mode == '0777'
+
+- name: modify symbolic mode for all
+ file: dest={{remote_tmp_dir_test}}/test_symbolic state=touch mode=a=r
+ register: result
+
+- name: assert file mode
+ assert:
+ that:
+ - result.mode == '0444'
+
+- name: modify symbolic mode for owner
+ file: dest={{remote_tmp_dir_test}}/test_symbolic state=touch mode=u+w
+ register: result
+
+- name: assert file mode
+ assert:
+ that:
+ - result.mode == '0644'
+
+- name: modify symbolic mode for group
+ file: dest={{remote_tmp_dir_test}}/test_symbolic state=touch mode=g+w
+ register: result
+
+- name: assert file mode
+ assert:
+ that:
+ - result.mode == '0664'
+
+- name: modify symbolic mode for world
+ file: dest={{remote_tmp_dir_test}}/test_symbolic state=touch mode=o+w
+ register: result
+
+- name: assert file mode
+ assert:
+ that:
+ - result.mode == '0666'
+
+- name: modify symbolic mode for owner
+ file: dest={{remote_tmp_dir_test}}/test_symbolic state=touch mode=u+x
+ register: result
+
+- name: assert file mode
+ assert:
+ that:
+ - result.mode == '0766'
+
+- name: modify symbolic mode for group
+ file: dest={{remote_tmp_dir_test}}/test_symbolic state=touch mode=g+x
+ register: result
+
+- name: assert file mode
+ assert:
+ that:
+ - result.mode == '0776'
+
+- name: modify symbolic mode for world
+ file: dest={{remote_tmp_dir_test}}/test_symbolic state=touch mode=o+x
+ register: result
+
+- name: assert file mode
+ assert:
+ that:
+ - result.mode == '0777'
+
+- name: remove symbolic mode for world
+ file: dest={{remote_tmp_dir_test}}/test_symbolic state=touch mode=o-wx
+ register: result
+
+- name: assert file mode
+ assert:
+ that:
+ - result.mode == '0774'
+
+- name: remove symbolic mode for group
+ file: dest={{remote_tmp_dir_test}}/test_symbolic state=touch mode=g-wx
+ register: result
+
+- name: assert file mode
+ assert:
+ that:
+ - result.mode == '0744'
+
+- name: remove symbolic mode for owner
+ file: dest={{remote_tmp_dir_test}}/test_symbolic state=touch mode=u-wx
+ register: result
+
+- name: assert file mode
+ assert:
+ that:
+ - result.mode == '0444'
+
+- name: set sticky bit with symbolic mode
+ file: dest={{remote_tmp_dir_test}}/test_symbolic state=touch mode=o+t
+ register: result
+
+- name: assert file mode
+ assert:
+ that:
+ - result.mode == '01444'
+
+- name: remove sticky bit with symbolic mode
+ file: dest={{remote_tmp_dir_test}}/test_symbolic state=touch mode=o-t
+ register: result
+
+- name: assert file mode
+ assert:
+ that:
+ - result.mode == '0444'
+
+- name: add setgid with symbolic mode
+ file: dest={{remote_tmp_dir_test}}/test_symbolic state=touch mode=g+s
+ register: result
+
+- name: assert file mode
+ assert:
+ that:
+ - result.mode == '02444'
+
+- name: remove setgid with symbolic mode
+ file: dest={{remote_tmp_dir_test}}/test_symbolic state=touch mode=g-s
+ register: result
+
+- name: assert file mode
+ assert:
+ that:
+ - result.mode == '0444'
+
+- name: add setuid with symbolic mode
+ file: dest={{remote_tmp_dir_test}}/test_symbolic state=touch mode=u+s
+ register: result
+
+- name: assert file mode
+ assert:
+ that:
+ - result.mode == '04444'
+
+- name: remove setuid with symbolic mode
+ file: dest={{remote_tmp_dir_test}}/test_symbolic state=touch mode=u-s
+ register: result
+
+- name: assert file mode
+ assert:
+ that:
+ - result.mode == '0444'
+
+# https://github.com/ansible/ansible/issues/67307
+# Test the module fails in check_mode when directory and owner/group do not exist
+# I don't use state=touch here intentionally to fail and catch warnings
+- name: owner does not exist in check_mode
+ file:
+ path: '/tmp/nonexistent'
+ owner: nonexistent
+ check_mode: yes
+ register: owner_no_exist
+ ignore_errors: yes
+
+- name: create owner
+ user:
+ name: nonexistent
+ notify: remove users
+
+# I don't use state=touch here intentionally to fail and catch warnings
+- name: owner exist in check_mode
+ file:
+ path: '/tmp/nonexistent'
+ owner: nonexistent
+ check_mode: yes
+ register: owner_exists
+ ignore_errors: yes
+
+# I don't use state=touch here intentionally to fail and catch warnings
+- name: owner does not exist in check_mode, using uid
+ file:
+ path: '/tmp/nonexistent'
+ owner: '111111'
+ check_mode: yes
+ ignore_errors: yes
+ register: owner_uid_no_exist
+
+- name: create owner using uid
+ user:
+ name: test_uid
+ uid: 111111
+ notify: remove users
+
+# I don't use state=touch here intentionally to fail and catch warnings
+- name: owner exists in check_mode, using uid
+ file:
+ path: '/tmp/nonexistent'
+ owner: '111111'
+ state: touch
+ check_mode: yes
+ ignore_errors: yes
+ register: owner_uid_exists
+
+# I don't use state=touch here intentionally to fail and catch warnings
+- name: group does not exist in check_mode
+ file:
+ path: '/tmp/nonexistent'
+ group: nonexistent1
+ check_mode: yes
+ register: group_no_exist
+ ignore_errors: yes
+
+- name: create group
+ group:
+ name: nonexistent1
+ notify: remove groups
+
+# I don't use state=touch here intentionally to fail and catch warnings
+- name: group exists in check_mode
+ file:
+ path: '/tmp/nonexistent'
+ group: nonexistent1
+ check_mode: yes
+ register: group_exists
+ ignore_errors: yes
+
+# I don't use state=touch here intentionally to fail and catch warnings
+- name: group does not exist in check_mode, using gid
+ file:
+ path: '/tmp/nonexistent'
+ group: '111112'
+ check_mode: yes
+ register: group_gid_no_exist
+ ignore_errors: yes
+
+- name: create group with gid
+ group:
+ name: test_gid
+ gid: 111112
+ notify: remove groups
+
+# I don't use state=touch here intentionally to fail and catch warnings
+- name: group exists in check_mode, using gid
+ file:
+ path: '/tmp/nonexistent'
+ group: '111112'
+ check_mode: yes
+ register: group_gid_exists
+ ignore_errors: yes
+
+- assert:
+ that:
+ - owner_no_exist.warnings[0] is search('failed to look up user')
+ - owner_uid_no_exist.warnings[0] is search('failed to look up user with uid')
+ - group_no_exist.warnings[0] is search('failed to look up group')
+ - group_gid_no_exist.warnings[0] is search('failed to look up group with gid')
+ - owner_exists.warnings is not defined
+ - owner_uid_exists.warnings is not defined
+ - group_exists.warnings is not defined
+ - group_gid_exists.warnings is not defined
+
+# ensures touching a file returns changed when needed
+# issue: https://github.com/ansible/ansible/issues/79360
+- name: touch a file returns changed in check mode if file does not exist
+ file:
+ path: '/tmp/touch_check_mode_test'
+ state: touch
+ check_mode: yes
+ register: touch_result_in_check_mode_not_existing
+
+- name: touch the file
+ file:
+ path: '/tmp/touch_check_mode_test'
+ mode: "0660"
+ state: touch
+
+- name: touch an existing file returns changed in check mode
+ file:
+ path: '/tmp/touch_check_mode_test'
+ state: touch
+ check_mode: yes
+ register: touch_result_in_check_mode_change_all_attr
+
+- name: touch an existing file returns changed in check mode when preserving access time
+ file:
+ path: '/tmp/touch_check_mode_test'
+ state: touch
+ access_time: "preserve"
+ check_mode: yes
+ register: touch_result_in_check_mode_preserve_access_time
+
+- name: touch an existing file returns changed in check mode when only mode changes
+ file:
+ path: '/tmp/touch_check_mode_test'
+ state: touch
+ access_time: "preserve"
+ modification_time: "preserve"
+ mode: "0640"
+ check_mode: yes
+ register: touch_result_in_check_mode_change_only_mode
+
+- name: touch an existing file returns ok if all attributes are preserved
+ file:
+ path: '/tmp/touch_check_mode_test'
+ state: touch
+ access_time: "preserve"
+ modification_time: "preserve"
+ check_mode: yes
+ register: touch_result_in_check_mode_all_attrs_preserved
+
+- name: touch an existing file fails in check mode when user does not exist
+ file:
+ path: '/tmp/touch_check_mode_test'
+ state: touch
+ owner: not-existing-user
+ check_mode: yes
+ ignore_errors: true
+ register: touch_result_in_check_mode_fails_not_existing_user
+
+- name: touch an existing file fails in check mode when group does not exist
+ file:
+ path: '/tmp/touch_check_mode_test'
+ state: touch
+ group: not-existing-group
+ check_mode: yes
+ ignore_errors: true
+ register: touch_result_in_check_mode_fails_not_existing_group
+
+- assert:
+ that:
+ - touch_result_in_check_mode_not_existing.changed
+ - touch_result_in_check_mode_preserve_access_time.changed
+ - touch_result_in_check_mode_change_only_mode.changed
+ - not touch_result_in_check_mode_all_attrs_preserved.changed
+ - touch_result_in_check_mode_fails_not_existing_user.warnings[0] is search('failed to look up user')
+ - touch_result_in_check_mode_fails_not_existing_group.warnings[0] is search('failed to look up group')
+
+# https://github.com/ansible/ansible/issues/50943
+# Need to use /tmp as nobody can't access remote_tmp_dir_test at all
+- name: create file as root with all write permissions
+ file: dest=/tmp/write_utime state=touch mode=0666 owner={{ansible_user_id}}
+
+- name: Pause to ensure stat times are not the exact same
+ pause:
+ seconds: 1
+
+- block:
+ - name: get previous time
+ stat: path=/tmp/write_utime
+ register: previous_time
+
+ - name: pause for 1 second to ensure the next touch is newer
+ pause: seconds=1
+
+ - name: touch file as nobody
+ file: dest=/tmp/write_utime state=touch
+ become: True
+ become_user: nobody
+ register: result
+
+ - name: get new time
+ stat: path=/tmp/write_utime
+ register: current_time
+
+ always:
+ - name: remove test utime file
+ file: path=/tmp/write_utime state=absent
+
+- name: assert touch file as nobody
+ assert:
+ that:
+ - result is changed
+ - current_time.stat.atime > previous_time.stat.atime
+ - current_time.stat.mtime > previous_time.stat.mtime
+
+# Follow + recursive tests
+- name: create a toplevel directory
+ file: path={{remote_tmp_dir_test}}/test_follow_rec state=directory mode=0755
+
+- name: create a file outside of the toplevel
+ file: path={{remote_tmp_dir_test}}/test_follow_rec_target_file state=touch mode=0700
+
+- name: create a directory outside of the toplevel
+ file: path={{remote_tmp_dir_test}}/test_follow_rec_target_dir state=directory mode=0700
+
+- name: create a file inside of the link target directory
+ file: path={{remote_tmp_dir_test}}/test_follow_rec_target_dir/foo state=touch mode=0700
+
+- name: create a symlink to the file
+ file: path={{remote_tmp_dir_test}}/test_follow_rec/test_link state=link src="../test_follow_rec_target_file"
+
+- name: create a symlink to the directory
+ file: path={{remote_tmp_dir_test}}/test_follow_rec/test_link_dir state=link src="../test_follow_rec_target_dir"
+
+- name: create a symlink to a nonexistent file
+ file: path={{remote_tmp_dir_test}}/test_follow_rec/nonexistent state=link src=does_not_exist force=True
+
+- name: try to change permissions without following symlinks
+ file: path={{remote_tmp_dir_test}}/test_follow_rec follow=False mode="a-x" recurse=True
+
+- name: stat the link file target
+ stat: path={{remote_tmp_dir_test}}/test_follow_rec_target_file
+ register: file_result
+
+- name: stat the link dir target
+ stat: path={{remote_tmp_dir_test}}/test_follow_rec_target_dir
+ register: dir_result
+
+- name: stat the file inside the link dir target
+ stat: path={{remote_tmp_dir_test}}/test_follow_rec_target_dir/foo
+ register: file_in_dir_result
+
+- name: assert that the link targets were unmodified
+ assert:
+ that:
+ - file_result.stat.mode == '0700'
+ - dir_result.stat.mode == '0700'
+ - file_in_dir_result.stat.mode == '0700'
+
+- name: try to change permissions with following symlinks
+ file: path={{remote_tmp_dir_test}}/test_follow_rec follow=True mode="a-x" recurse=True
+
+- name: stat the link file target
+ stat: path={{remote_tmp_dir_test}}/test_follow_rec_target_file
+ register: file_result
+
+- name: stat the link dir target
+ stat: path={{remote_tmp_dir_test}}/test_follow_rec_target_dir
+ register: dir_result
+
+- name: stat the file inside the link dir target
+ stat: path={{remote_tmp_dir_test}}/test_follow_rec_target_dir/foo
+ register: file_in_dir_result
+
+- name: assert that the link targets were modified
+ assert:
+ that:
+ - file_result.stat.mode == '0600'
+ - dir_result.stat.mode == '0600'
+ - file_in_dir_result.stat.mode == '0600'
+
+# https://github.com/ansible/ansible/issues/55971
+- name: Test missing src and path
+ file:
+ state: hard
+ register: file_error1
+ ignore_errors: yes
+
+- assert:
+ that:
+ - "file_error1 is failed"
+ - "file_error1.msg == 'missing required arguments: path'"
+
+- name: Test missing src
+ file:
+ dest: "{{ remote_tmp_dir_test }}/hard.txt"
+ state: hard
+ register: file_error2
+ ignore_errors: yes
+
+- assert:
+ that:
+ - "file_error2 is failed"
+ - "file_error2.msg == 'src is required for creating new hardlinks'"
+
+- name: Test non-existing src
+ file:
+ src: non-existing-file-that-does-not-exist.txt
+ dest: "{{ remote_tmp_dir_test }}/hard.txt"
+ state: hard
+ register: file_error3
+ ignore_errors: yes
+
+- assert:
+ that:
+ - "file_error3 is failed"
+ - "file_error3.msg == 'src does not exist'"
+ - "file_error3.dest == '{{ remote_tmp_dir_test }}/hard.txt' | expanduser"
+ - "file_error3.src == 'non-existing-file-that-does-not-exist.txt'"
+
+- block:
+ - name: Create a testing file
+ file:
+ dest: original_file.txt
+ state: touch
+
+ - name: Test relative path with state=hard
+ file:
+ src: original_file.txt
+ dest: hard_link_file.txt
+ state: hard
+ register: hard_link_relpath
+
+ - name: Just check if it was successful, we don't care about the actual hard link in this test
+ assert:
+ that:
+ - "hard_link_relpath is success"
+
+ always:
+ - name: Clean up
+ file:
+ path: "{{ item }}"
+ state: absent
+ loop:
+ - original_file.txt
+ - hard_link_file.txt
+
+# END #55971
diff --git a/test/integration/targets/file/tasks/modification_time.yml b/test/integration/targets/file/tasks/modification_time.yml
new file mode 100644
index 0000000..daec036
--- /dev/null
+++ b/test/integration/targets/file/tasks/modification_time.yml
@@ -0,0 +1,70 @@
+# file module tests for dealing with modification_time
+
+- name: Initialize the test output dir
+ import_tasks: initialize.yml
+
+- name: Setup the modification time for the tests
+ set_fact:
+ modification_timestamp: "202202081414.00"
+
+- name: Get stat info for the file
+ stat:
+ path: "{{ output_file }}"
+ register: initial_file_stat
+
+- name: Set a modification time in check_mode
+ ansible.builtin.file:
+ path: "{{ output_file }}"
+ modification_time: "{{ modification_timestamp }}"
+ modification_time_format: "%Y%m%d%H%M.%S"
+ check_mode: true
+ register: file_change_check_mode
+
+- name: Re-stat the file
+ stat:
+ path: "{{ output_file }}"
+ register: check_mode_stat
+
+- name: Confirm check_mode did not change the file
+ assert:
+ that:
+ - initial_file_stat.stat.mtime == check_mode_stat.stat.mtime
+ # Ensure the changed flag was set
+ - file_change_check_mode.changed
+ # Ensure the diff is present
+ # Note: file diff always contains the path
+ - file_change_check_mode.diff.after | length > 1
+
+- name: Set a modification time for real
+ ansible.builtin.file:
+ path: "{{ output_file }}"
+ modification_time: "{{ modification_timestamp }}"
+ modification_time_format: "%Y%m%d%H%M.%S"
+ register: file_change_no_check_mode
+
+- name: Stat of the file after the change
+ stat:
+ path: "{{ output_file }}"
+ register: change_stat
+
+- name: Confirm the modification time changed
+ assert:
+ that:
+ - initial_file_stat.stat.mtime != change_stat.stat.mtime
+ - file_change_no_check_mode.changed
+ # Note: file diff always contains the path
+ - file_change_no_check_mode.diff.after | length > 1
+
+- name: Set a modification time a second time to confirm no changes or diffs
+ ansible.builtin.file:
+ path: "{{ output_file }}"
+ modification_time: "{{ modification_timestamp }}"
+ modification_time_format: "%Y%m%d%H%M.%S"
+ register: file_change_no_check_mode_second
+
+- name: Confirm no changes made registered
+ assert:
+ that:
+ - not file_change_no_check_mode_second.changed
+ # Note: file diff always contains the path
+ - file_change_no_check_mode_second.diff.after | length == 1
diff --git a/test/integration/targets/file/tasks/selinux_tests.yml b/test/integration/targets/file/tasks/selinux_tests.yml
new file mode 100644
index 0000000..eda54f1
--- /dev/null
+++ b/test/integration/targets/file/tasks/selinux_tests.yml
@@ -0,0 +1,33 @@
+# Test code for the file module - selinux subtasks.
+# (c) 2014, Richard Isaacson <richard.c.isaacson@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: Initialize the test output dir
+ import_tasks: initialize.yml
+
+- name: touch a file for testing
+ file: path={{remote_tmp_dir_test}}/foo-se.txt state=touch
+ register: file_se_result
+
+- name: verify that the file was marked as changed
+ assert:
+ that:
+ - "file_se_result.changed == true"
+ - "file_se_result.secontext == 'unconfined_u:object_r:admin_home_t:s0'"
+
+- name: remove the file used for testing
+ file: path={{remote_tmp_dir_test}}/foo-se.txt state=absent
diff --git a/test/integration/targets/file/tasks/state_link.yml b/test/integration/targets/file/tasks/state_link.yml
new file mode 100644
index 0000000..673fe6f
--- /dev/null
+++ b/test/integration/targets/file/tasks/state_link.yml
@@ -0,0 +1,501 @@
+# file module tests for dealing with symlinks (state=link)
+
+- name: Initialize the test output dir
+ import_tasks: initialize.yml
+
+#
+# Basic absolute symlink to a file
+#
+- name: create soft link to file
+ file: src={{output_file}} dest={{remote_tmp_dir_test}}/soft.txt state=link
+ register: file1_result
+
+- name: Get stat info for the link
+ stat:
+ path: '{{ remote_tmp_dir_test }}/soft.txt'
+ follow: False
+ register: file1_link_stat
+
+- name: verify that the symlink was created correctly
+ assert:
+ that:
+ - 'file1_result is changed'
+ - 'file1_link_stat["stat"].islnk'
+ - 'file1_link_stat["stat"].lnk_target | expanduser == output_file | expanduser'
+
+#
+# Change an absolute soft link into a relative soft link
+#
+- name: change soft link to relative
+ file: src={{output_file|basename}} dest={{remote_tmp_dir_test}}/soft.txt state=link
+ register: file2_result
+
+- name: Get stat info for the link
+ stat:
+ path: '{{ remote_tmp_dir_test }}/soft.txt'
+ follow: False
+ register: file2_link_stat
+
+- name: verify that the file was marked as changed
+ assert:
+ that:
+ - "file2_result is changed"
+ - "file2_result.diff.before.src == remote_file_expanded"
+ - "file2_result.diff.after.src == remote_file_expanded|basename"
+ - "file2_link_stat['stat'].islnk"
+ - "file2_link_stat['stat'].lnk_target == remote_file_expanded | basename"
+
+#
+# Check that creating the soft link a second time was idempotent
+#
+- name: soft link idempotency check
+ file: src={{output_file|basename}} dest={{remote_tmp_dir_test}}/soft.txt state=link
+ register: file3_result
+
+- name: Get stat info for the link
+ stat:
+ path: '{{ remote_tmp_dir_test }}/soft.txt'
+ follow: False
+ register: file3_link_stat
+
+- name: verify that the file was not marked as changed
+ assert:
+ that:
+ - "not file3_result is changed"
+ - "file3_link_stat['stat'].islnk"
+ - "file3_link_stat['stat'].lnk_target == remote_file_expanded | basename"
+
+#
+# Test symlink to nonexistent files
+#
+- name: fail to create soft link to non existent file
+ file:
+ src: '/nonexistent'
+ dest: '{{remote_tmp_dir_test}}/soft2.txt'
+ state: 'link'
+ force: False
+ register: file4_result
+ ignore_errors: true
+
+- name: verify that link was not created
+ assert:
+ that:
+ - "file4_result is failed"
+
+- name: force creation soft link to non existent
+ file:
+ src: '/nonexistent'
+ dest: '{{ remote_tmp_dir_test}}/soft2.txt'
+ state: 'link'
+ force: True
+ register: file5_result
+
+- name: Get stat info for the link
+ stat:
+ path: '{{ remote_tmp_dir_test }}/soft2.txt'
+ follow: False
+ register: file5_link_stat
+
+- name: verify that link was created
+ assert:
+ that:
+ - "file5_result is changed"
+ - "file5_link_stat['stat'].islnk"
+ - "file5_link_stat['stat'].lnk_target == '/nonexistent'"
+
+- name: Prove idempotence of force creation soft link to non existent
+ file:
+ src: '/nonexistent'
+ dest: '{{ remote_tmp_dir_test }}/soft2.txt'
+ state: 'link'
+ force: True
+ register: file6a_result
+
+- name: verify that the link to nonexistent is idempotent
+ assert:
+ that:
+ - "file6a_result.changed == false"
+
+# In order for a symlink in a sticky world writable directory to be followed, it must
+# either be owned by the follower,
+# or the directory and symlink must have the same owner.
+- name: symlink in sticky directory
+ block:
+ - name: Create remote unprivileged remote user
+ user:
+ name: '{{ remote_unprivileged_user }}'
+ register: user
+ notify: remove users
+
+ - name: Create a local temporary directory
+ tempfile:
+ state: directory
+ register: tempdir
+
+ - name: Set sticky bit
+ file:
+ path: '{{ tempdir.path }}'
+ mode: o=rwXt
+
+ - name: 'Check mode: force creation soft link in sticky directory owned by another user (mode is used)'
+ file:
+ src: '{{ user.home }}/nonexistent'
+ dest: '{{ tempdir.path }}/soft3.txt'
+ mode: 0640
+ state: 'link'
+ owner: '{{ remote_unprivileged_user }}'
+ force: true
+ follow: false
+ check_mode: true
+ register: missing_dst_no_follow_enable_force_use_mode1
+
+ - name: force creation soft link in sticky directory owned by another user (mode is used)
+ file:
+ src: '{{ user.home }}/nonexistent'
+ dest: '{{ tempdir.path }}/soft3.txt'
+ mode: 0640
+ state: 'link'
+ owner: '{{ remote_unprivileged_user }}'
+ force: true
+ follow: false
+ register: missing_dst_no_follow_enable_force_use_mode2
+
+ - name: Get stat info for the link
+ stat:
+ path: '{{ tempdir.path }}/soft3.txt'
+ follow: false
+ register: soft3_result
+
+ - name: 'Idempotence: force creation soft link in sticky directory owned by another user (mode is used)'
+ file:
+ src: '{{ user.home }}/nonexistent'
+ dest: '{{ tempdir.path }}/soft3.txt'
+ mode: 0640
+ state: 'link'
+ owner: '{{ remote_unprivileged_user }}'
+ force: yes
+ follow: false
+ register: missing_dst_no_follow_enable_force_use_mode3
+ always:
+ - name: Delete remote unprivileged remote user
+ user:
+ name: '{{ remote_unprivileged_user }}'
+ state: absent
+ force: yes
+ remove: yes
+
+ - name: Delete unprivileged user home and tempdir
+ file:
+ path: "{{ item }}"
+ state: absent
+ loop:
+ - '{{ tempdir.path }}'
+ - '{{ user.home }}'
+
+- name: verify that link was created
+ assert:
+ that:
+ - "missing_dst_no_follow_enable_force_use_mode1 is changed"
+ - "missing_dst_no_follow_enable_force_use_mode2 is changed"
+ - "missing_dst_no_follow_enable_force_use_mode3 is not changed"
+ - "soft3_result['stat'].islnk"
+ - "soft3_result['stat'].lnk_target == '{{ user.home }}/nonexistent'"
+
+#
+# Test creating a link to a directory https://github.com/ansible/ansible/issues/1369
+#
+- name: create soft link to directory using absolute path
+ file:
+ src: '/'
+ dest: '{{ remote_tmp_dir_test }}/root'
+ state: 'link'
+ register: file6_result
+
+- name: Get stat info for the link
+ stat:
+ path: '{{ remote_tmp_dir_test }}/root'
+ follow: False
+ register: file6_link_stat
+
+- name: Get stat info for the pointed to file
+ stat:
+ path: '{{ remote_tmp_dir_test }}/root'
+ follow: True
+ register: file6_links_dest_stat
+
+- name: Get stat info for the file we intend to point to
+ stat:
+ path: '/'
+ follow: False
+ register: file6_dest_stat
+
+- name: verify that the link was created correctly
+ assert:
+ that:
+ # file command reports it created something
+ - "file6_result.changed == true"
+ # file command created a link
+ - 'file6_link_stat["stat"]["islnk"]'
+ # Link points to the right path
+ - 'file6_link_stat["stat"]["lnk_target"] == "/"'
+ # The link target and the file we intended to link to have the same inode
+ - 'file6_links_dest_stat["stat"]["inode"] == file6_dest_stat["stat"]["inode"]'
+
+#
+# Test creating a relative link
+#
+
+# Relative link to file
+- name: create a test sub-directory to link to
+ file:
+ dest: '{{ remote_tmp_dir_test }}/sub1'
+ state: 'directory'
+
+- name: create a file to link to in the test sub-directory
+ file:
+ dest: '{{ remote_tmp_dir_test }}/sub1/file1'
+ state: 'touch'
+
+- name: create another test sub-directory to place links within
+ file:
+ dest: '{{remote_tmp_dir_test}}/sub2'
+ state: 'directory'
+
+- name: create soft link to relative file
+ file:
+ src: '../sub1/file1'
+ dest: '{{ remote_tmp_dir_test }}/sub2/link1'
+ state: 'link'
+ register: file7_result
+
+- name: Get stat info for the link
+ stat:
+ path: '{{ remote_tmp_dir_test }}/sub2/link1'
+ follow: False
+ register: file7_link_stat
+
+- name: Get stat info for the pointed to file
+ stat:
+ path: '{{ remote_tmp_dir_test }}/sub2/link1'
+ follow: True
+ register: file7_links_dest_stat
+
+- name: Get stat info for the file we intend to point to
+ stat:
+ path: '{{ remote_tmp_dir_test }}/sub1/file1'
+ follow: False
+ register: file7_dest_stat
+
+- name: verify that the link was created correctly
+ assert:
+ that:
+ # file command reports it created something
+ - "file7_result.changed == true"
+ # file command created a link
+ - 'file7_link_stat["stat"]["islnk"]'
+ # Link points to the right path
+ - 'file7_link_stat["stat"]["lnk_target"] == "../sub1/file1"'
+ # The link target and the file we intended to link to have the same inode
+ - 'file7_links_dest_stat["stat"]["inode"] == file7_dest_stat["stat"]["inode"]'
+
+# Relative link to directory
+- name: create soft link to relative directory
+ file:
+ src: sub1
+ dest: '{{ remote_tmp_dir_test }}/sub1-link'
+ state: 'link'
+ register: file8_result
+
+- name: Get stat info for the link
+ stat:
+ path: '{{ remote_tmp_dir_test }}/sub1-link'
+ follow: False
+ register: file8_link_stat
+
+- name: Get stat info for the pointed to file
+ stat:
+ path: '{{ remote_tmp_dir_test }}/sub1-link'
+ follow: True
+ register: file8_links_dest_stat
+
+- name: Get stat info for the file we intend to point to
+ stat:
+ path: '{{ remote_tmp_dir_test }}/sub1'
+ follow: False
+ register: file8_dest_stat
+
+- name: verify that the link was created correctly
+ assert:
+ that:
+ # file command reports it created something
+ - "file8_result.changed == true"
+ # file command created a link
+ - 'file8_link_stat["stat"]["islnk"]'
+ # Link points to the right path
+ - 'file8_link_stat["stat"]["lnk_target"] == "sub1"'
+ # The link target and the file we intended to link to have the same inode
+ - 'file8_links_dest_stat["stat"]["inode"] == file8_dest_stat["stat"]["inode"]'
+
+# test the file module using follow=yes, so that the target of a
+# symlink is modified, rather than the link itself
+
+- name: create a test file
+ copy:
+ dest: '{{remote_tmp_dir_test}}/test_follow'
+ content: 'this is a test file\n'
+ mode: 0666
+
+- name: create a symlink to the test file
+ file:
+ path: '{{remote_tmp_dir_test}}/test_follow_link'
+ src: './test_follow'
+ state: 'link'
+
+- name: modify the permissions on the link using follow=yes
+ file:
+ path: '{{remote_tmp_dir_test}}/test_follow_link'
+ mode: 0644
+ follow: yes
+ register: file9_result
+
+- name: stat the link target
+ stat:
+ path: '{{remote_tmp_dir_test}}/test_follow'
+ register: file9_stat
+
+- name: assert that the chmod worked
+ assert:
+ that:
+ - 'file9_result is changed'
+ - 'file9_stat["stat"]["mode"] == "0644"'
+
+#
+# Test modifying the permissions of a link itself
+#
+- name: attempt to modify the permissions of the link itself
+ file:
+ path: '{{remote_tmp_dir_test}}/test_follow_link'
+ state: 'link'
+ mode: 0600
+ follow: False
+ register: file10_result
+
+# Whether the link itself changed is platform dependent! (BSD vs Linux?)
+# Just check that the underlying file was not changed
+- name: stat the link target
+ stat:
+ path: '{{remote_tmp_dir_test}}/test_follow'
+ register: file10_target_stat
+
+- name: assert that the link target was unmodified
+ assert:
+ that:
+ - 'file10_target_stat["stat"]["mode"] == "0644"'
+
+
+# https://github.com/ansible/ansible/issues/56928
+- block:
+
+ - name: Create a testing file
+ file:
+ path: "{{ remote_tmp_dir_test }}/test_follow1"
+ state: touch
+
+ - name: Create a symlink and change mode of the original file, since follow == yes by default
+ file:
+ src: "{{ remote_tmp_dir_test }}/test_follow1"
+ dest: "{{ remote_tmp_dir_test }}/test_follow1_link"
+ state: link
+ mode: 0700
+
+ - name: stat the original file
+ stat:
+ path: "{{ remote_tmp_dir_test }}/test_follow1"
+ register: stat_out
+
+ - name: Check if the mode of the original file was set
+ assert:
+ that:
+ - 'stat_out.stat.mode == "0700"'
+
+ always:
+ - name: Clean up
+ file:
+ path: "{{ item }}"
+ state: absent
+ loop:
+ - "{{ remote_tmp_dir_test }}/test_follow1"
+ - "{{ remote_tmp_dir_test }}/test_follow1_link"
+
+# END #56928
+
+
+# Test failure with src and no state parameter
+- name: Specify src without state
+ file:
+ src: "{{ output_file }}"
+ dest: "{{ remote_tmp_dir_test }}/link.txt"
+ ignore_errors: yes
+ register: src_state
+
+- name: Ensure src without state failed
+ assert:
+ that:
+ - src_state is failed
+ - "'src option requires state to be' in src_state.msg"
+
+- name: Create without src
+ file:
+ state: link
+ dest: "{{ output_dir }}/link.txt"
+ ignore_errors: yes
+ register: create_without_src
+
+- name: Ensure create without src failed
+ assert:
+ that:
+ - create_without_src is failed
+ - "'src is required' in create_without_src.msg"
+
+# Test creating a symlink when the destination exists and is a file
+- name: create a test file
+ copy:
+ dest: '{{ remote_tmp_dir_test }}/file.txt'
+ content: 'this is a test file\n'
+ mode: 0666
+
+- name: Create a symlink with dest already a file
+ file:
+ src: '{{ output_file }}'
+ dest: '{{ remote_tmp_dir_test }}/file.txt'
+ state: link
+ ignore_errors: true
+ register: dest_is_existing_file_fail
+
+- name: Stat to make sure the symlink was not created
+ stat:
+ path: '{{ remote_tmp_dir_test }}/file.txt'
+ follow: false
+ register: dest_is_existing_file_fail_stat
+
+- name: Forcefully a symlink with dest already a file
+ file:
+ src: '{{ output_file }}'
+ dest: '{{ remote_tmp_dir_test }}/file.txt'
+ state: link
+ force: true
+ register: dest_is_existing_file_force
+
+- name: Stat to make sure the symlink was created
+ stat:
+ path: '{{ remote_tmp_dir_test }}/file.txt'
+ follow: false
+ register: dest_is_existing_file_force_stat
+
+- assert:
+ that:
+ - dest_is_existing_file_fail is failed
+ - not dest_is_existing_file_fail_stat.stat.islnk
+ - dest_is_existing_file_force is changed
+ - dest_is_existing_file_force_stat.stat.exists
+ - dest_is_existing_file_force_stat.stat.islnk
diff --git a/test/integration/targets/file/tasks/unicode_path.yml b/test/integration/targets/file/tasks/unicode_path.yml
new file mode 100644
index 0000000..a4902a9
--- /dev/null
+++ b/test/integration/targets/file/tasks/unicode_path.yml
@@ -0,0 +1,10 @@
+- name: create local file with unicode filename and content
+ lineinfile:
+ dest: "{{ remote_tmp_dir_test }}/语/汉语.txt"
+ create: true
+ line: 汉语
+
+- name: remove local file with unicode filename and content
+ file:
+ path: "{{ remote_tmp_dir_test }}/语/汉语.txt"
+ state: absent
diff --git a/test/integration/targets/filter_core/aliases b/test/integration/targets/filter_core/aliases
new file mode 100644
index 0000000..3005e4b
--- /dev/null
+++ b/test/integration/targets/filter_core/aliases
@@ -0,0 +1 @@
+shippable/posix/group4
diff --git a/test/integration/targets/filter_core/files/9851.txt b/test/integration/targets/filter_core/files/9851.txt
new file mode 100644
index 0000000..70b1279
--- /dev/null
+++ b/test/integration/targets/filter_core/files/9851.txt
@@ -0,0 +1,3 @@
+ [{
+ "k": "Quotes \"'\n"
+}]
diff --git a/test/integration/targets/filter_core/files/fileglob/one.txt b/test/integration/targets/filter_core/files/fileglob/one.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/filter_core/files/fileglob/one.txt
diff --git a/test/integration/targets/filter_core/files/fileglob/two.txt b/test/integration/targets/filter_core/files/fileglob/two.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/filter_core/files/fileglob/two.txt
diff --git a/test/integration/targets/filter_core/files/foo.txt b/test/integration/targets/filter_core/files/foo.txt
new file mode 100644
index 0000000..9bd9b63
--- /dev/null
+++ b/test/integration/targets/filter_core/files/foo.txt
@@ -0,0 +1,69 @@
+This is a test of various filter plugins found in Ansible (ex: core.py), and
+not so much a test of the core filters in Jinja2.
+
+Dumping the same structure to YAML
+
+- this is a list element
+- this: is a hash element in a list
+ warp: 9
+ where: endor
+
+
+Dumping the same structure to JSON, but don't pretty print
+
+["this is a list element", {"this": "is a hash element in a list", "warp": 9, "where": "endor"}]
+
+Dumping the same structure to YAML, but don't pretty print
+
+- this is a list element
+- {this: is a hash element in a list, warp: 9, where: endor}
+
+
+From a recorded task, the changed, failed, success, and skipped
+tests are shortcuts to ask if those tasks produced changes, failed,
+succeeded, or skipped (as one might guess).
+
+Changed = True
+Failed = False
+Success = True
+Skipped = False
+
+The mandatory filter fails if a variable is not defined and returns the value.
+To avoid breaking this test, this variable is already defined.
+
+a = 1
+
+There are various casts available
+
+int = 1
+bool = True
+
+String quoting
+
+quoted = quoted
+
+The fileglob module returns the list of things matching a pattern.
+
+fileglob = one.txt, two.txt
+
+There are also various string operations that work on paths. These do not require
+files to exist and are passthrus to the python os.path functions
+
+/etc/motd with basename = motd
+/etc/motd with dirname = /etc
+
+path_join_simple = /etc/subdir/test
+path_join_with_slash = /test
+path_join_relative = etc/subdir/test
+
+TODO: realpath follows symlinks. There isn't a test for this just now.
+
+TODO: add tests for set theory operations like union
+
+regex_replace = bar
+# Check regex_replace with multiline
+#bar
+#bart
+regex_search = 0001
+regex_findall = ["car", "tar", "bar"]
+regex_escape = \^f\.\*o\(\.\*\)\$
diff --git a/test/integration/targets/filter_core/handle_undefined_type_errors.yml b/test/integration/targets/filter_core/handle_undefined_type_errors.yml
new file mode 100644
index 0000000..7062880
--- /dev/null
+++ b/test/integration/targets/filter_core/handle_undefined_type_errors.yml
@@ -0,0 +1,29 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - debug: msg={{item}}
+ with_dict: '{{myundef}}'
+ when:
+ - myundef is defined
+ register: shouldskip
+
+ - name: check if skipped
+ assert:
+ that:
+ - shouldskip is skipped
+
+ - debug: msg={{item}}
+ loop: '{{myundef|dict2items}}'
+ when:
+ - myundef is defined
+
+ - debug: msg={{item}}
+ with_dict: '{{myundef}}'
+ register: notskipped
+ ignore_errors: true
+
+ - name: check it failed
+ assert:
+ that:
+ - notskipped is not skipped
+ - notskipped is failed
diff --git a/test/integration/targets/filter_core/host_vars/localhost b/test/integration/targets/filter_core/host_vars/localhost
new file mode 100644
index 0000000..a8926a5
--- /dev/null
+++ b/test/integration/targets/filter_core/host_vars/localhost
@@ -0,0 +1 @@
+a: 1
diff --git a/test/integration/targets/filter_core/meta/main.yml b/test/integration/targets/filter_core/meta/main.yml
new file mode 100644
index 0000000..e430ea6
--- /dev/null
+++ b/test/integration/targets/filter_core/meta/main.yml
@@ -0,0 +1,3 @@
+dependencies:
+ - role: setup_passlib
+ when: ansible_facts.distribution == 'MacOSX'
diff --git a/test/integration/targets/filter_core/runme.sh b/test/integration/targets/filter_core/runme.sh
new file mode 100755
index 0000000..c055603
--- /dev/null
+++ b/test/integration/targets/filter_core/runme.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ANSIBLE_ROLES_PATH=../ ansible-playbook runme.yml "$@"
+ANSIBLE_ROLES_PATH=../ ansible-playbook handle_undefined_type_errors.yml "$@"
diff --git a/test/integration/targets/filter_core/runme.yml b/test/integration/targets/filter_core/runme.yml
new file mode 100644
index 0000000..4af4b23
--- /dev/null
+++ b/test/integration/targets/filter_core/runme.yml
@@ -0,0 +1,3 @@
+- hosts: localhost
+ roles:
+ - { role: filter_core }
diff --git a/test/integration/targets/filter_core/tasks/main.yml b/test/integration/targets/filter_core/tasks/main.yml
new file mode 100644
index 0000000..2d08419
--- /dev/null
+++ b/test/integration/targets/filter_core/tasks/main.yml
@@ -0,0 +1,708 @@
+# test code for filters
+# Copyright: (c) 2014, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright: (c) 2019, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Note: |groupby is already tested by the `groupby_filter` target.
+
+- set_fact:
+ output_dir: "{{ lookup('env', 'OUTPUT_DIR') }}"
+
+- name: a dummy task to test the changed and success filters
+ shell: echo hi
+ register: some_registered_var
+
+- debug:
+ var: some_registered_var
+
+- name: Verify that we workaround a py26 json bug
+ template:
+ src: py26json.j2
+ dest: "{{ output_dir }}/py26json.templated"
+ mode: 0644
+
+- name: 9851 - Verify that we don't trigger https://github.com/ansible/ansible/issues/9851
+ copy:
+ content: " [{{ item | to_nice_json }}]"
+ dest: "{{ output_dir }}/9851.out"
+ with_items:
+ - {"k": "Quotes \"'\n"}
+
+- name: 9851 - copy known good output into place
+ copy:
+ src: 9851.txt
+ dest: "{{ output_dir }}/9851.txt"
+
+- name: 9851 - Compare generated json to known good
+ shell: diff -w {{ output_dir }}/9851.out {{ output_dir }}/9851.txt
+ register: diff_result_9851
+
+- name: 9851 - verify generated file matches known good
+ assert:
+ that:
+ - 'diff_result_9851.stdout == ""'
+
+- name: fill in a basic template
+ template:
+ src: foo.j2
+ dest: "{{ output_dir }}/foo.templated"
+ mode: 0644
+ register: template_result
+
+- name: copy known good into place
+ copy:
+ src: foo.txt
+ dest: "{{ output_dir }}/foo.txt"
+
+- name: compare templated file to known good
+ shell: diff -w {{ output_dir }}/foo.templated {{ output_dir }}/foo.txt
+ register: diff_result
+
+- name: verify templated file matches known good
+ assert:
+ that:
+ - 'diff_result.stdout == ""'
+
+- name: Test extract
+ assert:
+ that:
+ - '"c" == 2 | extract(["a", "b", "c"])'
+ - '"b" == 1 | extract(["a", "b", "c"])'
+ - '"a" == 0 | extract(["a", "b", "c"])'
+
+- name: Container lookups with extract
+ assert:
+ that:
+ - "'x' == [0]|map('extract',['x','y'])|list|first"
+ - "'y' == [1]|map('extract',['x','y'])|list|first"
+ - "42 == ['x']|map('extract',{'x':42,'y':31})|list|first"
+ - "31 == ['x','y']|map('extract',{'x':42,'y':31})|list|last"
+ - "'local' == ['localhost']|map('extract',hostvars,'ansible_connection')|list|first"
+ - "'local' == ['localhost']|map('extract',hostvars,['ansible_connection'])|list|first"
+
+- name: Test extract filter with defaults
+ vars:
+ container:
+ key:
+ subkey: value
+ assert:
+ that:
+ - "'key' | extract(badcontainer) | default('a') == 'a'"
+ - "'key' | extract(badcontainer, 'subkey') | default('a') == 'a'"
+ - "('key' | extract(badcontainer)).subkey | default('a') == 'a'"
+ - "'badkey' | extract(container) | default('a') == 'a'"
+ - "'badkey' | extract(container, 'subkey') | default('a') == 'a'"
+ - "('badkey' | extract(container)).subsubkey | default('a') == 'a'"
+ - "'key' | extract(container, 'badsubkey') | default('a') == 'a'"
+ - "'key' | extract(container, ['badsubkey', 'subsubkey']) | default('a') == 'a'"
+ - "('key' | extract(container, 'badsubkey')).subsubkey | default('a') == 'a'"
+ - "'badkey' | extract(hostvars) | default('a') == 'a'"
+ - "'badkey' | extract(hostvars, 'subkey') | default('a') == 'a'"
+ - "('badkey' | extract(hostvars)).subsubkey | default('a') == 'a'"
+ - "'localhost' | extract(hostvars, 'badsubkey') | default('a') == 'a'"
+ - "'localhost' | extract(hostvars, ['badsubkey', 'subsubkey']) | default('a') == 'a'"
+ - "('localhost' | extract(hostvars, 'badsubkey')).subsubkey | default('a') == 'a'"
+
+- name: Test hash filter
+ assert:
+ that:
+ - '"{{ "hash" | hash("sha1") }}" == "2346ad27d7568ba9896f1b7da6b5991251debdf2"'
+ - '"{{ "café" | hash("sha1") }}" == "f424452a9673918c6f09b0cdd35b20be8e6ae7d7"'
+
+- name: Test unsupported hash type
+ debug:
+ msg: "{{ 'hash' | hash('unsupported_hash_type') }}"
+ ignore_errors: yes
+ register: unsupported_hash_type_res
+
+- assert:
+ that:
+ - "unsupported_hash_type_res is failed"
+ - "'unsupported hash type' in unsupported_hash_type_res.msg"
+
+- name: Flatten tests
+ tags: flatten
+ block:
+ - name: use flatten
+ set_fact:
+ flat_full: '{{orig_list|flatten}}'
+ flat_one: '{{orig_list|flatten(levels=1)}}'
+ flat_two: '{{orig_list|flatten(levels=2)}}'
+ flat_tuples: '{{ [1,3] | zip([2,4]) | list | flatten }}'
+ flat_full_null: '{{list_with_nulls|flatten(skip_nulls=False)}}'
+ flat_one_null: '{{list_with_nulls|flatten(levels=1, skip_nulls=False)}}'
+ flat_two_null: '{{list_with_nulls|flatten(levels=2, skip_nulls=False)}}'
+ flat_full_nonull: '{{list_with_nulls|flatten(skip_nulls=True)}}'
+ flat_one_nonull: '{{list_with_nulls|flatten(levels=1, skip_nulls=True)}}'
+ flat_two_nonull: '{{list_with_nulls|flatten(levels=2, skip_nulls=True)}}'
+
+ - name: Verify flatten filter works as expected
+ assert:
+ that:
+ - flat_full == [1, 2, 3, 4, 5, 6, 7]
+ - flat_one == [1, 2, 3, [4, [5]], 6, 7]
+ - flat_two == [1, 2, 3, 4, [5], 6, 7]
+ - flat_tuples == [1, 2, 3, 4]
+ - flat_full_null == [1, 'None', 3, 4, 5, 6, 7]
+ - flat_one_null == [1, 'None', 3, [4, [5]], 6, 7]
+ - flat_two_null == [1, 'None', 3, 4, [5], 6, 7]
+ - flat_full_nonull == [1, 3, 4, 5, 6, 7]
+ - flat_one_nonull == [1, 3, [4, [5]], 6, 7]
+ - flat_two_nonull == [1, 3, 4, [5], 6, 7]
+ - list_with_subnulls|flatten(skip_nulls=False) == [1, 2, 'None', 4, 5, 6, 7]
+ - list_with_subnulls|flatten(skip_nulls=True) == [1, 2, 4, 5, 6, 7]
+ vars:
+ orig_list: [1, 2, [3, [4, [5]], 6], 7]
+ list_with_nulls: [1, None, [3, [4, [5]], 6], 7]
+ list_with_subnulls: [1, 2, [None, [4, [5]], 6], 7]
+
+- name: Test base64 filter
+ assert:
+ that:
+ - "'Ansible - ãらã¨ã¿\n' | b64encode == 'QW5zaWJsZSAtIOOBj+OCieOBqOOBvwo='"
+ - "'QW5zaWJsZSAtIOOBj+OCieOBqOOBvwo=' | b64decode == 'Ansible - ãらã¨ã¿\n'"
+ - "'Ansible - ãらã¨ã¿\n' | b64encode(encoding='utf-16-le') == 'QQBuAHMAaQBiAGwAZQAgAC0AIABPMIkwaDB/MAoA'"
+ - "'QQBuAHMAaQBiAGwAZQAgAC0AIABPMIkwaDB/MAoA' | b64decode(encoding='utf-16-le') == 'Ansible - ãらã¨ã¿\n'"
+
+- set_fact:
+ x:
+ x: x
+ key: x
+ y:
+ y: y
+ key: y
+ z:
+ z: z
+ key: z
+
+ # Most complicated combine dicts from the documentation
+ default:
+ a:
+ a':
+ x: default_value
+ y: default_value
+ list:
+ - default_value
+ b:
+ - 1
+ - 1
+ - 2
+ - 3
+ patch:
+ a:
+ a':
+ y: patch_value
+ z: patch_value
+ list:
+ - patch_value
+ b:
+ - 3
+ - 4
+ - 4
+ - key: value
+ result:
+ a:
+ a':
+ x: default_value
+ y: patch_value
+ z: patch_value
+ list:
+ - default_value
+ - patch_value
+ b:
+ - 1
+ - 1
+ - 2
+ - 3
+ - 4
+ - 4
+ - key: value
+
+- name: Verify combine fails with extra kwargs
+ set_fact:
+ foo: "{{[1] | combine(foo='bar')}}"
+ ignore_errors: yes
+ register: combine_fail
+
+- name: Verify combine filter
+ assert:
+ that:
+ - "([x] | combine) == x"
+ - "(x | combine(y)) == {'x': 'x', 'y': 'y', 'key': 'y'}"
+ - "(x | combine(y, z)) == {'x': 'x', 'y': 'y', 'z': 'z', 'key': 'z'}"
+ - "([x, y, z] | combine) == {'x': 'x', 'y': 'y', 'z': 'z', 'key': 'z'}"
+ - "([x, y] | combine(z)) == {'x': 'x', 'y': 'y', 'z': 'z', 'key': 'z'}"
+ - "None|combine == {}"
+ # more advanced dict combination tests are done in the "merge_hash" function unit tests
+ # but even though it's redundant with those unit tests, we do at least the most complicated example of the documentation here
+ - "(default | combine(patch, recursive=True, list_merge='append_rp')) == result"
+ - combine_fail is failed
+ - "combine_fail.msg == \"'recursive' and 'list_merge' are the only valid keyword arguments\""
+
+- set_fact:
+ combine: "{{[x, [y]] | combine(z)}}"
+ ignore_errors: yes
+ register: result
+
+- name: Ensure combining objects which aren't dictionaries throws an error
+ assert:
+ that:
+ - "result.msg.startswith(\"failed to combine variables, expected dicts but got\")"
+
+- name: Ensure combining two dictionaries containing undefined variables provides a helpful error
+ block:
+ - set_fact:
+ foo:
+ key1: value1
+
+ - set_fact:
+ combined: "{{ foo | combine({'key2': undef_variable}) }}"
+ ignore_errors: yes
+ register: result
+
+ - assert:
+ that:
+ - "result.msg.startswith('The task includes an option with an undefined variable')"
+
+ - set_fact:
+ combined: "{{ foo | combine({'key2': {'nested': [undef_variable]}})}}"
+ ignore_errors: yes
+ register: result
+
+ - assert:
+ that:
+ - "result.msg.startswith('The task includes an option with an undefined variable')"
+
+- name: regex_search
+ set_fact:
+ match_case: "{{ 'hello' | regex_search('HELLO', ignorecase=false) }}"
+ ignore_case: "{{ 'hello' | regex_search('HELLO', ignorecase=true) }}"
+ single_line: "{{ 'hello\nworld' | regex_search('^world', multiline=false) }}"
+ multi_line: "{{ 'hello\nworld' | regex_search('^world', multiline=true) }}"
+ named_groups: "{{ 'goodbye' | regex_search('(?P<first>good)(?P<second>bye)', '\\g<second>', '\\g<first>') }}"
+ numbered_groups: "{{ 'goodbye' | regex_search('(good)(bye)', '\\2', '\\1') }}"
+ no_match_is_none_inline: "{{ 'hello' | regex_search('world') == none }}"
+
+- name: regex_search unknown argument (failure expected)
+ set_fact:
+ unknown_arg: "{{ 'hello' | regex_search('hello', 'unknown') }}"
+ ignore_errors: yes
+ register: failure
+
+- name: regex_search check
+ assert:
+ that:
+ - match_case == ''
+ - ignore_case == 'hello'
+ - single_line == ''
+ - multi_line == 'world'
+ - named_groups == ['bye', 'good']
+ - numbered_groups == ['bye', 'good']
+ - no_match_is_none_inline
+ - failure is failed
+
+- name: Verify to_bool
+ assert:
+ that:
+ - 'None|bool == None'
+ - 'False|bool == False'
+ - '"TrUe"|bool == True'
+ - '"FalSe"|bool == False'
+ - '7|bool == False'
+
+- name: Verify to_datetime
+ assert:
+ that:
+ - '"1993-03-26 01:23:45"|to_datetime < "1994-03-26 01:23:45"|to_datetime'
+
+- name: strftime invalid argument (failure expected)
+ set_fact:
+ foo: "{{ '%Y' | strftime('foo') }}"
+ ignore_errors: yes
+ register: strftime_fail
+
+- name: Verify strftime
+ assert:
+ that:
+ - '"%Y-%m-%d"|strftime(1585247522) == "2020-03-26"'
+ - '"%Y-%m-%d"|strftime("1585247522.0") == "2020-03-26"'
+ - '("%Y"|strftime(None)).startswith("20")' # Current date, can't check much there.
+ - strftime_fail is failed
+ - '"Invalid value for epoch value" in strftime_fail.msg'
+
+- name: Verify case-insensitive regex_replace
+ assert:
+ that:
+ - '"hElLo there"|regex_replace("hello", "hi", ignorecase=True) == "hi there"'
+
+- name: Verify case-insensitive regex_findall
+ assert:
+ that:
+ - '"hEllo there heLlo haha HELLO there"|regex_findall("h.... ", ignorecase=True)|length == 3'
+
+- name: Verify ternary
+ assert:
+ that:
+ - 'True|ternary("seven", "eight") == "seven"'
+ - 'None|ternary("seven", "eight") == "eight"'
+ - 'None|ternary("seven", "eight", "nine") == "nine"'
+ - 'False|ternary("seven", "eight") == "eight"'
+ - '123|ternary("seven", "eight") == "seven"'
+ - '"haha"|ternary("seven", "eight") == "seven"'
+
+- name: Verify regex_escape raises on posix_extended (failure expected)
+ set_fact:
+ foo: '{{"]]^"|regex_escape(re_type="posix_extended")}}'
+ ignore_errors: yes
+ register: regex_escape_fail_1
+
+- name: Verify regex_escape raises on other re_type (failure expected)
+ set_fact:
+ foo: '{{"]]^"|regex_escape(re_type="haha")}}'
+ ignore_errors: yes
+ register: regex_escape_fail_2
+
+- name: Verify regex_escape with re_type other than 'python'
+ assert:
+ that:
+ - '"]]^"|regex_escape(re_type="posix_basic") == "\\]\\]\\^"'
+ - regex_escape_fail_1 is failed
+ - 'regex_escape_fail_1.msg == "Regex type (posix_extended) not yet implemented"'
+ - regex_escape_fail_2 is failed
+ - 'regex_escape_fail_2.msg == "Invalid regex type (haha)"'
+
+- name: Verify from_yaml and from_yaml_all
+ assert:
+ that:
+ - "'---\nbananas: yellow\napples: red'|from_yaml == {'bananas': 'yellow', 'apples': 'red'}"
+ - "2|from_yaml == 2"
+ - "'---\nbananas: yellow\n---\napples: red'|from_yaml_all|list == [{'bananas': 'yellow'}, {'apples': 'red'}]"
+ - "2|from_yaml_all == 2"
+ - "unsafe_fruit|from_yaml == {'bananas': 'yellow', 'apples': 'red'}"
+ - "unsafe_fruit_all|from_yaml_all|list == [{'bananas': 'yellow'}, {'apples': 'red'}]"
+ vars:
+ unsafe_fruit: !unsafe |
+ ---
+ bananas: yellow
+ apples: red
+ unsafe_fruit_all: !unsafe |
+ ---
+ bananas: yellow
+ ---
+ apples: red
+
+- name: Verify random raises on non-iterable input (failure expected)
+ set_fact:
+ foo: '{{None|random}}'
+ ignore_errors: yes
+ register: random_fail_1
+
+- name: Verify random raises on iterable input with start (failure expected)
+ set_fact:
+ foo: '{{[1,2,3]|random(start=2)}}'
+ ignore_errors: yes
+ register: random_fail_2
+
+- name: Verify random raises on iterable input with step (failure expected)
+ set_fact:
+ foo: '{{[1,2,3]|random(step=2)}}'
+ ignore_errors: yes
+ register: random_fail_3
+
+- name: Verify random
+ assert:
+ that:
+ - '2|random in [0,1]'
+ - '2|random(seed=1337) in [0,1]'
+ - '["a", "b"]|random in ["a", "b"]'
+ - '20|random(start=10) in range(10, 20)'
+ - '20|random(start=10, step=2) % 2 == 0'
+ - random_fail_1 is failure
+ - '"random can only be used on" in random_fail_1.msg'
+ - random_fail_2 is failure
+ - '"start and step can only be used" in random_fail_2.msg'
+ - random_fail_3 is failure
+ - '"start and step can only be used" in random_fail_3.msg'
+
+# It's hard to actually verify much here since the result is, well, random.
+- name: Verify randomize_list
+ assert:
+ that:
+ - '[1,3,5,7,9]|shuffle|length == 5'
+ - '[1,3,5,7,9]|shuffle(seed=1337)|length == 5'
+ - '22|shuffle == 22'
+
+- name: Verify password_hash throws on weird salt_size type
+ set_fact:
+ foo: '{{"hey"|password_hash(salt_size=[999])}}'
+ ignore_errors: yes
+ register: password_hash_1
+
+- name: Verify password_hash throws on weird hashtype
+ set_fact:
+ foo: '{{"hey"|password_hash(hashtype="supersecurehashtype")}}'
+ ignore_errors: yes
+ register: password_hash_2
+
+- name: Verify password_hash
+ assert:
+ that:
+ - "'what in the WORLD is up?'|password_hash|length == 120 or 'what in the WORLD is up?'|password_hash|length == 106"
+ # This throws a vastly different error on py2 vs py3, so we just check
+ # that it's a failure, not a substring of the exception.
+ - password_hash_1 is failed
+ - password_hash_2 is failed
+ - "'not support' in password_hash_2.msg"
+
+- name: Verify to_uuid throws on weird namespace
+ set_fact:
+ foo: '{{"hey"|to_uuid(namespace=22)}}'
+ ignore_errors: yes
+ register: to_uuid_1
+
+- name: Verify to_uuid
+ assert:
+ that:
+ - '"monkeys"|to_uuid == "0d03a178-da0f-5b51-934e-cda9c76578c3"'
+ - to_uuid_1 is failed
+ - '"Invalid value" in to_uuid_1.msg'
+
+- name: Verify mandatory throws on undefined variable
+ set_fact:
+ foo: '{{hey|mandatory}}'
+ ignore_errors: yes
+ register: mandatory_1
+
+- name: Verify mandatory throws on undefined variable with custom message
+ set_fact:
+ foo: '{{hey|mandatory("You did not give me a variable. I am a sad wolf.")}}'
+ ignore_errors: yes
+ register: mandatory_2
+
+- name: Set a variable
+ set_fact:
+ mandatory_demo: 123
+
+- name: Verify mandatory
+ assert:
+ that:
+ - '{{mandatory_demo|mandatory}} == 123'
+ - mandatory_1 is failed
+ - "mandatory_1.msg == \"Mandatory variable 'hey' not defined.\""
+ - mandatory_2 is failed
+ - "mandatory_2.msg == 'You did not give me a variable. I am a sad wolf.'"
+
+- name: Verify undef throws if resolved
+ set_fact:
+ foo: '{{ fail_foo }}'
+ vars:
+ fail_foo: '{{ undef("Expected failure") }}'
+ ignore_errors: yes
+ register: fail_1
+
+- name: Setup fail_foo for overriding in test
+ block:
+ - name: Verify undef not executed if overridden
+ set_fact:
+ foo: '{{ fail_foo }}'
+ vars:
+ fail_foo: 'overridden value'
+ register: fail_2
+ vars:
+ fail_foo: '{{ undef(hint="Expected failure") }}'
+
+- name: Verify undef is inspectable
+ debug:
+ var: fail_foo
+ vars:
+ fail_foo: '{{ undef("Expected failure") }}'
+ register: fail_3
+
+- name: Verify undef
+ assert:
+ that:
+ - fail_1 is failed
+ - not (fail_2 is failed)
+ - not (fail_3 is failed)
+
+- name: Verify comment
+ assert:
+ that:
+ - '"boo!"|comment == "#\n# boo!\n#"'
+ - '"boo!"|comment(decoration="-- ") == "--\n-- boo!\n--"'
+ - '"boo!"|comment(style="cblock") == "/*\n *\n * boo!\n *\n */"'
+ - '"boo!"|comment(decoration="") == "boo!\n"'
+ - '"boo!"|comment(prefix="\n", prefix_count=20) == "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n# boo!\n#"'
+
+- name: Verify subelements throws on invalid obj
+ set_fact:
+ foo: '{{True|subelements("foo")}}'
+ ignore_errors: yes
+ register: subelements_1
+
+- name: Verify subelements throws on invalid subelements arg
+ set_fact:
+ foo: '{{{}|subelements(17)}}'
+ ignore_errors: yes
+ register: subelements_2
+
+- name: Set demo data for subelements
+ set_fact:
+ subelements_demo: '{{ [{"name": "alice", "groups": ["wheel"], "authorized": ["/tmp/alice/onekey.pub"]}] }}'
+
+- name: Verify subelements throws on bad key
+ set_fact:
+ foo: '{{subelements_demo | subelements("does not compute")}}'
+ ignore_errors: yes
+ register: subelements_3
+
+- name: Verify subelements throws on key pointing to bad value
+ set_fact:
+ foo: '{{subelements_demo | subelements("name")}}'
+ ignore_errors: yes
+ register: subelements_4
+
+- name: Verify subelements throws on list of keys ultimately pointing to bad value
+ set_fact:
+ foo: '{{subelements_demo | subelements(["groups", "authorized"])}}'
+ ignore_errors: yes
+ register: subelements_5
+
+- name: Verify subelements
+ assert:
+ that:
+ - subelements_1 is failed
+ - 'subelements_1.msg == "obj must be a list of dicts or a nested dict"'
+ - subelements_2 is failed
+ - '"subelements must be a list or a string" in subelements_2.msg'
+ - 'subelements_demo|subelements("does not compute", skip_missing=True) == []'
+ - subelements_3 is failed
+ - '"could not find" in subelements_3.msg'
+ - subelements_4 is failed
+ - '"should point to a list" in subelements_4.msg'
+ - subelements_5 is failed
+ - '"should point to a dictionary" in subelements_5.msg'
+ - 'subelements_demo|subelements("groups") == [({"name": "alice", "groups": ["wheel"], "authorized": ["/tmp/alice/onekey.pub"]}, "wheel")]'
+ - 'subelements_demo|subelements(["groups"]) == [({"name": "alice", "groups": ["wheel"], "authorized": ["/tmp/alice/onekey.pub"]}, "wheel")]'
+
+
+- name: Verify dict2items throws on non-Mapping
+ set_fact:
+ foo: '{{True|dict2items}}'
+ ignore_errors: yes
+ register: dict2items_fail
+
+- name: Verify dict2items
+ assert:
+ that:
+ - '{"foo": "bar", "banana": "fruit"}|dict2items == [{"key": "foo", "value": "bar"}, {"key": "banana", "value": "fruit"}]'
+ - dict2items_fail is failed
+ - '"dict2items requires a dictionary" in dict2items_fail.msg'
+
+- name: Verify items2dict throws on non-list
+ set_fact:
+ foo: '{{True|items2dict}}'
+ ignore_errors: yes
+ register: items2dict_fail
+
+- name: Verify items2dict
+ assert:
+ that:
+ - '[{"key": "foo", "value": "bar"}, {"key": "banana", "value": "fruit"}]|items2dict == {"foo": "bar", "banana": "fruit"}'
+ - items2dict_fail is failed
+ - '"items2dict requires a list" in items2dict_fail.msg'
+
+- name: Verify items2dict throws on list of non-Mapping
+ set_fact:
+ foo: '{{[True]|items2dict}}'
+ ignore_errors: yes
+ register: items2dict_fail
+
+- name: Verify items2dict
+ assert:
+ that:
+ - items2dict_fail is failed
+ - '"items2dict requires a list of dictionaries" in items2dict_fail.msg'
+
+- name: Verify items2dict throws on missing key
+ set_fact:
+ foo: '{{ list_of_dicts | items2dict}}'
+ vars:
+ list_of_dicts: [{"key": "foo", "value": "bar"}, {"notkey": "banana", "value": "fruit"}]
+ ignore_errors: yes
+ register: items2dict_fail
+
+- name: Verify items2dict
+ assert:
+ that:
+ - items2dict_fail is failed
+ - error in items2dict_fail.msg
+ vars:
+ error: "items2dict requires each dictionary in the list to contain the keys 'key' and 'value'"
+
+- name: Verify items2dict throws on missing value
+ set_fact:
+ foo: '{{ list_of_dicts | items2dict}}'
+ vars:
+ list_of_dicts: [{"key": "foo", "value": "bar"}, {"key": "banana", "notvalue": "fruit"}]
+ ignore_errors: yes
+ register: items2dict_fail
+
+- name: Verify items2dict
+ assert:
+ that:
+ - items2dict_fail is failed
+ - error in items2dict_fail.msg
+ vars:
+ error: "items2dict requires each dictionary in the list to contain the keys 'key' and 'value'"
+
+- name: Verify path_join throws on non-string and non-sequence
+ set_fact:
+ foo: '{{True|path_join}}'
+ ignore_errors: yes
+ register: path_join_fail
+
+- name: Verify path_join
+ assert:
+ that:
+ - '"foo"|path_join == "foo"'
+ - '["foo", "bar"]|path_join in ["foo/bar", "foo\bar"]'
+ - path_join_fail is failed
+ - '"expects string or sequence" in path_join_fail.msg'
+
+- name: Verify type_debug
+ assert:
+ that:
+ - '"foo"|type_debug == "str"'
+
+- name: Assert that a jinja2 filter that produces a map is auto unrolled
+ assert:
+ that:
+ - thing|map(attribute="bar")|first == 123
+ - thing_result|first == 123
+ - thing_items|first|last == 123
+ - thing_range == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+ vars:
+ thing:
+ - bar: 123
+ thing_result: '{{ thing|map(attribute="bar") }}'
+ thing_dict:
+ bar: 123
+ thing_items: '{{ thing_dict.items() }}'
+ thing_range: '{{ range(10) }}'
+
+- name: Assert that quote works on None
+ assert:
+ that:
+ - thing|quote == "''"
+ vars:
+ thing: null
+
+- name: split filter
+ assert:
+ that:
+ - splitty|map('split', ',')|flatten|map('int') == [1, 2, 3, 4, 5, 6]
+ vars:
+ splitty:
+ - "1,2,3"
+ - "4,5,6"
diff --git a/test/integration/targets/filter_core/templates/foo.j2 b/test/integration/targets/filter_core/templates/foo.j2
new file mode 100644
index 0000000..a69ba5e
--- /dev/null
+++ b/test/integration/targets/filter_core/templates/foo.j2
@@ -0,0 +1,62 @@
+This is a test of various filter plugins found in Ansible (ex: core.py), and
+not so much a test of the core filters in Jinja2.
+
+Dumping the same structure to YAML
+
+{{ some_structure | to_nice_yaml }}
+
+Dumping the same structure to JSON, but don't pretty print
+
+{{ some_structure | to_json(sort_keys=true) }}
+
+Dumping the same structure to YAML, but don't pretty print
+
+{{ some_structure | to_yaml }}
+
+From a recorded task, the changed, failed, success, and skipped
+tests are shortcuts to ask if those tasks produced changes, failed,
+succeeded, or skipped (as one might guess).
+
+Changed = {{ some_registered_var is changed }}
+Failed = {{ some_registered_var is failed }}
+Success = {{ some_registered_var is successful }}
+Skipped = {{ some_registered_var is skipped }}
+
+The mandatory filter fails if a variable is not defined and returns the value.
+To avoid breaking this test, this variable is already defined.
+
+a = {{ a | mandatory }}
+
+There are various casts available
+
+int = {{ a | int }}
+bool = {{ 1 | bool }}
+
+String quoting
+
+quoted = {{ 'quoted' | quote }}
+
+The fileglob module returns the list of things matching a pattern.
+
+fileglob = {{ (playbook_dir + '/files/fileglob/*') | fileglob | map('basename') | sort | join(', ') }}
+
+There are also various string operations that work on paths. These do not require
+files to exist and are passthrus to the python os.path functions
+
+/etc/motd with basename = {{ '/etc/motd' | basename }}
+/etc/motd with dirname = {{ '/etc/motd' | dirname }}
+
+path_join_simple = {{ ('/etc', 'subdir', 'test') | path_join }}
+path_join_with_slash = {{ ('/etc', 'subdir', '/test') | path_join }}
+path_join_relative = {{ ('etc', 'subdir', 'test') | path_join }}
+
+TODO: realpath follows symlinks. There isn't a test for this just now.
+
+TODO: add tests for set theory operations like union
+
+regex_replace = {{ 'foo' | regex_replace('^foo', 'bar') }}
+# Check regex_replace with multiline
+{{ '#foo\n#foot' | regex_replace('^#foo', '#bar', multiline=True) }}
+regex_search = {{ 'test_value_0001' | regex_search('([0-9]+)$')}}
+regex_findall = {{ 'car\ntar\nfoo\nbar\n' | regex_findall('^.ar$', multiline=True)|to_json }}
+regex_escape = {{ '^f.*o(.*)$' | regex_escape() }}
diff --git a/test/integration/targets/filter_core/templates/py26json.j2 b/test/integration/targets/filter_core/templates/py26json.j2
new file mode 100644
index 0000000..dba62ad
--- /dev/null
+++ b/test/integration/targets/filter_core/templates/py26json.j2
@@ -0,0 +1,2 @@
+Provoke a python2.6 json bug
+{{ hostvars[inventory_hostname] | to_nice_json }}
diff --git a/test/integration/targets/filter_core/vars/main.yml b/test/integration/targets/filter_core/vars/main.yml
new file mode 100644
index 0000000..aedecd8
--- /dev/null
+++ b/test/integration/targets/filter_core/vars/main.yml
@@ -0,0 +1,106 @@
+some_structure:
+ - "this is a list element"
+ -
+ this: "is a hash element in a list"
+ warp: 9
+ where: endor
+
+other_data:
+ level1:
+ foo: bar
+ blip: baz
+ nested:
+ abc: def
+ ghi: xyz
+ alist:
+ - alpha
+ - beta
+ - charlie
+ - delta
+ level2:
+ asd: df
+ xc: dsdfsfsd
+ nested:
+ abc: foo
+ alist:
+ - zebra
+ - yellow
+ - xray
+
+# from https://github.com/ansible/ansible/issues/20379#issuecomment-280492883
+example_20379: {
+ "ApplicationVersions": [
+ {
+ "ApplicationName": "gitlab_ci_elasticbeanstalk",
+ "Status": "UNPROCESSED",
+ "VersionLabel": "test-npm-check-626-1313",
+ "Description": "bla",
+ "DateCreated": "2017-01-22T02:02:31.798Z",
+ "DateUpdated": "2017-01-22T02:02:31.798Z",
+ "SourceBundle": {
+ "S3Bucket": "bla",
+ "S3Key": "ci/beanstalk/gitlab_ci_elasticbeanstalk/gitlab_ci_elasticbeanstalk-626-1313.war"
+ }
+ },
+ {
+ "ApplicationName": "gitlab_ci_elasticbeanstalk",
+ "Status": "UNPROCESSED",
+ "VersionLabel": "terminate-611-1289",
+ "Description": "bla",
+ "DateCreated": "2017-01-20T00:34:29.864Z",
+ "DateUpdated": "2017-01-20T00:34:29.864Z",
+ "SourceBundle": {
+ "S3Bucket": "bla",
+ "S3Key": "ci/beanstalk/gitlab_ci_elasticbeanstalk/gitlab_ci_elasticbeanstalk-611-1289.war"
+ }
+ },
+ {
+ "ApplicationName": "gitlab_ci_elasticbeanstalk",
+ "Status": "UNPROCESSED",
+ "VersionLabel": "terminate-610-1286",
+ "Description": "bla",
+ "DateCreated": "2017-01-20T00:22:02.229Z",
+ "DateUpdated": "2017-01-20T00:22:02.229Z",
+ "SourceBundle": {
+ "S3Bucket": "bla",
+ "S3Key": "ci/beanstalk/gitlab_ci_elasticbeanstalk/gitlab_ci_elasticbeanstalk-610-1286.war"
+ }
+ },
+ {
+ "ApplicationName": "gitlab_ci_elasticbeanstalk",
+ "Status": "UNPROCESSED",
+ "VersionLabel": "master-609-1284",
+ "Description": "bla",
+ "DateCreated": "2017-01-19T23:54:32.902Z",
+ "DateUpdated": "2017-01-19T23:54:32.902Z",
+ "SourceBundle": {
+ "S3Bucket": "bla",
+ "S3Key": "ci/beanstalk/gitlab_ci_elasticbeanstalk/gitlab_ci_elasticbeanstalk-609-1284.war"
+ }
+ },
+ {
+ "ApplicationName": "gitlab_ci_elasticbeanstalk",
+ "Status": "UNPROCESSED",
+ "VersionLabel": "master-608-1282",
+ "Description": "bla",
+ "DateCreated": "2017-01-19T23:02:44.902Z",
+ "DateUpdated": "2017-01-19T23:02:44.902Z",
+ "SourceBundle": {
+ "S3Bucket": "bla",
+ "S3Key": "ci/beanstalk/gitlab_ci_elasticbeanstalk/gitlab_ci_elasticbeanstalk-608-1282.war"
+ }
+ },
+ {
+ "ApplicationName": "gitlab_ci_elasticbeanstalk",
+ "Status": "UNPROCESSED",
+ "VersionLabel": "master-606-1278",
+ "Description": "bla'",
+ "DateCreated": "2017-01-19T22:47:57.741Z",
+ "DateUpdated": "2017-01-19T22:47:57.741Z",
+ "SourceBundle": {
+ "S3Bucket": "bla",
+ "S3Key": "ci/beanstalk/gitlab_ci_elasticbeanstalk/gitlab_ci_elasticbeanstalk-606-1278.war"
+ }
+ }
+ ]
+}
diff --git a/test/integration/targets/filter_encryption/aliases b/test/integration/targets/filter_encryption/aliases
new file mode 100644
index 0000000..3005e4b
--- /dev/null
+++ b/test/integration/targets/filter_encryption/aliases
@@ -0,0 +1 @@
+shippable/posix/group4
diff --git a/test/integration/targets/filter_encryption/base.yml b/test/integration/targets/filter_encryption/base.yml
new file mode 100644
index 0000000..8bf25f7
--- /dev/null
+++ b/test/integration/targets/filter_encryption/base.yml
@@ -0,0 +1,37 @@
+- hosts: localhost
+ gather_facts: true
+ vars:
+ data: secret
+ dvault: '{{ "secret"|vault("test")}}'
+ password: test
+ s_32: '{{(2**31-1)}}'
+ s_64: '{{(2**63-1)}}'
+ vaultedstring_32: "$ANSIBLE_VAULT;1.2;AES256;filter_default\n33360a30386436633031333665316161303732656333373131373935623033393964633637346464\n6234613765313539306138373564366363306533356464613334320a666339363037303764636538\n3131633564326637303237313463613864626231\n"
+ vaultedstring_64: "$ANSIBLE_VAULT;1.2;AES256;filter_default\n33370a34333734353636633035656232613935353432656132646533346233326431346232616261\n6133383034376566366261316365633931356133633337396363370a376664386236313834326561\n6338373864623763613165366636633031303739\n"
+ vault: !vault |
+ $ANSIBLE_VAULT;1.1;AES256
+ 33323332333033383335333533383338333333303339333733323339333833303334333133313339
+ 33373339333133323331333833373335333933323338333633343338333133343334333733383334
+ 33333335333433383337333133303339333433353332333333363339333733363335333233303330
+ 3337333733353331333633313335333733373334333733320a373938666533366165653830313163
+ 62386564343438653437333564383664646538653364343138303831613039313232636437336530
+ 3438376662373764650a633366646563386335623161646262366137393635633464333265613938
+ 6661
+ # allow testing against 32b/64b limited archs, normally you can set higher values for random (2**256)
+ is_64: '{{ "64" in ansible_facts["architecture"] }}'
+ salt: '{{ is_64|bool|ternary(s_64, s_32)|random(seed=inventory_hostname)}}'
+ vaultedstring: '{{ is_64|bool|ternary(vaultedstring_64, vaultedstring_32) }}'
+
+ tasks:
+ - name: check vaulting
+ assert:
+ that:
+ - data|vault(password, salt=salt) == vaultedstring
+ - "data|vault(password, salt=salt)|type_debug != 'AnsibleVaultEncryptedUnicode'"
+ - "data|vault(password, salt=salt, wrap_object=True)|type_debug == 'AnsibleVaultEncryptedUnicode'"
+
+ - name: check unvaulting
+ assert:
+ that:
+ - vaultedstring|unvault(password) == data
+ - vault|unvault(password) == data
diff --git a/test/integration/targets/filter_encryption/runme.sh b/test/integration/targets/filter_encryption/runme.sh
new file mode 100755
index 0000000..41b30b1
--- /dev/null
+++ b/test/integration/targets/filter_encryption/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ANSIBLE_GATHER_SUBSET='min' ansible-playbook base.yml "$@"
diff --git a/test/integration/targets/filter_mathstuff/aliases b/test/integration/targets/filter_mathstuff/aliases
new file mode 100644
index 0000000..3005e4b
--- /dev/null
+++ b/test/integration/targets/filter_mathstuff/aliases
@@ -0,0 +1 @@
+shippable/posix/group4
diff --git a/test/integration/targets/filter_mathstuff/host_vars/localhost.yml b/test/integration/targets/filter_mathstuff/host_vars/localhost.yml
new file mode 100644
index 0000000..1f5a9e0
--- /dev/null
+++ b/test/integration/targets/filter_mathstuff/host_vars/localhost.yml
@@ -0,0 +1 @@
+foo: test
diff --git a/test/integration/targets/filter_mathstuff/runme.sh b/test/integration/targets/filter_mathstuff/runme.sh
new file mode 100755
index 0000000..5a474cf
--- /dev/null
+++ b/test/integration/targets/filter_mathstuff/runme.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+set -eux
+
+export ANSIBLE_ROLES_PATH=../
+
+ansible-playbook runme.yml "$@"
diff --git a/test/integration/targets/filter_mathstuff/runme.yml b/test/integration/targets/filter_mathstuff/runme.yml
new file mode 100644
index 0000000..a1eaef7
--- /dev/null
+++ b/test/integration/targets/filter_mathstuff/runme.yml
@@ -0,0 +1,4 @@
+- hosts: localhost
+ gather_facts: false
+ roles:
+ - { role: filter_mathstuff }
diff --git a/test/integration/targets/filter_mathstuff/tasks/main.yml b/test/integration/targets/filter_mathstuff/tasks/main.yml
new file mode 100644
index 0000000..019f00e
--- /dev/null
+++ b/test/integration/targets/filter_mathstuff/tasks/main.yml
@@ -0,0 +1,320 @@
+- name: Verify unique's fallback's exception throwing for case_sensitive=False
+ set_fact:
+ unique_fallback_exc1: '{{ [{"foo": "bar", "moo": "cow"}]|unique(case_sensitive=False) }}'
+ ignore_errors: true
+ tags: unique
+ register: unique_fallback_exc1_res
+
+- name: Verify unique's fallback's exception throwing for a Hashable thing that triggers TypeError
+ set_fact:
+ unique_fallback_exc2: '{{ True|unique }}'
+ ignore_errors: true
+ tags: unique
+ register: unique_fallback_exc2_res
+
+- name: Verify unique
+ tags: unique
+ assert:
+ that:
+ - '[1,2,3,4,4,3,2,1]|unique == [1,2,3,4]'
+ - '["a", "b", "a", "b"]|unique == ["a", "b"]'
+ - '[{"foo": "bar", "moo": "cow"}, {"foo": "bar", "moo": "cow"}, {"haha": "bar", "moo": "mar"}]|unique == [{"foo": "bar", "moo": "cow"}, {"haha": "bar", "moo": "mar"}]'
+ - '[{"foo": "bar", "moo": "cow"}, {"foo": "bar", "moo": "mar"}]|unique == [{"foo": "bar", "moo": "cow"}, {"foo": "bar", "moo": "mar"}]'
+ - '{"foo": "bar", "moo": "cow"}|unique == ["foo", "moo"]'
+ - '"foo"|unique|sort|join == "fo"'
+ - '[1,2,3,4,5]|unique == [1,2,3,4,5]'
+ - unique_fallback_exc1_res is failed
+ - unique_fallback_exc2_res is failed
+ - "\"'bool' object is not iterable\" in unique_fallback_exc2_res.msg"
+
+# `unique` will fall back to a custom implementation if the Jinja2 version is
+# too old to support `jinja2.filters.do_unique`. However, the built-in fallback
+# is quite different by default. Namely, it ignores the case-sensitivity
+# setting. This means running:
+# ['a', 'b', 'A', 'B']|unique
+# ... will give a different result for someone running Jinja 2.9 vs 2.10 when
+# do_unique was added. So here, we do a test to see if we have `do_unique`. If
+# we do, then we do another test to make sure attribute and case_sensitive
+# work on it.
+- name: Test for do_unique
+ shell: "{{ansible_python_interpreter}} -c 'from jinja2 import filters; print(\"do_unique\" in dir(filters))'"
+ tags: unique
+ register: do_unique_res
+
+- name: Verify unique some more
+ tags: unique
+ assert:
+ that:
+ - '["a", "b", "A", "B"]|unique(case_sensitive=True) == ["a", "b", "A", "B"]'
+ - '[{"foo": "bar", "moo": "cow"}, {"foo": "bar", "moo": "mar"}]|unique(attribute="foo") == [{"foo": "bar", "moo": "cow"}]'
+ - '["a", "b", "A", "B"]|unique == ["a", "b"]' # defaults to case_sensitive=False
+ - "'cannot fall back' in unique_fallback_exc1_res.msg"
+ when: do_unique_res.stdout == 'True'
+
+- name: Verify unique some more
+ tags: unique
+ assert:
+ that:
+ - "'does not support case_sensitive' in unique_fallback_exc1_res.msg"
+ when: do_unique_res.stdout == 'False'
+
+- name: Verify intersect
+ tags: intersect
+ assert:
+ that:
+ - '[1,2,3]|intersect([4,5,6]) == []'
+ - '[1,2,3]|intersect([3,4,5,6]) == [3]'
+ - '[1,2,3]|intersect([3,2,1]) == [1,2,3]'
+ - '(1,2,3)|intersect((4,5,6))|list == []'
+ - '(1,2,3)|intersect((3,4,5,6))|list == [3]'
+ - '["a","A","b"]|intersect(["B","c","C"]) == []'
+ - '["a","A","b"]|intersect(["b","B","c","C"]) == ["b"]'
+ - '["a","A","b"]|intersect(["b","A","a"]) == ["a","A","b"]'
+ - '("a","A","b")|intersect(("B","c","C"))|list == []'
+ - '("a","A","b")|intersect(("b","B","c","C"))|list == ["b"]'
+
+- name: Verify difference
+ tags: difference
+ assert:
+ that:
+ - '[1,2,3]|difference([4,5,6]) == [1,2,3]'
+ - '[1,2,3]|difference([3,4,5,6]) == [1,2]'
+ - '[1,2,3]|difference([3,2,1]) == []'
+ - '(1,2,3)|difference((4,5,6))|list == [1,2,3]'
+ - '(1,2,3)|difference((3,4,5,6))|list == [1,2]'
+ - '["a","A","b"]|difference(["B","c","C"]) == ["a","A","b"]'
+ - '["a","A","b"]|difference(["b","B","c","C"]) == ["a","A"]'
+ - '["a","A","b"]|difference(["b","A","a"]) == []'
+ - '("a","A","b")|difference(("B","c","C"))|list|sort(case_sensitive=True) == ["A","a","b"]'
+ - '("a","A","b")|difference(("b","B","c","C"))|list|sort(case_sensitive=True) == ["A","a"]'
+
+- name: Verify symmetric_difference
+ tags: symmetric_difference
+ assert:
+ that:
+ - '[1,2,3]|symmetric_difference([4,5,6]) == [1,2,3,4,5,6]'
+ - '[1,2,3]|symmetric_difference([3,4,5,6]) == [1,2,4,5,6]'
+ - '[1,2,3]|symmetric_difference([3,2,1]) == []'
+ - '(1,2,3)|symmetric_difference((4,5,6))|list == [1,2,3,4,5,6]'
+ - '(1,2,3)|symmetric_difference((3,4,5,6))|list == [1,2,4,5,6]'
+ - '["a","A","b"]|symmetric_difference(["B","c","C"]) == ["a","A","b","B","c","C"]'
+ - '["a","A","b"]|symmetric_difference(["b","B","c","C"]) == ["a","A","B","c","C"]'
+ - '["a","A","b"]|symmetric_difference(["b","A","a"]) == []'
+ - '("a","A","b")|symmetric_difference(("B","c","C"))|list|sort(case_sensitive=True) == ["A","B","C","a","b","c"]'
+ - '("a","A","b")|symmetric_difference(("b","B","c","C"))|list|sort(case_sensitive=True) == ["A","B","C","a","c"]'
+
+- name: Verify union
+ tags: union
+ assert:
+ that:
+ - '[1,2,3]|union([4,5,6]) == [1,2,3,4,5,6]'
+ - '[1,2,3]|union([3,4,5,6]) == [1,2,3,4,5,6]'
+ - '[1,2,3]|union([3,2,1]) == [1,2,3]'
+ - '(1,2,3)|union((4,5,6))|list == [1,2,3,4,5,6]'
+ - '(1,2,3)|union((3,4,5,6))|list == [1,2,3,4,5,6]'
+ - '["a","A","b"]|union(["B","c","C"]) == ["a","A","b","B","c","C"]'
+ - '["a","A","b"]|union(["b","B","c","C"]) == ["a","A","b","B","c","C"]'
+ - '["a","A","b"]|union(["b","A","a"]) == ["a","A","b"]'
+ - '("a","A","b")|union(("B","c","C"))|list|sort(case_sensitive=True) == ["A","B","C","a","b","c"]'
+ - '("a","A","b")|union(("b","B","c","C"))|list|sort(case_sensitive=True) == ["A","B","C","a","b","c"]'
+
+- name: Verify min
+ tags: min
+ assert:
+ that:
+ - '[1000,-99]|min == -99'
+ - '[0,4]|min == 0'
+
+- name: Verify max
+ tags: max
+ assert:
+ that:
+ - '[1000,-99]|max == 1000'
+ - '[0,4]|max == 4'
+
+- name: Verify logarithm on a value of invalid type
+ set_fact:
+ logarithm_exc1: '{{ "yo"|log }}'
+ ignore_errors: true
+ tags: logarithm
+ register: logarithm_exc1_res
+
+- name: Verify logarithm (which is passed to Jinja as "log" because consistency is boring)
+ tags: logarithm
+ assert:
+ that:
+ - '1|log == 0.0'
+ - '100|log(10) == 2.0'
+ - '100|log(10) == 2.0'
+ - '21|log(21) == 1.0'
+ - '(2.3|log(42)|string).startswith("0.222841")'
+ - '(21|log(42)|string).startswith("0.814550")'
+ - logarithm_exc1_res is failed
+ - '"can only be used on numbers" in logarithm_exc1_res.msg'
+
+- name: Verify power on a value of invalid type
+ set_fact:
+ power_exc1: '{{ "yo"|pow(4) }}'
+ ignore_errors: true
+ tags: power
+ register: power_exc1_res
+
+- name: Verify power (which is passed to Jinja as "pow" because consistency is boring)
+ tags: power
+ assert:
+ that:
+ - '2|pow(4) == 16.0'
+ - power_exc1_res is failed
+ - '"can only be used on numbers" in power_exc1_res.msg'
+
+- name: Verify inversepower on a value of invalid type
+ set_fact:
+ inversepower_exc1: '{{ "yo"|root }}'
+ ignore_errors: true
+ tags: inversepower
+ register: inversepower_exc1_res
+
+- name: Verify inversepower (which is passed to Jinja as "root" because consistency is boring)
+ tags: inversepower
+ assert:
+ that:
+ - '4|root == 2.0'
+ - '4|root(2) == 2.0'
+ - '9|root(1) == 9.0'
+ - '(9|root(6)|string).startswith("1.4422495")'
+ - inversepower_exc1_res is failed
+ - '"can only be used on numbers" in inversepower_exc1_res.msg'
+
+- name: Verify human_readable on invalid input
+ set_fact:
+ human_readable_exc1: '{{ "monkeys"|human_readable }}'
+ ignore_errors: true
+ tags: human_readable
+ register: human_readable_exc1_res
+
+- name: Verify human_readable
+ tags: human_readable
+ assert:
+ that:
+ - '"1.00 Bytes" == 1|human_readable'
+ - '"1.00 bits" == 1|human_readable(isbits=True)'
+ - '"10.00 KB" == 10240|human_readable'
+ - '"97.66 MB" == 102400000|human_readable'
+ - '"0.10 GB" == 102400000|human_readable(unit="G")'
+ - '"0.10 Gb" == 102400000|human_readable(isbits=True, unit="G")'
+ - human_readable_exc1_res is failed
+ - '"failed on bad input" in human_readable_exc1_res.msg'
+
+- name: Verify human_to_bytes
+ tags: human_to_bytes
+ assert:
+ that:
+ - "{{'0'|human_to_bytes}} == 0"
+ - "{{'0.1'|human_to_bytes}} == 0"
+ - "{{'0.9'|human_to_bytes}} == 1"
+ - "{{'1'|human_to_bytes}} == 1"
+ - "{{'10.00 KB'|human_to_bytes}} == 10240"
+ - "{{ '11 MB'|human_to_bytes}} == 11534336"
+ - "{{ '1.1 GB'|human_to_bytes}} == 1181116006"
+ - "{{'10.00 Kb'|human_to_bytes(isbits=True)}} == 10240"
+
+- name: Verify human_to_bytes (bad string)
+ set_fact:
+ bad_string: "{{ '10.00 foo' | human_to_bytes }}"
+ ignore_errors: yes
+ tags: human_to_bytes
+ register: _human_bytes_test
+
+- name: Verify human_to_bytes (bad string)
+ tags: human_to_bytes
+ assert:
+ that: "{{_human_bytes_test.failed}}"
+
+- name: Verify that union can be chained
+ tags: union
+ vars:
+ unions: '{{ [1,2,3]|union([4,5])|union([6,7]) }}'
+ assert:
+ that:
+ - "unions|type_debug == 'list'"
+ - "unions|length == 7"
+
+- name: Test union with unhashable item
+ tags: union
+ vars:
+ unions: '{{ [1,2,3]|union([{}]) }}'
+ assert:
+ that:
+ - "unions|type_debug == 'list'"
+ - "unions|length == 4"
+
+- name: Verify rekey_on_member with invalid "duplicates" kwarg
+ set_fact:
+ rekey_on_member_exc1: '{{ []|rekey_on_member("asdf", duplicates="boo") }}'
+ ignore_errors: true
+ tags: rekey_on_member
+ register: rekey_on_member_exc1_res
+
+- name: Verify rekey_on_member with invalid data
+ set_fact:
+ rekey_on_member_exc2: '{{ "minkeys"|rekey_on_member("asdf") }}'
+ ignore_errors: true
+ tags: rekey_on_member
+ register: rekey_on_member_exc2_res
+
+- name: Verify rekey_on_member with partially invalid data (list item is not dict)
+ set_fact:
+ rekey_on_member_exc3: '{{ [True]|rekey_on_member("asdf") }}'
+ ignore_errors: true
+ tags: rekey_on_member
+ register: rekey_on_member_exc3_res
+
+- name: Verify rekey_on_member with partially invalid data (key not in all dicts)
+ set_fact:
+ rekey_on_member_exc4: '{{ [{"foo": "bar", "baz": "buzz"}, {"hello": 8, "different": "haha"}]|rekey_on_member("foo") }}'
+ ignore_errors: true
+ tags: rekey_on_member
+ register: rekey_on_member_exc4_res
+
+- name: Verify rekey_on_member with duplicates and duplicates=error
+ set_fact:
+ rekey_on_member_exc5: '{{ [{"proto": "eigrp", "state": "enabled"}, {"proto": "eigrp", "state": "enabled"}]|rekey_on_member("proto", duplicates="error") }}'
+ ignore_errors: true
+ tags: rekey_on_member
+ register: rekey_on_member_exc5_res
+
+- name: Verify rekey_on_member
+ tags: rekey_on_member
+ assert:
+ that:
+ - rekey_on_member_exc1_res is failed
+ - '"duplicates parameter to rekey_on_member has unknown value" in rekey_on_member_exc1_res.msg'
+ - '[{"proto": "eigrp", "state": "enabled"}, {"proto": "ospf", "state": "enabled"}]|rekey_on_member("proto") == {"eigrp": {"proto": "eigrp", "state": "enabled"}, "ospf": {"proto": "ospf", "state": "enabled"}}'
+ - '{"a": {"proto": "eigrp", "state": "enabled"}, "b": {"proto": "ospf", "state": "enabled"}}|rekey_on_member("proto") == {"eigrp": {"proto": "eigrp", "state": "enabled"}, "ospf": {"proto": "ospf", "state": "enabled"}}'
+ - '[{"proto": "eigrp", "state": "enabled"}, {"proto": "eigrp", "state": "enabled"}]|rekey_on_member("proto", duplicates="overwrite") == {"eigrp": {"proto": "eigrp", "state": "enabled"}}'
+ - rekey_on_member_exc2_res is failed
+ - '"Type is not a valid list, set, or dict" in rekey_on_member_exc2_res.msg'
+ - rekey_on_member_exc3_res is failed
+ - '"List item is not a valid dict" in rekey_on_member_exc3_res.msg'
+ - rekey_on_member_exc4_res is failed
+ - '"was not found" in rekey_on_member_exc4_res.msg'
+ - rekey_on_member_exc5_res is failed
+ - '"is not unique, cannot correctly turn into dict" in rekey_on_member_exc5_res.msg'
+
+- name: test undefined positional args for rekey_on_member are properly handled
+ vars:
+ all_vars: "{{ hostvars[inventory_hostname] }}"
+ test_var: "{{ all_vars.foo }}"
+ block:
+ - include_vars:
+ file: defined_later.yml
+ - assert:
+ that: "test_var == 'test'"
+ - assert:
+ that: "rekeyed == {'value': {'test': 'value'}}"
+
+# TODO: For some reason, the coverage tool isn't accounting for the last test
+# so add another "last test" to fake it...
+- assert:
+ that:
+ - true
diff --git a/test/integration/targets/filter_mathstuff/vars/defined_later.yml b/test/integration/targets/filter_mathstuff/vars/defined_later.yml
new file mode 100644
index 0000000..dfb2421
--- /dev/null
+++ b/test/integration/targets/filter_mathstuff/vars/defined_later.yml
@@ -0,0 +1,3 @@
+do_rekey:
+ - test: value
+rekeyed: "{{ do_rekey | rekey_on_member(defined_later) }}"
diff --git a/test/integration/targets/filter_mathstuff/vars/main.yml b/test/integration/targets/filter_mathstuff/vars/main.yml
new file mode 100644
index 0000000..bb61e12
--- /dev/null
+++ b/test/integration/targets/filter_mathstuff/vars/main.yml
@@ -0,0 +1 @@
+defined_later: "{{ test_var }}"
diff --git a/test/integration/targets/filter_urls/aliases b/test/integration/targets/filter_urls/aliases
new file mode 100644
index 0000000..3005e4b
--- /dev/null
+++ b/test/integration/targets/filter_urls/aliases
@@ -0,0 +1 @@
+shippable/posix/group4
diff --git a/test/integration/targets/filter_urls/tasks/main.yml b/test/integration/targets/filter_urls/tasks/main.yml
new file mode 100644
index 0000000..c062326
--- /dev/null
+++ b/test/integration/targets/filter_urls/tasks/main.yml
@@ -0,0 +1,24 @@
+- name: Test urldecode filter
+ set_fact:
+ urldecoded_string: key="@{}é&%£ foo bar '(;\<>""°)
+
+- name: Test urlencode filter
+ set_fact:
+ urlencoded_string: 'key%3D%22%40%7B%7D%C3%A9%26%25%C2%A3%20foo%20bar%20%27%28%3B%5C%3C%3E%22%22%C2%B0%29'
+
+- name: Verify urlencode / urldecode isomorphism
+ assert:
+ that:
+ - urldecoded_string == urlencoded_string|urldecode
+ - urlencoded_string == urldecoded_string|urlencode
+
+- name: Verify urlencode handles dicts properly
+ assert:
+ that:
+ - "{'foo': 'bar'}|urlencode == 'foo=bar'"
+ - "{'foo': 'bar', 'baz': 'buz'}|urlencode == 'foo=bar&baz=buz'"
+ - "()|urlencode == ''"
+
+# Needed (temporarily) due to coverage reports not including the last task.
+- assert:
+ that: true
diff --git a/test/integration/targets/filter_urlsplit/aliases b/test/integration/targets/filter_urlsplit/aliases
new file mode 100644
index 0000000..3005e4b
--- /dev/null
+++ b/test/integration/targets/filter_urlsplit/aliases
@@ -0,0 +1 @@
+shippable/posix/group4
diff --git a/test/integration/targets/filter_urlsplit/tasks/main.yml b/test/integration/targets/filter_urlsplit/tasks/main.yml
new file mode 100644
index 0000000..c3ff3ec
--- /dev/null
+++ b/test/integration/targets/filter_urlsplit/tasks/main.yml
@@ -0,0 +1,30 @@
+- debug:
+ var: "'http://mary:MySecret@www.acme.com:9000/dir/index.html?query=term#fragment' | urlsplit"
+ verbosity: 1
+ tags: debug
+
+- name: Test urlsplit filter
+ assert:
+ that:
+ - "'http://mary:MySecret@www.acme.com:9000/dir/index.html?query=term#fragment' | urlsplit('fragment') == 'fragment'"
+ - "'http://mary:MySecret@www.acme.com:9000/dir/index.html?query=term#fragment' | urlsplit('hostname') == 'www.acme.com'"
+ - "'http://mary:MySecret@www.acme.com:9000/dir/index.html?query=term#fragment' | urlsplit('netloc') == 'mary:MySecret@www.acme.com:9000'"
+ - "'http://mary:MySecret@www.acme.com:9000/dir/index.html?query=term#fragment' | urlsplit('path') == '/dir/index.html'"
+ - "'http://mary:MySecret@www.acme.com:9000/dir/index.html?query=term#fragment' | urlsplit('port') == 9000"
+ - "'http://mary:MySecret@www.acme.com:9000/dir/index.html?query=term#fragment' | urlsplit('query') == 'query=term'"
+ - "'http://mary:MySecret@www.acme.com:9000/dir/index.html?query=term#fragment' | urlsplit('scheme') == 'http'"
+ - "'http://mary:MySecret@www.acme.com:9000/dir/index.html?query=term#fragment' | urlsplit('username') == 'mary'"
+ - "'http://mary:MySecret@www.acme.com:9000/dir/index.html?query=term#fragment' | urlsplit('password') == 'MySecret'"
+ - "'http://mary:MySecret@www.acme.com:9000/dir/index.html?query=term#fragment' | urlsplit == { 'fragment': 'fragment', 'hostname': 'www.acme.com', 'netloc': 'mary:MySecret@www.acme.com:9000', 'password': 'MySecret', 'path': '/dir/index.html', 'port': 9000, 'query': 'query=term', 'scheme': 'http', 'username': 'mary' }"
+
+- name: Test urlsplit filter bad argument
+ debug:
+ var: "'http://www.acme.com:9000/dir/index.html' | urlsplit('bad_filter')"
+ register: _bad_urlsplit_filter
+ ignore_errors: yes
+
+- name: Verify urlsplit filter showed an error message
+ assert:
+ that:
+ - _bad_urlsplit_filter is failed
+ - "'unknown URL component' in _bad_urlsplit_filter.msg"
diff --git a/test/integration/targets/find/aliases b/test/integration/targets/find/aliases
new file mode 100644
index 0000000..a6dafcf
--- /dev/null
+++ b/test/integration/targets/find/aliases
@@ -0,0 +1 @@
+shippable/posix/group1
diff --git a/test/integration/targets/find/files/a.txt b/test/integration/targets/find/files/a.txt
new file mode 100644
index 0000000..30b622a
--- /dev/null
+++ b/test/integration/targets/find/files/a.txt
@@ -0,0 +1,2 @@
+this is a file that has
+a few lines in it
diff --git a/test/integration/targets/find/files/log.txt b/test/integration/targets/find/files/log.txt
new file mode 100644
index 0000000..679893b
--- /dev/null
+++ b/test/integration/targets/find/files/log.txt
@@ -0,0 +1,4 @@
+01/01- OK
+01/02- OK
+01/03- KO
+01/04- OK
diff --git a/test/integration/targets/find/meta/main.yml b/test/integration/targets/find/meta/main.yml
new file mode 100644
index 0000000..cb6005d
--- /dev/null
+++ b/test/integration/targets/find/meta/main.yml
@@ -0,0 +1,3 @@
+dependencies:
+ - prepare_tests
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/find/tasks/main.yml b/test/integration/targets/find/tasks/main.yml
new file mode 100644
index 0000000..5381a14
--- /dev/null
+++ b/test/integration/targets/find/tasks/main.yml
@@ -0,0 +1,376 @@
+# Test code for the find module.
+# (c) 2017, James Tanner <tanner.jc@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- set_fact: remote_tmp_dir_test={{remote_tmp_dir}}/test_find
+
+- name: make sure our testing sub-directory does not exist
+ file:
+ path: "{{ remote_tmp_dir_test }}"
+ state: absent
+
+- name: create our testing sub-directory
+ file:
+ path: "{{ remote_tmp_dir_test }}"
+ state: directory
+
+##
+## find
+##
+
+- name: make some directories
+ file:
+ path: "{{ remote_tmp_dir_test }}/{{ item }}"
+ state: directory
+ with_items:
+ - a/b/c/d
+ - e/f/g/h
+
+- name: make some files
+ copy:
+ dest: "{{ remote_tmp_dir_test }}/{{ item }}"
+ content: 'data'
+ with_items:
+ - a/1.txt
+ - a/b/2.jpg
+ - a/b/c/3
+ - a/b/c/d/4.xml
+ - e/5.json
+ - e/f/6.swp
+ - e/f/g/7.img
+ - e/f/g/h/8.ogg
+
+- name: find the directories
+ find:
+ paths: "{{ remote_tmp_dir_test }}"
+ file_type: directory
+ recurse: yes
+ register: find_test0
+- debug: var=find_test0
+- name: validate directory results
+ assert:
+ that:
+ - 'find_test0.changed is defined'
+ - 'find_test0.examined is defined'
+ - 'find_test0.files is defined'
+ - 'find_test0.matched is defined'
+ - 'find_test0.msg is defined'
+ - 'find_test0.matched == 8'
+ - 'find_test0.files | length == 8'
+ - 'find_test0.examined == 16'
+
+- name: find the xml and img files
+ find:
+ paths: "{{ remote_tmp_dir_test }}"
+ file_type: file
+ patterns: "*.xml,*.img"
+ recurse: yes
+ register: find_test1
+- debug: var=find_test1
+- name: validate directory results
+ assert:
+ that:
+ - 'find_test1.matched == 2'
+ - 'find_test1.files | length == 2'
+
+- name: find the xml file
+ find:
+ paths: "{{ remote_tmp_dir_test }}"
+ patterns: "*.xml"
+ recurse: yes
+ register: find_test2
+- debug: var=find_test2
+- name: validate gr_name and pw_name are defined
+ assert:
+ that:
+ - 'find_test2.matched == 1'
+ - 'find_test2.files[0].pw_name is defined'
+ - 'find_test2.files[0].gr_name is defined'
+
+- name: find the xml file with empty excludes
+ find:
+ paths: "{{ remote_tmp_dir_test }}"
+ patterns: "*.xml"
+ recurse: yes
+ excludes: []
+ register: find_test3
+- debug: var=find_test3
+- name: validate gr_name and pw_name are defined
+ assert:
+ that:
+ - 'find_test3.matched == 1'
+ - 'find_test3.files[0].pw_name is defined'
+ - 'find_test3.files[0].gr_name is defined'
+
+- name: Copy some files into the test dir
+ copy:
+ src: "{{ item }}"
+ dest: "{{ remote_tmp_dir_test }}/{{ item }}"
+ mode: 0644
+ with_items:
+ - a.txt
+ - log.txt
+
+- name: Ensure '$' only matches the true end of the file with read_whole_file, not a line
+ find:
+ paths: "{{ remote_tmp_dir_test }}"
+ patterns: "*.txt"
+ contains: "KO$"
+ read_whole_file: true
+ register: whole_no_match
+
+- debug: var=whole_no_match
+
+- assert:
+ that:
+ - whole_no_match.matched == 0
+
+- name: Match the end of the file successfully
+ find:
+ paths: "{{ remote_tmp_dir_test }}"
+ patterns: "*.txt"
+ contains: "OK$"
+ read_whole_file: true
+ register: whole_match
+
+- debug: var=whole_match
+
+- assert:
+ that:
+ - whole_match.matched == 1
+
+- name: When read_whole_file=False, $ should match an individual line
+ find:
+ paths: "{{ remote_tmp_dir_test }}"
+ patterns: "*.txt"
+ contains: ".*KO$"
+ read_whole_file: false
+ register: match_end_of_line
+
+- debug: var=match_end_of_line
+
+- assert:
+ that:
+ - match_end_of_line.matched == 1
+
+- name: When read_whole_file=True, match across line boundaries
+ find:
+ paths: "{{ remote_tmp_dir_test }}"
+ patterns: "*.txt"
+ contains: "has\na few"
+ read_whole_file: true
+ register: match_line_boundaries
+
+- debug: var=match_line_boundaries
+
+- assert:
+ that:
+ - match_line_boundaries.matched == 1
+
+- name: When read_whole_file=False, do not match across line boundaries
+ find:
+ paths: "{{ remote_tmp_dir_test }}"
+ patterns: "*.txt"
+ contains: "has\na few"
+ read_whole_file: false
+ register: no_match_line_boundaries
+
+- debug: var=no_match_line_boundaries
+
+- assert:
+ that:
+ - no_match_line_boundaries.matched == 0
+
+- block:
+ - set_fact:
+ mypath: /idontexist{{lookup('pipe', 'mktemp')}}
+
+ - find:
+ paths: '{{mypath}}'
+ patterns: '*'
+ register: failed_path
+
+ - assert:
+ that:
+ - failed_path.files == []
+ - 'failed_path.msg == "Not all paths examined, check warnings for details"'
+ - mypath in failed_path.skipped_paths
+
+- name: test number of examined directories/files
+ block:
+ - name: Get all files/directories in the path
+ find:
+ paths: "{{ remote_tmp_dir_test }}"
+ recurse: yes
+ file_type: any
+ register: total_contents
+
+ - assert:
+ that:
+ - total_contents.matched == 18
+ - total_contents.examined == 18
+
+ - name: Get files and directories with depth
+ find:
+ paths: "{{ remote_tmp_dir_test }}"
+ recurse: yes
+ file_type: any
+ depth: 2
+ register: contents_with_depth
+
+ - assert:
+ that:
+ - contents_with_depth.matched == 8
+ # dir contents are considered until the depth exceeds the requested depth
+ # there are 8 files/directories in the requested depth and 4 that exceed it by 1
+ - contents_with_depth.examined == 12
+
+ - name: Find files with depth
+ find:
+ paths: "{{ remote_tmp_dir_test }}"
+ depth: 2
+ recurse: yes
+ register: files_with_depth
+
+ - assert:
+ that:
+ - files_with_depth.matched == 4
+ # dir contents are considered until the depth exceeds the requested depth
+ # there are 8 files/directories in the requested depth and 4 that exceed it by 1
+ - files_with_depth.examined == 12
+
+- name: exclude with regex
+ find:
+ paths: "{{ remote_tmp_dir_test }}"
+ recurse: yes
+ use_regex: true
+ exclude: .*\.ogg
+ register: find_test3
+
+- set_fact:
+ find_test3_list: "{{ find_test3.files|map(attribute='path') }}"
+
+- name: assert we skipped the ogg file
+ assert:
+ that:
+ - '"{{ remote_tmp_dir_test }}/e/f/g/h/8.ogg" not in find_test3_list'
+
+- name: patterns with regex
+ find:
+ paths: "{{ remote_tmp_dir_test }}"
+ recurse: yes
+ use_regex: true
+ patterns: .*\.ogg
+ register: find_test4
+
+- name: assert we matched the ogg file
+ assert:
+ that:
+ - remote_tmp_dir_test ~ "/e/f/g/h/8.ogg" in find_test4.files|map(attribute="path")
+
+- name: create our age/size testing sub-directory
+ file:
+ path: "{{ remote_tmp_dir_test }}/astest"
+ state: directory
+
+- name: create test file with old timestamps
+ file:
+ path: "{{ remote_tmp_dir_test }}/astest/old.txt"
+ state: touch
+ modification_time: "202001011200.0"
+
+- name: create test file with current timestamps
+ file:
+ path: "{{ remote_tmp_dir_test }}/astest/new.txt"
+ state: touch
+
+- name: create hidden test file with current timestamps
+ file:
+ path: "{{ remote_tmp_dir_test }}/astest/.hidden.txt"
+ state: touch
+
+- name: find files older than 1 week
+ find:
+ path: "{{ remote_tmp_dir_test }}/astest"
+ age: 1w
+ hidden: true
+ register: result
+
+- set_fact:
+ astest_list: "{{ result.files|map(attribute='path') }}"
+
+- name: assert we only find the old file
+ assert:
+ that:
+ - result.matched == 1
+ - '"{{ remote_tmp_dir_test }}/astest/old.txt" in astest_list'
+
+- name: find files newer than 1 week
+ find:
+ path: "{{ remote_tmp_dir_test }}/astest"
+ age: -1w
+ register: result
+
+- set_fact:
+ astest_list: "{{ result.files|map(attribute='path') }}"
+
+- name: assert we only find the current file
+ assert:
+ that:
+ - result.matched == 1
+ - '"{{ remote_tmp_dir_test }}/astest/new.txt" in astest_list'
+
+- name: add some content to the new file
+ shell: "echo hello world > {{ remote_tmp_dir_test }}/astest/new.txt"
+
+- name: find files with MORE than 5 bytes, also get checksums
+ find:
+ path: "{{ remote_tmp_dir_test }}/astest"
+ size: 5
+ hidden: true
+ get_checksum: true
+ register: result
+
+- set_fact:
+ astest_list: "{{ result.files|map(attribute='path') }}"
+
+- name: assert we only find the hello world file
+ assert:
+ that:
+ - result.matched == 1
+ - '"{{ remote_tmp_dir_test }}/astest/new.txt" in astest_list'
+ - '"checksum" in result.files[0]'
+
+- name: find ANY item with LESS than 5 bytes, also get checksums
+ find:
+ path: "{{ remote_tmp_dir_test }}/astest"
+ size: -5
+ hidden: true
+ get_checksum: true
+ file_type: any
+ register: result
+
+- set_fact:
+ astest_list: "{{ result.files|map(attribute='path') }}"
+
+- name: assert we do not find the hello world file and a checksum is present
+ assert:
+ that:
+ - result.matched == 2
+ - '"{{ remote_tmp_dir_test }}/astest/old.txt" in astest_list'
+ - '"{{ remote_tmp_dir_test }}/astest/.hidden.txt" in astest_list'
+ - '"checksum" in result.files[0]'
diff --git a/test/integration/targets/fork_safe_stdio/aliases b/test/integration/targets/fork_safe_stdio/aliases
new file mode 100644
index 0000000..e968db7
--- /dev/null
+++ b/test/integration/targets/fork_safe_stdio/aliases
@@ -0,0 +1,3 @@
+shippable/posix/group3
+context/controller
+skip/macos
diff --git a/test/integration/targets/fork_safe_stdio/callback_plugins/spewstdio.py b/test/integration/targets/fork_safe_stdio/callback_plugins/spewstdio.py
new file mode 100644
index 0000000..6ed6ef3
--- /dev/null
+++ b/test/integration/targets/fork_safe_stdio/callback_plugins/spewstdio.py
@@ -0,0 +1,58 @@
+import atexit
+import os
+import sys
+
+from ansible.plugins.callback import CallbackBase
+from ansible.utils.display import Display
+from threading import Thread
+
+# This callback plugin reliably triggers the deadlock from https://github.com/ansible/ansible-runner/issues/1164 when
+# run on a TTY/PTY. It starts a thread in the controller that spews unprintable characters to stdout as fast as
+# possible, while causing forked children to write directly to the inherited stdout immediately post-fork. If a fork
+# occurs while the spew thread holds stdout's internal BufferedIOWriter lock, the lock will be orphaned in the child,
+# and attempts to write to stdout there will hang forever.
+
+# Any mechanism that ensures non-main threads do not hold locks before forking should allow this test to pass.
+
+# ref: https://docs.python.org/3/library/io.html#multi-threading
+# ref: https://github.com/python/cpython/blob/0547a981ae413248b21a6bb0cb62dda7d236fe45/Modules/_io/bufferedio.c#L268
+
+
+class CallbackModule(CallbackBase):
+ CALLBACK_VERSION = 2.0
+ CALLBACK_NAME = 'spewstdio'
+
+ def __init__(self):
+ super().__init__()
+ self.display = Display()
+
+ if os.environ.get('SPEWSTDIO_ENABLED', '0') != '1':
+ self.display.warning('spewstdio test plugin loaded but disabled; set SPEWSTDIO_ENABLED=1 to enable')
+ return
+
+ self.display = Display()
+ self._keep_spewing = True
+
+ # cause the child to write directly to stdout immediately post-fork
+ os.register_at_fork(after_in_child=lambda: print(f"hi from forked child pid {os.getpid()}"))
+
+ # in passing cases, stop spewing when the controller is exiting to prevent fatal errors on final flush
+ atexit.register(self.stop_spew)
+
+ self._spew_thread = Thread(target=self.spew, daemon=True)
+ self._spew_thread.start()
+
+ def stop_spew(self):
+ self._keep_spewing = False
+
+ def spew(self):
+ # dump a message so we know the callback thread has started
+ self.display.warning("spewstdio STARTING NONPRINTING SPEW ON BACKGROUND THREAD")
+
+ while self._keep_spewing:
+ # dump a non-printing control character directly to stdout to avoid junking up the screen while still
+ # doing lots of writes and flushes.
+ sys.stdout.write('\x1b[K')
+ sys.stdout.flush()
+
+ self.display.warning("spewstdio STOPPING SPEW")
diff --git a/test/integration/targets/fork_safe_stdio/hosts b/test/integration/targets/fork_safe_stdio/hosts
new file mode 100644
index 0000000..675e82a
--- /dev/null
+++ b/test/integration/targets/fork_safe_stdio/hosts
@@ -0,0 +1,5 @@
+[all]
+local-[1:10]
+
+[all:vars]
+ansible_connection=local
diff --git a/test/integration/targets/fork_safe_stdio/run-with-pty.py b/test/integration/targets/fork_safe_stdio/run-with-pty.py
new file mode 100755
index 0000000..4639152
--- /dev/null
+++ b/test/integration/targets/fork_safe_stdio/run-with-pty.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+"""Run a command using a PTY."""
+
+import sys
+
+if sys.version_info < (3, 10):
+ import vendored_pty as pty
+else:
+ import pty
+
+sys.exit(1 if pty.spawn(sys.argv[1:]) else 0)
diff --git a/test/integration/targets/fork_safe_stdio/runme.sh b/test/integration/targets/fork_safe_stdio/runme.sh
new file mode 100755
index 0000000..4438c3f
--- /dev/null
+++ b/test/integration/targets/fork_safe_stdio/runme.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+
+set -eu
+
+echo "testing for stdio deadlock on forked workers (10s timeout)..."
+
+# Enable a callback that trips deadlocks on forked-child stdout, time out after 10s; forces running
+# in a pty, since that tends to be much slower than raw file I/O and thus more likely to trigger the deadlock.
+# Redirect stdout to /dev/null since it's full of non-printable garbage we don't want to display unless it failed
+ANSIBLE_CALLBACKS_ENABLED=spewstdio SPEWSTDIO_ENABLED=1 python run-with-pty.py timeout 10s ansible-playbook -i hosts -f 5 test.yml > stdout.txt && RC=$? || RC=$?
+
+if [ $RC != 0 ]; then
+ echo "failed; likely stdout deadlock. dumping raw output (may be very large)"
+ cat stdout.txt
+ exit 1
+fi
+
+grep -q -e "spewstdio STARTING NONPRINTING SPEW ON BACKGROUND THREAD" stdout.txt || (echo "spewstdio callback was not enabled"; exit 1)
+
+echo "PASS"
diff --git a/test/integration/targets/fork_safe_stdio/test.yml b/test/integration/targets/fork_safe_stdio/test.yml
new file mode 100644
index 0000000..d60f071
--- /dev/null
+++ b/test/integration/targets/fork_safe_stdio/test.yml
@@ -0,0 +1,5 @@
+- hosts: all
+ gather_facts: no
+ tasks:
+ - debug:
+ msg: yo
diff --git a/test/integration/targets/fork_safe_stdio/vendored_pty.py b/test/integration/targets/fork_safe_stdio/vendored_pty.py
new file mode 100644
index 0000000..bc70803
--- /dev/null
+++ b/test/integration/targets/fork_safe_stdio/vendored_pty.py
@@ -0,0 +1,189 @@
+# Vendored copy of https://github.com/python/cpython/blob/3680ebed7f3e529d01996dd0318601f9f0d02b4b/Lib/pty.py
+# PSF License (see licenses/PSF-license.txt or https://opensource.org/licenses/Python-2.0)
+"""Pseudo terminal utilities."""
+
+# Bugs: No signal handling. Doesn't set slave termios and window size.
+# Only tested on Linux, FreeBSD, and macOS.
+# See: W. Richard Stevens. 1992. Advanced Programming in the
+# UNIX Environment. Chapter 19.
+# Author: Steen Lumholt -- with additions by Guido.
+
+from select import select
+import os
+import sys
+import tty
+
+# names imported directly for test mocking purposes
+from os import close, waitpid
+from tty import setraw, tcgetattr, tcsetattr
+
+__all__ = ["openpty", "fork", "spawn"]
+
+STDIN_FILENO = 0
+STDOUT_FILENO = 1
+STDERR_FILENO = 2
+
+CHILD = 0
+
+def openpty():
+ """openpty() -> (master_fd, slave_fd)
+ Open a pty master/slave pair, using os.openpty() if possible."""
+
+ try:
+ return os.openpty()
+ except (AttributeError, OSError):
+ pass
+ master_fd, slave_name = _open_terminal()
+ slave_fd = slave_open(slave_name)
+ return master_fd, slave_fd
+
+def master_open():
+ """master_open() -> (master_fd, slave_name)
+ Open a pty master and return the fd, and the filename of the slave end.
+ Deprecated, use openpty() instead."""
+
+ try:
+ master_fd, slave_fd = os.openpty()
+ except (AttributeError, OSError):
+ pass
+ else:
+ slave_name = os.ttyname(slave_fd)
+ os.close(slave_fd)
+ return master_fd, slave_name
+
+ return _open_terminal()
+
+def _open_terminal():
+ """Open pty master and return (master_fd, tty_name)."""
+ for x in 'pqrstuvwxyzPQRST':
+ for y in '0123456789abcdef':
+ pty_name = '/dev/pty' + x + y
+ try:
+ fd = os.open(pty_name, os.O_RDWR)
+ except OSError:
+ continue
+ return (fd, '/dev/tty' + x + y)
+ raise OSError('out of pty devices')
+
+def slave_open(tty_name):
+ """slave_open(tty_name) -> slave_fd
+ Open the pty slave and acquire the controlling terminal, returning
+ opened filedescriptor.
+ Deprecated, use openpty() instead."""
+
+ result = os.open(tty_name, os.O_RDWR)
+ try:
+ from fcntl import ioctl, I_PUSH
+ except ImportError:
+ return result
+ try:
+ ioctl(result, I_PUSH, "ptem")
+ ioctl(result, I_PUSH, "ldterm")
+ except OSError:
+ pass
+ return result
+
+def fork():
+ """fork() -> (pid, master_fd)
+ Fork and make the child a session leader with a controlling terminal."""
+
+ try:
+ pid, fd = os.forkpty()
+ except (AttributeError, OSError):
+ pass
+ else:
+ if pid == CHILD:
+ try:
+ os.setsid()
+ except OSError:
+ # os.forkpty() already set us session leader
+ pass
+ return pid, fd
+
+ master_fd, slave_fd = openpty()
+ pid = os.fork()
+ if pid == CHILD:
+ # Establish a new session.
+ os.setsid()
+ os.close(master_fd)
+
+ # Slave becomes stdin/stdout/stderr of child.
+ os.dup2(slave_fd, STDIN_FILENO)
+ os.dup2(slave_fd, STDOUT_FILENO)
+ os.dup2(slave_fd, STDERR_FILENO)
+ if slave_fd > STDERR_FILENO:
+ os.close(slave_fd)
+
+ # Explicitly open the tty to make it become a controlling tty.
+ tmp_fd = os.open(os.ttyname(STDOUT_FILENO), os.O_RDWR)
+ os.close(tmp_fd)
+ else:
+ os.close(slave_fd)
+
+ # Parent and child process.
+ return pid, master_fd
+
+def _writen(fd, data):
+ """Write all the data to a descriptor."""
+ while data:
+ n = os.write(fd, data)
+ data = data[n:]
+
+def _read(fd):
+ """Default read function."""
+ return os.read(fd, 1024)
+
+def _copy(master_fd, master_read=_read, stdin_read=_read):
+ """Parent copy loop.
+ Copies
+ pty master -> standard output (master_read)
+ standard input -> pty master (stdin_read)"""
+ fds = [master_fd, STDIN_FILENO]
+ while fds:
+ rfds, _wfds, _xfds = select(fds, [], [])
+
+ if master_fd in rfds:
+ # Some OSes signal EOF by returning an empty byte string,
+ # some throw OSErrors.
+ try:
+ data = master_read(master_fd)
+ except OSError:
+ data = b""
+ if not data: # Reached EOF.
+ return # Assume the child process has exited and is
+ # unreachable, so we clean up.
+ else:
+ os.write(STDOUT_FILENO, data)
+
+ if STDIN_FILENO in rfds:
+ data = stdin_read(STDIN_FILENO)
+ if not data:
+ fds.remove(STDIN_FILENO)
+ else:
+ _writen(master_fd, data)
+
+def spawn(argv, master_read=_read, stdin_read=_read):
+ """Create a spawned process."""
+ if isinstance(argv, str):
+ argv = (argv,)
+ sys.audit('pty.spawn', argv)
+
+ pid, master_fd = fork()
+ if pid == CHILD:
+ os.execlp(argv[0], *argv)
+
+ try:
+ mode = tcgetattr(STDIN_FILENO)
+ setraw(STDIN_FILENO)
+ restore = True
+ except tty.error: # This is the same as termios.error
+ restore = False
+
+ try:
+ _copy(master_fd, master_read, stdin_read)
+ finally:
+ if restore:
+ tcsetattr(STDIN_FILENO, tty.TCSAFLUSH, mode)
+
+ close(master_fd)
+ return waitpid(pid, 0)[1]
diff --git a/test/integration/targets/gathering/aliases b/test/integration/targets/gathering/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/gathering/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/gathering/explicit.yml b/test/integration/targets/gathering/explicit.yml
new file mode 100644
index 0000000..453dfb6
--- /dev/null
+++ b/test/integration/targets/gathering/explicit.yml
@@ -0,0 +1,14 @@
+- hosts: testhost
+ tasks:
+ - name: ensure facts have not been collected
+ assert:
+ that:
+ - ansible_facts is undefined or not 'fqdn' in ansible_facts
+
+- hosts: testhost
+ gather_facts: True
+ tasks:
+ - name: ensure facts have been collected
+ assert:
+ that:
+ - ansible_facts is defined and 'fqdn' in ansible_facts
diff --git a/test/integration/targets/gathering/implicit.yml b/test/integration/targets/gathering/implicit.yml
new file mode 100644
index 0000000..f1ea965
--- /dev/null
+++ b/test/integration/targets/gathering/implicit.yml
@@ -0,0 +1,23 @@
+- hosts: testhost
+ tasks:
+ - name: check that facts were gathered but no local facts exist
+ assert:
+ that:
+ - ansible_facts is defined and 'fqdn' in ansible_facts
+ - not 'uuid' in ansible_local
+ - name: create 'local facts' for next gathering
+ copy:
+ src: uuid.fact
+ dest: /etc/ansible/facts.d/
+ mode: 0755
+
+- hosts: testhost
+ tasks:
+ - name: ensure facts are gathered and includes the new 'local facts' created above
+ assert:
+ that:
+ - ansible_facts is defined and 'fqdn' in ansible_facts
+ - "'uuid' in ansible_local"
+
+ - name: cleanup 'local facts' from target
+ file: path=/etc/ansible/facts.d/uuid.fact state=absent
diff --git a/test/integration/targets/gathering/runme.sh b/test/integration/targets/gathering/runme.sh
new file mode 100755
index 0000000..1c0832c
--- /dev/null
+++ b/test/integration/targets/gathering/runme.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ANSIBLE_GATHERING=smart ansible-playbook smart.yml --flush-cache -i ../../inventory -v "$@"
+ANSIBLE_GATHERING=implicit ansible-playbook implicit.yml --flush-cache -i ../../inventory -v "$@"
+ANSIBLE_GATHERING=explicit ansible-playbook explicit.yml --flush-cache -i ../../inventory -v "$@"
diff --git a/test/integration/targets/gathering/smart.yml b/test/integration/targets/gathering/smart.yml
new file mode 100644
index 0000000..735cb46
--- /dev/null
+++ b/test/integration/targets/gathering/smart.yml
@@ -0,0 +1,23 @@
+- hosts: testhost
+ tasks:
+ - name: ensure facts are gathered but no local exists
+ assert:
+ that:
+ - ansible_facts is defined and 'fqdn' in ansible_facts
+ - not 'uuid' in ansible_local
+ - name: create local facts for latter test
+ copy:
+ src: uuid.fact
+ dest: /etc/ansible/facts.d/
+ mode: 0755
+
+- hosts: testhost
+ tasks:
+ - name: ensure we still have facts, but didnt pickup new local ones
+ assert:
+ that:
+ - ansible_facts is defined and 'fqdn' in ansible_facts
+ - not 'uuid' in ansible_local
+
+ - name: remove local facts file
+ file: path=/etc/ansible/facts.d/uuid.fact state=absent
diff --git a/test/integration/targets/gathering/uuid.fact b/test/integration/targets/gathering/uuid.fact
new file mode 100644
index 0000000..79e3f62
--- /dev/null
+++ b/test/integration/targets/gathering/uuid.fact
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+import json
+import uuid
+
+
+# return a random string
+print(json.dumps(str(uuid.uuid4())))
diff --git a/test/integration/targets/gathering_facts/aliases b/test/integration/targets/gathering_facts/aliases
new file mode 100644
index 0000000..4cc0d88
--- /dev/null
+++ b/test/integration/targets/gathering_facts/aliases
@@ -0,0 +1,3 @@
+shippable/posix/group5
+needs/root
+context/controller
diff --git a/test/integration/targets/gathering_facts/cache_plugins/none.py b/test/integration/targets/gathering_facts/cache_plugins/none.py
new file mode 100644
index 0000000..5681dee
--- /dev/null
+++ b/test/integration/targets/gathering_facts/cache_plugins/none.py
@@ -0,0 +1,50 @@
+# (c) 2014, Brian Coca, Josh Drake, et al
+# (c) 2017 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.cache import BaseCacheModule
+
+DOCUMENTATION = '''
+ cache: none
+ short_description: write-only cache (no cache)
+ description:
+ - No caching at all
+ version_added: historical
+ author: core team (@ansible-core)
+'''
+
+
+class CacheModule(BaseCacheModule):
+ def __init__(self, *args, **kwargs):
+ self.empty = {}
+
+ def get(self, key):
+ return self.empty.get(key)
+
+ def set(self, key, value):
+ return value
+
+ def keys(self):
+ return self.empty.keys()
+
+ def contains(self, key):
+ return key in self.empty
+
+ def delete(self, key):
+ del self.emtpy[key]
+
+ def flush(self):
+ self.empty = {}
+
+ def copy(self):
+ return self.empty.copy()
+
+ def __getstate__(self):
+ return self.copy()
+
+ def __setstate__(self, data):
+ self.empty = data
diff --git a/test/integration/targets/gathering_facts/collections/ansible_collections/cisco/ios/plugins/modules/ios_facts.py b/test/integration/targets/gathering_facts/collections/ansible_collections/cisco/ios/plugins/modules/ios_facts.py
new file mode 100644
index 0000000..b79f794
--- /dev/null
+++ b/test/integration/targets/gathering_facts/collections/ansible_collections/cisco/ios/plugins/modules/ios_facts.py
@@ -0,0 +1,38 @@
+#!/usr/bin/python
+
+DOCUMENTATION = """
+---
+module: ios_facts
+short_description: supporting network facts module
+description:
+ - supporting network facts module for gather_facts + module_defaults tests
+options:
+ gather_subset:
+ description:
+ - When supplied, this argument restricts the facts collected
+ to a given subset.
+ - Possible values for this argument include
+ C(all), C(hardware), C(config), and C(interfaces).
+ - Specify a list of values to include a larger subset.
+ - Use a value with an initial C(!) to collect all facts except that subset.
+ required: false
+ default: '!config'
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ """main entry point for module execution
+ """
+ argument_spec = dict(
+ gather_subset=dict(default='!config')
+ )
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True)
+
+ module.exit_json(ansible_facts={'gather_subset': module.params['gather_subset'], '_ansible_facts_gathered': True})
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/gathering_facts/inventory b/test/integration/targets/gathering_facts/inventory
new file mode 100644
index 0000000..6352a7d
--- /dev/null
+++ b/test/integration/targets/gathering_facts/inventory
@@ -0,0 +1,2 @@
+[local]
+facthost[0:26] ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
diff --git a/test/integration/targets/gathering_facts/library/bogus_facts b/test/integration/targets/gathering_facts/library/bogus_facts
new file mode 100644
index 0000000..a6aeede
--- /dev/null
+++ b/test/integration/targets/gathering_facts/library/bogus_facts
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+echo '{
+ "changed": false,
+ "ansible_facts": {
+ "ansible_facts": {
+ "discovered_interpreter_python": "(touch /tmp/pwned-$(date -Iseconds)-$(whoami) ) 2>/dev/null >/dev/null && /usr/bin/python",
+ "bogus_overwrite": "yes"
+ },
+ "dansible_iscovered_interpreter_python": "(touch /tmp/pwned-$(date -Iseconds)-$(whoami) ) 2>/dev/null >/dev/null && /usr/bin/python"
+ }
+}'
diff --git a/test/integration/targets/gathering_facts/library/facts_one b/test/integration/targets/gathering_facts/library/facts_one
new file mode 100644
index 0000000..c74ab9a
--- /dev/null
+++ b/test/integration/targets/gathering_facts/library/facts_one
@@ -0,0 +1,25 @@
+#!/bin/sh
+
+echo '{
+ "changed": false,
+ "ansible_facts": {
+ "factsone": "from facts_one module",
+ "common_fact": "also from facts_one module",
+ "common_dict_fact": {
+ "key_one": "from facts_one",
+ "key_two": "from facts_one"
+ },
+ "common_list_fact": [
+ "one",
+ "three",
+ "five"
+ ],
+ "common_list_fact2": [
+ "one",
+ "two",
+ "three",
+ "five",
+ "five"
+ ]
+ }
+}'
diff --git a/test/integration/targets/gathering_facts/library/facts_two b/test/integration/targets/gathering_facts/library/facts_two
new file mode 100644
index 0000000..4e7c668
--- /dev/null
+++ b/test/integration/targets/gathering_facts/library/facts_two
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+echo '{
+ "changed": false,
+ "ansible_facts": {
+ "factstwo": "from facts_two module",
+ "common_fact": "also from facts_two module",
+ "common_dict_fact": {
+ "key_two": "from facts_two",
+ "key_four": "from facts_two"
+ },
+ "common_list_fact": [
+ "one",
+ "two",
+ "four"
+ ],
+ "common_list_fact2": [
+ "one",
+ "two",
+ "four",
+ "four"
+ ]
+ }
+}'
diff --git a/test/integration/targets/gathering_facts/library/file_utils.py b/test/integration/targets/gathering_facts/library/file_utils.py
new file mode 100644
index 0000000..5853802
--- /dev/null
+++ b/test/integration/targets/gathering_facts/library/file_utils.py
@@ -0,0 +1,54 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+import sys
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.facts.utils import (
+ get_file_content,
+ get_file_lines,
+ get_mount_size,
+)
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ test=dict(type='str', default='strip'),
+ touch_file=dict(type='str', default='/dev/null'),
+ line_sep_file=dict(type='str', default='/dev/null'),
+ line_sep_sep=dict(type='str', default='\n'),
+ )
+ )
+
+ test = module.params['test']
+ facts = {}
+
+ if test == 'strip':
+ etc_passwd = get_file_content('/etc/passwd')
+ etc_passwd_unstripped = get_file_content('/etc/passwd', strip=False)
+ facts['etc_passwd_newlines'] = etc_passwd.count('\n')
+ facts['etc_passwd_newlines_unstripped'] = etc_passwd_unstripped.count('\n')
+
+ elif test == 'default':
+ path = module.params['touch_file']
+ facts['touch_default'] = get_file_content(path, default='i am a default')
+
+ elif test == 'line_sep':
+ path = module.params['line_sep_file']
+ sep = module.params['line_sep_sep']
+ facts['line_sep'] = get_file_lines(path, line_sep=sep)
+
+ elif test == 'invalid_mountpoint':
+ facts['invalid_mountpoint'] = get_mount_size('/doesnotexist')
+
+ result = {
+ 'changed': False,
+ 'ansible_facts': facts,
+ }
+
+ module.exit_json(**result)
+
+
+main()
diff --git a/test/integration/targets/gathering_facts/one_two.json b/test/integration/targets/gathering_facts/one_two.json
new file mode 100644
index 0000000..ecc698c
--- /dev/null
+++ b/test/integration/targets/gathering_facts/one_two.json
@@ -0,0 +1,27 @@
+{
+ "_ansible_facts_gathered": true,
+ "common_dict_fact": {
+ "key_four": "from facts_two",
+ "key_one": "from facts_one",
+ "key_two": "from facts_two"
+ },
+ "common_fact": "also from facts_two module",
+ "common_list_fact": [
+ "three",
+ "five",
+ "one",
+ "two",
+ "four"
+ ],
+ "common_list_fact2": [
+ "three",
+ "five",
+ "five",
+ "one",
+ "two",
+ "four",
+ "four"
+ ],
+ "factsone": "from facts_one module",
+ "factstwo": "from facts_two module"
+} \ No newline at end of file
diff --git a/test/integration/targets/gathering_facts/prevent_clobbering.yml b/test/integration/targets/gathering_facts/prevent_clobbering.yml
new file mode 100644
index 0000000..94bb451
--- /dev/null
+++ b/test/integration/targets/gathering_facts/prevent_clobbering.yml
@@ -0,0 +1,8 @@
+- name: Verify existing facts don't go undefined on unrelated new facts in loop
+ hosts: localhost
+ gather_facts: True
+ tasks:
+ - name: Ensure that 'virtualization_type' is not undefined after first loop iteration
+ bogus_facts:
+ loop: [1, 2, 3]
+ when: ansible_facts['virtualization_type'] != 'NotDocker'
diff --git a/test/integration/targets/gathering_facts/runme.sh b/test/integration/targets/gathering_facts/runme.sh
new file mode 100755
index 0000000..c1df560
--- /dev/null
+++ b/test/integration/targets/gathering_facts/runme.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+
+set -eux
+
+#ANSIBLE_CACHE_PLUGINS=cache_plugins/ ANSIBLE_CACHE_PLUGIN=none ansible-playbook test_gathering_facts.yml -i inventory -v "$@"
+ansible-playbook test_gathering_facts.yml -i inventory -e output_dir="$OUTPUT_DIR" -v "$@"
+#ANSIBLE_CACHE_PLUGIN=base ansible-playbook test_gathering_facts.yml -i inventory -v "$@"
+
+ANSIBLE_GATHERING=smart ansible-playbook test_run_once.yml -i inventory -v "$@"
+
+# ensure clean_facts is working properly
+ansible-playbook test_prevent_injection.yml -i inventory -v "$@"
+
+# ensure fact merging is working properly
+ansible-playbook verify_merge_facts.yml -v "$@" -e 'ansible_facts_parallel: False'
+
+# ensure we dont clobber facts in loop
+ansible-playbook prevent_clobbering.yml -v "$@"
+
+# ensure we dont fail module on bad subset
+ansible-playbook verify_subset.yml "$@"
+
+# ensure we can set defaults for the action plugin and facts module
+ansible-playbook test_module_defaults.yml "$@" --tags default_fact_module
+ANSIBLE_FACTS_MODULES='ansible.legacy.setup' ansible-playbook test_module_defaults.yml "$@" --tags custom_fact_module
+
+ansible-playbook test_module_defaults.yml "$@" --tags networking
diff --git a/test/integration/targets/gathering_facts/test_gathering_facts.yml b/test/integration/targets/gathering_facts/test_gathering_facts.yml
new file mode 100644
index 0000000..47027e8
--- /dev/null
+++ b/test/integration/targets/gathering_facts/test_gathering_facts.yml
@@ -0,0 +1,536 @@
+---
+- hosts: facthost7
+ tags: [ 'fact_negation' ]
+ connection: local
+ gather_subset: "!hardware"
+ gather_facts: no
+ tasks:
+ - name: setup with not hardware
+ setup:
+ gather_subset:
+ - "!hardware"
+ register: not_hardware_facts
+
+- name: min and network test for platform added
+ hosts: facthost21
+ tags: [ 'fact_network' ]
+ connection: local
+ gather_subset: ["!all", "network"]
+ gather_facts: yes
+ tasks:
+ - name: Test that retrieving network facts works and gets prereqs from platform and distribution
+ assert:
+ that:
+ - 'ansible_default_ipv4|default("UNDEF") != "UNDEF"'
+ - 'ansible_interfaces|default("UNDEF") != "UNDEF"'
+ # these are true for linux, but maybe not for other os
+ - 'ansible_system|default("UNDEF") != "UNDEF"'
+ - 'ansible_distribution|default("UNDEF") != "UNDEF"'
+ # we dont really require these but they are in the min set
+ # - 'ansible_virtualization_role|default("UNDEF") == "UNDEF"'
+ # - 'ansible_user_id|default("UNDEF") == "UNDEF"'
+ # - 'ansible_env|default("UNDEF") == "UNDEF"'
+ # - 'ansible_selinux|default("UNDEF") == "UNDEF"'
+ # - 'ansible_pkg_mgr|default("UNDEF") == "UNDEF"'
+
+- name: min and hardware test for platform added
+ hosts: facthost22
+ tags: [ 'fact_hardware' ]
+ connection: local
+ gather_subset: "hardware"
+ gather_facts: yes
+ tasks:
+ - name: debug stuff
+ debug:
+ var: hostvars['facthost22']
+ # we should also collect platform, but not distribution
+ - name: Test that retrieving hardware facts works and gets prereqs from platform and distribution
+ when: ansible_system|default("UNDEF") == "Linux"
+ assert:
+ # LinuxHardwareCollector requires 'platform' facts
+ that:
+ - 'ansible_memory_mb|default("UNDEF") != "UNDEF"'
+ - 'ansible_default_ipv4|default("UNDEF") == "UNDEF"'
+ - 'ansible_interfaces|default("UNDEF") == "UNDEF"'
+ # these are true for linux, but maybe not for other os
+ # hardware requires 'platform'
+ - 'ansible_system|default("UNDEF") != "UNDEF"'
+ - 'ansible_machine|default("UNDEF") != "UNDEF"'
+ # hardware does not require 'distribution' but it is min set
+ # - 'ansible_distribution|default("UNDEF") == "UNDEF"'
+ # we dont really require these but they are in the min set
+ # - 'ansible_virtualization_role|default("UNDEF") == "UNDEF"'
+ # - 'ansible_user_id|default("UNDEF") == "UNDEF"'
+ # - 'ansible_env|default("UNDEF") == "UNDEF"'
+ # - 'ansible_selinux|default("UNDEF") == "UNDEF"'
+ # - 'ansible_pkg_mgr|default("UNDEF") == "UNDEF"'
+
+- name: min and service_mgr test for platform added
+ hosts: facthost23
+ tags: [ 'fact_service_mgr' ]
+ connection: local
+ gather_subset: ["!all", "service_mgr"]
+ gather_facts: yes
+ tasks:
+ - name: Test that retrieving service_mgr facts works and gets prereqs from platform and distribution
+ assert:
+ that:
+ - 'ansible_service_mgr|default("UNDEF") != "UNDEF"'
+ - 'ansible_default_ipv4|default("UNDEF") == "UNDEF"'
+ - 'ansible_interfaces|default("UNDEF") == "UNDEF"'
+ # these are true for linux, but maybe not for other os
+ - 'ansible_system|default("UNDEF") != "UNDEF"'
+ - 'ansible_distribution|default("UNDEF") != "UNDEF"'
+ # we dont really require these but they are in the min set
+ # - 'ansible_virtualization_role|default("UNDEF") == "UNDEF"'
+ # - 'ansible_user_id|default("UNDEF") == "UNDEF"'
+ # - 'ansible_env|default("UNDEF") == "UNDEF"'
+ # - 'ansible_selinux|default("UNDEF") == "UNDEF"'
+ # - 'ansible_pkg_mgr|default("UNDEF") == "UNDEF"'
+
+- hosts: facthost0
+ tags: [ 'fact_min' ]
+ connection: local
+ gather_subset: "all"
+ gather_facts: yes
+ tasks:
+ #- setup:
+ # register: facts
+ - name: Test that retrieving all facts works
+ assert:
+ that:
+ - 'ansible_user_id|default("UNDEF_MIN") != "UNDEF_MIN"'
+ - 'ansible_interfaces|default("UNDEF_NET") != "UNDEF_NET"'
+ - 'ansible_mounts|default("UNDEF_MOUNT") != "UNDEF_MOUNT" or ansible_distribution == "MacOSX"'
+ - 'ansible_virtualization_role|default("UNDEF_VIRT") != "UNDEF_VIRT"'
+
+- hosts: facthost1
+ tags: [ 'fact_min' ]
+ connection: local
+ gather_facts: no
+ tasks:
+ - name: Test that we can retrieve the loadavg fact
+ setup:
+ filter: "ansible_loadavg"
+
+ - name: Test the contents of the loadavg fact
+ assert:
+ that:
+ - 'ansible_loadavg|default("UNDEF_ENV") != "UNDEF_ENV"'
+ - '"1m" in ansible_loadavg and ansible_loadavg["1m"] is float'
+ - '"5m" in ansible_loadavg and ansible_loadavg["5m"] is float'
+ - '"15m" in ansible_loadavg and ansible_loadavg["15m"] is float'
+
+- hosts: facthost19
+ tags: [ 'fact_min' ]
+ connection: local
+ gather_facts: no
+ tasks:
+ - setup:
+ filter: "*env*"
+ # register: fact_results
+
+ - name: Test that retrieving all facts filtered to env works
+ assert:
+ that:
+ - 'ansible_interfaces|default("UNDEF_NET") == "UNDEF_NET"'
+ - 'ansible_mounts|default("UNDEF_MOUNT") == "UNDEF_MOUNT"'
+ - 'ansible_virtualization_role|default("UNDEF_VIRT") == "UNDEF_VIRT"'
+ - 'ansible_env|default("UNDEF_ENV") != "UNDEF_ENV"'
+
+- hosts: facthost24
+ tags: [ 'fact_min' ]
+ connection: local
+ gather_facts: no
+ tasks:
+ - setup:
+ filter:
+ - "*env*"
+ - "*virt*"
+
+ - name: Test that retrieving all facts filtered to env and virt works
+ assert:
+ that:
+ - 'ansible_interfaces|default("UNDEF_NET") == "UNDEF_NET"'
+ - 'ansible_mounts|default("UNDEF_MOUNT") == "UNDEF_MOUNT"'
+ - 'ansible_virtualization_role|default("UNDEF_VIRT") != "UNDEF_VIRT"'
+ - 'ansible_env|default("UNDEF_ENV") != "UNDEF_ENV"'
+
+- hosts: facthost25
+ tags: [ 'fact_min' ]
+ gather_facts: no
+ tasks:
+ - setup:
+ filter:
+ - "date_time"
+
+ - name: Test that retrieving all facts filtered to date_time even w/o using ansible_ prefix
+ assert:
+ that:
+ - 'ansible_facts["date_time"]|default("UNDEF_MOUNT") != "UNDEF_MOUNT"'
+ - 'ansible_date_time|default("UNDEF_MOUNT") != "UNDEF_MOUNT"'
+
+- hosts: facthost26
+ tags: [ 'fact_min' ]
+ gather_facts: no
+ tasks:
+ - setup:
+ filter:
+ - "ansible_date_time"
+
+ - name: Test that retrieving all facts filtered to date_time even using ansible_ prefix
+ assert:
+ that:
+ - 'ansible_facts["date_time"]|default("UNDEF_MOUNT") != "UNDEF_MOUNT"'
+ - 'ansible_date_time|default("UNDEF_MOUNT") != "UNDEF_MOUNT"'
+
+- hosts: facthost13
+ tags: [ 'fact_min' ]
+ connection: local
+ gather_facts: no
+ tasks:
+ - setup:
+ filter: "ansible_user_id"
+ # register: fact_results
+
+ - name: Test that retrieving all facts filtered to specific fact ansible_user_id works
+ assert:
+ that:
+ - 'ansible_user_id|default("UNDEF_USER") != "UNDEF_USER"'
+ - 'ansible_interfaces|default("UNDEF_NET") == "UNDEF_NET"'
+ - 'ansible_mounts|default("UNDEF_MOUNT") == "UNDEF_MOUNT"'
+ - 'ansible_virtualization_role|default("UNDEF_VIRT") == "UNDEF_VIRT"'
+ - 'ansible_env|default("UNDEF_ENV") == "UNDEF_ENV"'
+ - 'ansible_pkg_mgr|default("UNDEF_PKG_MGR") == "UNDEF_PKG_MGR"'
+
+- hosts: facthost11
+ tags: [ 'fact_min' ]
+ connection: local
+ gather_facts: no
+ tasks:
+ - setup:
+ filter: "*"
+ # register: fact_results
+
+ - name: Test that retrieving all facts filtered to splat
+ assert:
+ that:
+ - 'ansible_user_id|default("UNDEF_MIN") != "UNDEF_MIN"'
+ - 'ansible_interfaces|default("UNDEF_NET") != "UNDEF_NET"'
+ - 'ansible_mounts|default("UNDEF_MOUNT") != "UNDEF_MOUNT" or ansible_distribution == "MacOSX"'
+ - 'ansible_virtualization_role|default("UNDEF_VIRT") != "UNDEF_VIRT"'
+
+- hosts: facthost12
+ tags: [ 'fact_min' ]
+ connection: local
+ gather_facts: no
+ tasks:
+ - setup:
+ filter: ""
+ # register: fact_results
+
+ - name: Test that retrieving all facts filtered to empty filter_spec works
+ assert:
+ that:
+ - 'ansible_user_id|default("UNDEF_MIN") != "UNDEF_MIN"'
+ - 'ansible_interfaces|default("UNDEF_NET") != "UNDEF_NET"'
+ - 'ansible_mounts|default("UNDEF_MOUNT") != "UNDEF_MOUNT" or ansible_distribution == "MacOSX"'
+ - 'ansible_virtualization_role|default("UNDEF_VIRT") != "UNDEF_VIRT"'
+
+- hosts: facthost1
+ tags: [ 'fact_min' ]
+ connection: local
+ gather_subset: "!all"
+ gather_facts: yes
+ tasks:
+ - name: Test that only retrieving minimal facts work
+ assert:
+ that:
+ # from the min set, which should still collect
+ - 'ansible_user_id|default("UNDEF_MIN") != "UNDEF_MIN"'
+ - 'ansible_env|default("UNDEF_ENV") != "UNDEF_ENV"'
+ # non min facts that are not collected
+ - 'ansible_interfaces|default("UNDEF_NET") == "UNDEF_NET"'
+ - 'ansible_mounts|default("UNDEF_MOUNT") == "UNDEF_MOUNT"'
+ - 'ansible_virtualization_role|default("UNDEF_VIRT") == "UNDEF_VIRT"'
+
+- hosts: facthost2
+ tags: [ 'fact_network' ]
+ connection: local
+ gather_subset: ["!all", "!min", "network"]
+ gather_facts: yes
+ tasks:
+ - name: Test that retrieving network facts work
+ assert:
+ that:
+ - 'ansible_user_id|default("UNDEF") == "UNDEF"'
+ - 'ansible_interfaces|default("UNDEF_NET") != "UNDEF_NET"'
+ - 'ansible_mounts|default("UNDEF") == "UNDEF"'
+ - 'ansible_virtualization_role|default("UNDEF") == "UNDEF"'
+
+- hosts: facthost3
+ tags: [ 'fact_hardware' ]
+ connection: local
+ gather_subset: "hardware"
+ gather_facts: yes
+ tasks:
+ - name: Test that retrieving hardware facts work
+ assert:
+ that:
+ - 'ansible_user_id|default("UNDEF_MIN") != "UNDEF_MIN"'
+ - 'ansible_interfaces|default("UNDEF_NET") == "UNDEF_NET"'
+ - 'ansible_mounts|default("UNDEF_MOUNT") != "UNDEF_MOUNT" or ansible_distribution == "MacOSX"'
+ - 'ansible_virtualization_role|default("UNDEF_VIRT") == "UNDEF_VIRT"'
+
+- hosts: facthost4
+ tags: [ 'fact_virtual' ]
+ connection: local
+ gather_subset: "virtual"
+ gather_facts: yes
+ tasks:
+ - name: Test that retrieving virtualization facts work
+ assert:
+ that:
+ - 'ansible_user_id|default("UNDEF_MIN") != "UNDEF_MIN"'
+ - 'ansible_interfaces|default("UNDEF_NET") == "UNDEF_NET"'
+ - 'ansible_mounts|default("UNDEF_MOUNT") == "UNDEF_MOUNT"'
+ - 'ansible_virtualization_role|default("UNDEF_VIRT") != "UNDEF_VIRT"'
+
+- hosts: facthost5
+ tags: [ 'fact_comma_string' ]
+ connection: local
+ gather_subset: ["virtual", "network"]
+ gather_facts: yes
+ tasks:
+ - name: Test that retrieving virtualization and network as a string works
+ assert:
+ that:
+ - 'ansible_user_id|default("UNDEF_MIN") != "UNDEF_MIN"'
+ - 'ansible_interfaces|default("UNDEF_NET") != "UNDEF_NET"'
+ - 'ansible_mounts|default("UNDEF_MOUNT") == "UNDEF_MOUNT"'
+ - 'ansible_virtualization_role|default("UNDEF_VIRT") != "UNDEF_VIRT"'
+
+- hosts: facthost6
+ tags: [ 'fact_yaml_list' ]
+ connection: local
+ gather_subset:
+ - virtual
+ - network
+ gather_facts: yes
+ tasks:
+ - name: Test that retrieving virtualization and network as a string works
+ assert:
+ that:
+ - 'ansible_user_id|default("UNDEF_MIN") != "UNDEF_MIN"'
+ - 'ansible_interfaces|default("UNDEF_NET") != "UNDEF_NET"'
+ - 'ansible_mounts|default("UNDEF_MOUNT") == "UNDEF_MOUNT"'
+ - 'ansible_virtualization_role|default("UNDEF_VIRT") != "UNDEF_VIRT"'
+
+
+- hosts: facthost7
+ tags: [ 'fact_negation' ]
+ connection: local
+ gather_subset: "!hardware"
+ gather_facts: yes
+ tasks:
+ - name: Test that negation of fact subsets work
+ assert:
+ that:
+ # network, not collected since it is not in min
+ - 'ansible_interfaces|default("UNDEF_NET") == "UNDEF_NET"'
+ # not collecting virt, should be undef
+ - 'ansible_virtualization_role|default("UNDEF_VIRT") == "UNDEF_VIRT"'
+ # mounts/devices are collected by hardware, so should be not collected and undef
+ - 'ansible_mounts|default("UNDEF_MOUNT") == "UNDEF_MOUNT"'
+ - 'ansible_devices|default("UNDEF_DEVICES") == "UNDEF_DEVICES"'
+ # from the min set, which should still collect
+ - 'ansible_user_id|default("UNDEF_MIN") != "UNDEF_MIN"'
+ - 'ansible_env|default("UNDEF_ENV") != "UNDEF_ENV"'
+
+- hosts: facthost8
+ tags: [ 'fact_mixed_negation_addition' ]
+ connection: local
+ gather_subset: ["!hardware", "network"]
+ gather_facts: yes
+ tasks:
+ - name: Test that negation and additional subsets work together
+ assert:
+ that:
+ - 'ansible_user_id|default("UNDEF_MIN") != "UNDEF_MIN"'
+ - 'ansible_interfaces|default("UNDEF_NET") != "UNDEF_NET"'
+ - 'ansible_mounts|default("UNDEF_MOUNT") == "UNDEF_MOUNT"'
+ - 'ansible_virtualization_role|default("UNDEF_VIRT") == "UNDEF_VIRT"'
+
+- hosts: facthost14
+ tags: [ 'fact_mixed_negation_addition_min' ]
+ connection: local
+ gather_subset: ["!all", "!min", "network"]
+ gather_facts: yes
+ tasks:
+ - name: Test that negation and additional subsets work together for min subset
+ assert:
+ that:
+ - 'ansible_user_id|default("UNDEF_MIN") == "UNDEF_MIN"'
+ - 'ansible_interfaces|default("UNDEF_NET") != "UNDEF_NET"'
+ - 'ansible_default_ipv4|default("UNDEF_DEFAULT_IPV4") != "UNDEF_DEFAULT_IPV4"'
+ - 'ansible_all_ipv4_addresses|default("UNDEF_ALL_IPV4") != "UNDEF_ALL_IPV4"'
+ - 'ansible_mounts|default("UNDEF_MOUNT") == "UNDEF_MOUNT"'
+ - 'ansible_virtualization_role|default("UNDEF_VIRT") == "UNDEF_VIRT"'
+ - 'ansible_env|default("UNDEF_ENV") == "UNDEF_ENV"'
+
+- hosts: facthost15
+ tags: [ 'fact_negate_all_min_add_pkg_mgr' ]
+ connection: local
+ gather_subset: ["!all", "!min", "pkg_mgr"]
+ gather_facts: yes
+ tasks:
+ - name: Test that negation and additional subsets work together for min subset
+ assert:
+ that:
+ # network, not collected since it is not in min
+ - 'ansible_interfaces|default("UNDEF_NET") == "UNDEF_NET"'
+ # not collecting virt, should be undef
+ - 'ansible_virtualization_role|default("UNDEF_VIRT") == "UNDEF_VIRT"'
+ # mounts/devices are collected by hardware, so should be not collected and undef
+ - 'ansible_mounts|default("UNDEF_MOUNT") == "UNDEF_MOUNT"'
+ - 'ansible_devices|default("UNDEF_DEVICES") == "UNDEF_DEVICES"'
+ # from the min set, which should not collect
+ - 'ansible_user_id|default("UNDEF_MIN") == "UNDEF_MIN"'
+ - 'ansible_env|default("UNDEF_ENV") == "UNDEF_ENV"'
+ # the pkg_mgr fact we requested explicitly
+ - 'ansible_pkg_mgr|default("UNDEF_PKG_MGR") != "UNDEF_PKG_MGR"'
+
+
+- hosts: facthost9
+ tags: [ 'fact_local']
+ connection: local
+ gather_facts: no
+ tasks:
+ - name: Create fact directories
+ become: true
+ with_items:
+ - /etc/ansible/facts.d
+ - /tmp/custom_facts.d
+ file:
+ state: directory
+ path: "{{ item }}"
+ mode: '0777'
+ - name: Deploy local facts
+ with_items:
+ - path: /etc/ansible/facts.d/testfact.fact
+ content: '{ "fact_dir": "default" }'
+ - path: /tmp/custom_facts.d/testfact.fact
+ content: '{ "fact_dir": "custom" }'
+ copy:
+ dest: "{{ item.path }}"
+ content: "{{ item.content }}"
+
+- hosts: facthost9
+ tags: [ 'fact_local']
+ connection: local
+ gather_facts: yes
+ tasks:
+ - name: Test reading facts from default fact_path
+ assert:
+ that:
+ - '"{{ ansible_local.testfact.fact_dir }}" == "default"'
+
+- hosts: facthost9
+ tags: [ 'fact_local']
+ connection: local
+ gather_facts: yes
+ fact_path: /tmp/custom_facts.d
+ tasks:
+ - name: Test reading facts from custom fact_path
+ assert:
+ that:
+ - '"{{ ansible_local.testfact.fact_dir }}" == "custom"'
+
+- hosts: facthost20
+ tags: [ 'fact_facter_ohai' ]
+ connection: local
+ gather_subset:
+ - facter
+ - ohai
+ gather_facts: yes
+ tasks:
+ - name: Test that retrieving facter and ohai doesnt fail
+ assert:
+ # not much to assert here, aside from not crashing, since test images dont have
+ # facter/ohai
+ that:
+ - 'ansible_user_id|default("UNDEF_MIN") != "UNDEF_MIN"'
+
+- hosts: facthost9
+ tags: [ 'fact_file_utils' ]
+ connection: local
+ gather_facts: false
+ tasks:
+ - block:
+ - name: Ensure get_file_content works when strip=False
+ file_utils:
+ test: strip
+
+ - assert:
+ that:
+ - ansible_facts.get('etc_passwd_newlines', 0) + 1 == ansible_facts.get('etc_passwd_newlines_unstripped', 0)
+
+ - name: Make an empty file
+ file:
+ path: "{{ output_dir }}/empty_file"
+ state: touch
+
+ - name: Ensure get_file_content gives default when file is empty
+ file_utils:
+ test: default
+ touch_file: "{{ output_dir }}/empty_file"
+
+ - assert:
+ that:
+ - ansible_facts.get('touch_default') == 'i am a default'
+
+ - copy:
+ dest: "{{ output_dir }}/1charsep"
+ content: "foo:bar:baz:buzz:"
+
+ - copy:
+ dest: "{{ output_dir }}/2charsep"
+ content: "foo::bar::baz::buzz::"
+
+ - name: Ensure get_file_lines works as expected with specified 1-char line_sep
+ file_utils:
+ test: line_sep
+ line_sep_file: "{{ output_dir }}/1charsep"
+ line_sep_sep: ":"
+
+ - assert:
+ that:
+ - ansible_facts.get('line_sep') == ['foo', 'bar', 'baz', 'buzz']
+
+ - name: Ensure get_file_lines works as expected with specified 1-char line_sep
+ file_utils:
+ test: line_sep
+ line_sep_file: "{{ output_dir }}/2charsep"
+ line_sep_sep: "::"
+
+ - assert:
+ that:
+ - ansible_facts.get('line_sep') == ['foo', 'bar', 'baz', 'buzz', '']
+
+ - name: Ensure get_mount_size fails gracefully
+ file_utils:
+ test: invalid_mountpoint
+
+ - assert:
+ that:
+ - ansible_facts['invalid_mountpoint']|length == 0
+
+ always:
+ - name: Remove test files
+ file:
+ path: "{{ item }}"
+ state: absent
+ with_items:
+ - "{{ output_dir }}/empty_file"
+ - "{{ output_dir }}/1charsep"
+ - "{{ output_dir }}/2charsep"
diff --git a/test/integration/targets/gathering_facts/test_module_defaults.yml b/test/integration/targets/gathering_facts/test_module_defaults.yml
new file mode 100644
index 0000000..038b8ec
--- /dev/null
+++ b/test/integration/targets/gathering_facts/test_module_defaults.yml
@@ -0,0 +1,130 @@
+---
+- hosts: localhost
+ # The gather_facts keyword has default values for its
+ # options so module_defaults doesn't have much affect.
+ gather_facts: no
+ tags:
+ - default_fact_module
+ tasks:
+ - name: set defaults for the action plugin
+ gather_facts:
+ module_defaults:
+ gather_facts:
+ gather_subset: min
+
+ - assert:
+ that: "gather_subset == ['min']"
+
+ - name: set defaults for the module
+ gather_facts:
+ module_defaults:
+ setup:
+ gather_subset: '!all'
+
+ - assert:
+ that: "gather_subset == ['!all']"
+
+ # Defaults for the action plugin win because they are
+ # loaded first and options need to be omitted for
+ # defaults to be used.
+ - name: set defaults for the action plugin and module
+ gather_facts:
+ module_defaults:
+ setup:
+ gather_subset: '!all'
+ gather_facts:
+ gather_subset: min
+
+ - assert:
+ that: "gather_subset == ['min']"
+
+ # The gather_facts 'smart' facts module is 'ansible.legacy.setup' by default.
+ # If 'setup' (the unqualified name) is explicitly requested, FQCN module_defaults
+ # would not apply.
+ - name: set defaults for the fqcn module
+ gather_facts:
+ module_defaults:
+ ansible.legacy.setup:
+ gather_subset: '!all'
+
+ - assert:
+ that: "gather_subset == ['!all']"
+
+- hosts: localhost
+ gather_facts: no
+ tags:
+ - custom_fact_module
+ tasks:
+ - name: set defaults for the module
+ gather_facts:
+ module_defaults:
+ ansible.legacy.setup:
+ gather_subset: '!all'
+
+ - assert:
+ that:
+ - "gather_subset == ['!all']"
+
+ # Defaults for the action plugin win.
+ - name: set defaults for the action plugin and module
+ gather_facts:
+ module_defaults:
+ gather_facts:
+ gather_subset: min
+ ansible.legacy.setup:
+ gather_subset: '!all'
+
+ - assert:
+ that:
+ - "gather_subset == ['min']"
+
+- hosts: localhost
+ gather_facts: no
+ tags:
+ - networking
+ tasks:
+ - name: test that task args aren't used for fqcn network facts
+ gather_facts:
+ gather_subset: min
+ vars:
+ ansible_network_os: 'cisco.ios.ios'
+ register: result
+
+ - assert:
+ that:
+ - "ansible_facts.gather_subset == '!config'"
+
+ - name: test that module_defaults are used for fqcn network facts
+ gather_facts:
+ vars:
+ ansible_network_os: 'cisco.ios.ios'
+ module_defaults:
+ 'cisco.ios.ios_facts': {'gather_subset': 'min'}
+ register: result
+
+ - assert:
+ that:
+ - "ansible_facts.gather_subset == 'min'"
+
+ - name: test that task args aren't used for legacy network facts
+ gather_facts:
+ gather_subset: min
+ vars:
+ ansible_network_os: 'ios'
+ register: result
+
+ - assert:
+ that:
+ - "ansible_facts.gather_subset == '!config'"
+
+ - name: test that module_defaults are used for legacy network facts
+ gather_facts:
+ vars:
+ ansible_network_os: 'ios'
+ module_defaults:
+ 'ios_facts': {'gather_subset': 'min'}
+ register: result
+
+ - assert:
+ that:
+ - "ansible_facts.gather_subset == 'min'"
diff --git a/test/integration/targets/gathering_facts/test_prevent_injection.yml b/test/integration/targets/gathering_facts/test_prevent_injection.yml
new file mode 100644
index 0000000..064b7a9
--- /dev/null
+++ b/test/integration/targets/gathering_facts/test_prevent_injection.yml
@@ -0,0 +1,14 @@
+- name: Ensure clean_facts is working properly
+ hosts: facthost1
+ gather_facts: false
+ tasks:
+ - name: gather 'bad' facts
+ action: bogus_facts
+
+ - name: ensure that the 'bad' facts didn't pollute what they are not supposed to
+ assert:
+ that:
+ - "'touch' not in discovered_interpreter_python|default('')"
+ - "'touch' not in ansible_facts.get('discovered_interpreter_python', '')"
+ - "'touch' not in ansible_facts.get('ansible_facts', {}).get('discovered_interpreter_python', '')"
+ - bogus_overwrite is undefined
diff --git a/test/integration/targets/gathering_facts/test_run_once.yml b/test/integration/targets/gathering_facts/test_run_once.yml
new file mode 100644
index 0000000..37023b2
--- /dev/null
+++ b/test/integration/targets/gathering_facts/test_run_once.yml
@@ -0,0 +1,32 @@
+---
+- hosts: facthost1
+ gather_facts: no
+ tasks:
+ - name: check that smart gathering is enabled
+ fail:
+ msg: 'smart gathering must be enabled'
+ when: 'lookup("env", "ANSIBLE_GATHERING") != "smart"'
+ - name: install test local facts
+ copy:
+ src: uuid.fact
+ dest: /etc/ansible/facts.d/
+ mode: 0755
+
+- hosts: facthost1,facthost2
+ gather_facts: yes
+ run_once: yes
+ tasks:
+ - block:
+ - name: 'Check the same host is used'
+ assert:
+ that: 'hostvars.facthost1.ansible_fqdn == hostvars.facthost2.ansible_fqdn'
+ msg: 'This test requires 2 inventory hosts referring to the same host.'
+ - name: "Check that run_once doesn't prevent fact gathering (#39453)"
+ assert:
+ that: 'hostvars.facthost1.ansible_local.uuid != hostvars.facthost2.ansible_local.uuid'
+ msg: "{{ 'Same value for ansible_local.uuid on both hosts: ' ~ hostvars.facthost1.ansible_local.uuid }}"
+ always:
+ - name: remove test local facts
+ file:
+ path: /etc/ansible/facts.d/uuid.fact
+ state: absent
diff --git a/test/integration/targets/gathering_facts/two_one.json b/test/integration/targets/gathering_facts/two_one.json
new file mode 100644
index 0000000..4b34a2d
--- /dev/null
+++ b/test/integration/targets/gathering_facts/two_one.json
@@ -0,0 +1,27 @@
+{
+ "_ansible_facts_gathered": true,
+ "common_dict_fact": {
+ "key_four": "from facts_two",
+ "key_one": "from facts_one",
+ "key_two": "from facts_one"
+ },
+ "common_fact": "also from facts_one module",
+ "common_list_fact": [
+ "two",
+ "four",
+ "one",
+ "three",
+ "five"
+ ],
+ "common_list_fact2": [
+ "four",
+ "four",
+ "one",
+ "two",
+ "three",
+ "five",
+ "five"
+ ],
+ "factsone": "from facts_one module",
+ "factstwo": "from facts_two module"
+} \ No newline at end of file
diff --git a/test/integration/targets/gathering_facts/uuid.fact b/test/integration/targets/gathering_facts/uuid.fact
new file mode 100644
index 0000000..79e3f62
--- /dev/null
+++ b/test/integration/targets/gathering_facts/uuid.fact
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+import json
+import uuid
+
+
+# return a random string
+print(json.dumps(str(uuid.uuid4())))
diff --git a/test/integration/targets/gathering_facts/verify_merge_facts.yml b/test/integration/targets/gathering_facts/verify_merge_facts.yml
new file mode 100644
index 0000000..d214402
--- /dev/null
+++ b/test/integration/targets/gathering_facts/verify_merge_facts.yml
@@ -0,0 +1,41 @@
+- name: rune one and two, verify merge is as expected
+ hosts: localhost
+ vars:
+ ansible_facts_modules:
+ - facts_one
+ - facts_two
+ tasks:
+
+ - name: populate original
+ include_vars:
+ name: original
+ file: one_two.json
+
+ - name: fail if ref file is updated
+ assert:
+ msg: '{{ansible_facts}} vs {{original}}'
+ that:
+ - ansible_facts|to_json(indent=4, sort_keys=True) == original|to_json(indent=4, sort_keys=True)
+
+ - name: clear existing facts for next play
+ meta: clear_facts
+
+
+- name: rune two and one, verify merge is as expected
+ hosts: localhost
+ vars:
+ ansible_facts_modules:
+ - facts_two
+ - facts_one
+ tasks:
+
+ - name: populate original
+ include_vars:
+ name: original
+ file: two_one.json
+
+ - name: fail if ref file is updated
+ assert:
+ msg: '{{ansible_facts}} vs {{original}}'
+ that:
+ - ansible_facts|to_json(indent=4, sort_keys=True) == original|to_json(indent=4, sort_keys=True)
diff --git a/test/integration/targets/gathering_facts/verify_subset.yml b/test/integration/targets/gathering_facts/verify_subset.yml
new file mode 100644
index 0000000..8913275
--- /dev/null
+++ b/test/integration/targets/gathering_facts/verify_subset.yml
@@ -0,0 +1,13 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: bad subset used
+ setup: gather_subset=nonsense
+ register: bad_sub
+ ignore_errors: true
+
+ - name: verify we fail the right way
+ assert:
+ that:
+ - bad_sub is failed
+ - "'MODULE FAILURE' not in bad_sub['msg']"
diff --git a/test/integration/targets/get_url/aliases b/test/integration/targets/get_url/aliases
new file mode 100644
index 0000000..90ef161
--- /dev/null
+++ b/test/integration/targets/get_url/aliases
@@ -0,0 +1,3 @@
+destructive
+shippable/posix/group1
+needs/httptester
diff --git a/test/integration/targets/get_url/files/testserver.py b/test/integration/targets/get_url/files/testserver.py
new file mode 100644
index 0000000..24967d4
--- /dev/null
+++ b/test/integration/targets/get_url/files/testserver.py
@@ -0,0 +1,23 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import sys
+
+if __name__ == '__main__':
+ if sys.version_info[0] >= 3:
+ import http.server
+ import socketserver
+ PORT = int(sys.argv[1])
+
+ class Handler(http.server.SimpleHTTPRequestHandler):
+ pass
+
+ Handler.extensions_map['.json'] = 'application/json'
+ httpd = socketserver.TCPServer(("", PORT), Handler)
+ httpd.serve_forever()
+ else:
+ import mimetypes
+ mimetypes.init()
+ mimetypes.add_type('application/json', '.json')
+ import SimpleHTTPServer
+ SimpleHTTPServer.test()
diff --git a/test/integration/targets/get_url/meta/main.yml b/test/integration/targets/get_url/meta/main.yml
new file mode 100644
index 0000000..2c2155a
--- /dev/null
+++ b/test/integration/targets/get_url/meta/main.yml
@@ -0,0 +1,4 @@
+dependencies:
+ - prepare_tests
+ - prepare_http_tests
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/get_url/tasks/ciphers.yml b/test/integration/targets/get_url/tasks/ciphers.yml
new file mode 100644
index 0000000..c7d9979
--- /dev/null
+++ b/test/integration/targets/get_url/tasks/ciphers.yml
@@ -0,0 +1,19 @@
+- name: test good cipher
+ get_url:
+ url: https://{{ httpbin_host }}/get
+ ciphers: ECDHE-RSA-AES128-SHA256
+ dest: '{{ remote_tmp_dir }}/good_cipher_get.json'
+ register: good_ciphers
+
+- name: test bad cipher
+ get_url:
+ url: https://{{ httpbin_host }}/get
+ ciphers: ECDHE-ECDSA-AES128-SHA
+ dest: '{{ remote_tmp_dir }}/bad_cipher_get.json'
+ ignore_errors: true
+ register: bad_ciphers
+
+- assert:
+ that:
+ - good_ciphers is successful
+ - bad_ciphers is failed
diff --git a/test/integration/targets/get_url/tasks/main.yml b/test/integration/targets/get_url/tasks/main.yml
new file mode 100644
index 0000000..09814c7
--- /dev/null
+++ b/test/integration/targets/get_url/tasks/main.yml
@@ -0,0 +1,674 @@
+# Test code for the get_url module
+# (c) 2014, Richard Isaacson <richard.c.isaacson@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <https://www.gnu.org/licenses/>.
+
+- name: Determine if python looks like it will support modern ssl features like SNI
+ command: "{{ ansible_python.executable }} -c 'from ssl import SSLContext'"
+ ignore_errors: True
+ register: python_test
+
+- name: Set python_has_sslcontext if we have it
+ set_fact:
+ python_has_ssl_context: True
+ when: python_test.rc == 0
+
+- name: Set python_has_sslcontext False if we don't have it
+ set_fact:
+ python_has_ssl_context: False
+ when: python_test.rc != 0
+
+- name: Define test files for file schema
+ set_fact:
+ geturl_srcfile: "{{ remote_tmp_dir }}/aurlfile.txt"
+ geturl_dstfile: "{{ remote_tmp_dir }}/aurlfile_copy.txt"
+
+- name: Create source file
+ copy:
+ dest: "{{ geturl_srcfile }}"
+ content: "foobar"
+ register: source_file_copied
+
+- name: test file fetch
+ get_url:
+ url: "file://{{ source_file_copied.dest }}"
+ dest: "{{ geturl_dstfile }}"
+ register: result
+
+- name: assert success and change
+ assert:
+ that:
+ - result is changed
+ - '"OK" in result.msg'
+
+- name: test nonexisting file fetch
+ get_url:
+ url: "file://{{ source_file_copied.dest }}NOFILE"
+ dest: "{{ geturl_dstfile }}NOFILE"
+ register: result
+ ignore_errors: True
+
+- name: assert success and change
+ assert:
+ that:
+ - result is failed
+
+- name: test HTTP HEAD request for file in check mode
+ get_url:
+ url: "https://{{ httpbin_host }}/get"
+ dest: "{{ remote_tmp_dir }}/get_url_check.txt"
+ force: yes
+ check_mode: True
+ register: result
+
+- name: assert that the HEAD request was successful in check mode
+ assert:
+ that:
+ - result is changed
+ - '"OK" in result.msg'
+
+- name: test HTTP HEAD for nonexistent URL in check mode
+ get_url:
+ url: "https://{{ httpbin_host }}/DOESNOTEXIST"
+ dest: "{{ remote_tmp_dir }}/shouldnotexist.html"
+ force: yes
+ check_mode: True
+ register: result
+ ignore_errors: True
+
+- name: assert that HEAD request for nonexistent URL failed
+ assert:
+ that:
+ - result is failed
+
+- name: test https fetch
+ get_url: url="https://{{ httpbin_host }}/get" dest={{remote_tmp_dir}}/get_url.txt force=yes
+ register: result
+
+- name: assert the get_url call was successful
+ assert:
+ that:
+ - result is changed
+ - '"OK" in result.msg'
+
+- name: test https fetch to a site with mismatched hostname and certificate
+ get_url:
+ url: "https://{{ badssl_host }}/"
+ dest: "{{ remote_tmp_dir }}/shouldnotexist.html"
+ ignore_errors: True
+ register: result
+
+- stat:
+ path: "{{ remote_tmp_dir }}/shouldnotexist.html"
+ register: stat_result
+
+- name: Assert that the file was not downloaded
+ assert:
+ that:
+ - "result is failed"
+ - "'Failed to validate the SSL certificate' in result.msg or 'Hostname mismatch' in result.msg or ( result.msg is match('hostname .* doesn.t match .*'))"
+ - "stat_result.stat.exists == false"
+
+- name: test https fetch to a site with mismatched hostname and certificate and validate_certs=no
+ get_url:
+ url: "https://{{ badssl_host }}/"
+ dest: "{{ remote_tmp_dir }}/get_url_no_validate.html"
+ validate_certs: no
+ register: result
+
+- stat:
+ path: "{{ remote_tmp_dir }}/get_url_no_validate.html"
+ register: stat_result
+
+- name: Assert that the file was downloaded
+ assert:
+ that:
+ - result is changed
+ - "stat_result.stat.exists == true"
+
+# SNI Tests
+# SNI is only built into the stdlib from python-2.7.9 onwards
+- name: Test that SNI works
+ get_url:
+ url: 'https://{{ sni_host }}/'
+ dest: "{{ remote_tmp_dir }}/sni.html"
+ register: get_url_result
+ ignore_errors: True
+
+- command: "grep '{{ sni_host }}' {{ remote_tmp_dir}}/sni.html"
+ register: data_result
+ when: python_has_ssl_context
+
+- debug:
+ var: get_url_result
+
+- name: Assert that SNI works with this python version
+ assert:
+ that:
+ - 'data_result.rc == 0'
+ when: python_has_ssl_context
+
+# If the client doesn't support SNI then get_url should have failed with a certificate mismatch
+- name: Assert that hostname verification failed because SNI is not supported on this version of python
+ assert:
+ that:
+ - 'get_url_result is failed'
+ when: not python_has_ssl_context
+
+# These tests are just side effects of how the site is hosted. It's not
+# specifically a test site. So the tests may break due to the hosting changing
+- name: Test that SNI works
+ get_url:
+ url: 'https://{{ sni_host }}/'
+ dest: "{{ remote_tmp_dir }}/sni.html"
+ register: get_url_result
+ ignore_errors: True
+
+- command: "grep '{{ sni_host }}' {{ remote_tmp_dir}}/sni.html"
+ register: data_result
+ when: python_has_ssl_context
+
+- debug:
+ var: get_url_result
+
+- name: Assert that SNI works with this python version
+ assert:
+ that:
+ - 'data_result.rc == 0'
+ - 'get_url_result is not failed'
+ when: python_has_ssl_context
+
+# If the client doesn't support SNI then get_url should have failed with a certificate mismatch
+- name: Assert that hostname verification failed because SNI is not supported on this version of python
+ assert:
+ that:
+ - 'get_url_result is failed'
+ when: not python_has_ssl_context
+# End hacky SNI test section
+
+- name: Test get_url with redirect
+ get_url:
+ url: 'https://{{ httpbin_host }}/redirect/6'
+ dest: "{{ remote_tmp_dir }}/redirect.json"
+
+- name: Test that setting file modes work
+ get_url:
+ url: 'https://{{ httpbin_host }}/'
+ dest: '{{ remote_tmp_dir }}/test'
+ mode: '0707'
+ register: result
+
+- stat:
+ path: "{{ remote_tmp_dir }}/test"
+ register: stat_result
+
+- name: Assert that the file has the right permissions
+ assert:
+ that:
+ - result is changed
+ - "stat_result.stat.mode == '0707'"
+
+- name: Test that setting file modes on an already downloaded file work
+ get_url:
+ url: 'https://{{ httpbin_host }}/'
+ dest: '{{ remote_tmp_dir }}/test'
+ mode: '0070'
+ register: result
+
+- stat:
+ path: "{{ remote_tmp_dir }}/test"
+ register: stat_result
+
+- name: Assert that the file has the right permissions
+ assert:
+ that:
+ - result is changed
+ - "stat_result.stat.mode == '0070'"
+
+# https://github.com/ansible/ansible/pull/65307/
+- name: Test that on http status 304, we get a status_code field.
+ get_url:
+ url: 'https://{{ httpbin_host }}/status/304'
+ dest: '{{ remote_tmp_dir }}/test'
+ register: result
+
+- name: Assert that we get the appropriate status_code
+ assert:
+ that:
+ - "'status_code' in result"
+ - "result.status_code == 304"
+
+# https://github.com/ansible/ansible/issues/29614
+- name: Change mode on an already downloaded file and specify checksum
+ get_url:
+ url: 'https://{{ httpbin_host }}/base64/cHR1eA=='
+ dest: '{{ remote_tmp_dir }}/test'
+ checksum: 'sha256:b1b6ce5073c8fac263a8fc5edfffdbd5dec1980c784e09c5bc69f8fb6056f006.'
+ mode: '0775'
+ register: result
+
+- stat:
+ path: "{{ remote_tmp_dir }}/test"
+ register: stat_result
+
+- name: Assert that file permissions on already downloaded file were changed
+ assert:
+ that:
+ - result is changed
+ - "stat_result.stat.mode == '0775'"
+
+- name: test checksum match in check mode
+ get_url:
+ url: 'https://{{ httpbin_host }}/base64/cHR1eA=='
+ dest: '{{ remote_tmp_dir }}/test'
+ checksum: 'sha256:b1b6ce5073c8fac263a8fc5edfffdbd5dec1980c784e09c5bc69f8fb6056f006.'
+ check_mode: True
+ register: result
+
+- name: Assert that check mode was green
+ assert:
+ that:
+ - result is not changed
+
+- name: Get a file that already exists with a checksum
+ get_url:
+ url: 'https://{{ httpbin_host }}/cache'
+ dest: '{{ remote_tmp_dir }}/test'
+ checksum: 'sha1:{{ stat_result.stat.checksum }}'
+ register: result
+
+- name: Assert that the file was not downloaded
+ assert:
+ that:
+ - result.msg == 'file already exists'
+
+- name: Get a file that already exists
+ get_url:
+ url: 'https://{{ httpbin_host }}/cache'
+ dest: '{{ remote_tmp_dir }}/test'
+ register: result
+
+- name: Assert that we didn't re-download unnecessarily
+ assert:
+ that:
+ - result is not changed
+ - "'304' in result.msg"
+
+- name: get a file that doesn't respond to If-Modified-Since without checksum
+ get_url:
+ url: 'https://{{ httpbin_host }}/get'
+ dest: '{{ remote_tmp_dir }}/test'
+ register: result
+
+- name: Assert that we downloaded the file
+ assert:
+ that:
+ - result is changed
+
+# https://github.com/ansible/ansible/issues/27617
+
+- name: set role facts
+ set_fact:
+ http_port: 27617
+ files_dir: '{{ remote_tmp_dir }}/files'
+
+- name: create files_dir
+ file:
+ dest: "{{ files_dir }}"
+ state: directory
+
+- name: create src file
+ copy:
+ dest: '{{ files_dir }}/27617.txt'
+ content: "ptux"
+
+- name: create duplicate src file
+ copy:
+ dest: '{{ files_dir }}/71420.txt'
+ content: "ptux"
+
+- name: create sha1 checksum file of src
+ copy:
+ dest: '{{ files_dir }}/sha1sum.txt'
+ content: |
+ a97e6837f60cec6da4491bab387296bbcd72bdba 27617.txt
+ a97e6837f60cec6da4491bab387296bbcd72bdba 71420.txt
+ 3911340502960ca33aece01129234460bfeb2791 not_target1.txt
+ 1b4b6adf30992cedb0f6edefd6478ff0a593b2e4 not_target2.txt
+
+- name: create sha256 checksum file of src
+ copy:
+ dest: '{{ files_dir }}/sha256sum.txt'
+ content: |
+ b1b6ce5073c8fac263a8fc5edfffdbd5dec1980c784e09c5bc69f8fb6056f006. 27617.txt
+ b1b6ce5073c8fac263a8fc5edfffdbd5dec1980c784e09c5bc69f8fb6056f006. 71420.txt
+ 30949cc401e30ac494d695ab8764a9f76aae17c5d73c67f65e9b558f47eff892 not_target1.txt
+ d0dbfc1945bc83bf6606b770e442035f2c4e15c886ee0c22fb3901ba19900b5b not_target2.txt
+
+- name: create sha256 checksum file of src with a dot leading path
+ copy:
+ dest: '{{ files_dir }}/sha256sum_with_dot.txt'
+ content: |
+ b1b6ce5073c8fac263a8fc5edfffdbd5dec1980c784e09c5bc69f8fb6056f006. ./27617.txt
+ b1b6ce5073c8fac263a8fc5edfffdbd5dec1980c784e09c5bc69f8fb6056f006. ./71420.txt
+ 30949cc401e30ac494d695ab8764a9f76aae17c5d73c67f65e9b558f47eff892 ./not_target1.txt
+ d0dbfc1945bc83bf6606b770e442035f2c4e15c886ee0c22fb3901ba19900b5b ./not_target2.txt
+
+- name: create sha256 checksum file of src with a * leading path
+ copy:
+ dest: '{{ files_dir }}/sha256sum_with_asterisk.txt'
+ content: |
+ b1b6ce5073c8fac263a8fc5edfffdbd5dec1980c784e09c5bc69f8fb6056f006. *27617.txt
+ b1b6ce5073c8fac263a8fc5edfffdbd5dec1980c784e09c5bc69f8fb6056f006. *71420.txt
+ 30949cc401e30ac494d695ab8764a9f76aae17c5d73c67f65e9b558f47eff892 *not_target1.txt
+ d0dbfc1945bc83bf6606b770e442035f2c4e15c886ee0c22fb3901ba19900b5b *not_target2.txt
+
+# completing 27617 with bug 54390
+- name: create sha256 checksum only with no filename inside
+ copy:
+ dest: '{{ files_dir }}/sha256sum_checksum_only.txt'
+ content: |
+ b1b6ce5073c8fac263a8fc5edfffdbd5dec1980c784e09c5bc69f8fb6056f006
+
+- copy:
+ src: "testserver.py"
+ dest: "{{ remote_tmp_dir }}/testserver.py"
+
+- name: start SimpleHTTPServer for issues 27617
+ shell: cd {{ files_dir }} && {{ ansible_python.executable }} {{ remote_tmp_dir}}/testserver.py {{ http_port }}
+ async: 90
+ poll: 0
+
+- name: Wait for SimpleHTTPServer to come up online
+ wait_for:
+ host: 'localhost'
+ port: '{{ http_port }}'
+ state: started
+
+- name: download src with sha1 checksum url in check mode
+ get_url:
+ url: 'http://localhost:{{ http_port }}/27617.txt'
+ dest: '{{ remote_tmp_dir }}'
+ checksum: 'sha1:http://localhost:{{ http_port }}/sha1sum.txt'
+ register: result_sha1_check_mode
+ check_mode: True
+
+- name: download src with sha1 checksum url
+ get_url:
+ url: 'http://localhost:{{ http_port }}/27617.txt'
+ dest: '{{ remote_tmp_dir }}'
+ checksum: 'sha1:http://localhost:{{ http_port }}/sha1sum.txt'
+ register: result_sha1
+
+- stat:
+ path: "{{ remote_tmp_dir }}/27617.txt"
+ register: stat_result_sha1
+
+- name: download src with sha256 checksum url
+ get_url:
+ url: 'http://localhost:{{ http_port }}/27617.txt'
+ dest: '{{ remote_tmp_dir }}/27617sha256.txt'
+ checksum: 'sha256:http://localhost:{{ http_port }}/sha256sum.txt'
+ register: result_sha256
+
+- stat:
+ path: "{{ remote_tmp_dir }}/27617.txt"
+ register: stat_result_sha256
+
+- name: download src with sha256 checksum url with dot leading paths
+ get_url:
+ url: 'http://localhost:{{ http_port }}/27617.txt'
+ dest: '{{ remote_tmp_dir }}/27617sha256_with_dot.txt'
+ checksum: 'sha256:http://localhost:{{ http_port }}/sha256sum_with_dot.txt'
+ register: result_sha256_with_dot
+
+- stat:
+ path: "{{ remote_tmp_dir }}/27617sha256_with_dot.txt"
+ register: stat_result_sha256_with_dot
+
+- name: download src with sha256 checksum url with asterisk leading paths
+ get_url:
+ url: 'http://localhost:{{ http_port }}/27617.txt'
+ dest: '{{ remote_tmp_dir }}/27617sha256_with_asterisk.txt'
+ checksum: 'sha256:http://localhost:{{ http_port }}/sha256sum_with_asterisk.txt'
+ register: result_sha256_with_asterisk
+
+- stat:
+ path: "{{ remote_tmp_dir }}/27617sha256_with_asterisk.txt"
+ register: stat_result_sha256_with_asterisk
+
+- name: download src with sha256 checksum url with file scheme
+ get_url:
+ url: 'http://localhost:{{ http_port }}/27617.txt'
+ dest: '{{ remote_tmp_dir }}/27617sha256_with_file_scheme.txt'
+ checksum: 'sha256:file://{{ files_dir }}/sha256sum.txt'
+ register: result_sha256_with_file_scheme
+
+- stat:
+ path: "{{ remote_tmp_dir }}/27617sha256_with_dot.txt"
+ register: stat_result_sha256_with_file_scheme
+
+- name: download 71420.txt with sha1 checksum url
+ get_url:
+ url: 'http://localhost:{{ http_port }}/71420.txt'
+ dest: '{{ remote_tmp_dir }}'
+ checksum: 'sha1:http://localhost:{{ http_port }}/sha1sum.txt'
+ register: result_sha1_71420
+
+- stat:
+ path: "{{ remote_tmp_dir }}/71420.txt"
+ register: stat_result_sha1_71420
+
+- name: download 71420.txt with sha256 checksum url
+ get_url:
+ url: 'http://localhost:{{ http_port }}/71420.txt'
+ dest: '{{ remote_tmp_dir }}/71420sha256.txt'
+ checksum: 'sha256:http://localhost:{{ http_port }}/sha256sum.txt'
+ register: result_sha256_71420
+
+- stat:
+ path: "{{ remote_tmp_dir }}/71420.txt"
+ register: stat_result_sha256_71420
+
+- name: download 71420.txt with sha256 checksum url with dot leading paths
+ get_url:
+ url: 'http://localhost:{{ http_port }}/71420.txt'
+ dest: '{{ remote_tmp_dir }}/71420sha256_with_dot.txt'
+ checksum: 'sha256:http://localhost:{{ http_port }}/sha256sum_with_dot.txt'
+ register: result_sha256_with_dot_71420
+
+- stat:
+ path: "{{ remote_tmp_dir }}/71420sha256_with_dot.txt"
+ register: stat_result_sha256_with_dot_71420
+
+- name: download 71420.txt with sha256 checksum url with asterisk leading paths
+ get_url:
+ url: 'http://localhost:{{ http_port }}/71420.txt'
+ dest: '{{ remote_tmp_dir }}/71420sha256_with_asterisk.txt'
+ checksum: 'sha256:http://localhost:{{ http_port }}/sha256sum_with_asterisk.txt'
+ register: result_sha256_with_asterisk_71420
+
+- stat:
+ path: "{{ remote_tmp_dir }}/71420sha256_with_asterisk.txt"
+ register: stat_result_sha256_with_asterisk_71420
+
+- name: download 71420.txt with sha256 checksum url with file scheme
+ get_url:
+ url: 'http://localhost:{{ http_port }}/71420.txt'
+ dest: '{{ remote_tmp_dir }}/71420sha256_with_file_scheme.txt'
+ checksum: 'sha256:file://{{ files_dir }}/sha256sum.txt'
+ register: result_sha256_with_file_scheme_71420
+
+- stat:
+ path: "{{ remote_tmp_dir }}/71420sha256_with_dot.txt"
+ register: stat_result_sha256_with_file_scheme_71420
+
+- name: download src with sha256 checksum url with no filename
+ get_url:
+ url: 'http://localhost:{{ http_port }}/27617.txt'
+ dest: '{{ remote_tmp_dir }}/27617sha256_with_no_filename.txt'
+ checksum: 'sha256:http://localhost:{{ http_port }}/sha256sum_checksum_only.txt'
+ register: result_sha256_checksum_only
+
+- stat:
+ path: "{{ remote_tmp_dir }}/27617.txt"
+ register: stat_result_sha256_checksum_only
+
+- name: Assert that the file was downloaded
+ assert:
+ that:
+ - result_sha1 is changed
+ - result_sha1_check_mode is changed
+ - result_sha256 is changed
+ - result_sha256_with_dot is changed
+ - result_sha256_with_asterisk is changed
+ - result_sha256_with_file_scheme is changed
+ - "stat_result_sha1.stat.exists == true"
+ - "stat_result_sha256.stat.exists == true"
+ - "stat_result_sha256_with_dot.stat.exists == true"
+ - "stat_result_sha256_with_asterisk.stat.exists == true"
+ - "stat_result_sha256_with_file_scheme.stat.exists == true"
+ - result_sha1_71420 is changed
+ - result_sha256_71420 is changed
+ - result_sha256_with_dot_71420 is changed
+ - result_sha256_with_asterisk_71420 is changed
+ - result_sha256_checksum_only is changed
+ - result_sha256_with_file_scheme_71420 is changed
+ - "stat_result_sha1_71420.stat.exists == true"
+ - "stat_result_sha256_71420.stat.exists == true"
+ - "stat_result_sha256_with_dot_71420.stat.exists == true"
+ - "stat_result_sha256_with_asterisk_71420.stat.exists == true"
+ - "stat_result_sha256_with_file_scheme_71420.stat.exists == true"
+ - "stat_result_sha256_checksum_only.stat.exists == true"
+
+#https://github.com/ansible/ansible/issues/16191
+- name: Test url split with no filename
+ get_url:
+ url: https://{{ httpbin_host }}
+ dest: "{{ remote_tmp_dir }}"
+
+- name: Test headers dict
+ get_url:
+ url: https://{{ httpbin_host }}/headers
+ headers:
+ Foo: bar
+ Baz: qux
+ dest: "{{ remote_tmp_dir }}/headers_dict.json"
+
+- name: Get downloaded file
+ slurp:
+ src: "{{ remote_tmp_dir }}/headers_dict.json"
+ register: result
+
+- name: Test headers dict
+ assert:
+ that:
+ - (result.content | b64decode | from_json).headers.get('Foo') == 'bar'
+ - (result.content | b64decode | from_json).headers.get('Baz') == 'qux'
+
+- name: Test gzip decompression
+ get_url:
+ url: https://{{ httpbin_host }}/gzip
+ dest: "{{ remote_tmp_dir }}/gzip.json"
+
+- name: Get gzip file contents
+ slurp:
+ path: "{{ remote_tmp_dir }}/gzip.json"
+ register: gzip_json
+
+- name: validate gzip decompression
+ assert:
+ that:
+ - (gzip_json.content|b64decode|from_json).gzipped
+
+- name: Test gzip no decompression
+ get_url:
+ url: https://{{ httpbin_host }}/gzip
+ dest: "{{ remote_tmp_dir }}/gzip.json.gz"
+ decompress: no
+
+- name: Get gzip file contents
+ command: 'gunzip -c {{ remote_tmp_dir }}/gzip.json.gz'
+ register: gzip_json
+
+- name: validate gzip no decompression
+ assert:
+ that:
+ - (gzip_json.stdout|from_json).gzipped
+
+- name: Test client cert auth, with certs
+ get_url:
+ url: "https://ansible.http.tests/ssl_client_verify"
+ client_cert: "{{ remote_tmp_dir }}/client.pem"
+ client_key: "{{ remote_tmp_dir }}/client.key"
+ dest: "{{ remote_tmp_dir }}/ssl_client_verify"
+ when: has_httptester
+
+- name: Get downloaded file
+ slurp:
+ src: "{{ remote_tmp_dir }}/ssl_client_verify"
+ register: result
+ when: has_httptester
+
+- name: Assert that the ssl_client_verify file contains the correct content
+ assert:
+ that:
+ - '(result.content | b64decode) == "ansible.http.tests:SUCCESS"'
+ when: has_httptester
+
+- name: test unredirected_headers
+ get_url:
+ url: 'https://{{ httpbin_host }}/redirect-to?status_code=301&url=/basic-auth/user/passwd'
+ username: user
+ password: passwd
+ force_basic_auth: true
+ unredirected_headers:
+ - authorization
+ dest: "{{ remote_tmp_dir }}/doesnt_matter"
+ ignore_errors: true
+ register: unredirected_headers
+
+- name: test unredirected_headers
+ get_url:
+ url: 'https://{{ httpbin_host }}/redirect-to?status_code=301&url=/basic-auth/user/passwd'
+ username: user
+ password: passwd
+ force_basic_auth: true
+ dest: "{{ remote_tmp_dir }}/doesnt_matter"
+ register: redirected_headers
+
+- name: ensure unredirected_headers caused auth to fail
+ assert:
+ that:
+ - unredirected_headers is failed
+ - unredirected_headers.status_code == 401
+ - redirected_headers is successful
+ - redirected_headers.status_code == 200
+
+- name: Test use_gssapi=True
+ include_tasks:
+ file: use_gssapi.yml
+ apply:
+ environment:
+ KRB5_CONFIG: '{{ krb5_config }}'
+ KRB5CCNAME: FILE:{{ remote_tmp_dir }}/krb5.cc
+ when: krb5_config is defined
+
+- name: Test ciphers
+ import_tasks: ciphers.yml
+
+- name: Test use_netrc=False
+ import_tasks: use_netrc.yml
diff --git a/test/integration/targets/get_url/tasks/use_gssapi.yml b/test/integration/targets/get_url/tasks/use_gssapi.yml
new file mode 100644
index 0000000..f3d2d1f
--- /dev/null
+++ b/test/integration/targets/get_url/tasks/use_gssapi.yml
@@ -0,0 +1,45 @@
+- name: test Negotiate auth over HTTP with explicit credentials
+ get_url:
+ url: http://{{ httpbin_host }}/gssapi
+ dest: '{{ remote_tmp_dir }}/gssapi_explicit.txt'
+ use_gssapi: yes
+ url_username: '{{ krb5_username }}'
+ url_password: '{{ krb5_password }}'
+ register: http_explicit
+
+- name: get result of test Negotiate auth over HTTP with explicit credentials
+ slurp:
+ path: '{{ remote_tmp_dir }}/gssapi_explicit.txt'
+ register: http_explicit_actual
+
+- name: assert test Negotiate auth with implicit credentials
+ assert:
+ that:
+ - http_explicit.status_code == 200
+ - http_explicit_actual.content | b64decode | trim == 'Microsoft Rulz'
+
+- name: skip tests on macOS, I cannot seem to get it to read a credential from a custom ccache
+ when: ansible_facts.distribution != 'MacOSX'
+ block:
+ - name: get Kerberos ticket for implicit auth tests
+ httptester_kinit:
+ username: '{{ krb5_username }}'
+ password: '{{ krb5_password }}'
+
+ - name: test Negotiate auth over HTTPS with implicit credentials
+ get_url:
+ url: https://{{ httpbin_host }}/gssapi
+ dest: '{{ remote_tmp_dir }}/gssapi_implicit.txt'
+ use_gssapi: yes
+ register: https_implicit
+
+ - name: get result of test Negotiate auth over HTTPS with implicit credentials
+ slurp:
+ path: '{{ remote_tmp_dir }}/gssapi_implicit.txt'
+ register: https_implicit_actual
+
+ - name: assert test Negotiate auth with implicit credentials
+ assert:
+ that:
+ - https_implicit.status_code == 200
+ - https_implicit_actual.content | b64decode | trim == 'Microsoft Rulz'
diff --git a/test/integration/targets/get_url/tasks/use_netrc.yml b/test/integration/targets/get_url/tasks/use_netrc.yml
new file mode 100644
index 0000000..e1852a8
--- /dev/null
+++ b/test/integration/targets/get_url/tasks/use_netrc.yml
@@ -0,0 +1,67 @@
+- name: Write out netrc
+ copy:
+ dest: "{{ remote_tmp_dir }}/netrc"
+ content: |
+ machine {{ httpbin_host }}
+ login foo
+ password bar
+
+- name: Test Bearer authorization is failed with netrc
+ get_url:
+ url: https://{{ httpbin_host }}/bearer
+ headers:
+ Authorization: Bearer foobar
+ dest: "{{ remote_tmp_dir }}/msg.txt"
+ ignore_errors: yes
+ environment:
+ NETRC: "{{ remote_tmp_dir }}/netrc"
+
+- name: Read msg.txt file
+ ansible.builtin.slurp:
+ src: "{{ remote_tmp_dir }}/msg.txt"
+ register: response_failed
+
+- name: Parse token from msg.txt
+ set_fact:
+ token: "{{ (response_failed['content'] | b64decode | from_json).token }}"
+
+- name: assert Test Bearer authorization is failed with netrc
+ assert:
+ that:
+ - "token.find('v=' ~ 'Zm9vOmJhcg') == -1"
+ fail_msg: "Was expecting 'foo:bar' in base64, but received: {{ response_failed['content'] | b64decode | from_json }}"
+ success_msg: "Expected Basic authentication even Bearer headers were sent"
+
+- name: Test Bearer authorization is successfull with use_netrc=False
+ get_url:
+ url: https://{{ httpbin_host }}/bearer
+ use_netrc: false
+ headers:
+ Authorization: Bearer foobar
+ dest: "{{ remote_tmp_dir }}/msg.txt"
+ environment:
+ NETRC: "{{ remote_tmp_dir }}/netrc"
+
+- name: Read msg.txt file
+ ansible.builtin.slurp:
+ src: "{{ remote_tmp_dir }}/msg.txt"
+ register: response
+
+- name: Parse token from msg.txt
+ set_fact:
+ token: "{{ (response['content'] | b64decode | from_json).token }}"
+
+- name: assert Test Bearer authorization is successfull with use_netrc=False
+ assert:
+ that:
+ - "token.find('v=' ~ 'foobar') == -1"
+ fail_msg: "Was expecting Bearer token 'foobar', but received: {{ response['content'] | b64decode | from_json }}"
+ success_msg: "Bearer authentication successfull without netrc"
+
+- name: Clean up
+ file:
+ path: "{{ item }}"
+ state: absent
+ with_items:
+ - "{{ remote_tmp_dir }}/netrc"
+ - "{{ remote_tmp_dir }}/msg.txt" \ No newline at end of file
diff --git a/test/integration/targets/getent/aliases b/test/integration/targets/getent/aliases
new file mode 100644
index 0000000..a6dafcf
--- /dev/null
+++ b/test/integration/targets/getent/aliases
@@ -0,0 +1 @@
+shippable/posix/group1
diff --git a/test/integration/targets/getent/meta/main.yml b/test/integration/targets/getent/meta/main.yml
new file mode 100644
index 0000000..07faa21
--- /dev/null
+++ b/test/integration/targets/getent/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - prepare_tests
diff --git a/test/integration/targets/getent/tasks/main.yml b/test/integration/targets/getent/tasks/main.yml
new file mode 100644
index 0000000..bd17bd6
--- /dev/null
+++ b/test/integration/targets/getent/tasks/main.yml
@@ -0,0 +1,46 @@
+# Test code for the getent module.
+# (c) 2017, James Tanner <tanner.jc@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+- name: check for getent command
+ shell: which getent
+ failed_when: False
+ register: getent_check
+##
+## getent
+##
+- block:
+ - name: run getent with specified service
+ getent:
+ database: passwd
+ key: root
+ service: files
+ register: getent_test0
+ when: ansible_system != 'FreeBSD' and ansible_distribution != 'Alpine'
+ - name: run getent w/o specified service (FreeBSD)
+ getent:
+ database: passwd
+ key: root
+ register: getent_test0
+ when: ansible_system == 'FreeBSD' or ansible_distribution == 'Alpine'
+ - debug: var=getent_test0
+ - name: validate results
+ assert:
+ that:
+ - 'getent_passwd is defined'
+ - 'getent_passwd.root is defined'
+ - 'getent_passwd.root|length == 6'
+ when: getent_check.rc == 0
diff --git a/test/integration/targets/git/aliases b/test/integration/targets/git/aliases
new file mode 100644
index 0000000..a6dafcf
--- /dev/null
+++ b/test/integration/targets/git/aliases
@@ -0,0 +1 @@
+shippable/posix/group1
diff --git a/test/integration/targets/git/handlers/cleanup-default.yml b/test/integration/targets/git/handlers/cleanup-default.yml
new file mode 100644
index 0000000..02a7988
--- /dev/null
+++ b/test/integration/targets/git/handlers/cleanup-default.yml
@@ -0,0 +1,6 @@
+# TODO remove everything we'd installed (see git_required_packages), not just git
+# problem is that we should not remove what we hadn't installed
+- name: remove git
+ package:
+ name: git
+ state: absent
diff --git a/test/integration/targets/git/handlers/cleanup-freebsd.yml b/test/integration/targets/git/handlers/cleanup-freebsd.yml
new file mode 100644
index 0000000..29220c3
--- /dev/null
+++ b/test/integration/targets/git/handlers/cleanup-freebsd.yml
@@ -0,0 +1,5 @@
+- name: remove git from FreeBSD
+ pkgng:
+ name: git
+ state: absent
+ autoremove: yes
diff --git a/test/integration/targets/git/handlers/main.yml b/test/integration/targets/git/handlers/main.yml
new file mode 100644
index 0000000..875f513
--- /dev/null
+++ b/test/integration/targets/git/handlers/main.yml
@@ -0,0 +1,7 @@
+- name: cleanup
+ include_tasks: "{{ cleanup_filename }}"
+ with_first_found:
+ - "cleanup-{{ ansible_distribution | lower }}.yml"
+ - "cleanup-default.yml"
+ loop_control:
+ loop_var: cleanup_filename
diff --git a/test/integration/targets/git/meta/main.yml b/test/integration/targets/git/meta/main.yml
new file mode 100644
index 0000000..5e46138
--- /dev/null
+++ b/test/integration/targets/git/meta/main.yml
@@ -0,0 +1,4 @@
+dependencies:
+ - prepare_tests
+ - setup_gnutar
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/git/tasks/ambiguous-ref.yml b/test/integration/targets/git/tasks/ambiguous-ref.yml
new file mode 100644
index 0000000..f06112e
--- /dev/null
+++ b/test/integration/targets/git/tasks/ambiguous-ref.yml
@@ -0,0 +1,37 @@
+# test for https://github.com/ansible/ansible-modules-core/pull/3386
+
+- name: AMBIGUOUS-REF | clone repo
+ git:
+ repo: '{{ repo_format1 }}'
+ dest: '{{ checkout_dir }}'
+
+- name: AMBIGUOUS-REF | rename remote to be ambiguous
+ command: git remote rename origin v0.1
+ args:
+ chdir: "{{ checkout_dir }}"
+
+- name: AMBIGUOUS-REF | switch to HEAD
+ git:
+ repo: '{{ repo_format1 }}'
+ dest: '{{ checkout_dir }}'
+ remote: v0.1
+
+- name: AMBIGUOUS-REF | rev-parse remote HEAD
+ command: git rev-parse v0.1/HEAD
+ args:
+ chdir: "{{ checkout_dir }}"
+ register: git_remote_head
+
+- name: AMBIGUOUS-REF | rev-parse local HEAD
+ command: git rev-parse HEAD
+ args:
+ chdir: "{{ checkout_dir }}"
+ register: git_local_head
+
+- assert:
+ that: git_remote_head.stdout == git_local_head.stdout
+
+- name: AMBIGUOUS-REF | clear checkout_dir
+ file:
+ state: absent
+ path: "{{ checkout_dir }}"
diff --git a/test/integration/targets/git/tasks/archive.yml b/test/integration/targets/git/tasks/archive.yml
new file mode 100644
index 0000000..588148d
--- /dev/null
+++ b/test/integration/targets/git/tasks/archive.yml
@@ -0,0 +1,122 @@
+- name: ARCHIVE | Clear checkout_dir
+ file:
+ state: absent
+ path: "{{ checkout_dir }}"
+
+- name: ARCHIVE | Archive repo using various archival format
+ git:
+ repo: '{{ repo_format1 }}'
+ dest: '{{ checkout_dir }}'
+ archive: '{{ checkout_dir }}/test_role.{{ item }}'
+ register: git_archive
+ with_items: "{{ git_archive_extensions[ansible_os_family ~ ansible_distribution_major_version | default('default') ] | default(git_archive_extensions.default) }}"
+
+- name: ARCHIVE | Assert that archives were downloaded
+ assert:
+ that: (git_archive.results | map(attribute='changed') | unique | list)[0]
+
+- name: ARCHIVE | Check if archive file is created or not
+ stat:
+ path: '{{ checkout_dir }}/test_role.{{ item }}'
+ register: archive_check
+ with_items: "{{ git_archive_extensions[ansible_os_family ~ ansible_distribution_major_version | default('default') ] | default(git_archive_extensions.default) }}"
+
+- name: ARCHIVE | Assert that archive files exist
+ assert:
+ that: (archive_check.results | map(attribute='stat.exists') | unique | list)[0]
+ when:
+ - "ansible_os_family == 'RedHat'"
+ - ansible_distribution_major_version is version('7', '>=')
+
+- name: ARCHIVE | Clear checkout_dir
+ file:
+ state: absent
+ path: "{{ checkout_dir }}"
+
+- name: ARCHIVE | Clone clean repo
+ git:
+ repo: '{{ repo_format1 }}'
+ dest: '{{ checkout_dir }}'
+
+# Check git archive functionality without update
+- name: ARCHIVE | Archive repo using various archival format and without update
+ git:
+ repo: '{{ repo_format1 }}'
+ dest: '{{ checkout_dir }}'
+ update: no
+ archive: '{{ checkout_dir }}/test_role.{{ item }}'
+ register: git_archive
+ with_items: "{{ git_archive_extensions[ansible_os_family ~ ansible_distribution_major_version | default('default') ] | default(git_archive_extensions.default) }}"
+
+- name: ARCHIVE | Assert that archives were downloaded
+ assert:
+ that: (git_archive.results | map(attribute='changed') | unique | list)[0]
+
+- name: ARCHIVE | Check if archive file is created or not
+ stat:
+ path: '{{ checkout_dir }}/test_role.{{ item }}'
+ register: archive_check
+ with_items: "{{ git_archive_extensions[ansible_os_family ~ ansible_distribution_major_version | default('default') ] | default(git_archive_extensions.default) }}"
+
+- name: ARCHIVE | Assert that archive files exist
+ assert:
+ that: (archive_check.results | map(attribute='stat.exists') | unique | list)[0]
+ when:
+ - "ansible_os_family == 'RedHat'"
+ - ansible_distribution_major_version is version('7', '>=')
+
+- name: ARCHIVE | Inspect archive file
+ command:
+ cmd: "{{ git_list_commands[item] }} {{ checkout_dir }}/test_role.{{ item }}"
+ register: archive_content
+ with_items: "{{ git_archive_extensions[ansible_os_family ~ ansible_distribution_major_version | default('default') ] | default(git_archive_extensions.default) }}"
+
+- name: ARCHIVE | Ensure archive content is correct
+ assert:
+ that:
+ - item.stdout_lines | sort | first == 'defaults/'
+ with_items: "{{ archive_content.results }}"
+
+- name: ARCHIVE | Clear checkout_dir
+ file:
+ state: absent
+ path: "{{ checkout_dir }}"
+
+- name: ARCHIVE | Generate an archive prefix
+ set_fact:
+ git_archive_prefix: '{{ range(2 ** 31, 2 ** 32) | random }}' # Generate some random archive prefix
+
+- name: ARCHIVE | Archive repo using various archival format and with an archive prefix
+ git:
+ repo: '{{ repo_format1 }}'
+ dest: '{{ checkout_dir }}'
+ archive: '{{ checkout_dir }}/test_role.{{ item }}'
+ archive_prefix: '{{ git_archive_prefix }}/'
+ register: git_archive
+ with_items: "{{ git_archive_extensions[ansible_os_family ~ ansible_distribution_major_version | default('default') ] | default(git_archive_extensions.default) }}"
+
+- name: ARCHIVE | Prepare the target for archive(s) extraction
+ file:
+ state: directory
+ path: '{{ checkout_dir }}/{{ git_archive_prefix }}.{{ item }}'
+ with_items: "{{ git_archive_extensions[ansible_os_family ~ ansible_distribution_major_version | default('default') ] | default(git_archive_extensions.default) }}"
+
+- name: ARCHIVE | Extract the archive(s) into that target
+ unarchive:
+ src: '{{ checkout_dir }}/test_role.{{ item }}'
+ dest: '{{ checkout_dir }}/{{ git_archive_prefix }}.{{ item }}'
+ remote_src: yes
+ with_items: "{{ git_archive_extensions[ansible_os_family ~ ansible_distribution_major_version | default('default') ] | default(git_archive_extensions.default) }}"
+
+- name: ARCHIVE | Check if prefix directory exists in what's extracted
+ find:
+ path: '{{ checkout_dir }}/{{ git_archive_prefix }}.{{ item }}'
+ patterns: '{{ git_archive_prefix }}'
+ file_type: directory
+ register: archive_check
+ with_items: "{{ git_archive_extensions[ansible_os_family ~ ansible_distribution_major_version | default('default') ] | default(git_archive_extensions.default) }}"
+
+- name: ARCHIVE | Assert that prefix directory is found
+ assert:
+ that: '{{ item.matched == 1 }}'
+ with_items: "{{ archive_check.results }}"
diff --git a/test/integration/targets/git/tasks/change-repo-url.yml b/test/integration/targets/git/tasks/change-repo-url.yml
new file mode 100644
index 0000000..b12fca1
--- /dev/null
+++ b/test/integration/targets/git/tasks/change-repo-url.yml
@@ -0,0 +1,132 @@
+# test change of repo url
+# see https://github.com/ansible/ansible-modules-core/pull/721
+
+- name: CHANGE-REPO-URL | clear checkout_dir
+ file:
+ state: absent
+ path: "{{ checkout_dir }}"
+
+- name: CHANGE-REPO-URL | Clone example git repo
+ git:
+ repo: "{{ repo_update_url_1 }}"
+ dest: "{{ checkout_dir }}"
+
+- name: CHANGE-REPO-URL | Clone repo with changed url to the same place
+ git:
+ repo: "{{ repo_update_url_2 }}"
+ dest: "{{ checkout_dir }}"
+ register: clone2
+
+- assert:
+ that: "clone2 is successful"
+
+- name: CHANGE-REPO-URL | check url updated
+ shell: git remote show origin | grep Fetch
+ register: remote_url
+ args:
+ chdir: "{{ checkout_dir }}"
+ environment:
+ LC_ALL: C
+
+- assert:
+ that:
+ - "'git-test-new' in remote_url.stdout"
+ - "'git-test-old' not in remote_url.stdout"
+
+- name: CHANGE-REPO-URL | check for new content in git-test-new
+ stat: path={{ checkout_dir }}/newfilename
+ register: repo_content
+
+- name: CHANGE-REPO-URL | assert presence of new file in repo (i.e. working copy updated)
+ assert:
+ that: "repo_content.stat.exists"
+
+# Make sure 'changed' result is accurate in check mode.
+# See https://github.com/ansible/ansible-modules-core/pull/4243
+
+- name: CHANGE-REPO-URL | clear checkout_dir
+ file:
+ state: absent
+ path: "{{ checkout_dir }}"
+
+- name: CHANGE-REPO-URL | clone repo
+ git:
+ repo: "{{ repo_update_url_1 }}"
+ dest: "{{ checkout_dir }}"
+
+- name: CHANGE-REPO-URL | clone repo with same url to same destination
+ git:
+ repo: "{{ repo_update_url_1 }}"
+ dest: "{{ checkout_dir }}"
+ register: checkout_same_url
+
+- name: CHANGE-REPO-URL | check repo not changed
+ assert:
+ that:
+ - checkout_same_url is not changed
+
+
+- name: CHANGE-REPO-URL | clone repo with new url to same destination
+ git:
+ repo: "{{ repo_update_url_2 }}"
+ dest: "{{ checkout_dir }}"
+ register: checkout_new_url
+
+- name: CHANGE-REPO-URL | check repo changed
+ assert:
+ that:
+ - checkout_new_url is changed
+
+
+- name: CHANGE-REPO-URL | clone repo with new url in check mode
+ git:
+ repo: "{{ repo_update_url_1 }}"
+ dest: "{{ checkout_dir }}"
+ register: checkout_new_url_check_mode
+ check_mode: True
+
+- name: CHANGE-REPO-URL | check repo reported changed in check mode
+ assert:
+ that:
+ - checkout_new_url_check_mode is changed
+ when: git_version.stdout is version(git_version_supporting_ls_remote, '>=')
+
+- name: CHANGE-REPO-URL | clone repo with new url after check mode
+ git:
+ repo: "{{ repo_update_url_1 }}"
+ dest: "{{ checkout_dir }}"
+ register: checkout_new_url_after_check_mode
+
+- name: CHANGE-REPO-URL | check repo still changed after check mode
+ assert:
+ that:
+ - checkout_new_url_after_check_mode is changed
+
+
+# Test that checkout by branch works when the branch is not in our current repo but the sha is
+
+- name: CHANGE-REPO-URL | clear checkout_dir
+ file:
+ state: absent
+ path: "{{ checkout_dir }}"
+
+- name: CHANGE-REPO-URL | "Clone example git repo that we're going to modify"
+ git:
+ repo: "{{ repo_update_url_1 }}"
+ dest: "{{ checkout_dir }}/repo"
+
+- name: CHANGE-REPO-URL | Clone the repo again - this is what we test
+ git:
+ repo: "{{ checkout_dir }}/repo"
+ dest: "{{ checkout_dir }}/checkout"
+
+- name: CHANGE-REPO-URL | Add a branch to the repo
+ command: git branch new-branch
+ args:
+ chdir: "{{ checkout_dir }}/repo"
+
+- name: CHANGE-REPO-URL | Checkout the new branch in the checkout
+ git:
+ repo: "{{ checkout_dir}}/repo"
+ version: 'new-branch'
+ dest: "{{ checkout_dir }}/checkout"
diff --git a/test/integration/targets/git/tasks/checkout-new-tag.yml b/test/integration/targets/git/tasks/checkout-new-tag.yml
new file mode 100644
index 0000000..eac73f6
--- /dev/null
+++ b/test/integration/targets/git/tasks/checkout-new-tag.yml
@@ -0,0 +1,54 @@
+# test for https://github.com/ansible/ansible-modules-core/issues/527
+# clone a repo, add a tag to the same commit and try to checkout the new commit
+
+
+- name: clear checkout_dir
+ file:
+ state: absent
+ path: "{{ checkout_dir }}"
+
+- name: checkout example repo
+ git:
+ repo: "{{ repo_dir }}/format1"
+ dest: "{{ checkout_dir }}"
+
+- name: get tags of head
+ command: git tag --contains
+ args:
+ chdir: "{{ checkout_dir }}"
+ register: listoftags
+
+- name: make sure the tag does not yet exist
+ assert:
+ that:
+ - "'newtag' not in listoftags.stdout_lines"
+
+- name: add tag in orig repo
+ command: git tag newtag
+ args:
+ chdir: "{{ repo_dir }}/format1"
+
+- name: update copy with new tag
+ git:
+ repo: "{{ repo_dir }}/format1"
+ dest: "{{checkout_dir}}"
+ version: newtag
+ register: update_new_tag
+
+- name: get tags of new head
+ command: git tag --contains
+ args:
+ chdir: "{{ checkout_dir }}"
+ register: listoftags
+
+- name: check new head
+ assert:
+ that:
+ - update_new_tag is not changed
+ - "'newtag' in listoftags.stdout_lines"
+
+
+- name: clear checkout_dir
+ file:
+ state: absent
+ path: "{{ checkout_dir }}"
diff --git a/test/integration/targets/git/tasks/depth.yml b/test/integration/targets/git/tasks/depth.yml
new file mode 100644
index 0000000..547f84f
--- /dev/null
+++ b/test/integration/targets/git/tasks/depth.yml
@@ -0,0 +1,229 @@
+# Test the depth option and fetching revisions that were ignored first
+
+- name: DEPTH | clear checkout_dir
+ file:
+ state: absent
+ path: "{{ checkout_dir }}"
+
+- name: DEPTH | Clone example git repo with depth 1
+ git:
+ repo: 'file://{{ repo_dir|expanduser }}/shallow'
+ dest: '{{ checkout_dir }}'
+ depth: 1
+
+- name: DEPTH | try to access earlier commit
+ command: "git checkout {{git_shallow_head_1.stdout}}"
+ register: checkout_early
+ failed_when: False
+ args:
+ chdir: '{{ checkout_dir }}'
+
+- name: DEPTH | make sure the old commit was not fetched
+ assert:
+ that: 'checkout_early.rc != 0'
+ when: git_version.stdout is version(git_version_supporting_depth, '>=')
+
+# tests https://github.com/ansible/ansible/issues/14954
+- name: DEPTH | fetch repo again with depth=1
+ git:
+ repo: 'file://{{ repo_dir|expanduser }}/shallow'
+ dest: '{{ checkout_dir }}'
+ depth: 1
+ register: checkout2
+
+- assert:
+ that: "checkout2 is not changed"
+ when: git_version.stdout is version(git_version_supporting_depth, '>=')
+
+- name: DEPTH | again try to access earlier commit
+ shell: "git checkout {{git_shallow_head_1.stdout}}"
+ register: checkout_early
+ failed_when: False
+ args:
+ chdir: '{{ checkout_dir }}'
+
+- name: DEPTH | again make sure the old commit was not fetched
+ assert:
+ that: 'checkout_early.rc != 0'
+ when: git_version.stdout is version(git_version_supporting_depth, '>=')
+
+# make sure we are still able to fetch other versions
+- name: DEPTH | Clone same repo with older version
+ git:
+ repo: 'file://{{ repo_dir|expanduser }}/shallow'
+ dest: '{{ checkout_dir }}'
+ depth: 1
+ version: earlytag
+ register: cloneold
+
+- assert:
+ that: cloneold is successful
+
+- name: DEPTH | try to access earlier commit
+ shell: "git checkout {{git_shallow_head_1.stdout}}"
+ args:
+ chdir: '{{ checkout_dir }}'
+
+- name: DEPTH | clear checkout_dir
+ file:
+ state: absent
+ path: "{{ checkout_dir }}"
+
+# Test for https://github.com/ansible/ansible/issues/21316
+- name: DEPTH | Shallow clone with tag
+ git:
+ repo: 'file://{{ repo_dir|expanduser }}/shallow'
+ dest: '{{ checkout_dir }}'
+ depth: 1
+ version: earlytag
+ register: cloneold
+
+- assert:
+ that: cloneold is successful
+
+- name: DEPTH | clear checkout_dir
+ file:
+ state: absent
+ path: "{{ checkout_dir }}"
+
+
+ # Test for https://github.com/ansible/ansible-modules-core/issues/3456
+ # clone a repo with depth and version specified
+
+- name: DEPTH | clone repo with both version and depth specified
+ git:
+ repo: 'file://{{ repo_dir|expanduser }}/shallow'
+ dest: '{{ checkout_dir }}'
+ depth: 1
+ version: master
+
+- name: DEPTH | run a second time (now fetch, not clone)
+ git:
+ repo: 'file://{{ repo_dir|expanduser }}/shallow'
+ dest: '{{ checkout_dir }}'
+ depth: 1
+ version: master
+ register: git_fetch
+
+- name: DEPTH | ensure the fetch succeeded
+ assert:
+ that: git_fetch is successful
+
+
+- name: DEPTH | clear checkout_dir
+ file:
+ state: absent
+ path: "{{ checkout_dir }}"
+
+- name: DEPTH | clone repo with both version and depth specified
+ git:
+ repo: 'file://{{ repo_dir|expanduser }}/shallow'
+ dest: '{{ checkout_dir }}'
+ depth: 1
+ version: master
+
+- name: DEPTH | switch to older branch with depth=1 (uses fetch)
+ git:
+ repo: 'file://{{ repo_dir|expanduser }}/shallow'
+ dest: '{{ checkout_dir }}'
+ depth: 1
+ version: earlybranch
+ register: git_fetch
+
+- name: DEPTH | ensure the fetch succeeded
+ assert:
+ that: git_fetch is successful
+
+- name: DEPTH | clear checkout_dir
+ file:
+ state: absent
+ path: "{{ checkout_dir }}"
+
+# test for https://github.com/ansible/ansible-modules-core/issues/3782
+# make sure shallow fetch works when no version is specified
+
+- name: DEPTH | checkout old repo
+ git:
+ repo: 'file://{{ repo_dir|expanduser }}/shallow'
+ dest: '{{ checkout_dir }}'
+ depth: 1
+
+- name: DEPTH | "update repo"
+ shell: echo "3" > a; git commit -a -m "3"
+ args:
+ chdir: "{{ repo_dir }}/shallow"
+
+- name: DEPTH | fetch updated repo
+ git:
+ repo: 'file://{{ repo_dir|expanduser }}/shallow'
+ dest: '{{ checkout_dir }}'
+ depth: 1
+ register: git_fetch
+ ignore_errors: yes
+
+- name: DEPTH | get "a" file
+ slurp:
+ src: '{{ checkout_dir }}/a'
+ register: a_file
+
+- name: DEPTH | check update arrived
+ assert:
+ that:
+ - "{{ a_file.content | b64decode | trim }} == 3"
+ - git_fetch is changed
+
+- name: DEPTH | clear checkout_dir
+ file:
+ state: absent
+ path: "{{ checkout_dir }}"
+
+#
+# Make sure shallow fetch works when switching to (fetching) a new a branch
+#
+
+- name: DEPTH | clone from branch with depth specified
+ git:
+ repo: 'file://{{ repo_dir|expanduser }}/shallow_branches'
+ dest: '{{ checkout_dir }}'
+ depth: 1
+ version: test_branch
+
+- name: DEPTH | check if clone is shallow
+ stat: path={{ checkout_dir }}/.git/shallow
+ register: is_shallow
+ when: git_version.stdout is version(git_version_supporting_depth, '>=')
+
+- name: DEPTH | assert that clone is shallow
+ assert:
+ that:
+ - is_shallow.stat.exists
+ when: git_version.stdout is version(git_version_supporting_depth, '>=')
+
+- name: DEPTH | switch to new branch (fetch) with the shallow clone
+ git:
+ repo: 'file://{{ repo_dir|expanduser }}/shallow_branches'
+ dest: '{{ checkout_dir }}'
+ depth: 1
+ version: new_branch
+ register: git_fetch
+
+- name: DEPTH | assert if switching a shallow clone to a new branch worked
+ assert:
+ that:
+ - git_fetch is changed
+
+- name: DEPTH | check if clone is still shallow
+ stat: path={{ checkout_dir }}/.git/shallow
+ register: is_shallow
+ when: git_version.stdout is version(git_version_supporting_depth, '>=')
+
+- name: DEPTH | assert that clone still is shallow
+ assert:
+ that:
+ - is_shallow.stat.exists
+ when: git_version.stdout is version(git_version_supporting_depth, '>=')
+
+- name: DEPTH | clear checkout_dir
+ file:
+ state: absent
+ path: "{{ checkout_dir }}"
diff --git a/test/integration/targets/git/tasks/forcefully-fetch-tag.yml b/test/integration/targets/git/tasks/forcefully-fetch-tag.yml
new file mode 100644
index 0000000..47c3747
--- /dev/null
+++ b/test/integration/targets/git/tasks/forcefully-fetch-tag.yml
@@ -0,0 +1,38 @@
+# Tests against https://github.com/ansible/ansible/issues/67972
+
+# Do our first clone manually; there are no commits yet and Ansible doesn't like
+# that.
+- name: FORCEFULLY-FETCH-TAG | Clone the bare repo in a non-bare clone
+ shell: git clone {{ repo_dir }}/tag_force_push {{ repo_dir }}/tag_force_push_clone1
+
+- name: FORCEFULLY-FETCH-TAG | Prepare repo with a tag
+ shell: |
+ echo 1337 > leet;
+ git add leet;
+ git commit -m uh-oh;
+ git tag -f herewego;
+ git push --tags origin master
+ args:
+ chdir: "{{ repo_dir }}/tag_force_push_clone1"
+
+- name: FORCEFULLY-FETCH-TAG | clone the repo for the second time
+ git:
+ repo: "{{ repo_dir }}/tag_force_push"
+ dest: "{{ repo_dir }}/tag_force_push_clone2"
+
+- name: FORCEFULLY-FETCH-TAG | Forcefully overwrite the tag in clone1
+ shell: |
+ echo 1338 > leet;
+ git add leet;
+ git commit -m uh-oh;
+ git tag -f herewego;
+ git push -f --tags origin master
+ args:
+ chdir: "{{ repo_dir }}/tag_force_push_clone1"
+
+- name: FORCEFULLY-FETCH-TAG | Try to update the second clone
+ git:
+ repo: "{{ repo_dir }}/tag_force_push"
+ dest: "{{ repo_dir }}/tag_force_push_clone2"
+ force: yes
+ register: git_res
diff --git a/test/integration/targets/git/tasks/formats.yml b/test/integration/targets/git/tasks/formats.yml
new file mode 100644
index 0000000..e5fcda7
--- /dev/null
+++ b/test/integration/targets/git/tasks/formats.yml
@@ -0,0 +1,40 @@
+- name: FORMATS | initial checkout
+ git:
+ repo: "{{ repo_format1 }}"
+ dest: "{{ repo_dir }}/format1"
+ register: git_result
+
+- name: FORMATS | verify information about the initial clone
+ assert:
+ that:
+ - "'before' in git_result"
+ - "'after' in git_result"
+ - "not git_result.before"
+ - "git_result.changed"
+
+- name: FORMATS | repeated checkout
+ git:
+ repo: "{{ repo_format1 }}"
+ dest: "{{ repo_dir }}/format1"
+ register: git_result2
+
+- name: FORMATS | check for tags
+ stat:
+ path: "{{ repo_dir }}/format1/.git/refs/tags"
+ register: tags
+
+- name: FORMATS | check for HEAD
+ stat:
+ path: "{{ repo_dir }}/format1/.git/HEAD"
+ register: head
+
+- name: FORMATS | assert presence of tags/trunk/branches
+ assert:
+ that:
+ - "tags.stat.isdir"
+ - "head.stat.isreg"
+
+- name: FORMATS | verify on a reclone things are marked unchanged
+ assert:
+ that:
+ - "not git_result2.changed"
diff --git a/test/integration/targets/git/tasks/gpg-verification.yml b/test/integration/targets/git/tasks/gpg-verification.yml
new file mode 100644
index 0000000..8c8834a
--- /dev/null
+++ b/test/integration/targets/git/tasks/gpg-verification.yml
@@ -0,0 +1,212 @@
+# Test for verification of GnuPG signatures
+
+- name: GPG-VERIFICATION | Create GnuPG verification workdir
+ tempfile:
+ state: directory
+ register: git_gpg_workdir
+
+- name: GPG-VERIFICATION | Define variables based on workdir
+ set_fact:
+ git_gpg_keyfile: "{{ git_gpg_workdir.path }}/testkey.asc"
+ git_gpg_source: "{{ git_gpg_workdir.path }}/source"
+ git_gpg_dest: "{{ git_gpg_workdir.path }}/dest"
+ git_gpg_gpghome: "{{ git_gpg_workdir.path }}/gpg"
+
+- name: GPG-VERIFICATION | Temporary store GnuPG test key
+ copy:
+ content: "{{ git_gpg_testkey }}"
+ dest: "{{ git_gpg_keyfile }}"
+
+- name: GPG-VERIFICATION | Create temporary GNUPGHOME directory
+ file:
+ path: "{{ git_gpg_gpghome }}"
+ state: directory
+ mode: 0700
+
+- name: GPG-VERIFICATION | Import GnuPG test key
+ environment:
+ - GNUPGHOME: "{{ git_gpg_gpghome }}"
+ command: gpg --import {{ git_gpg_keyfile }}
+
+- name: GPG-VERIFICATION | Create local GnuPG signed repository directory
+ file:
+ path: "{{ git_gpg_source }}"
+ state: directory
+
+- name: GPG-VERIFICATION | Generate local GnuPG signed repository
+ environment:
+ - GNUPGHOME: "{{ git_gpg_gpghome }}"
+ shell: |
+ set -e
+ git init
+ touch an_empty_file
+ git add an_empty_file
+ git commit --no-gpg-sign --message "Commit, and don't sign"
+ git tag lightweight_tag/unsigned_commit HEAD
+ git commit --allow-empty --gpg-sign --message "Commit, and sign"
+ git tag lightweight_tag/signed_commit HEAD
+ git tag --annotate --message "This is not a signed tag" unsigned_annotated_tag HEAD
+ git commit --allow-empty --gpg-sign --message "Commit, and sign"
+ git tag --sign --message "This is a signed tag" signed_annotated_tag HEAD
+ git checkout -b some_branch/signed_tip master
+ git commit --allow-empty --gpg-sign --message "Commit, and sign"
+ git checkout -b another_branch/unsigned_tip master
+ git commit --allow-empty --no-gpg-sign --message "Commit, and don't sign"
+ git checkout master
+ args:
+ chdir: "{{ git_gpg_source }}"
+
+- name: GPG-VERIFICATION | Get hash of an unsigned commit
+ command: git show-ref --hash --verify refs/tags/lightweight_tag/unsigned_commit
+ args:
+ chdir: "{{ git_gpg_source }}"
+ register: git_gpg_unsigned_commit
+
+- name: GPG-VERIFICATION | Get hash of a signed commit
+ command: git show-ref --hash --verify refs/tags/lightweight_tag/signed_commit
+ args:
+ chdir: "{{ git_gpg_source }}"
+ register: git_gpg_signed_commit
+
+- name: GPG-VERIFICATION | Clone repo and verify signed HEAD
+ environment:
+ - GNUPGHOME: "{{ git_gpg_gpghome }}"
+ git:
+ repo: "{{ git_gpg_source }}"
+ dest: "{{ git_gpg_dest }}"
+ verify_commit: yes
+ when:
+ - git_version.stdout is version("2.1.0", '>=')
+
+- name: GPG-VERIFICATION | Clone repo and verify a signed lightweight tag
+ environment:
+ - GNUPGHOME: "{{ git_gpg_gpghome }}"
+ git:
+ repo: "{{ git_gpg_source }}"
+ dest: "{{ git_gpg_dest }}"
+ version: lightweight_tag/signed_commit
+ verify_commit: yes
+ when:
+ - git_version.stdout is version("2.1.0", '>=')
+
+- name: GPG-VERIFICATION | Clone repo and verify an unsigned lightweight tag (should fail)
+ environment:
+ - GNUPGHOME: "{{ git_gpg_gpghome }}"
+ git:
+ repo: "{{ git_gpg_source }}"
+ dest: "{{ git_gpg_dest }}"
+ version: lightweight_tag/unsigned_commit
+ verify_commit: yes
+ register: git_verify
+ ignore_errors: yes
+ when:
+ - git_version.stdout is version("2.1.0", '>=')
+
+- name: GPG-VERIFICATION | Check that unsigned lightweight tag verification failed
+ assert:
+ that:
+ - git_verify is failed
+ - git_verify.msg is match("Failed to verify GPG signature of commit/tag.+")
+ when:
+ - git_version.stdout is version("2.1.0", '>=')
+
+- name: GPG-VERIFICATION | Clone repo and verify a signed commit
+ environment:
+ - GNUPGHOME: "{{ git_gpg_gpghome }}"
+ git:
+ repo: "{{ git_gpg_source }}"
+ dest: "{{ git_gpg_dest }}"
+ version: "{{ git_gpg_signed_commit.stdout }}"
+ verify_commit: yes
+ when:
+ - git_version.stdout is version("2.1.0", '>=')
+
+- name: GPG-VERIFICATION | Clone repo and verify an unsigned commit
+ environment:
+ - GNUPGHOME: "{{ git_gpg_gpghome }}"
+ git:
+ repo: "{{ git_gpg_source }}"
+ dest: "{{ git_gpg_dest }}"
+ version: "{{ git_gpg_unsigned_commit.stdout }}"
+ verify_commit: yes
+ register: git_verify
+ ignore_errors: yes
+ when:
+ - git_version.stdout is version("2.1.0", '>=')
+
+- name: GPG-VERIFICATION | Check that unsigned commit verification failed
+ assert:
+ that:
+ - git_verify is failed
+ - git_verify.msg is match("Failed to verify GPG signature of commit/tag.+")
+ when:
+ - git_version.stdout is version("2.1.0", '>=')
+
+- name: GPG-VERIFICATION | Clone repo and verify a signed annotated tag
+ environment:
+ - GNUPGHOME: "{{ git_gpg_gpghome }}"
+ git:
+ repo: "{{ git_gpg_source }}"
+ dest: "{{ git_gpg_dest }}"
+ version: signed_annotated_tag
+ verify_commit: yes
+
+- name: GPG-VERIFICATION | Clone repo and verify an unsigned annotated tag (should fail)
+ environment:
+ - GNUPGHOME: "{{ git_gpg_gpghome }}"
+ git:
+ repo: "{{ git_gpg_source }}"
+ dest: "{{ git_gpg_dest }}"
+ version: unsigned_annotated_tag
+ verify_commit: yes
+ register: git_verify
+ ignore_errors: yes
+
+- name: GPG-VERIFICATION | Check that unsigned annotated tag verification failed
+ assert:
+ that:
+ - git_verify is failed
+ - git_verify.msg is match("Failed to verify GPG signature of commit/tag.+")
+
+- name: GPG-VERIFICATION | Clone repo and verify a signed branch
+ environment:
+ - GNUPGHOME: "{{ git_gpg_gpghome }}"
+ git:
+ repo: "{{ git_gpg_source }}"
+ dest: "{{ git_gpg_dest }}"
+ version: some_branch/signed_tip
+ verify_commit: yes
+ when:
+ - git_version.stdout is version("2.1.0", '>=')
+
+- name: GPG-VERIFICATION | Clone repo and verify an unsigned branch (should fail)
+ environment:
+ - GNUPGHOME: "{{ git_gpg_gpghome }}"
+ git:
+ repo: "{{ git_gpg_source }}"
+ dest: "{{ git_gpg_dest }}"
+ version: another_branch/unsigned_tip
+ verify_commit: yes
+ register: git_verify
+ ignore_errors: yes
+ when:
+ - git_version.stdout is version("2.1.0", '>=')
+
+- name: GPG-VERIFICATION | Check that unsigned branch verification failed
+ assert:
+ that:
+ - git_verify is failed
+ - git_verify.msg is match("Failed to verify GPG signature of commit/tag.+")
+ when:
+ - git_version.stdout is version("2.1.0", '>=')
+
+- name: GPG-VERIFICATION | Stop gpg-agent so we can remove any locks on the GnuPG dir
+ command: gpgconf --kill gpg-agent
+ environment:
+ GNUPGHOME: "{{ git_gpg_gpghome }}"
+ ignore_errors: yes
+
+- name: GPG-VERIFICATION | Remove GnuPG verification workdir
+ file:
+ path: "{{ git_gpg_workdir.path }}"
+ state: absent
diff --git a/test/integration/targets/git/tasks/localmods.yml b/test/integration/targets/git/tasks/localmods.yml
new file mode 100644
index 0000000..09a1326
--- /dev/null
+++ b/test/integration/targets/git/tasks/localmods.yml
@@ -0,0 +1,112 @@
+# test for https://github.com/ansible/ansible-modules-core/pull/5505
+- name: LOCALMODS | prepare old git repo
+ shell: rm -rf localmods; mkdir localmods; cd localmods; git init; echo "1" > a; git add a; git commit -m "1"
+ args:
+ chdir: "{{repo_dir}}"
+
+- name: LOCALMODS | checkout old repo
+ git:
+ repo: '{{ repo_dir }}/localmods'
+ dest: '{{ checkout_dir }}'
+
+- name: LOCALMODS | "update repo"
+ shell: echo "2" > a; git commit -a -m "2"
+ args:
+ chdir: "{{repo_dir}}/localmods"
+
+- name: LOCALMODS | "add local mods"
+ shell: echo "3" > a
+ args:
+ chdir: "{{ checkout_dir }}"
+
+- name: LOCALMODS | fetch with local mods without force (should fail)
+ git:
+ repo: '{{ repo_dir }}/localmods'
+ dest: '{{ checkout_dir }}'
+ register: git_fetch
+ ignore_errors: yes
+
+- name: LOCALMODS | check fetch with localmods failed
+ assert:
+ that:
+ - git_fetch is failed
+
+- name: LOCALMODS | fetch with local mods with force
+ git:
+ repo: '{{ repo_dir }}/localmods'
+ dest: '{{ checkout_dir }}'
+ force: True
+ register: git_fetch_force
+ ignore_errors: yes
+
+- name: LOCALMODS | get "a" file
+ slurp:
+ src: '{{ checkout_dir }}/a'
+ register: a_file
+
+- name: LOCALMODS | check update arrived
+ assert:
+ that:
+ - "{{ a_file.content | b64decode | trim }} == 2"
+ - git_fetch_force is changed
+
+- name: LOCALMODS | clear checkout_dir
+ file: state=absent path={{ checkout_dir }}
+
+# localmods and shallow clone
+- name: LOCALMODS | prepare old git repo
+ shell: rm -rf localmods; mkdir localmods; cd localmods; git init; echo "1" > a; git add a; git commit -m "1"
+ args:
+ chdir: "{{repo_dir}}"
+
+- name: LOCALMODS | checkout old repo
+ git:
+ repo: '{{ repo_dir }}/localmods'
+ dest: '{{ checkout_dir }}'
+ depth: 1
+
+- name: LOCALMODS | "update repo"
+ shell: echo "2" > a; git commit -a -m "2"
+ args:
+ chdir: "{{repo_dir}}/localmods"
+
+- name: LOCALMODS | "add local mods"
+ shell: echo "3" > a
+ args:
+ chdir: "{{ checkout_dir }}"
+
+- name: LOCALMODS | fetch with local mods without force (should fail)
+ git:
+ repo: '{{ repo_dir }}/localmods'
+ dest: '{{ checkout_dir }}'
+ depth: 1
+ register: git_fetch
+ ignore_errors: yes
+
+- name: LOCALMODS | check fetch with localmods failed
+ assert:
+ that:
+ - git_fetch is failed
+
+- name: LOCALMODS | fetch with local mods with force
+ git:
+ repo: '{{ repo_dir }}/localmods'
+ dest: '{{ checkout_dir }}'
+ depth: 1
+ force: True
+ register: git_fetch_force
+ ignore_errors: yes
+
+- name: LOCALMODS | get "a" file
+ slurp:
+ src: '{{ checkout_dir }}/a'
+ register: a_file
+
+- name: LOCALMODS | check update arrived
+ assert:
+ that:
+ - "{{ a_file.content | b64decode | trim }} == 2"
+ - git_fetch_force is changed
+
+- name: LOCALMODS | clear checkout_dir
+ file: state=absent path={{ checkout_dir }}
diff --git a/test/integration/targets/git/tasks/main.yml b/test/integration/targets/git/tasks/main.yml
new file mode 100644
index 0000000..ed06eab
--- /dev/null
+++ b/test/integration/targets/git/tasks/main.yml
@@ -0,0 +1,42 @@
+# test code for the git module
+# (c) 2014, James Tanner <tanner.jc@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- import_tasks: setup.yml
+- import_tasks: setup-local-repos.yml
+
+- import_tasks: formats.yml
+- import_tasks: missing_hostkey.yml
+- import_tasks: missing_hostkey_acceptnew.yml
+- import_tasks: no-destination.yml
+- import_tasks: specific-revision.yml
+- import_tasks: submodules.yml
+- import_tasks: change-repo-url.yml
+- import_tasks: depth.yml
+- import_tasks: single-branch.yml
+- import_tasks: checkout-new-tag.yml
+- include_tasks: gpg-verification.yml
+ when:
+ - not gpg_version.stderr
+ - gpg_version.stdout
+ - not (ansible_os_family == 'RedHat' and ansible_distribution_major_version is version('7', '<'))
+- import_tasks: localmods.yml
+- import_tasks: reset-origin.yml
+- import_tasks: ambiguous-ref.yml
+- import_tasks: archive.yml
+- import_tasks: separate-git-dir.yml
+- import_tasks: forcefully-fetch-tag.yml
diff --git a/test/integration/targets/git/tasks/missing_hostkey.yml b/test/integration/targets/git/tasks/missing_hostkey.yml
new file mode 100644
index 0000000..136c5d5
--- /dev/null
+++ b/test/integration/targets/git/tasks/missing_hostkey.yml
@@ -0,0 +1,61 @@
+- name: MISSING-HOSTKEY | checkout ssh://git@github.com repo without accept_hostkey (expected fail)
+ git:
+ repo: '{{ repo_format2 }}'
+ dest: '{{ checkout_dir }}'
+ ssh_opts: '-o UserKnownHostsFile={{ remote_tmp_dir }}/known_hosts'
+ register: git_result
+ ignore_errors: true
+
+- assert:
+ that:
+ - git_result is failed
+
+- name: MISSING-HOSTKEY | checkout git@github.com repo with accept_hostkey (expected pass)
+ git:
+ repo: '{{ repo_format2 }}'
+ dest: '{{ checkout_dir }}'
+ accept_hostkey: true
+ key_file: '{{ github_ssh_private_key }}'
+ ssh_opts: '-o UserKnownHostsFile={{ remote_tmp_dir }}/known_hosts'
+ register: git_result
+ when: github_ssh_private_key is defined
+
+- assert:
+ that:
+ - git_result is changed
+ when: github_ssh_private_key is defined
+
+- name: MISSING-HOSTKEY | clear checkout_dir
+ file:
+ state: absent
+ path: '{{ checkout_dir }}'
+ when: github_ssh_private_key is defined
+
+- name: MISSING-HOSTKEY | checkout ssh://git@github.com repo with accept_hostkey (expected pass)
+ git:
+ repo: '{{ repo_format3 }}'
+ dest: '{{ checkout_dir }}'
+ version: 'master'
+ accept_hostkey: false # should already have been accepted
+ key_file: '{{ github_ssh_private_key }}'
+ ssh_opts: '-o UserKnownHostsFile={{ remote_tmp_dir }}/known_hosts'
+ register: git_result
+ when: github_ssh_private_key is defined
+
+- assert:
+ that:
+ - git_result is changed
+ when: github_ssh_private_key is defined
+
+- name: MISSING-HOSTEKY | Remove github.com hostkey from known_hosts
+ lineinfile:
+ dest: '{{ remote_tmp_dir }}/known_hosts'
+ regexp: "github.com"
+ state: absent
+ when: github_ssh_private_key is defined
+
+- name: MISSING-HOSTKEY | clear checkout_dir
+ file:
+ state: absent
+ path: '{{ checkout_dir }}'
+ when: github_ssh_private_key is defined
diff --git a/test/integration/targets/git/tasks/missing_hostkey_acceptnew.yml b/test/integration/targets/git/tasks/missing_hostkey_acceptnew.yml
new file mode 100644
index 0000000..3fd1906
--- /dev/null
+++ b/test/integration/targets/git/tasks/missing_hostkey_acceptnew.yml
@@ -0,0 +1,78 @@
+- name: MISSING-HOSTKEY | check accept_newhostkey support
+ shell: ssh -o StrictHostKeyChecking=accept-new -V
+ register: ssh_supports_accept_newhostkey
+ ignore_errors: true
+
+- block:
+ - name: MISSING-HOSTKEY | accept_newhostkey when ssh does not support the option
+ git:
+ repo: '{{ repo_format2 }}'
+ dest: '{{ checkout_dir }}'
+ accept_newhostkey: true
+ ssh_opts: '-o UserKnownHostsFile={{ remote_tmp_dir }}/known_hosts'
+ register: git_result
+ ignore_errors: true
+
+ - assert:
+ that:
+ - git_result is failed
+ - git_result.warnings is search("does not support")
+
+ when: ssh_supports_accept_newhostkey.rc != 0
+
+- name: MISSING-HOSTKEY | checkout ssh://git@github.com repo without accept_newhostkey (expected fail)
+ git:
+ repo: '{{ repo_format2 }}'
+ dest: '{{ checkout_dir }}'
+ ssh_opts: '-o UserKnownHostsFile={{ remote_tmp_dir }}/known_hosts'
+ register: git_result
+ ignore_errors: true
+
+- assert:
+ that:
+ - git_result is failed
+
+- block:
+ - name: MISSING-HOSTKEY | checkout git@github.com repo with accept_newhostkey (expected pass)
+ git:
+ repo: '{{ repo_format2 }}'
+ dest: '{{ checkout_dir }}'
+ accept_newhostkey: true
+ key_file: '{{ github_ssh_private_key }}'
+ ssh_opts: '-o UserKnownHostsFile={{ remote_tmp_dir }}/known_hosts'
+ register: git_result
+
+ - assert:
+ that:
+ - git_result is changed
+
+ - name: MISSING-HOSTKEY | clear checkout_dir
+ file:
+ state: absent
+ path: '{{ checkout_dir }}'
+
+ - name: MISSING-HOSTKEY | checkout ssh://git@github.com repo with accept_newhostkey (expected pass)
+ git:
+ repo: '{{ repo_format3 }}'
+ dest: '{{ checkout_dir }}'
+ version: 'master'
+ accept_newhostkey: false # should already have been accepted
+ key_file: '{{ github_ssh_private_key }}'
+ ssh_opts: '-o UserKnownHostsFile={{ remote_tmp_dir }}/known_hosts'
+ register: git_result
+
+ - assert:
+ that:
+ - git_result is changed
+
+ - name: MISSING-HOSTEKY | Remove github.com hostkey from known_hosts
+ lineinfile:
+ dest: '{{ remote_tmp_dir }}/known_hosts'
+ regexp: "github.com"
+ state: absent
+
+ - name: MISSING-HOSTKEY | clear checkout_dir
+ file:
+ state: absent
+ path: '{{ checkout_dir }}'
+ when: github_ssh_private_key is defined and ssh_supports_accept_newhostkey.rc == 0
diff --git a/test/integration/targets/git/tasks/no-destination.yml b/test/integration/targets/git/tasks/no-destination.yml
new file mode 100644
index 0000000..1ef7f2f
--- /dev/null
+++ b/test/integration/targets/git/tasks/no-destination.yml
@@ -0,0 +1,13 @@
+# Test a non-updating repo query with no destination specified
+
+- name: NO-DESTINATION | get info on a repo without updating and with no destination specified
+ git:
+ repo: '{{ repo_dir }}/minimal'
+ update: no
+ clone: no
+ accept_hostkey: yes
+ register: git_result
+
+- assert:
+ that:
+ - git_result is changed
diff --git a/test/integration/targets/git/tasks/reset-origin.yml b/test/integration/targets/git/tasks/reset-origin.yml
new file mode 100644
index 0000000..8fddd4b
--- /dev/null
+++ b/test/integration/targets/git/tasks/reset-origin.yml
@@ -0,0 +1,25 @@
+- name: RESET-ORIGIN | Clean up the directories
+ file:
+ state: absent
+ path: "{{ item }}"
+ with_items:
+ - "{{ repo_dir }}/origin"
+ - "{{ checkout_dir }}"
+
+- name: RESET-ORIGIN | Create a directory
+ file:
+ name: "{{ repo_dir }}/origin"
+ state: directory
+
+- name: RESET-ORIGIN | Initialise the repo with a file named origin,see github.com/ansible/ansible/pull/22502
+ shell: git init; echo "PR 22502" > origin; git add origin; git commit -m "PR 22502"
+ args:
+ chdir: "{{ repo_dir }}/origin"
+
+- name: RESET-ORIGIN | Clone a git repo with file named origin
+ git:
+ repo: "{{ repo_dir }}/origin"
+ dest: "{{ checkout_dir }}"
+ remote: origin
+ update: no
+ register: status
diff --git a/test/integration/targets/git/tasks/separate-git-dir.yml b/test/integration/targets/git/tasks/separate-git-dir.yml
new file mode 100644
index 0000000..5b87404
--- /dev/null
+++ b/test/integration/targets/git/tasks/separate-git-dir.yml
@@ -0,0 +1,132 @@
+# test code for repositories with separate git dir updating
+# see https://github.com/ansible/ansible/pull/38016
+# see https://github.com/ansible/ansible/issues/30034
+
+- name: SEPARATE-GIT-DIR | clear checkout_dir
+ file:
+ state: absent
+ path: '{{ checkout_dir }}'
+
+- name: SEPARATE-GIT-DIR | make a pre-exist repo dir
+ file:
+ state: directory
+ path: '{{ separate_git_dir }}'
+
+- name: SEPARATE-GIT-DIR | clone with a separate git dir
+ git:
+ repo: '{{ repo_format1 }}'
+ dest: '{{ checkout_dir }}'
+ separate_git_dir: '{{ separate_git_dir }}'
+ ignore_errors: yes
+ register: result
+
+- name: SEPARATE-GIT-DIR | the clone will fail due to pre-exist dir
+ assert:
+ that: 'result is failed'
+
+- name: SEPARATE-GIT-DIR | delete pre-exist dir
+ file:
+ state: absent
+ path: '{{ separate_git_dir }}'
+
+- name: SEPARATE-GIT-DIR | clone again with a separate git dir
+ git:
+ repo: '{{ repo_format1 }}'
+ dest: '{{ checkout_dir }}'
+ separate_git_dir: '{{ separate_git_dir }}'
+
+- name: SEPARATE-GIT-DIR | check the stat of git dir
+ stat:
+ path: '{{ separate_git_dir }}'
+ register: stat_result
+
+- name: SEPARATE-GIT-DIR | the git dir should exist
+ assert:
+ that: 'stat_result.stat.exists == True'
+
+- name: SEPARATE-GIT-DIR | update repo the usual way
+ git:
+ repo: '{{ repo_format1 }}'
+ dest: '{{ checkout_dir }}'
+ separate_git_dir: '{{ separate_git_dir }}'
+ register: result
+
+- name: SEPARATE-GIT-DIR | update should not fail
+ assert:
+ that:
+ - result is not failed
+
+- name: SEPARATE-GIT-DIR | move the git dir to new place
+ git:
+ repo: '{{ repo_format1 }}'
+ dest: '{{ checkout_dir }}'
+ separate_git_dir: '{{ separate_git_dir }}_new'
+ register: result
+
+- name: SEPARATE-GIT-DIR | the movement should not failed
+ assert:
+ that: 'result is not failed'
+
+- name: SEPARATE-GIT-DIR | check the stat of new git dir
+ stat:
+ path: '{{ separate_git_dir }}_new'
+ register: stat_result
+
+- name: SEPARATE-GIT-DIR | the new git dir should exist
+ assert:
+ that: 'stat_result.stat.exists == True'
+
+- name: SEPARATE-GIT-DIR | test the update
+ git:
+ repo: '{{ repo_format1 }}'
+ dest: '{{ checkout_dir }}'
+ register: result
+
+- name: SEPARATE-GIT-DIR | the update should not failed
+ assert:
+ that:
+ - result is not failed
+
+- name: SEPARATE-GIT-DIR | set git dir to non-existent dir
+ shell: "echo gitdir: /dev/null/non-existent-dir > .git"
+ args:
+ chdir: "{{ checkout_dir }}"
+
+- name: SEPARATE-GIT-DIR | update repo the usual way
+ git:
+ repo: "{{ repo_format1 }}"
+ dest: "{{ checkout_dir }}"
+ ignore_errors: yes
+ register: result
+
+- name: SEPARATE-GIT-DIR | check update has failed
+ assert:
+ that:
+ - result is failed
+
+- name: SEPARATE-GIT-DIR | set .git file to bad format
+ shell: "echo some text gitdir: {{ checkout_dir }} > .git"
+ args:
+ chdir: "{{ checkout_dir }}"
+
+- name: SEPARATE-GIT-DIR | update repo the usual way
+ git:
+ repo: "{{ repo_format1 }}"
+ dest: "{{ checkout_dir }}"
+ ignore_errors: yes
+ register: result
+
+- name: SEPARATE-GIT-DIR | check update has failed
+ assert:
+ that:
+ - result is failed
+
+- name: SEPARATE-GIT-DIR | clear separate git dir
+ file:
+ state: absent
+ path: "{{ separate_git_dir }}_new"
+
+- name: SEPARATE-GIT-DIR | clear checkout_dir
+ file:
+ state: absent
+ path: '{{ checkout_dir }}'
diff --git a/test/integration/targets/git/tasks/setup-local-repos.yml b/test/integration/targets/git/tasks/setup-local-repos.yml
new file mode 100644
index 0000000..584a169
--- /dev/null
+++ b/test/integration/targets/git/tasks/setup-local-repos.yml
@@ -0,0 +1,45 @@
+- name: SETUP-LOCAL-REPOS | create dirs
+ file:
+ name: "{{ item }}"
+ state: directory
+ with_items:
+ - "{{ repo_dir }}/minimal"
+ - "{{ repo_dir }}/shallow"
+ - "{{ repo_dir }}/shallow_branches"
+ - "{{ repo_dir }}/tag_force_push"
+
+- name: SETUP-LOCAL-REPOS | prepare minimal git repo
+ shell: git init; echo "1" > a; git add a; git commit -m "1"
+ args:
+ chdir: "{{ repo_dir }}/minimal"
+
+- name: SETUP-LOCAL-REPOS | prepare git repo for shallow clone
+ shell: |
+ git init;
+ echo "1" > a; git add a; git commit -m "1"; git tag earlytag; git branch earlybranch;
+ echo "2" > a; git add a; git commit -m "2";
+ args:
+ chdir: "{{ repo_dir }}/shallow"
+
+- name: SETUP-LOCAL-REPOS | set old hash var for shallow test
+ command: 'git rev-parse HEAD~1'
+ register: git_shallow_head_1
+ args:
+ chdir: "{{ repo_dir }}/shallow"
+
+- name: SETUP-LOCAL-REPOS | prepare tmp git repo with two branches
+ shell: |
+ git init
+ echo "1" > a; git add a; git commit -m "1"
+ git checkout -b test_branch; echo "2" > a; git commit -m "2 on branch" a
+ git checkout -b new_branch; echo "3" > a; git commit -m "3 on new branch" a
+ args:
+ chdir: "{{ repo_dir }}/shallow_branches"
+
+# Make this a bare one, we need to be able to push to it from clones
+# We make the repo here for consistency with the other repos,
+# but we finish setting it up in forcefully-fetch-tag.yml.
+- name: SETUP-LOCAL-REPOS | prepare tag_force_push git repo
+ shell: git init --bare
+ args:
+ chdir: "{{ repo_dir }}/tag_force_push"
diff --git a/test/integration/targets/git/tasks/setup.yml b/test/integration/targets/git/tasks/setup.yml
new file mode 100644
index 0000000..0651105
--- /dev/null
+++ b/test/integration/targets/git/tasks/setup.yml
@@ -0,0 +1,43 @@
+- name: SETUP | clean out the remote_tmp_dir
+ file:
+ path: "{{ remote_tmp_dir }}"
+ state: absent
+
+- name: SETUP | create clean remote_tmp_dir
+ file:
+ path: "{{ remote_tmp_dir }}"
+ state: directory
+
+- name: SETUP | install git
+ package:
+ name: '{{ item }}'
+ when: ansible_distribution not in ["MacOSX", "Alpine"]
+ notify:
+ - cleanup
+ with_items: "{{ git_required_packages[ansible_os_family | default('default') ] | default(git_required_packages.default) }}"
+
+- name: SETUP | verify that git is installed so this test can continue
+ shell: which git
+
+- name: SETUP | get git version, only newer than {{git_version_supporting_depth}} has fixed git depth
+ shell: git --version | grep 'git version' | sed 's/git version //'
+ register: git_version
+
+- name: SETUP | get gpg version
+ shell: gpg --version 2>1 | head -1 | sed -e 's/gpg (GnuPG) //'
+ register: gpg_version
+
+- name: SETUP | set git global user.email if not already set
+ shell: git config --global user.email || git config --global user.email "noreply@example.com"
+
+- name: SETUP | set git global user.name if not already set
+ shell: git config --global user.name || git config --global user.name "Ansible Test Runner"
+
+- name: SETUP | create repo_dir
+ file:
+ path: "{{ repo_dir }}"
+ state: directory
+
+- name: SETUP | show git version
+ debug:
+ msg: "Running test with git {{ git_version.stdout }}"
diff --git a/test/integration/targets/git/tasks/single-branch.yml b/test/integration/targets/git/tasks/single-branch.yml
new file mode 100644
index 0000000..5cfb4d5
--- /dev/null
+++ b/test/integration/targets/git/tasks/single-branch.yml
@@ -0,0 +1,87 @@
+# Test single_branch parameter
+
+- name: SINGLE_BRANCH | clear checkout_dir
+ file:
+ state: absent
+ path: "{{ checkout_dir }}"
+
+- name: SINGLE_BRANCH | Clone example git repo using single_branch
+ git:
+ repo: 'file://{{ repo_dir|expanduser }}/shallow_branches'
+ dest: '{{ checkout_dir }}'
+ single_branch: yes
+ register: single_branch_1
+
+- name: SINGLE_BRANCH | Clone example git repo using single_branch again
+ git:
+ repo: 'file://{{ repo_dir|expanduser }}/shallow_branches'
+ dest: '{{ checkout_dir }}'
+ single_branch: yes
+ register: single_branch_2
+
+- name: SINGLE_BRANCH | List revisions
+ command: git rev-list --all --count
+ args:
+ chdir: '{{ checkout_dir }}'
+ register: rev_list1
+ when: git_version.stdout is version(git_version_supporting_single_branch, '>=')
+
+- name: SINGLE_BRANCH | Ensure single_branch did the right thing with git >= {{ git_version_supporting_single_branch }}
+ assert:
+ that:
+ - single_branch_1 is changed
+ - single_branch_2 is not changed
+ when: git_version.stdout is version(git_version_supporting_single_branch, '>=')
+
+- name: SINGLE_BRANCH | Ensure single_branch did the right thing with git < {{ git_version_supporting_single_branch }}
+ assert:
+ that:
+ - single_branch_1 is changed
+ - single_branch_1.warnings | length == 1
+ - single_branch_2 is not changed
+ when: git_version.stdout is version(git_version_supporting_single_branch, '<')
+
+
+- name: SINGLE_BRANCH | clear checkout_dir
+ file:
+ state: absent
+ path: "{{ checkout_dir }}"
+
+- name: SINGLE_BRANCH | Clone example git repo using single_branch with version
+ git:
+ repo: 'file://{{ repo_dir|expanduser }}/shallow_branches'
+ dest: '{{ checkout_dir }}'
+ single_branch: yes
+ version: master
+ register: single_branch_3
+
+- name: SINGLE_BRANCH | Clone example git repo using single_branch with version again
+ git:
+ repo: 'file://{{ repo_dir|expanduser }}/shallow_branches'
+ dest: '{{ checkout_dir }}'
+ single_branch: yes
+ version: master
+ register: single_branch_4
+
+- name: SINGLE_BRANCH | List revisions
+ command: git rev-list --all --count
+ args:
+ chdir: '{{ checkout_dir }}'
+ register: rev_list2
+ when: git_version.stdout is version(git_version_supporting_single_branch, '>=')
+
+- name: SINGLE_BRANCH | Ensure single_branch did the right thing with git >= {{ git_version_supporting_single_branch }}
+ assert:
+ that:
+ - single_branch_3 is changed
+ - single_branch_4 is not changed
+ - rev_list2.stdout == '1'
+ when: git_version.stdout is version(git_version_supporting_single_branch, '>=')
+
+- name: SINGLE_BRANCH | Ensure single_branch did the right thing with git < {{ git_version_supporting_single_branch }}
+ assert:
+ that:
+ - single_branch_3 is changed
+ - single_branch_3.warnings | length == 1
+ - single_branch_4 is not changed
+ when: git_version.stdout is version(git_version_supporting_single_branch, '<')
diff --git a/test/integration/targets/git/tasks/specific-revision.yml b/test/integration/targets/git/tasks/specific-revision.yml
new file mode 100644
index 0000000..26fa7cf
--- /dev/null
+++ b/test/integration/targets/git/tasks/specific-revision.yml
@@ -0,0 +1,238 @@
+# Test that a specific revision can be checked out
+
+- name: SPECIFIC-REVISION | clear checkout_dir
+ file:
+ state: absent
+ path: '{{ checkout_dir }}'
+
+- name: SPECIFIC-REVISION | clone to specific revision
+ git:
+ repo: "{{ repo_dir }}/format1"
+ dest: "{{ checkout_dir }}"
+ version: df4612ba925fbc1b3c51cbb006f51a0443bd2ce9
+
+- name: SPECIFIC-REVISION | check HEAD after clone to revision
+ command: git rev-parse HEAD
+ args:
+ chdir: "{{ checkout_dir }}"
+ register: git_result
+
+- assert:
+ that:
+ - 'git_result.stdout == "df4612ba925fbc1b3c51cbb006f51a0443bd2ce9"'
+
+- name: SPECIFIC-REVISION | update to specific revision
+ git:
+ repo: "{{ repo_dir }}/format1"
+ dest: "{{ checkout_dir }}"
+ version: 4e739a34719654db7b04896966e2354e1256ea5d
+ register: git_result
+
+- assert:
+ that:
+ - git_result is changed
+
+- name: SPECIFIC-REVISION | check HEAD after update to revision
+ command: git rev-parse HEAD
+ args:
+ chdir: "{{ checkout_dir }}"
+ register: git_result
+
+- assert:
+ that:
+ - 'git_result.stdout == "4e739a34719654db7b04896966e2354e1256ea5d"'
+
+- name: SPECIFIC-REVISION | update to HEAD from detached HEAD state
+ git:
+ repo: "{{ repo_dir }}/format1"
+ dest: "{{ checkout_dir }}"
+ version: HEAD
+ register: git_result
+
+- assert:
+ that:
+ - git_result is changed
+
+# Test a revision not available under refs/heads/ or refs/tags/
+
+- name: SPECIFIC-REVISION | attempt to get unavailable revision
+ git:
+ repo: "{{ repo_dir }}/format1"
+ dest: "{{ checkout_dir }}"
+ version: 5473e343e33255f2da0b160f53135c56921d875c
+ ignore_errors: true
+ register: git_result
+
+- assert:
+ that:
+ - git_result is failed
+
+# Same as the previous test, but this time we specify which ref
+# contains the SHA1
+- name: SPECIFIC-REVISION | update to revision by specifying the refspec
+ git:
+ repo: https://github.com/ansible/ansible-examples.git
+ dest: '{{ checkout_dir }}'
+ version: 5473e343e33255f2da0b160f53135c56921d875c
+ refspec: refs/pull/7/merge
+
+- name: SPECIFIC-REVISION | check HEAD after update with refspec
+ command: git rev-parse HEAD
+ args:
+ chdir: "{{ checkout_dir }}"
+ register: git_result
+
+- assert:
+ that:
+ - 'git_result.stdout == "5473e343e33255f2da0b160f53135c56921d875c"'
+
+# try out combination of refspec and depth
+- name: SPECIFIC-REVISION | clear checkout_dir
+ file:
+ state: absent
+ path: "{{ checkout_dir }}"
+
+- name: SPECIFIC-REVISION | update to revision by specifying the refspec with depth=1
+ git:
+ repo: https://github.com/ansible/ansible-examples.git
+ dest: '{{ checkout_dir }}'
+ version: 5473e343e33255f2da0b160f53135c56921d875c
+ refspec: refs/pull/7/merge
+ depth: 1
+
+- name: SPECIFIC-REVISION | check HEAD after update with refspec
+ command: git rev-parse HEAD
+ args:
+ chdir: "{{ checkout_dir }}"
+ register: git_result
+
+- assert:
+ that:
+ - 'git_result.stdout == "5473e343e33255f2da0b160f53135c56921d875c"'
+
+- name: SPECIFIC-REVISION | try to access other commit
+ shell: git checkout 0ce1096
+ register: checkout_shallow
+ failed_when: False
+ args:
+ chdir: "{{ checkout_dir }}"
+
+- name: SPECIFIC-REVISION | "make sure the old commit was not fetched, task is 'forced success'"
+ assert:
+ that:
+ - checkout_shallow.rc != 0
+ - checkout_shallow is successful
+ when: git_version.stdout is version(git_version_supporting_depth, '>=')
+
+- name: SPECIFIC-REVISION | clear checkout_dir
+ file:
+ state: absent
+ path: "{{ checkout_dir }}"
+
+- name: SPECIFIC-REVISION | clone to revision by specifying the refspec
+ git:
+ repo: https://github.com/ansible/ansible-examples.git
+ dest: "{{ checkout_dir }}"
+ version: 5473e343e33255f2da0b160f53135c56921d875c
+ refspec: refs/pull/7/merge
+
+- name: SPECIFIC-REVISION | check HEAD after update with refspec
+ command: git rev-parse HEAD
+ args:
+ chdir: "{{ checkout_dir }}"
+ register: git_result
+
+- assert:
+ that:
+ - 'git_result.stdout == "5473e343e33255f2da0b160f53135c56921d875c"'
+
+# Test that a forced shallow checkout referincing branch only always fetches latest head
+
+- name: SPECIFIC-REVISION | clear checkout_dir
+ file:
+ state: absent
+ path: "{{ item }}"
+ with_items:
+ - "{{ checkout_dir }}"
+ - "{{ checkout_dir }}.copy"
+
+- name: SPECIFIC-REVISION | create original repo dir
+ file:
+ state: directory
+ path: "{{ checkout_dir }}"
+
+- name: SPECIFIC-REVISION | prepare origina repo
+ shell: git init; echo "1" > a; git add a; git commit -m "1"
+ args:
+ chdir: "{{ checkout_dir }}"
+
+- name: SPECIFIC-REVISION | clone example repo locally
+ git:
+ repo: "{{ checkout_dir }}"
+ dest: "{{ checkout_dir }}.copy"
+
+- name: SPECIFIC-REVISION | create branch in original
+ command: git checkout -b test/branch
+ args:
+ chdir: "{{ checkout_dir }}"
+
+- name: SPECIFIC-REVISION | get commit for HEAD on new branch
+ command: git rev-parse HEAD
+ args:
+ chdir: "{{ checkout_dir }}.copy"
+ register: originaltip0
+
+- name: SPECIFIC-REVISION | shallow force checkout new branch in copy
+ git:
+ repo: "{{ checkout_dir }}"
+ dest: "{{ checkout_dir }}.copy"
+ version: test/branch
+ depth: 1
+ force: yes
+
+- name: SPECIFIC-REVISION | create new commit in original
+ shell: git init; echo "2" > b; git add b; git commit -m "2"
+ args:
+ chdir: "{{ checkout_dir }}"
+
+- name: SPECIFIC-REVISION | get commit for new HEAD on original branch
+ command: git rev-parse HEAD
+ args:
+ chdir: "{{ checkout_dir }}"
+ register: originaltip1
+
+- name: SPECIFIC-REVISION | get commit for HEAD on new branch
+ command: git rev-parse HEAD
+ args:
+ chdir: "{{ checkout_dir }}.copy"
+ register: newtip
+
+- name: SPECIFIC-REVISION | assert that copy is still pointing at previous tip
+ assert:
+ that:
+ - newtip.stdout == originaltip0.stdout
+
+- name: SPECIFIC-REVISION | create a local modification in the copy
+ shell: echo "3" > c
+ args:
+ chdir: "{{ checkout_dir }}.copy"
+
+- name: SPECIFIC-REVISION | shallow force checkout new branch in copy (again)
+ git:
+ repo: "{{ checkout_dir }}"
+ dest: "{{ checkout_dir }}.copy"
+ version: test/branch
+ depth: 1
+ force: yes
+
+- name: SPECIFIC-REVISION | get commit for HEAD on new branch
+ command: git rev-parse HEAD
+ args:
+ chdir: "{{ checkout_dir }}.copy"
+ register: newtip
+
+- name: SPECIFIC-REVISION | make sure copy tip is not pointing at previous sha and that new tips match
+ assert:
+ that:
+ - newtip.stdout != originaltip0.stdout
+ - newtip.stdout == originaltip1.stdout
diff --git a/test/integration/targets/git/tasks/submodules.yml b/test/integration/targets/git/tasks/submodules.yml
new file mode 100644
index 0000000..0b311e7
--- /dev/null
+++ b/test/integration/targets/git/tasks/submodules.yml
@@ -0,0 +1,150 @@
+#
+# Submodule tests
+#
+
+# Repository A with submodules defined (repo_submodules)
+# .gitmodules file points to Repository I
+# Repository B forked from A that has newer commits (repo_submodules_newer)
+# .gitmodules file points to Repository II instead of I
+# .gitmodules file also points to Repository III
+# Repository I for submodule1 (repo_submodule1)
+# Has 1 file checked in
+# Repository II forked from I that has newer commits (repo_submodule1_newer)
+# Has 2 files checked in
+# Repository III for a second submodule (repo_submodule2)
+# Has 1 file checked in
+
+- name: SUBMODULES | clear checkout_dir
+ file:
+ state: absent
+ path: "{{ checkout_dir }}"
+
+- name: SUBMODULES | Test that clone without recursive does not retrieve submodules
+ git:
+ repo: "{{ repo_submodules }}"
+ version: 45c6c07ef10fd9e453d90207e63da1ce5bd3ae1e
+ dest: "{{ checkout_dir }}"
+ recursive: no
+
+- name: SUBMODULES | List submodule1
+ command: 'ls -1a {{ checkout_dir }}/submodule1'
+ register: submodule1
+
+- name: SUBMODULES | Ensure submodu1 is at the appropriate commit
+ assert:
+ that: '{{ submodule1.stdout_lines | length }} == 2'
+
+- name: SUBMODULES | clear checkout_dir
+ file:
+ state: absent
+ path: "{{ checkout_dir }}"
+
+
+- name: SUBMODULES | Test that clone with recursive retrieves submodules
+ git:
+ repo: "{{ repo_submodules }}"
+ dest: "{{ checkout_dir }}"
+ version: 45c6c07ef10fd9e453d90207e63da1ce5bd3ae1e
+ recursive: yes
+
+- name: SUBMODULES | List submodule1
+ command: 'ls -1a {{ checkout_dir }}/submodule1'
+ register: submodule1
+
+- name: SUBMODULES | Ensure submodule1 is at the appropriate commit
+ assert:
+ that: '{{ submodule1.stdout_lines | length }} == 4'
+
+- name: SUBMODULES | Copy the checkout so we can run several different tests on it
+ command: 'cp -pr {{ checkout_dir }} {{ checkout_dir }}.bak'
+
+
+- name: SUBMODULES | Test that update without recursive does not change submodules
+ git:
+ repo: "{{ repo_submodules }}"
+ version: d2974e4bbccdb59368f1d5eff2205f0fa863297e
+ dest: "{{ checkout_dir }}"
+ recursive: no
+ update: yes
+ track_submodules: yes
+
+- name: SUBMODULES | List submodule1
+ command: 'ls -1a {{ checkout_dir }}/submodule1'
+ register: submodule1
+
+- name: SUBMODULES | Stat submodule2
+ stat:
+ path: "{{ checkout_dir }}/submodule2"
+ register: submodule2
+
+- name: SUBMODULES | List submodule2
+ command: ls -1a {{ checkout_dir }}/submodule2
+ register: submodule2
+
+- name: SUBMODULES | Ensure both submodules are at the appropriate commit
+ assert:
+ that:
+ - '{{ submodule1.stdout_lines|length }} == 4'
+ - '{{ submodule2.stdout_lines|length }} == 2'
+
+
+- name: SUBMODULES | Remove checkout dir
+ file:
+ state: absent
+ path: "{{ checkout_dir }}"
+
+- name: SUBMODULES | Restore checkout to prior state
+ command: 'cp -pr {{ checkout_dir }}.bak {{ checkout_dir }}'
+
+
+- name: SUBMODULES | Test that update with recursive updated existing submodules
+ git:
+ repo: "{{ repo_submodules }}"
+ version: d2974e4bbccdb59368f1d5eff2205f0fa863297e
+ dest: "{{ checkout_dir }}"
+ update: yes
+ recursive: yes
+ track_submodules: yes
+
+- name: SUBMODULES | List submodule 1
+ command: 'ls -1a {{ checkout_dir }}/submodule1'
+ register: submodule1
+
+- name: SUBMODULES | Ensure submodule1 is at the appropriate commit
+ assert:
+ that: '{{ submodule1.stdout_lines | length }} == 5'
+
+
+- name: SUBMODULES | Test that update with recursive found new submodules
+ command: 'ls -1a {{ checkout_dir }}/submodule2'
+ register: submodule2
+
+- name: SUBMODULES | Enusre submodule2 is at the appropriate commit
+ assert:
+ that: '{{ submodule2.stdout_lines | length }} == 4'
+
+- name: SUBMODULES | clear checkout_dir
+ file:
+ state: absent
+ path: "{{ checkout_dir }}"
+
+
+- name: SUBMODULES | Clone main submodule repository
+ git:
+ repo: "{{ repo_submodules }}"
+ dest: "{{ checkout_dir }}/test.gitdir"
+ version: 45c6c07ef10fd9e453d90207e63da1ce5bd3ae1e
+ recursive: yes
+
+- name: SUBMODULES | Test that cloning submodule with .git in directory name works
+ git:
+ repo: "{{ repo_submodule1 }}"
+ dest: "{{ checkout_dir }}/test.gitdir/submodule1"
+
+- name: SUBMODULES | List submodule1
+ command: 'ls -1a {{ checkout_dir }}/test.gitdir/submodule1'
+ register: submodule1
+
+- name: SUBMODULES | Ensure submodule1 is at the appropriate commit
+ assert:
+ that: '{{ submodule1.stdout_lines | length }} == 4'
diff --git a/test/integration/targets/git/vars/main.yml b/test/integration/targets/git/vars/main.yml
new file mode 100644
index 0000000..b38531f
--- /dev/null
+++ b/test/integration/targets/git/vars/main.yml
@@ -0,0 +1,98 @@
+git_archive_extensions:
+ default:
+ - tar.gz
+ - tar
+ - tgz
+ - zip
+ RedHat6:
+ - tar
+ - zip
+
+git_required_packages:
+ default:
+ - git
+ - gzip
+ - tar
+ - unzip
+ - zip
+ FreeBSD:
+ - git
+ - gzip
+ - unzip
+ - zip
+
+git_list_commands:
+ tar.gz: tar -tf
+ tar: tar -tf
+ tgz: tar -tf
+ zip: unzip -Z1
+
+checkout_dir: '{{ remote_tmp_dir }}/git'
+repo_dir: '{{ remote_tmp_dir }}/local_repos'
+separate_git_dir: '{{ remote_tmp_dir }}/sep_git_dir'
+repo_format1: 'https://github.com/jimi-c/test_role'
+repo_format2: 'git@github.com:jimi-c/test_role.git'
+repo_format3: 'ssh://git@github.com/jimi-c/test_role.git'
+repo_submodules: 'https://github.com/abadger/test_submodules_newer.git'
+repo_submodule1: 'https://github.com/abadger/test_submodules_subm1.git'
+repo_submodule2: 'https://github.com/abadger/test_submodules_subm2.git'
+repo_update_url_1: 'https://github.com/ansible-test-robinro/git-test-old'
+repo_update_url_2: 'https://github.com/ansible-test-robinro/git-test-new'
+known_host_files:
+ - "{{ lookup('env','HOME') }}/.ssh/known_hosts"
+ - '/etc/ssh/ssh_known_hosts'
+git_version_supporting_depth: 1.9.1
+git_version_supporting_ls_remote: 1.7.5
+git_version_supporting_single_branch: 1.7.10
+# path to a SSH private key for use with github.com (tests skipped if undefined)
+# github_ssh_private_key: "{{ lookup('env', 'HOME') }}/.ssh/id_rsa"
+git_gpg_testkey: |
+ -----BEGIN PGP PRIVATE KEY BLOCK-----
+
+ lQOYBFlkmX0BCACtE81Xj/351nnvwnAWMf8ZUP9B1YOPe9ohqNsCQY1DxODVJc9y
+ ljCoh9fTdoHXuaUMUFistozxCMP81RuZxfbfsGePnl8OAOgWT5Sln6yEG45oClJ0
+ RmJJZdDT1lF3VaVwK9NQ5E1oqmk1IOjISi7iFa9TmMn1h7ISP/p+/xtMxQhzUXt8
+ APAEhRdc9FfwxaxCHKZBiM7ND+pAm6vpom07ZUgxSppsrXZAxDncTwAeCumDpeOL
+ LAcSBsw02swOIHFfqHNrkELLr4KJqws+zeAk6R2nq0k16AVdNX+Rb7T3OKmuLawx
+ HXe8rKpaw0RC+JCogZK4tz0KDNuZPLW2Y5JJABEBAAEAB/4zkKpFk79p35YNskLd
+ wgCMRN7/+MKNDavUCnBRsEELt0z7BBxVudx+YZaSSITvxj4fuJJqxqqgJ2no2n8y
+ JdJjG7YHCnqse+WpvAUAAV4PL/ySD704Kj4fOwfoDTrRUIGNNWlseNB9RgQ5UXg5
+ MCzeq/JD+En3bnnFySzzCENUcAQfu2FVYgKEiKaKL5Djs6p5w/jTm+Let3EsIczb
+ ykJ8D4/G/tSrNdp/g10DDy+VclWMhMFqmFesedvytE8jzCVxPKOoRkFTGrX76gIK
+ eMVxHIYxdCfSTHLjBykMGO9gxfk9lf18roNYs0VV2suyi4fVFxEozSAxwWlwKrXn
+ 0arvBADPsm5NjlZ5uR06YKbpUUwPTYcwLbasic0qHuUWgNsTVv8dd2il/jbha77m
+ StU7qRJ1jwbFEFxx7HnTmeGfPbdyKe2qyLJUyD/rpQSC5YirisUchtG8nZsHlnzn
+ k10SIeB480tkgkdMQx1Eif40aiuQb09/TxaaXAEFKttZhEO4RwQA1VQ8a0IrMBI2
+ i4WqaIDNDl3x61JvvFD74v43I0AHKmZUPwcgAd6q2IvCDaKH0hIuBKu6BGq6DPvx
+ Oc/4r3iRn/xccconxRop2A9ffa00B/eQXrBq+uLBQfyiFL9UfkU8eTAAgbDKRxjY
+ ScaevoBbbYxkpgJUCL6VnoSdXlbNOO8EAL2ypsVkDmXNgR8ZT8cKSUft47di5T+9
+ mhT1qmD62B+D86892y2QAohmUDadYRK9m9WD91Y7gOMeNhYj9qbxyPprPYUL0aPt
+ L8KS1H73C5WQMOsl2RyIw81asss30LWghsFIJ1gz8gVEjXhV+YC6W9XQ42iabmRR
+ A67f5sqK1scuO0q0KUFuc2libGUgVGVzdCBSdW5uZXIgPG5vcmVwbHlAZXhhbXBs
+ ZS5jb20+iQE3BBMBCAAhBQJZZJl9AhsDBQsJCAcCBhUICQoLAgQWAgMBAh4BAheA
+ AAoJEK0vcLBcXpbYi/kH/R0xk42MFpGd4pndTAsVIjRk/VhmhFc1v6sBeR40GXlt
+ hyEeOQQnIeHKLhsVT6YnfFZa8b4JwgTD6NeIiibOAlLgaKOWNwZu8toixMPVAzfQ
+ cRei+/gFXNil0FmBwWreVBDppuIn6XiSEPik0C7eCcw4lD+A+BbL3WGkp+OSQPho
+ hodIU02hgkrgs/6YJPats8Rgzw9hICsa2j0MjnG6P2z9atMz6tw2SiE5iBl7mZ2Z
+ zG/HiplleMhf/G8OZOskrWkKiLbpSPfQSKdOFkw1C6yqOlQ+HmuCZ56oyxtpItET
+ R11uAKt+ABdi4DX3FQQ+A+bGJ1+aKrcorZ8Z8s0XhPo=
+ =tV71
+ -----END PGP PRIVATE KEY BLOCK-----
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
+
+ mQENBFlkmX0BCACtE81Xj/351nnvwnAWMf8ZUP9B1YOPe9ohqNsCQY1DxODVJc9y
+ ljCoh9fTdoHXuaUMUFistozxCMP81RuZxfbfsGePnl8OAOgWT5Sln6yEG45oClJ0
+ RmJJZdDT1lF3VaVwK9NQ5E1oqmk1IOjISi7iFa9TmMn1h7ISP/p+/xtMxQhzUXt8
+ APAEhRdc9FfwxaxCHKZBiM7ND+pAm6vpom07ZUgxSppsrXZAxDncTwAeCumDpeOL
+ LAcSBsw02swOIHFfqHNrkELLr4KJqws+zeAk6R2nq0k16AVdNX+Rb7T3OKmuLawx
+ HXe8rKpaw0RC+JCogZK4tz0KDNuZPLW2Y5JJABEBAAG0KUFuc2libGUgVGVzdCBS
+ dW5uZXIgPG5vcmVwbHlAZXhhbXBsZS5jb20+iQE3BBMBCAAhBQJZZJl9AhsDBQsJ
+ CAcCBhUICQoLAgQWAgMBAh4BAheAAAoJEK0vcLBcXpbYi/kH/R0xk42MFpGd4pnd
+ TAsVIjRk/VhmhFc1v6sBeR40GXlthyEeOQQnIeHKLhsVT6YnfFZa8b4JwgTD6NeI
+ iibOAlLgaKOWNwZu8toixMPVAzfQcRei+/gFXNil0FmBwWreVBDppuIn6XiSEPik
+ 0C7eCcw4lD+A+BbL3WGkp+OSQPhohodIU02hgkrgs/6YJPats8Rgzw9hICsa2j0M
+ jnG6P2z9atMz6tw2SiE5iBl7mZ2ZzG/HiplleMhf/G8OZOskrWkKiLbpSPfQSKdO
+ Fkw1C6yqOlQ+HmuCZ56oyxtpItETR11uAKt+ABdi4DX3FQQ+A+bGJ1+aKrcorZ8Z
+ 8s0XhPo=
+ =mUYY
+ -----END PGP PUBLIC KEY BLOCK-----
diff --git a/test/integration/targets/group/aliases b/test/integration/targets/group/aliases
new file mode 100644
index 0000000..a6dafcf
--- /dev/null
+++ b/test/integration/targets/group/aliases
@@ -0,0 +1 @@
+shippable/posix/group1
diff --git a/test/integration/targets/group/files/gidget.py b/test/integration/targets/group/files/gidget.py
new file mode 100644
index 0000000..4b77151
--- /dev/null
+++ b/test/integration/targets/group/files/gidget.py
@@ -0,0 +1,15 @@
+#!/usr/bin/env python
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import grp
+
+gids = [g.gr_gid for g in grp.getgrall()]
+
+i = 0
+while True:
+ if i not in gids:
+ print(i)
+ break
+ i += 1
diff --git a/test/integration/targets/group/files/grouplist.sh b/test/integration/targets/group/files/grouplist.sh
new file mode 100644
index 0000000..d3129df
--- /dev/null
+++ b/test/integration/targets/group/files/grouplist.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+
+#- name: make a list of groups
+# shell: |
+# cat /etc/group | cut -d: -f1
+# register: group_names
+# when: 'ansible_distribution != "MacOSX"'
+
+#- name: make a list of groups [mac]
+# shell: dscl localhost -list /Local/Default/Groups
+# register: group_names
+# when: 'ansible_distribution == "MacOSX"'
+
+DISTRO="$*"
+
+if [[ "$DISTRO" == "MacOSX" ]]; then
+ dscl localhost -list /Local/Default/Groups
+else
+ grep -E -v ^\# /etc/group | cut -d: -f1
+fi
diff --git a/test/integration/targets/group/meta/main.yml b/test/integration/targets/group/meta/main.yml
new file mode 100644
index 0000000..07faa21
--- /dev/null
+++ b/test/integration/targets/group/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - prepare_tests
diff --git a/test/integration/targets/group/tasks/main.yml b/test/integration/targets/group/tasks/main.yml
new file mode 100644
index 0000000..eb8126d
--- /dev/null
+++ b/test/integration/targets/group/tasks/main.yml
@@ -0,0 +1,40 @@
+# Test code for the group module.
+# (c) 2017, James Tanner <tanner.jc@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: ensure test groups are deleted before the test
+ group:
+ name: '{{ item }}'
+ state: absent
+ loop:
+ - ansibullgroup
+ - ansibullgroup2
+ - ansibullgroup3
+
+- block:
+ - name: run tests
+ include_tasks: tests.yml
+
+ always:
+ - name: remove test groups after test
+ group:
+ name: '{{ item }}'
+ state: absent
+ loop:
+ - ansibullgroup
+ - ansibullgroup2
+ - ansibullgroup3 \ No newline at end of file
diff --git a/test/integration/targets/group/tasks/tests.yml b/test/integration/targets/group/tasks/tests.yml
new file mode 100644
index 0000000..f9a8122
--- /dev/null
+++ b/test/integration/targets/group/tasks/tests.yml
@@ -0,0 +1,343 @@
+---
+##
+## group add
+##
+
+- name: create group (check mode)
+ group:
+ name: ansibullgroup
+ state: present
+ register: create_group_check
+ check_mode: True
+
+- name: get result of create group (check mode)
+ script: 'grouplist.sh "{{ ansible_distribution }}"'
+ register: create_group_actual_check
+
+- name: assert create group (check mode)
+ assert:
+ that:
+ - create_group_check is changed
+ - '"ansibullgroup" not in create_group_actual_check.stdout_lines'
+
+- name: create group
+ group:
+ name: ansibullgroup
+ state: present
+ register: create_group
+
+- name: get result of create group
+ script: 'grouplist.sh "{{ ansible_distribution }}"'
+ register: create_group_actual
+
+- name: assert create group
+ assert:
+ that:
+ - create_group is changed
+ - create_group.gid is defined
+ - '"ansibullgroup" in create_group_actual.stdout_lines'
+
+- name: create group (idempotent)
+ group:
+ name: ansibullgroup
+ state: present
+ register: create_group_again
+
+- name: assert create group (idempotent)
+ assert:
+ that:
+ - not create_group_again is changed
+
+##
+## group check
+##
+
+- name: run existing group check tests
+ group:
+ name: "{{ create_group_actual.stdout_lines|random }}"
+ state: present
+ with_sequence: start=1 end=5
+ register: group_test1
+
+- name: validate results for testcase 1
+ assert:
+ that:
+ - group_test1.results is defined
+ - group_test1.results|length == 5
+
+- name: validate change results for testcase 1
+ assert:
+ that:
+ - not group_test1 is changed
+
+##
+## group add with gid
+##
+
+- name: get the next available gid
+ script: gidget.py
+ args:
+ executable: '{{ ansible_python_interpreter }}'
+ register: gid
+
+- name: create a group with a gid (check mode)
+ group:
+ name: ansibullgroup2
+ gid: '{{ gid.stdout_lines[0] }}'
+ state: present
+ register: create_group_gid_check
+ check_mode: True
+
+- name: get result of create a group with a gid (check mode)
+ script: 'grouplist.sh "{{ ansible_distribution }}"'
+ register: create_group_gid_actual_check
+
+- name: assert create group with a gid (check mode)
+ assert:
+ that:
+ - create_group_gid_check is changed
+ - '"ansibullgroup2" not in create_group_gid_actual_check.stdout_lines'
+
+- name: create a group with a gid
+ group:
+ name: ansibullgroup2
+ gid: '{{ gid.stdout_lines[0] }}'
+ state: present
+ register: create_group_gid
+
+- name: get gid of created group
+ command: "{{ ansible_python_interpreter | quote }} -c \"import grp; print(grp.getgrnam('ansibullgroup2').gr_gid)\""
+ register: create_group_gid_actual
+
+- name: assert create group with a gid
+ assert:
+ that:
+ - create_group_gid is changed
+ - create_group_gid.gid | int == gid.stdout_lines[0] | int
+ - create_group_gid_actual.stdout | trim | int == gid.stdout_lines[0] | int
+
+- name: create a group with a gid (idempotent)
+ group:
+ name: ansibullgroup2
+ gid: '{{ gid.stdout_lines[0] }}'
+ state: present
+ register: create_group_gid_again
+
+- name: assert create group with a gid (idempotent)
+ assert:
+ that:
+ - not create_group_gid_again is changed
+ - create_group_gid_again.gid | int == gid.stdout_lines[0] | int
+
+- block:
+ - name: create a group with a non-unique gid
+ group:
+ name: ansibullgroup3
+ gid: '{{ gid.stdout_lines[0] }}'
+ non_unique: true
+ state: present
+ register: create_group_gid_non_unique
+
+ - name: validate gid required with non_unique
+ group:
+ name: foo
+ non_unique: true
+ register: missing_gid
+ ignore_errors: true
+
+ - name: assert create group with a non unique gid
+ assert:
+ that:
+ - create_group_gid_non_unique is changed
+ - create_group_gid_non_unique.gid | int == gid.stdout_lines[0] | int
+ - missing_gid is failed
+ when: ansible_facts.distribution not in ['MacOSX', 'Alpine']
+
+##
+## group remove
+##
+
+- name: delete group (check mode)
+ group:
+ name: ansibullgroup
+ state: absent
+ register: delete_group_check
+ check_mode: True
+
+- name: get result of delete group (check mode)
+ script: grouplist.sh "{{ ansible_distribution }}"
+ register: delete_group_actual_check
+
+- name: assert delete group (check mode)
+ assert:
+ that:
+ - delete_group_check is changed
+ - '"ansibullgroup" in delete_group_actual_check.stdout_lines'
+
+- name: delete group
+ group:
+ name: ansibullgroup
+ state: absent
+ register: delete_group
+
+- name: get result of delete group
+ script: grouplist.sh "{{ ansible_distribution }}"
+ register: delete_group_actual
+
+- name: assert delete group
+ assert:
+ that:
+ - delete_group is changed
+ - '"ansibullgroup" not in delete_group_actual.stdout_lines'
+
+- name: delete group (idempotent)
+ group:
+ name: ansibullgroup
+ state: absent
+ register: delete_group_again
+
+- name: assert delete group (idempotent)
+ assert:
+ that:
+ - not delete_group_again is changed
+
+- name: Ensure lgroupadd is present
+ action: "{{ ansible_facts.pkg_mgr }}"
+ args:
+ name: libuser
+ state: present
+ when: ansible_facts.system in ['Linux'] and ansible_distribution != 'Alpine' and ansible_os_family != 'Suse'
+ tags:
+ - user_test_local_mode
+
+- name: Ensure lgroupadd is present - Alpine
+ command: apk add -U libuser
+ when: ansible_distribution == 'Alpine'
+ tags:
+ - user_test_local_mode
+
+# https://github.com/ansible/ansible/issues/56481
+- block:
+ - name: Test duplicate GID with local=yes
+ group:
+ name: "{{ item }}"
+ gid: 1337
+ local: yes
+ loop:
+ - group1_local_test
+ - group2_local_test
+ ignore_errors: yes
+ register: local_duplicate_gid_result
+
+ - assert:
+ that:
+ - local_duplicate_gid_result['results'][0] is success
+ - local_duplicate_gid_result['results'][1]['msg'] == "GID '1337' already exists with group 'group1_local_test'"
+ always:
+ - name: Cleanup
+ group:
+ name: group1_local_test
+ state: absent
+ # only applicable to Linux, limit further to CentOS where 'luseradd' is installed
+ when: ansible_distribution == 'CentOS'
+
+# https://github.com/ansible/ansible/pull/59769
+- block:
+ - name: create a local group with a gid
+ group:
+ name: group1_local_test
+ gid: 1337
+ local: yes
+ state: present
+ register: create_local_group_gid
+
+ - name: get gid of created local group
+ command: "{{ ansible_python_interpreter | quote }} -c \"import grp; print(grp.getgrnam('group1_local_test').gr_gid)\""
+ register: create_local_group_gid_actual
+
+ - name: assert create local group with a gid
+ assert:
+ that:
+ - create_local_group_gid is changed
+ - create_local_group_gid.gid | int == 1337 | int
+ - create_local_group_gid_actual.stdout | trim | int == 1337 | int
+
+ - name: create a local group with a gid (idempotent)
+ group:
+ name: group1_local_test
+ gid: 1337
+ state: present
+ register: create_local_group_gid_again
+
+ - name: assert create local group with a gid (idempotent)
+ assert:
+ that:
+ - not create_local_group_gid_again is changed
+ - create_local_group_gid_again.gid | int == 1337 | int
+ always:
+ - name: Cleanup create local group with a gid
+ group:
+ name: group1_local_test
+ state: absent
+ # only applicable to Linux, limit further to CentOS where 'luseradd' is installed
+ when: ansible_distribution == 'CentOS'
+
+# https://github.com/ansible/ansible/pull/59772
+- block:
+ - name: create group with a gid
+ group:
+ name: group1_test
+ gid: 1337
+ local: no
+ state: present
+ register: create_group_gid
+
+ - name: get gid of created group
+ command: "{{ ansible_python_interpreter | quote }} -c \"import grp; print(grp.getgrnam('group1_test').gr_gid)\""
+ register: create_group_gid_actual
+
+ - name: assert create group with a gid
+ assert:
+ that:
+ - create_group_gid is changed
+ - create_group_gid.gid | int == 1337 | int
+ - create_group_gid_actual.stdout | trim | int == 1337 | int
+
+ - name: create local group with the same gid
+ group:
+ name: group1_test
+ gid: 1337
+ local: yes
+ state: present
+ register: create_local_group_gid
+
+ - name: assert create local group with a gid
+ assert:
+ that:
+ - create_local_group_gid.gid | int == 1337 | int
+ always:
+ - name: Cleanup create group with a gid
+ group:
+ name: group1_test
+ local: no
+ state: absent
+ - name: Cleanup create local group with the same gid
+ group:
+ name: group1_test
+ local: yes
+ state: absent
+ # only applicable to Linux, limit further to CentOS where 'lgroupadd' is installed
+ when: ansible_distribution == 'CentOS'
+
+# create system group
+
+- name: remove group
+ group:
+ name: ansibullgroup
+ state: absent
+
+- name: create system group
+ group:
+ name: ansibullgroup
+ state: present
+ system: yes
diff --git a/test/integration/targets/group_by/aliases b/test/integration/targets/group_by/aliases
new file mode 100644
index 0000000..765b70d
--- /dev/null
+++ b/test/integration/targets/group_by/aliases
@@ -0,0 +1 @@
+shippable/posix/group2
diff --git a/test/integration/targets/group_by/create_groups.yml b/test/integration/targets/group_by/create_groups.yml
new file mode 100644
index 0000000..3494a20
--- /dev/null
+++ b/test/integration/targets/group_by/create_groups.yml
@@ -0,0 +1,39 @@
+# test code for the group_by module
+# (c) 2014, Chris Church <cchurch@ansible.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- debug: var=genus
+
+- name: group by genus
+ group_by: key={{ genus }}
+ register: grouped_by_genus
+
+- debug: var=grouped_by_genus
+
+- name: ensure we reflect 'changed' on change
+ assert:
+ that:
+ - grouped_by_genus is changed
+
+- name: group by first three letters of genus with key in quotes
+ group_by: key="{{ genus[:3] }}"
+
+- name: group by first two letters of genus with key not in quotes
+ group_by: key={{ genus[:2] }}
+
+- name: group by genus in uppercase using complex args
+ group_by: { key: "{{ genus | upper() }}" }
diff --git a/test/integration/targets/group_by/group_vars/all b/test/integration/targets/group_by/group_vars/all
new file mode 100644
index 0000000..0b674e0
--- /dev/null
+++ b/test/integration/targets/group_by/group_vars/all
@@ -0,0 +1,3 @@
+uno: 1
+dos: 2
+tres: 3
diff --git a/test/integration/targets/group_by/group_vars/camelus b/test/integration/targets/group_by/group_vars/camelus
new file mode 100644
index 0000000..b214ad6
--- /dev/null
+++ b/test/integration/targets/group_by/group_vars/camelus
@@ -0,0 +1 @@
+dos: 'two'
diff --git a/test/integration/targets/group_by/group_vars/vicugna b/test/integration/targets/group_by/group_vars/vicugna
new file mode 100644
index 0000000..8feb93f
--- /dev/null
+++ b/test/integration/targets/group_by/group_vars/vicugna
@@ -0,0 +1 @@
+tres: 'three'
diff --git a/test/integration/targets/group_by/inventory.group_by b/test/integration/targets/group_by/inventory.group_by
new file mode 100644
index 0000000..9c7fe7e
--- /dev/null
+++ b/test/integration/targets/group_by/inventory.group_by
@@ -0,0 +1,9 @@
+# ungrouped
+camel genus=camelus ansible_connection=local
+
+[lamini]
+alpaca genus=vicugna
+llama genus=lama
+
+[lamini:vars]
+ansible_connection=local
diff --git a/test/integration/targets/group_by/runme.sh b/test/integration/targets/group_by/runme.sh
new file mode 100755
index 0000000..d119268
--- /dev/null
+++ b/test/integration/targets/group_by/runme.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook test_group_by.yml -i inventory.group_by -v "$@"
+ANSIBLE_HOST_PATTERN_MISMATCH=warning ansible-playbook test_group_by_skipped.yml -i inventory.group_by -v "$@"
diff --git a/test/integration/targets/group_by/test_group_by.yml b/test/integration/targets/group_by/test_group_by.yml
new file mode 100644
index 0000000..07368df
--- /dev/null
+++ b/test/integration/targets/group_by/test_group_by.yml
@@ -0,0 +1,187 @@
+# test code for the group_by module
+# (c) 2014, Chris Church <cchurch@ansible.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: Create overall groups
+ hosts: all
+ gather_facts: false
+ tasks:
+ - include_tasks: create_groups.yml
+
+- name: Vicunga group validation
+ hosts: vicugna
+ gather_facts: false
+ tasks:
+ - name: verify that only the alpaca is in this group
+ assert: { that: "inventory_hostname == 'alpaca'" }
+ - name: set a fact to check that we ran this play
+ set_fact: genus_vicugna=true
+
+- name: Lama group validation
+ hosts: lama
+ gather_facts: false
+ tasks:
+ - name: verify that only the llama is in this group
+ assert: { that: "inventory_hostname == 'llama'" }
+ - name: set a fact to check that we ran this play
+ set_fact: genus_lama=true
+
+- name: Camelus group validation
+ hosts: camelus
+ gather_facts: false
+ tasks:
+ - name: verify that only the camel is in this group
+ assert: { that: "inventory_hostname == 'camel'" }
+ - name: set a fact to check that we ran this play
+ set_fact: genus_camelus=true
+
+- name: Vic group validation
+ hosts: vic
+ gather_facts: false
+ tasks:
+ - name: verify that only the alpaca is in this group
+ assert: { that: "inventory_hostname == 'alpaca'" }
+ - name: set a fact to check that we ran this play
+ set_fact: genus_vic=true
+
+- name: Lam group validation
+ hosts: lam
+ gather_facts: false
+ tasks:
+ - name: verify that only the llama is in this group
+ assert: { that: "inventory_hostname == 'llama'" }
+ - name: set a fact to check that we ran this play
+ set_fact: genus_lam=true
+
+- name: Cam group validation
+ hosts: cam
+ gather_facts: false
+ tasks:
+ - name: verify that only the camel is in this group
+ assert: { that: "inventory_hostname == 'camel'" }
+ - name: set a fact to check that we ran this play
+ set_fact: genus_cam=true
+
+- name: Vi group validation
+ hosts: vi
+ gather_facts: false
+ tasks:
+ - name: verify that only the alpaca is in this group
+ assert: { that: "inventory_hostname == 'alpaca'" }
+ - name: set a fact to check that we ran this play
+ set_fact: genus_vi=true
+
+- name: La group validation
+ hosts: la
+ gather_facts: false
+ tasks:
+ - name: verify that only the llama is in this group
+ assert: { that: "inventory_hostname == 'llama'" }
+ - name: set a fact to check that we ran this play
+ set_fact: genus_la=true
+
+- name: Ca group validation
+ hosts: ca
+ gather_facts: false
+ tasks:
+ - name: verify that only the camel is in this group
+ assert: { that: "inventory_hostname == 'camel'" }
+ - name: set a fact to check that we ran this play
+ set_fact: genus_ca=true
+
+- name: VICUGNA group validation
+ hosts: VICUGNA
+ gather_facts: false
+ tasks:
+ - name: verify that only the alpaca is in this group
+ assert: { that: "inventory_hostname == 'alpaca'" }
+ - name: set a fact to check that we ran this play
+ set_fact: genus_VICUGNA=true
+
+- name: LAMA group validation
+ hosts: LAMA
+ gather_facts: false
+ tasks:
+ - name: verify that only the llama is in this group
+ assert: { that: "inventory_hostname == 'llama'" }
+ - name: set a fact to check that we ran this play
+ set_fact: genus_LAMA=true
+
+- name: CAMELUS group validation
+ hosts: CAMELUS
+ gather_facts: false
+ tasks:
+ - name: verify that only the camel is in this group
+ assert: { that: "inventory_hostname == 'camel'" }
+ - name: set a fact to check that we ran this play
+ set_fact: genus_CAMELUS=true
+
+- name: alpaca validation of groups
+ hosts: alpaca
+ gather_facts: false
+ tasks:
+ - name: check that alpaca matched all four groups
+ assert: { that: ["genus_vicugna", "genus_vic", "genus_vi", "genus_VICUGNA"] }
+
+- name: llama validation of groups
+ hosts: llama
+ gather_facts: false
+ tasks:
+ - name: check that llama matched all four groups
+ assert: { that: ["genus_lama", "genus_lam", "genus_la", "genus_LAMA"] }
+
+- hosts: camel
+ gather_facts: false
+ tasks:
+ - name: check that camel matched all four groups
+ assert: { that: ["genus_camelus", "genus_cam", "genus_ca", "genus_CAMELUS"] }
+
+- hosts: vicugna
+ gather_facts: false
+ tasks:
+ - name: check group_vars variable overrides for vicugna
+ assert: { that: ["uno == 1", "dos == 2", "tres == 'three'"] }
+
+- hosts: lama
+ gather_facts: false
+ tasks:
+ - name: check group_vars variable overrides for lama
+ assert: { that: ["uno == 1", "dos == 2", "tres == 3"] }
+
+- hosts: camelus
+ gather_facts: false
+ tasks:
+ - name: check group_vars variable overrides for camelus
+ assert: { that: ["uno == 1", "dos == 'two'", "tres == 3"] }
+
+- name: Nested group validation
+ hosts: lama
+ gather_facts: false
+ tasks:
+ - name: group by genus with parent
+ group_by: key=vicugna-{{ genus }} parents=vicugna
+ - name: check group_vars variable overrides for vicugna-lama
+ assert: { that: ["uno == 1", "dos == 2", "tres == 'three'"] }
+
+ - name: group by genus with nonexistent parent
+ group_by:
+ key: "{{ genus }}"
+ parents:
+ - oxydactylus
+ - stenomylus
+ - name: check parent groups
+ assert: { that: ["'oxydactylus' in group_names", "'stenomylus' in group_names"] }
diff --git a/test/integration/targets/group_by/test_group_by_skipped.yml b/test/integration/targets/group_by/test_group_by_skipped.yml
new file mode 100644
index 0000000..6c18b4e
--- /dev/null
+++ b/test/integration/targets/group_by/test_group_by_skipped.yml
@@ -0,0 +1,30 @@
+# test code for the group_by module
+# (c) 2014, Chris Church <cchurch@ansible.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: Create overall groups
+ hosts: all
+ gather_facts: false
+ tasks:
+ - include_tasks: create_groups.yml
+
+- name: genus group validation (expect skipped)
+ hosts: 'genus'
+ gather_facts: false
+ tasks:
+ - name: no hosts should match this group
+ fail: msg="should never get here"
diff --git a/test/integration/targets/groupby_filter/aliases b/test/integration/targets/groupby_filter/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/groupby_filter/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/groupby_filter/tasks/main.yml b/test/integration/targets/groupby_filter/tasks/main.yml
new file mode 100644
index 0000000..45c8687
--- /dev/null
+++ b/test/integration/targets/groupby_filter/tasks/main.yml
@@ -0,0 +1,16 @@
+- set_fact:
+ result: "{{ fruits | groupby('enjoy') }}"
+ vars:
+ fruits:
+ - name: apple
+ enjoy: yes
+ - name: orange
+ enjoy: no
+ - name: strawberry
+ enjoy: yes
+
+- assert:
+ that:
+ - result == expected
+ vars:
+ expected: [[false, [{"enjoy": false, "name": "orange"}]], [true, [{"enjoy": true, "name": "apple"}, {"enjoy": true, "name": "strawberry"}]]]
diff --git a/test/integration/targets/handler_race/aliases b/test/integration/targets/handler_race/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/handler_race/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/handler_race/inventory b/test/integration/targets/handler_race/inventory
new file mode 100644
index 0000000..8787929
--- /dev/null
+++ b/test/integration/targets/handler_race/inventory
@@ -0,0 +1,30 @@
+host001 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host002 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host003 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host004 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host005 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host006 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host007 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host008 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host009 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host010 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host011 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host012 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host013 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host014 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host015 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host016 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host017 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host018 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host019 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host020 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host021 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host022 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host023 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host024 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host025 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host026 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host027 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host028 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host029 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host030 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
diff --git a/test/integration/targets/handler_race/roles/do_handlers/handlers/main.yml b/test/integration/targets/handler_race/roles/do_handlers/handlers/main.yml
new file mode 100644
index 0000000..4c43df8
--- /dev/null
+++ b/test/integration/targets/handler_race/roles/do_handlers/handlers/main.yml
@@ -0,0 +1,4 @@
+---
+# handlers file for do_handlers
+- name: My Handler
+ shell: sleep 5
diff --git a/test/integration/targets/handler_race/roles/do_handlers/tasks/main.yml b/test/integration/targets/handler_race/roles/do_handlers/tasks/main.yml
new file mode 100644
index 0000000..028e9a5
--- /dev/null
+++ b/test/integration/targets/handler_race/roles/do_handlers/tasks/main.yml
@@ -0,0 +1,9 @@
+---
+# tasks file for do_handlers
+- name: Invoke handler
+ shell: sleep 1
+ notify:
+ - My Handler
+
+- name: Flush handlers
+ meta: flush_handlers
diff --git a/test/integration/targets/handler_race/roles/more_sleep/tasks/main.yml b/test/integration/targets/handler_race/roles/more_sleep/tasks/main.yml
new file mode 100644
index 0000000..aefbce2
--- /dev/null
+++ b/test/integration/targets/handler_race/roles/more_sleep/tasks/main.yml
@@ -0,0 +1,8 @@
+---
+# tasks file for more_sleep
+- name: Random more sleep
+ set_fact:
+ more_sleep_time: "{{ 5 | random }}"
+
+- name: Moar sleep
+ shell: sleep "{{ more_sleep_time }}"
diff --git a/test/integration/targets/handler_race/roles/random_sleep/tasks/main.yml b/test/integration/targets/handler_race/roles/random_sleep/tasks/main.yml
new file mode 100644
index 0000000..0bc4c38
--- /dev/null
+++ b/test/integration/targets/handler_race/roles/random_sleep/tasks/main.yml
@@ -0,0 +1,8 @@
+---
+# tasks file for random_sleep
+- name: Generate sleep time
+ set_fact:
+ sleep_time: "{{ 30 | random }}"
+
+- name: Do random sleep
+ shell: sleep "{{ sleep_time }}"
diff --git a/test/integration/targets/handler_race/runme.sh b/test/integration/targets/handler_race/runme.sh
new file mode 100755
index 0000000..ba0f987
--- /dev/null
+++ b/test/integration/targets/handler_race/runme.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook test_handler_race.yml -i inventory -v "$@"
+
diff --git a/test/integration/targets/handler_race/test_handler_race.yml b/test/integration/targets/handler_race/test_handler_race.yml
new file mode 100644
index 0000000..ef71382
--- /dev/null
+++ b/test/integration/targets/handler_race/test_handler_race.yml
@@ -0,0 +1,10 @@
+- hosts: all
+ gather_facts: no
+ strategy: free
+ tasks:
+ - include_role:
+ name: random_sleep
+ - include_role:
+ name: do_handlers
+ - include_role:
+ name: more_sleep
diff --git a/test/integration/targets/handlers/46447.yml b/test/integration/targets/handlers/46447.yml
new file mode 100644
index 0000000..d2812b5
--- /dev/null
+++ b/test/integration/targets/handlers/46447.yml
@@ -0,0 +1,16 @@
+- hosts: A,B
+ gather_facts: no
+ any_errors_fatal: True
+ tasks:
+ - command: /bin/true
+ notify: test_handler
+
+ - meta: flush_handlers
+
+ - name: Should not get here
+ debug:
+ msg: "SHOULD NOT GET HERE"
+
+ handlers:
+ - name: test_handler
+ command: /usr/bin/{{ (inventory_hostname == 'A') | ternary('true', 'false') }}
diff --git a/test/integration/targets/handlers/52561.yml b/test/integration/targets/handlers/52561.yml
new file mode 100644
index 0000000..f2e2b58
--- /dev/null
+++ b/test/integration/targets/handlers/52561.yml
@@ -0,0 +1,20 @@
+- hosts: A,B
+ gather_facts: false
+ tasks:
+ - block:
+ - debug:
+ changed_when: true
+ notify:
+ - handler1
+ - name: EXPECTED FAILURE
+ fail:
+ when: inventory_hostname == 'B'
+ always:
+ - debug:
+ msg: 'always'
+ - debug:
+ msg: 'after always'
+ handlers:
+ - name: handler1
+ debug:
+ msg: 'handler1 ran'
diff --git a/test/integration/targets/handlers/54991.yml b/test/integration/targets/handlers/54991.yml
new file mode 100644
index 0000000..c7424ed
--- /dev/null
+++ b/test/integration/targets/handlers/54991.yml
@@ -0,0 +1,11 @@
+- hosts: A,B,C,D
+ gather_facts: false
+ serial: 2
+ tasks:
+ - command: echo
+ notify: handler
+ handlers:
+ - name: handler
+ debug:
+ msg: 'handler ran'
+ failed_when: inventory_hostname == 'A'
diff --git a/test/integration/targets/handlers/58841.yml b/test/integration/targets/handlers/58841.yml
new file mode 100644
index 0000000..eea5c2f
--- /dev/null
+++ b/test/integration/targets/handlers/58841.yml
@@ -0,0 +1,9 @@
+---
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - include_role:
+ name: import_template_handler_names
+ tags:
+ - lazy_evaluation
+ - evaluation_time
diff --git a/test/integration/targets/handlers/79776-handlers.yml b/test/integration/targets/handlers/79776-handlers.yml
new file mode 100644
index 0000000..639c9ca
--- /dev/null
+++ b/test/integration/targets/handlers/79776-handlers.yml
@@ -0,0 +1,2 @@
+- debug:
+ msg: "Handler for {{ inventory_hostname }}"
diff --git a/test/integration/targets/handlers/79776.yml b/test/integration/targets/handlers/79776.yml
new file mode 100644
index 0000000..08d2227
--- /dev/null
+++ b/test/integration/targets/handlers/79776.yml
@@ -0,0 +1,10 @@
+- hosts: A,B
+ gather_facts: false
+ force_handlers: true
+ tasks:
+ - command: echo
+ notify: handler1
+ when: inventory_hostname == "A"
+ handlers:
+ - name: handler1
+ include_tasks: 79776-handlers.yml
diff --git a/test/integration/targets/handlers/aliases b/test/integration/targets/handlers/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/handlers/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/handlers/from_handlers.yml b/test/integration/targets/handlers/from_handlers.yml
new file mode 100644
index 0000000..7b2dea2
--- /dev/null
+++ b/test/integration/targets/handlers/from_handlers.yml
@@ -0,0 +1,39 @@
+- name: verify handlers_from on include_role
+ hosts: A
+ gather_facts: False
+ tags: ['scenario1']
+ tasks:
+ - name: test include_role
+ include_role: name=test_handlers_meta handlers_from=alternate.yml
+
+ - name: force handler run
+ meta: flush_handlers
+
+ - name: verify handlers ran
+ assert:
+ that:
+ - "'handler1_alt_called' in hostvars[inventory_hostname]"
+ - "'handler2_alt_called' in hostvars[inventory_hostname]"
+ tags: ['scenario1']
+
+
+- name: verify handlers_from on import_role
+ hosts: A
+ gather_facts: False
+ tasks:
+ - name: set facts to false
+ set_fact:
+ handler1_alt_called: False
+ handler2_alt_called: False
+
+ - import_role: name=test_handlers_meta handlers_from=alternate.yml
+
+ - name: force handler run
+ meta: flush_handlers
+
+ - name: verify handlers ran
+ assert:
+ that:
+ - handler1_alt_called|bool
+ - handler2_alt_called|bool
+ tags: ['scenario1']
diff --git a/test/integration/targets/handlers/handlers.yml b/test/integration/targets/handlers/handlers.yml
new file mode 100644
index 0000000..aed75bd
--- /dev/null
+++ b/test/integration/targets/handlers/handlers.yml
@@ -0,0 +1,2 @@
+- name: test handler
+ debug: msg="handler called"
diff --git a/test/integration/targets/handlers/include_handlers_fail_force-handlers.yml b/test/integration/targets/handlers/include_handlers_fail_force-handlers.yml
new file mode 100644
index 0000000..8867b06
--- /dev/null
+++ b/test/integration/targets/handlers/include_handlers_fail_force-handlers.yml
@@ -0,0 +1,2 @@
+- debug:
+ msg: included handler ran
diff --git a/test/integration/targets/handlers/include_handlers_fail_force.yml b/test/integration/targets/handlers/include_handlers_fail_force.yml
new file mode 100644
index 0000000..f2289ba
--- /dev/null
+++ b/test/integration/targets/handlers/include_handlers_fail_force.yml
@@ -0,0 +1,11 @@
+- hosts: A
+ gather_facts: false
+ tasks:
+ - command: echo
+ notify:
+ - handler
+ - name: EXPECTED FAILURE
+ fail:
+ handlers:
+ - name: handler
+ include_tasks: include_handlers_fail_force-handlers.yml
diff --git a/test/integration/targets/handlers/inventory.handlers b/test/integration/targets/handlers/inventory.handlers
new file mode 100644
index 0000000..268cf65
--- /dev/null
+++ b/test/integration/targets/handlers/inventory.handlers
@@ -0,0 +1,10 @@
+[testgroup]
+A
+B
+C
+D
+E
+
+[testgroup:vars]
+ansible_connection=local
+ansible_python_interpreter="{{ ansible_playbook_python }}"
diff --git a/test/integration/targets/handlers/order.yml b/test/integration/targets/handlers/order.yml
new file mode 100644
index 0000000..8143ef7
--- /dev/null
+++ b/test/integration/targets/handlers/order.yml
@@ -0,0 +1,34 @@
+- name: Test handlers are executed in the order they are defined, not notified
+ hosts: localhost
+ gather_facts: false
+ tasks:
+ - set_fact:
+ foo: ''
+ changed_when: true
+ notify:
+ - handler4
+ - handler3
+ - handler1
+ - handler2
+ - handler5
+ - name: EXPECTED FAILURE
+ fail:
+ when: test_force_handlers | default(false) | bool
+ handlers:
+ - name: handler1
+ set_fact:
+ foo: "{{ foo ~ 1 }}"
+ - name: handler2
+ set_fact:
+ foo: "{{ foo ~ 2 }}"
+ - name: handler3
+ set_fact:
+ foo: "{{ foo ~ 3 }}"
+ - name: handler4
+ set_fact:
+ foo: "{{ foo ~ 4 }}"
+ - name: handler5
+ assert:
+ that:
+ - foo == '1234'
+ fail_msg: "{{ foo }}"
diff --git a/test/integration/targets/handlers/roles/import_template_handler_names/tasks/main.yml b/test/integration/targets/handlers/roles/import_template_handler_names/tasks/main.yml
new file mode 100644
index 0000000..3bc285e
--- /dev/null
+++ b/test/integration/targets/handlers/roles/import_template_handler_names/tasks/main.yml
@@ -0,0 +1,11 @@
+- import_role:
+ name: template_handler_names
+ tasks_from: lazy_evaluation
+ tags:
+ - lazy_evaluation
+
+- import_role:
+ name: template_handler_names
+ tasks_from: evaluation_time
+ tags:
+ - evaluation_time
diff --git a/test/integration/targets/handlers/roles/template_handler_names/handlers/main.yml b/test/integration/targets/handlers/roles/template_handler_names/handlers/main.yml
new file mode 100644
index 0000000..bf8ca85
--- /dev/null
+++ b/test/integration/targets/handlers/roles/template_handler_names/handlers/main.yml
@@ -0,0 +1,5 @@
+- name: handler name with {{ test_var }}
+ debug: msg='handler with var ran'
+
+- name: handler name
+ debug: msg='handler ran'
diff --git a/test/integration/targets/handlers/roles/template_handler_names/tasks/evaluation_time.yml b/test/integration/targets/handlers/roles/template_handler_names/tasks/evaluation_time.yml
new file mode 100644
index 0000000..c0706fc
--- /dev/null
+++ b/test/integration/targets/handlers/roles/template_handler_names/tasks/evaluation_time.yml
@@ -0,0 +1,5 @@
+- debug: msg='notify handler with variable in name'
+ notify: handler name with myvar
+ changed_when: True
+ tags:
+ - evaluation_time
diff --git a/test/integration/targets/handlers/roles/template_handler_names/tasks/lazy_evaluation.yml b/test/integration/targets/handlers/roles/template_handler_names/tasks/lazy_evaluation.yml
new file mode 100644
index 0000000..e82dca0
--- /dev/null
+++ b/test/integration/targets/handlers/roles/template_handler_names/tasks/lazy_evaluation.yml
@@ -0,0 +1,5 @@
+- debug: msg='notify handler'
+ notify: handler name
+ changed_when: True
+ tags:
+ - lazy_evaluation
diff --git a/test/integration/targets/handlers/roles/test_force_handlers/handlers/main.yml b/test/integration/targets/handlers/roles/test_force_handlers/handlers/main.yml
new file mode 100644
index 0000000..962d756
--- /dev/null
+++ b/test/integration/targets/handlers/roles/test_force_handlers/handlers/main.yml
@@ -0,0 +1,2 @@
+- name: echoing handler
+ command: echo CALLED_HANDLER_{{ inventory_hostname }}
diff --git a/test/integration/targets/handlers/roles/test_force_handlers/tasks/main.yml b/test/integration/targets/handlers/roles/test_force_handlers/tasks/main.yml
new file mode 100644
index 0000000..f5d78c7
--- /dev/null
+++ b/test/integration/targets/handlers/roles/test_force_handlers/tasks/main.yml
@@ -0,0 +1,26 @@
+---
+
+# We notify for A and B, and hosts B and C fail.
+# When forcing, we expect A and B to run handlers
+# When not forcing, we expect only B to run handlers
+
+- name: notify the handler for host A and B
+ shell: echo
+ notify:
+ - echoing handler
+ when: inventory_hostname == 'A' or inventory_hostname == 'B'
+
+- name: EXPECTED FAILURE fail task for all
+ fail: msg="Fail All"
+ when: fail_all is defined and fail_all
+
+- name: EXPECTED FAILURE fail task for A
+ fail: msg="Fail A"
+ when: inventory_hostname == 'A'
+
+- name: EXPECTED FAILURE fail task for C
+ fail: msg="Fail C"
+ when: inventory_hostname == 'C'
+
+- name: echo after A and C have failed
+ command: echo CALLED_TASK_{{ inventory_hostname }}
diff --git a/test/integration/targets/handlers/roles/test_handlers/handlers/main.yml b/test/integration/targets/handlers/roles/test_handlers/handlers/main.yml
new file mode 100644
index 0000000..0261f93
--- /dev/null
+++ b/test/integration/targets/handlers/roles/test_handlers/handlers/main.yml
@@ -0,0 +1,5 @@
+- name: set handler fact
+ set_fact:
+ handler_called: True
+- name: test handler
+ debug: msg="handler called"
diff --git a/test/integration/targets/handlers/roles/test_handlers/meta/main.yml b/test/integration/targets/handlers/roles/test_handlers/meta/main.yml
new file mode 100644
index 0000000..32cf5dd
--- /dev/null
+++ b/test/integration/targets/handlers/roles/test_handlers/meta/main.yml
@@ -0,0 +1 @@
+dependencies: []
diff --git a/test/integration/targets/handlers/roles/test_handlers/tasks/main.yml b/test/integration/targets/handlers/roles/test_handlers/tasks/main.yml
new file mode 100644
index 0000000..a857dac
--- /dev/null
+++ b/test/integration/targets/handlers/roles/test_handlers/tasks/main.yml
@@ -0,0 +1,52 @@
+# test code for the async keyword
+# (c) 2014, James Tanner <tanner.jc@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+
+- name: reset handler_called variable to false for all hosts
+ set_fact:
+ handler_called: False
+ tags: scenario1
+
+- name: notify the handler for host A only
+ shell: echo
+ notify:
+ - set handler fact
+ when: inventory_hostname == 'A'
+ tags: scenario1
+
+- name: force handler execution now
+ meta: "flush_handlers"
+ tags: scenario1
+
+- debug: var=handler_called
+ tags: scenario1
+
+- name: validate the handler only ran on one host
+ assert:
+ that:
+ - "inventory_hostname == 'A' and handler_called == True or handler_called == False"
+ tags: scenario1
+
+- name: 'test notify with loop'
+ debug: msg='a task'
+ changed_when: item == 1
+ notify: test handler
+ with_items:
+ - 1
+ - 2
+ tags: scenario2
diff --git a/test/integration/targets/handlers/roles/test_handlers_include/handlers/main.yml b/test/integration/targets/handlers/roles/test_handlers_include/handlers/main.yml
new file mode 100644
index 0000000..6c3b73c
--- /dev/null
+++ b/test/integration/targets/handlers/roles/test_handlers_include/handlers/main.yml
@@ -0,0 +1 @@
+- import_tasks: handlers.yml
diff --git a/test/integration/targets/handlers/roles/test_handlers_include/tasks/main.yml b/test/integration/targets/handlers/roles/test_handlers_include/tasks/main.yml
new file mode 100644
index 0000000..84f0a58
--- /dev/null
+++ b/test/integration/targets/handlers/roles/test_handlers_include/tasks/main.yml
@@ -0,0 +1,4 @@
+- name: 'main task'
+ debug: msg='main task'
+ changed_when: True
+ notify: test handler
diff --git a/test/integration/targets/handlers/roles/test_handlers_include_role/handlers/main.yml b/test/integration/targets/handlers/roles/test_handlers_include_role/handlers/main.yml
new file mode 100644
index 0000000..0261f93
--- /dev/null
+++ b/test/integration/targets/handlers/roles/test_handlers_include_role/handlers/main.yml
@@ -0,0 +1,5 @@
+- name: set handler fact
+ set_fact:
+ handler_called: True
+- name: test handler
+ debug: msg="handler called"
diff --git a/test/integration/targets/handlers/roles/test_handlers_include_role/meta/main.yml b/test/integration/targets/handlers/roles/test_handlers_include_role/meta/main.yml
new file mode 100644
index 0000000..32cf5dd
--- /dev/null
+++ b/test/integration/targets/handlers/roles/test_handlers_include_role/meta/main.yml
@@ -0,0 +1 @@
+dependencies: []
diff --git a/test/integration/targets/handlers/roles/test_handlers_include_role/tasks/main.yml b/test/integration/targets/handlers/roles/test_handlers_include_role/tasks/main.yml
new file mode 100644
index 0000000..fbc3d1c
--- /dev/null
+++ b/test/integration/targets/handlers/roles/test_handlers_include_role/tasks/main.yml
@@ -0,0 +1,47 @@
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+
+- name: reset handler_called variable to false for all hosts
+ set_fact:
+ handler_called: False
+ tags: scenario1
+
+- name: notify the handler for host A only
+ shell: echo
+ notify:
+ - set handler fact
+ when: inventory_hostname == 'A'
+ tags: scenario1
+
+- name: force handler execution now
+ meta: "flush_handlers"
+ tags: scenario1
+
+- debug: var=handler_called
+ tags: scenario1
+
+- name: validate the handler only ran on one host
+ assert:
+ that:
+ - "inventory_hostname == 'A' and handler_called == True or handler_called == False"
+ tags: scenario1
+
+# item below is passed in by the playbook that calls this
+- name: 'test notify with loop'
+ debug: msg='a task'
+ changed_when: item == 1
+ notify: test handler
+ tags: scenario2
diff --git a/test/integration/targets/handlers/roles/test_handlers_listen/handlers/main.yml b/test/integration/targets/handlers/roles/test_handlers_listen/handlers/main.yml
new file mode 100644
index 0000000..3bfd82a
--- /dev/null
+++ b/test/integration/targets/handlers/roles/test_handlers_listen/handlers/main.yml
@@ -0,0 +1,10 @@
+---
+- name: notify_listen_ran_4_3
+ set_fact:
+ notify_listen_ran_4_3: True
+ listen: notify_listen
+
+- name: notify_listen_in_role_4
+ set_fact:
+ notify_listen_in_role_4: True
+ listen: notify_listen_in_role
diff --git a/test/integration/targets/handlers/roles/test_handlers_listen/tasks/main.yml b/test/integration/targets/handlers/roles/test_handlers_listen/tasks/main.yml
new file mode 100644
index 0000000..bac9b71
--- /dev/null
+++ b/test/integration/targets/handlers/roles/test_handlers_listen/tasks/main.yml
@@ -0,0 +1,6 @@
+---
+- name: notify some handlers from a role
+ command: uptime
+ notify:
+ - notify_listen_from_role
+ - notify_listen_in_role
diff --git a/test/integration/targets/handlers/roles/test_handlers_meta/handlers/alternate.yml b/test/integration/targets/handlers/roles/test_handlers_meta/handlers/alternate.yml
new file mode 100644
index 0000000..9268ce5
--- /dev/null
+++ b/test/integration/targets/handlers/roles/test_handlers_meta/handlers/alternate.yml
@@ -0,0 +1,12 @@
+- name: set_handler_fact_1
+ set_fact:
+ handler1_called: True
+ handler1_alt_called: True
+
+- name: set_handler_fact_2
+ set_fact:
+ handler2_called: True
+ handler2_alt_called: True
+
+- name: count_handler
+ shell: echo . >> {{ handler_countpath }}
diff --git a/test/integration/targets/handlers/roles/test_handlers_meta/handlers/main.yml b/test/integration/targets/handlers/roles/test_handlers_meta/handlers/main.yml
new file mode 100644
index 0000000..0dd408b
--- /dev/null
+++ b/test/integration/targets/handlers/roles/test_handlers_meta/handlers/main.yml
@@ -0,0 +1,10 @@
+- name: set_handler_fact_1
+ set_fact:
+ handler1_called: True
+
+- name: set_handler_fact_2
+ set_fact:
+ handler2_called: True
+
+- name: count_handler
+ shell: echo . >> {{ handler_countpath }}
diff --git a/test/integration/targets/handlers/roles/test_handlers_meta/tasks/main.yml b/test/integration/targets/handlers/roles/test_handlers_meta/tasks/main.yml
new file mode 100644
index 0000000..d9f5c57
--- /dev/null
+++ b/test/integration/targets/handlers/roles/test_handlers_meta/tasks/main.yml
@@ -0,0 +1,75 @@
+# test code for the async keyword
+# (c) 2014, James Tanner <tanner.jc@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: notify the first handler
+ shell: echo
+ notify:
+ - set_handler_fact_1
+
+- name: force handler execution now
+ meta: "flush_handlers"
+
+- name: assert handler1 ran and not handler2
+ assert:
+ that:
+ - "handler1_called is defined"
+ - "handler2_called is not defined"
+
+- name: make a tempfile for counting
+ shell: mktemp
+ register: mktemp_out
+
+- name: register tempfile path
+ set_fact:
+ handler_countpath: "{{ mktemp_out.stdout }}"
+
+- name: notify the counting handler
+ shell: echo
+ notify:
+ - count_handler
+
+- name: notify the counting handler again
+ shell: echo
+ notify:
+ - count_handler
+
+- name: force handler execution now
+ meta: flush_handlers
+
+- name: get handler execution count
+ shell: cat {{ handler_countpath }} | grep -o . | wc -l
+ register: exec_count_out
+
+- debug: var=exec_count_out.stdout
+
+- name: ensure single execution
+ assert:
+ that:
+ - exec_count_out.stdout | int == 1
+
+- name: cleanup tempfile
+ file: path={{ handler_countpath }} state=absent
+
+- name: reset handler1_called
+ set_fact:
+ handler1_called: False
+
+- name: notify the second handler
+ shell: echo
+ notify:
+ - set_handler_fact_2
diff --git a/test/integration/targets/handlers/roles/test_role_handlers_include_tasks/handlers/A.yml b/test/integration/targets/handlers/roles/test_role_handlers_include_tasks/handlers/A.yml
new file mode 100644
index 0000000..b7520c7
--- /dev/null
+++ b/test/integration/targets/handlers/roles/test_role_handlers_include_tasks/handlers/A.yml
@@ -0,0 +1 @@
+- debug: msg="handler with tasks from A.yml called"
diff --git a/test/integration/targets/handlers/roles/test_role_handlers_include_tasks/handlers/main.yml b/test/integration/targets/handlers/roles/test_role_handlers_include_tasks/handlers/main.yml
new file mode 100644
index 0000000..ba2e8f6
--- /dev/null
+++ b/test/integration/targets/handlers/roles/test_role_handlers_include_tasks/handlers/main.yml
@@ -0,0 +1,5 @@
+- name: role-based handler from handler subdir
+ include_tasks: A.yml
+
+- name: role-based handler from tasks subdir
+ include_tasks: B.yml
diff --git a/test/integration/targets/handlers/roles/test_role_handlers_include_tasks/tasks/B.yml b/test/integration/targets/handlers/roles/test_role_handlers_include_tasks/tasks/B.yml
new file mode 100644
index 0000000..956c975
--- /dev/null
+++ b/test/integration/targets/handlers/roles/test_role_handlers_include_tasks/tasks/B.yml
@@ -0,0 +1 @@
+- debug: msg="handler with tasks from B.yml called"
diff --git a/test/integration/targets/handlers/roles/test_templating_in_handlers/handlers/main.yml b/test/integration/targets/handlers/roles/test_templating_in_handlers/handlers/main.yml
new file mode 100644
index 0000000..7dbf334
--- /dev/null
+++ b/test/integration/targets/handlers/roles/test_templating_in_handlers/handlers/main.yml
@@ -0,0 +1,21 @@
+---
+- name: name1
+ set_fact:
+ role_non_templated_name: True
+- name: "{{ handler2 }}"
+ set_fact:
+ role_templated_name: True
+- name: testlistener1
+ set_fact:
+ role_non_templated_listener: True
+ listen: name3
+- name: testlistener2
+ set_fact:
+ role_templated_listener: True
+ listen: "{{ handler4 }}"
+- name: name5
+ set_fact:
+ role_handler5: True
+- set_fact:
+ role_handler6: True
+ listen: name6
diff --git a/test/integration/targets/handlers/roles/test_templating_in_handlers/tasks/main.yml b/test/integration/targets/handlers/roles/test_templating_in_handlers/tasks/main.yml
new file mode 100644
index 0000000..5417417
--- /dev/null
+++ b/test/integration/targets/handlers/roles/test_templating_in_handlers/tasks/main.yml
@@ -0,0 +1,26 @@
+---
+- command: echo Hello World
+ notify:
+ - "{{ handler1 }}"
+ - "{{ handler2 }}"
+ - "{{ handler3 }}"
+ - "{{ handler4 }}"
+
+- meta: flush_handlers
+
+- assert:
+ that:
+ - role_non_templated_name is defined
+ - role_templated_name is defined
+ - role_non_templated_listener is defined
+ - role_templated_listener is undefined
+
+- command: echo
+ notify: "{{ handler_list }}"
+
+- meta: flush_handlers
+
+- assert:
+ that:
+ - role_handler5 is defined
+ - role_handler6 is defined
diff --git a/test/integration/targets/handlers/runme.sh b/test/integration/targets/handlers/runme.sh
new file mode 100755
index 0000000..76fc99d
--- /dev/null
+++ b/test/integration/targets/handlers/runme.sh
@@ -0,0 +1,183 @@
+#!/usr/bin/env bash
+
+set -eux
+
+export ANSIBLE_FORCE_HANDLERS
+
+ANSIBLE_FORCE_HANDLERS=false
+
+# simple handler test
+ansible-playbook test_handlers.yml -i inventory.handlers -v "$@" --tags scenario1
+
+# simple from_handlers test
+ansible-playbook from_handlers.yml -i inventory.handlers -v "$@" --tags scenario1
+
+ansible-playbook test_listening_handlers.yml -i inventory.handlers -v "$@"
+
+[ "$(ansible-playbook test_handlers.yml -i inventory.handlers -v "$@" --tags scenario2 -l A \
+| grep -E -o 'RUNNING HANDLER \[test_handlers : .*]')" = "RUNNING HANDLER [test_handlers : test handler]" ]
+
+# Test forcing handlers using the linear and free strategy
+for strategy in linear free; do
+
+ export ANSIBLE_STRATEGY=$strategy
+
+ # Not forcing, should only run on successful host
+ [ "$(ansible-playbook test_force_handlers.yml -i inventory.handlers -v "$@" --tags normal \
+ | grep -E -o CALLED_HANDLER_. | sort | uniq | xargs)" = "CALLED_HANDLER_B" ]
+
+ # Forcing from command line
+ [ "$(ansible-playbook test_force_handlers.yml -i inventory.handlers -v "$@" --tags normal --force-handlers \
+ | grep -E -o CALLED_HANDLER_. | sort | uniq | xargs)" = "CALLED_HANDLER_A CALLED_HANDLER_B" ]
+
+ # Forcing from command line, should only run later tasks on unfailed hosts
+ [ "$(ansible-playbook test_force_handlers.yml -i inventory.handlers -v "$@" --tags normal --force-handlers \
+ | grep -E -o CALLED_TASK_. | sort | uniq | xargs)" = "CALLED_TASK_B CALLED_TASK_D CALLED_TASK_E" ]
+
+ # Forcing from command line, should call handlers even if all hosts fail
+ [ "$(ansible-playbook test_force_handlers.yml -i inventory.handlers -v "$@" --tags normal --force-handlers -e fail_all=yes \
+ | grep -E -o CALLED_HANDLER_. | sort | uniq | xargs)" = "CALLED_HANDLER_A CALLED_HANDLER_B" ]
+
+ # Forcing from ansible.cfg
+ [ "$(ANSIBLE_FORCE_HANDLERS=true ansible-playbook test_force_handlers.yml -i inventory.handlers -v "$@" --tags normal \
+ | grep -E -o CALLED_HANDLER_. | sort | uniq | xargs)" = "CALLED_HANDLER_A CALLED_HANDLER_B" ]
+
+ # Forcing true in play
+ [ "$(ansible-playbook test_force_handlers.yml -i inventory.handlers -v "$@" --tags force_true_in_play \
+ | grep -E -o CALLED_HANDLER_. | sort | uniq | xargs)" = "CALLED_HANDLER_A CALLED_HANDLER_B" ]
+
+ # Forcing false in play, which overrides command line
+ [ "$(ansible-playbook test_force_handlers.yml -i inventory.handlers -v "$@" --tags force_false_in_play --force-handlers \
+ | grep -E -o CALLED_HANDLER_. | sort | uniq | xargs)" = "CALLED_HANDLER_B" ]
+
+ unset ANSIBLE_STRATEGY
+
+done
+
+[ "$(ansible-playbook test_handlers_include.yml -i ../../inventory -v "$@" --tags playbook_include_handlers \
+| grep -E -o 'RUNNING HANDLER \[.*]')" = "RUNNING HANDLER [test handler]" ]
+
+[ "$(ansible-playbook test_handlers_include.yml -i ../../inventory -v "$@" --tags role_include_handlers \
+| grep -E -o 'RUNNING HANDLER \[test_handlers_include : .*]')" = "RUNNING HANDLER [test_handlers_include : test handler]" ]
+
+[ "$(ansible-playbook test_handlers_include_role.yml -i ../../inventory -v "$@" \
+| grep -E -o 'RUNNING HANDLER \[test_handlers_include_role : .*]')" = "RUNNING HANDLER [test_handlers_include_role : test handler]" ]
+
+# Notify handler listen
+ansible-playbook test_handlers_listen.yml -i inventory.handlers -v "$@"
+
+# Notify inexistent handlers results in error
+set +e
+result="$(ansible-playbook test_handlers_inexistent_notify.yml -i inventory.handlers "$@" 2>&1)"
+set -e
+grep -q "ERROR! The requested handler 'notify_inexistent_handler' was not found in either the main handlers list nor in the listening handlers list" <<< "$result"
+
+# Notify inexistent handlers without errors when ANSIBLE_ERROR_ON_MISSING_HANDLER=false
+ANSIBLE_ERROR_ON_MISSING_HANDLER=false ansible-playbook test_handlers_inexistent_notify.yml -i inventory.handlers -v "$@"
+
+ANSIBLE_ERROR_ON_MISSING_HANDLER=false ansible-playbook test_templating_in_handlers.yml -v "$@"
+
+# https://github.com/ansible/ansible/issues/36649
+output_dir=/tmp
+set +e
+result="$(ansible-playbook test_handlers_any_errors_fatal.yml -e output_dir=$output_dir -i inventory.handlers -v "$@" 2>&1)"
+set -e
+[ ! -f $output_dir/should_not_exist_B ] || (rm -f $output_dir/should_not_exist_B && exit 1)
+
+# https://github.com/ansible/ansible/issues/47287
+[ "$(ansible-playbook test_handlers_including_task.yml -i ../../inventory -v "$@" | grep -E -o 'failed=[0-9]+')" = "failed=0" ]
+
+# https://github.com/ansible/ansible/issues/71222
+ansible-playbook test_role_handlers_including_tasks.yml -i ../../inventory -v "$@"
+
+# https://github.com/ansible/ansible/issues/27237
+set +e
+result="$(ansible-playbook test_handlers_template_run_once.yml -i inventory.handlers "$@" 2>&1)"
+set -e
+grep -q "handler A" <<< "$result"
+grep -q "handler B" <<< "$result"
+
+# Test an undefined variable in another handler name isn't a failure
+ansible-playbook 58841.yml "$@" --tags lazy_evaluation 2>&1 | tee out.txt ; cat out.txt
+grep out.txt -e "\[WARNING\]: Handler 'handler name with {{ test_var }}' is unusable"
+[ "$(grep out.txt -ce 'handler ran')" = "1" ]
+[ "$(grep out.txt -ce 'handler with var ran')" = "0" ]
+
+# Test templating a handler name with a defined variable
+ansible-playbook 58841.yml "$@" --tags evaluation_time -e test_var=myvar | tee out.txt ; cat out.txt
+[ "$(grep out.txt -ce 'handler ran')" = "0" ]
+[ "$(grep out.txt -ce 'handler with var ran')" = "1" ]
+
+# Test the handler is not found when the variable is undefined
+ansible-playbook 58841.yml "$@" --tags evaluation_time 2>&1 | tee out.txt ; cat out.txt
+grep out.txt -e "ERROR! The requested handler 'handler name with myvar' was not found"
+grep out.txt -e "\[WARNING\]: Handler 'handler name with {{ test_var }}' is unusable"
+[ "$(grep out.txt -ce 'handler ran')" = "0" ]
+[ "$(grep out.txt -ce 'handler with var ran')" = "0" ]
+
+# Test include_role and import_role cannot be used as handlers
+ansible-playbook test_role_as_handler.yml "$@" 2>&1 | tee out.txt
+grep out.txt -e "ERROR! Using 'include_role' as a handler is not supported."
+
+# Test notifying a handler from within include_tasks does not work anymore
+ansible-playbook test_notify_included.yml "$@" 2>&1 | tee out.txt
+[ "$(grep out.txt -ce 'I was included')" = "1" ]
+grep out.txt -e "ERROR! The requested handler 'handler_from_include' was not found in either the main handlers list nor in the listening handlers list"
+
+ansible-playbook test_handlers_meta.yml -i inventory.handlers -vv "$@" | tee out.txt
+[ "$(grep out.txt -ce 'RUNNING HANDLER \[noop_handler\]')" = "1" ]
+[ "$(grep out.txt -ce 'META: noop')" = "1" ]
+
+# https://github.com/ansible/ansible/issues/46447
+set +e
+test "$(ansible-playbook 46447.yml -i inventory.handlers -vv "$@" 2>&1 | grep -c 'SHOULD NOT GET HERE')"
+set -e
+
+# https://github.com/ansible/ansible/issues/52561
+ansible-playbook 52561.yml -i inventory.handlers "$@" 2>&1 | tee out.txt
+[ "$(grep out.txt -ce 'handler1 ran')" = "1" ]
+
+# Test flush_handlers meta task does not imply any_errors_fatal
+ansible-playbook 54991.yml -i inventory.handlers "$@" 2>&1 | tee out.txt
+[ "$(grep out.txt -ce 'handler ran')" = "4" ]
+
+ansible-playbook order.yml -i inventory.handlers "$@" 2>&1
+set +e
+ansible-playbook order.yml --force-handlers -e test_force_handlers=true -i inventory.handlers "$@" 2>&1
+set -e
+
+ansible-playbook include_handlers_fail_force.yml --force-handlers -i inventory.handlers "$@" 2>&1 | tee out.txt
+[ "$(grep out.txt -ce 'included handler ran')" = "1" ]
+
+ansible-playbook test_flush_handlers_as_handler.yml -i inventory.handlers "$@" 2>&1 | tee out.txt
+grep out.txt -e "ERROR! flush_handlers cannot be used as a handler"
+
+ansible-playbook test_skip_flush.yml -i inventory.handlers "$@" 2>&1 | tee out.txt
+[ "$(grep out.txt -ce 'handler ran')" = "0" ]
+
+ansible-playbook test_flush_in_rescue_always.yml -i inventory.handlers "$@" 2>&1 | tee out.txt
+[ "$(grep out.txt -ce 'handler ran in rescue')" = "1" ]
+[ "$(grep out.txt -ce 'handler ran in always')" = "2" ]
+[ "$(grep out.txt -ce 'lockstep works')" = "2" ]
+
+ansible-playbook test_handlers_infinite_loop.yml -i inventory.handlers "$@" 2>&1
+
+ansible-playbook test_flush_handlers_rescue_always.yml -i inventory.handlers "$@" 2>&1 | tee out.txt
+[ "$(grep out.txt -ce 'rescue ran')" = "1" ]
+[ "$(grep out.txt -ce 'always ran')" = "2" ]
+[ "$(grep out.txt -ce 'should run for both hosts')" = "2" ]
+
+ansible-playbook test_fqcn_meta_flush_handlers.yml -i inventory.handlers "$@" 2>&1 | tee out.txt
+grep out.txt -e "handler ran"
+grep out.txt -e "after flush"
+
+ansible-playbook 79776.yml -i inventory.handlers "$@"
+
+ansible-playbook test_block_as_handler.yml "$@" 2>&1 | tee out.txt
+grep out.txt -e "ERROR! Using a block as a handler is not supported."
+
+ansible-playbook test_block_as_handler-include.yml "$@" 2>&1 | tee out.txt
+grep out.txt -e "ERROR! Using a block as a handler is not supported."
+
+ansible-playbook test_block_as_handler-import.yml "$@" 2>&1 | tee out.txt
+grep out.txt -e "ERROR! Using a block as a handler is not supported."
diff --git a/test/integration/targets/handlers/test_block_as_handler-import.yml b/test/integration/targets/handlers/test_block_as_handler-import.yml
new file mode 100644
index 0000000..ad6bb0d
--- /dev/null
+++ b/test/integration/targets/handlers/test_block_as_handler-import.yml
@@ -0,0 +1,7 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - debug:
+ handlers:
+ - name: handler
+ import_tasks: test_block_as_handler-include_import-handlers.yml
diff --git a/test/integration/targets/handlers/test_block_as_handler-include.yml b/test/integration/targets/handlers/test_block_as_handler-include.yml
new file mode 100644
index 0000000..5b03b0a
--- /dev/null
+++ b/test/integration/targets/handlers/test_block_as_handler-include.yml
@@ -0,0 +1,8 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - command: echo
+ notify: handler
+ handlers:
+ - name: handler
+ include_tasks: test_block_as_handler-include_import-handlers.yml
diff --git a/test/integration/targets/handlers/test_block_as_handler-include_import-handlers.yml b/test/integration/targets/handlers/test_block_as_handler-include_import-handlers.yml
new file mode 100644
index 0000000..61c058b
--- /dev/null
+++ b/test/integration/targets/handlers/test_block_as_handler-include_import-handlers.yml
@@ -0,0 +1,8 @@
+- name: handler
+ block:
+ - name: due to how handlers are implemented, this is correct as it is equivalent to an implicit block
+ debug:
+ - name: this is a parser error, blocks as handlers are not supported
+ block:
+ - name: handler in a nested block
+ debug:
diff --git a/test/integration/targets/handlers/test_block_as_handler.yml b/test/integration/targets/handlers/test_block_as_handler.yml
new file mode 100644
index 0000000..bd4f5b9
--- /dev/null
+++ b/test/integration/targets/handlers/test_block_as_handler.yml
@@ -0,0 +1,13 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - debug:
+ handlers:
+ - name: handler
+ block:
+ - name: due to how handlers are implemented, this is correct as it is equivalent to an implicit block
+ debug:
+ - name: this is a parser error, blocks as handlers are not supported
+ block:
+ - name: handler in a nested block
+ debug:
diff --git a/test/integration/targets/handlers/test_flush_handlers_as_handler.yml b/test/integration/targets/handlers/test_flush_handlers_as_handler.yml
new file mode 100644
index 0000000..6d19408
--- /dev/null
+++ b/test/integration/targets/handlers/test_flush_handlers_as_handler.yml
@@ -0,0 +1,9 @@
+- hosts: A
+ gather_facts: false
+ tasks:
+ - command: echo
+ notify:
+ - handler
+ handlers:
+ - name: handler
+ meta: flush_handlers
diff --git a/test/integration/targets/handlers/test_flush_handlers_rescue_always.yml b/test/integration/targets/handlers/test_flush_handlers_rescue_always.yml
new file mode 100644
index 0000000..4a1f741
--- /dev/null
+++ b/test/integration/targets/handlers/test_flush_handlers_rescue_always.yml
@@ -0,0 +1,22 @@
+- hosts: A,B
+ gather_facts: false
+ tasks:
+ - block:
+ - command: echo
+ notify: sometimes_fail
+
+ - meta: flush_handlers
+ rescue:
+ - debug:
+ msg: 'rescue ran'
+ always:
+ - debug:
+ msg: 'always ran'
+
+ - debug:
+ msg: 'should run for both hosts'
+
+ handlers:
+ - name: sometimes_fail
+ fail:
+ when: inventory_hostname == 'A'
diff --git a/test/integration/targets/handlers/test_flush_in_rescue_always.yml b/test/integration/targets/handlers/test_flush_in_rescue_always.yml
new file mode 100644
index 0000000..7257a42
--- /dev/null
+++ b/test/integration/targets/handlers/test_flush_in_rescue_always.yml
@@ -0,0 +1,35 @@
+- hosts: A,B
+ gather_facts: false
+ tasks:
+ - block:
+ - name: EXPECTED_FAILURE
+ fail:
+ when: inventory_hostname == 'A'
+ rescue:
+ - command: echo
+ notify: handler_rescue
+
+ - meta: flush_handlers
+
+ - set_fact:
+ was_in_rescue: true
+
+ - name: EXPECTED_FAILURE
+ fail:
+ always:
+ - assert:
+ that:
+ - hostvars['A']['was_in_rescue']|default(false)
+ success_msg: lockstep works
+
+ - command: echo
+ notify: handler_always
+
+ - meta: flush_handlers
+ handlers:
+ - name: handler_rescue
+ debug:
+ msg: handler ran in rescue
+ - name: handler_always
+ debug:
+ msg: handler ran in always
diff --git a/test/integration/targets/handlers/test_force_handlers.yml b/test/integration/targets/handlers/test_force_handlers.yml
new file mode 100644
index 0000000..9cff772
--- /dev/null
+++ b/test/integration/targets/handlers/test_force_handlers.yml
@@ -0,0 +1,27 @@
+---
+
+- name: test force handlers (default)
+ tags: normal
+ hosts: testgroup
+ gather_facts: False
+ roles:
+ - { role: test_force_handlers }
+ tasks:
+ - debug: msg="you should see this with --tags=normal"
+
+- name: test force handlers (set to true)
+ tags: force_true_in_play
+ hosts: testgroup
+ gather_facts: False
+ force_handlers: True
+ roles:
+ - { role: test_force_handlers, tags: force_true_in_play }
+
+
+- name: test force handlers (set to false)
+ tags: force_false_in_play
+ hosts: testgroup
+ gather_facts: False
+ force_handlers: False
+ roles:
+ - { role: test_force_handlers, tags: force_false_in_play }
diff --git a/test/integration/targets/handlers/test_fqcn_meta_flush_handlers.yml b/test/integration/targets/handlers/test_fqcn_meta_flush_handlers.yml
new file mode 100644
index 0000000..f9c67cf
--- /dev/null
+++ b/test/integration/targets/handlers/test_fqcn_meta_flush_handlers.yml
@@ -0,0 +1,14 @@
+- hosts: A
+ gather_facts: false
+ tasks:
+ - command: echo
+ notify: handler
+
+ - ansible.builtin.meta: flush_handlers
+
+ - debug:
+ msg: after flush
+ handlers:
+ - name: handler
+ debug:
+ msg: handler ran
diff --git a/test/integration/targets/handlers/test_handlers.yml b/test/integration/targets/handlers/test_handlers.yml
new file mode 100644
index 0000000..ae9847b
--- /dev/null
+++ b/test/integration/targets/handlers/test_handlers.yml
@@ -0,0 +1,47 @@
+---
+- name: run handlers
+ hosts: A
+ gather_facts: False
+ roles:
+ - { role: test_handlers_meta, tags: ['scenario1'] }
+
+- name: verify final handler was run
+ hosts: A
+ gather_facts: False
+ tasks:
+ - name: verify handler2 ran
+ assert:
+ that:
+ - "not hostvars[inventory_hostname]['handler1_called']"
+ - "'handler2_called' in hostvars[inventory_hostname]"
+ tags: ['scenario1']
+
+- name: verify listening handlers
+ hosts: A
+ gather_facts: False
+ tasks:
+ - name: notify some handlers
+ command: echo foo
+ notify:
+ - notify_listen
+ post_tasks:
+ - name: assert all defined handlers ran without error
+ assert:
+ that:
+ - "notify_listen_ran_1 is defined"
+ - "notify_listen_ran_2 is defined"
+ handlers:
+ - name: first listening handler has a name
+ set_fact:
+ notify_listen_ran_1: True
+ listen: notify_listen
+ # second listening handler does not
+ - set_fact:
+ notify_listen_ran_2: True
+ listen: notify_listen
+
+- name: test handlers
+ hosts: testgroup
+ gather_facts: False
+ roles:
+ - { role: test_handlers }
diff --git a/test/integration/targets/handlers/test_handlers_any_errors_fatal.yml b/test/integration/targets/handlers/test_handlers_any_errors_fatal.yml
new file mode 100644
index 0000000..6b791a3
--- /dev/null
+++ b/test/integration/targets/handlers/test_handlers_any_errors_fatal.yml
@@ -0,0 +1,24 @@
+- hosts:
+ - A
+ - B
+ gather_facts: no
+ any_errors_fatal: yes
+ vars:
+ output_dir: /tmp
+ tasks:
+ - name: Task one
+ debug:
+ msg: 'task 1'
+ changed_when: yes
+ notify: EXPECTED FAILURE failed_handler
+
+ - meta: flush_handlers
+
+ - name: This task should never happen
+ file:
+ path: "{{ output_dir }}/should_not_exist_{{ inventory_hostname }}"
+ state: touch
+ handlers:
+ - name: EXPECTED FAILURE failed_handler
+ fail:
+ when: 'inventory_hostname == "A"'
diff --git a/test/integration/targets/handlers/test_handlers_include.yml b/test/integration/targets/handlers/test_handlers_include.yml
new file mode 100644
index 0000000..158266d
--- /dev/null
+++ b/test/integration/targets/handlers/test_handlers_include.yml
@@ -0,0 +1,14 @@
+- name: verify that play can include handler
+ hosts: testhost
+ tasks:
+ - debug: msg="main task"
+ changed_when: True
+ notify: test handler
+ tags: ['playbook_include_handlers']
+ handlers:
+ - import_tasks: handlers.yml
+
+- name: verify that role can include handler
+ hosts: testhost
+ roles:
+ - { role: test_handlers_include, tags: ['role_include_handlers'] }
diff --git a/test/integration/targets/handlers/test_handlers_include_role.yml b/test/integration/targets/handlers/test_handlers_include_role.yml
new file mode 100644
index 0000000..77e6b53
--- /dev/null
+++ b/test/integration/targets/handlers/test_handlers_include_role.yml
@@ -0,0 +1,8 @@
+- name: verify that play can include handler
+ hosts: testhost
+ tasks:
+ - include_role:
+ name: test_handlers_include_role
+ with_items:
+ - 1
+ - 2
diff --git a/test/integration/targets/handlers/test_handlers_including_task.yml b/test/integration/targets/handlers/test_handlers_including_task.yml
new file mode 100644
index 0000000..8f7933a
--- /dev/null
+++ b/test/integration/targets/handlers/test_handlers_including_task.yml
@@ -0,0 +1,16 @@
+---
+- name: Verify handler can include other tasks (#47287)
+ hosts: testhost
+ tasks:
+ - name: include a task from the tasks section
+ include_tasks: handlers.yml
+
+ - name: notify a handler
+ debug:
+ msg: notifying handler
+ changed_when: yes
+ notify: include a task from the handlers section
+
+ handlers:
+ - name: include a task from the handlers section
+ include_tasks: handlers.yml
diff --git a/test/integration/targets/handlers/test_handlers_inexistent_notify.yml b/test/integration/targets/handlers/test_handlers_inexistent_notify.yml
new file mode 100644
index 0000000..15de38a
--- /dev/null
+++ b/test/integration/targets/handlers/test_handlers_inexistent_notify.yml
@@ -0,0 +1,10 @@
+---
+- name: notify inexistent handler
+ hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: test notify an inexistent handler
+ command: uptime
+ notify:
+ - notify_inexistent_handler
+ register: result
diff --git a/test/integration/targets/handlers/test_handlers_infinite_loop.yml b/test/integration/targets/handlers/test_handlers_infinite_loop.yml
new file mode 100644
index 0000000..413b492
--- /dev/null
+++ b/test/integration/targets/handlers/test_handlers_infinite_loop.yml
@@ -0,0 +1,25 @@
+- hosts: A
+ gather_facts: false
+ tasks:
+ - command: echo
+ notify:
+ - handler1
+ - self_notify
+ handlers:
+ - name: handler1
+ debug:
+ msg: handler1 ran
+ changed_when: true
+ notify: handler2
+
+ - name: handler2
+ debug:
+ msg: handler2 ran
+ changed_when: true
+ notify: handler1
+
+ - name: self_notify
+ debug:
+ msg: self_notify ran
+ changed_when: true
+ notify: self_notify
diff --git a/test/integration/targets/handlers/test_handlers_listen.yml b/test/integration/targets/handlers/test_handlers_listen.yml
new file mode 100644
index 0000000..dd2cd87
--- /dev/null
+++ b/test/integration/targets/handlers/test_handlers_listen.yml
@@ -0,0 +1,128 @@
+---
+- name: test listen with named handlers
+ hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: test notify handlers listen
+ command: uptime
+ notify:
+ - notify_listen
+ - meta: flush_handlers
+ - name: verify test notify handlers listen
+ assert:
+ that:
+ - "notify_listen_ran_1_1 is defined"
+ - "notify_listen_ran_1_2 is defined"
+ - "notify_listen_ran_1_3 is undefined"
+ handlers:
+ - name: notify_handler_ran_1_1
+ set_fact:
+ notify_listen_ran_1_1: True
+ listen: notify_listen
+ - name: notify_handler_ran_1_2
+ set_fact:
+ notify_listen_ran_1_2: True
+ listen: notify_listen
+ - name: notify_handler_ran_1_3
+ set_fact:
+ notify_handler_ran_1_3: True
+ listen: notify_listen2
+
+- name: test listen unnamed handlers
+ hosts: localhost
+ gather_facts: false
+ pre_tasks:
+ - name: notify some handlers
+ command: echo foo
+ notify:
+ - notify_listen
+ tasks:
+ - meta: flush_handlers
+ - name: assert all defined handlers ran without error
+ assert:
+ that:
+ - "notify_listen_ran_1 is defined"
+ - "notify_listen_ran_2 is defined"
+ - "notify_listen_ran_3 is undefined"
+ handlers:
+ - set_fact:
+ notify_listen_ran_1: True
+ listen: notify_listen
+ - set_fact:
+ notify_listen_ran_2: True
+ listen: notify_listen
+ - set_fact:
+ notify_handler_ran_3: True
+ listen: notify_listen2
+
+- name: test with mixed notify by name and listen
+ hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: test notify handlers names and identical listen
+ command: uptime
+ notify:
+ - notify_listen
+ - meta: flush_handlers
+ - name: verify test notify handlers names and identical listen
+ assert:
+ that:
+ - "notify_handler_name_ran_3 is defined"
+ - "notify_handler_name_ran_3_1 is not defined"
+ - "notify_listen_ran_3_2 is defined"
+ - "notify_listen_ran_3_3 is defined"
+ - "not_notify_listen_3_4 is not defined"
+ handlers:
+ - name: notify_listen
+ set_fact:
+ notify_handler_name_ran_3: True
+ # this will not run as we have a handler with a identical name notified first
+ - name: notify_listen
+ set_fact:
+ notify_handler_name_ran_3_1: True
+ - name: notify_handler_ran_3_2
+ set_fact:
+ notify_listen_ran_3_2: True
+ listen: notify_listen
+ - name: notify_handler_ran_3_3
+ set_fact:
+ notify_listen_ran_3_3: True
+ listen: notify_listen
+ # this one is not notified
+ - name: not_notify_listen_3_4
+ set_fact:
+ not_notify_listen_3_4: True
+ listen: not_notified
+
+- name: test listen in roles
+ hosts: localhost
+ gather_facts: false
+ roles:
+ - role: test_handlers_listen
+ tasks:
+ - name: test notify handlers listen in roles
+ command: uptime
+ notify:
+ - notify_listen
+ - meta: flush_handlers
+ - name: verify test notify handlers listen in roles
+ assert:
+ that:
+ - "notify_listen_ran_4_1 is defined"
+ - "notify_listen_ran_4_2 is defined"
+ - "notify_listen_ran_4_3 is defined"
+ - "notify_listen_in_role_4 is defined"
+ - "notify_listen_from_role_4 is defined"
+ handlers:
+ - name: notify_listen_ran_4_1
+ set_fact:
+ notify_listen_ran_4_1: True
+ listen: notify_listen
+ - name: notify_listen_ran_4_2
+ set_fact:
+ notify_listen_ran_4_2: True
+ listen: notify_listen
+ - name: notify_listen_from_role_4
+ set_fact:
+ notify_listen_from_role_4: True
+ listen: notify_listen_from_role
diff --git a/test/integration/targets/handlers/test_handlers_meta.yml b/test/integration/targets/handlers/test_handlers_meta.yml
new file mode 100644
index 0000000..636513a
--- /dev/null
+++ b/test/integration/targets/handlers/test_handlers_meta.yml
@@ -0,0 +1,9 @@
+- hosts: A
+ gather_facts: false
+ tasks:
+ - command: echo
+ notify:
+ - noop_handler
+ handlers:
+ - name: noop_handler
+ meta: noop
diff --git a/test/integration/targets/handlers/test_handlers_template_run_once.yml b/test/integration/targets/handlers/test_handlers_template_run_once.yml
new file mode 100644
index 0000000..6edc32e
--- /dev/null
+++ b/test/integration/targets/handlers/test_handlers_template_run_once.yml
@@ -0,0 +1,12 @@
+- hosts: A,B
+ gather_facts: no
+ tasks:
+ - debug:
+ changed_when: true
+ notify:
+ - handler
+ handlers:
+ - name: handler
+ debug:
+ msg: "handler {{ inventory_hostname }}"
+ run_once: "{{ testvar | default(False) }}"
diff --git a/test/integration/targets/handlers/test_listening_handlers.yml b/test/integration/targets/handlers/test_listening_handlers.yml
new file mode 100644
index 0000000..f4c3cae
--- /dev/null
+++ b/test/integration/targets/handlers/test_listening_handlers.yml
@@ -0,0 +1,39 @@
+---
+- name: verify listening handlers
+ hosts: A
+ gather_facts: False
+ tasks:
+ - name: notify some handlers
+ command: echo foo
+ notify:
+ - notify_listen
+
+ - name: notify another handler
+ debug:
+ changed_when: true
+ notify: another_listen
+
+ - name: nofity another handler 2
+ debug:
+ changed_when: true
+ notify: another_listen
+ post_tasks:
+ - name: assert all defined handlers ran without error
+ assert:
+ that:
+ - "notify_listen_ran_1 is defined"
+ - "notify_listen_ran_2 is defined"
+ - "another_listen_ran is true"
+ handlers:
+ - name: first listening handler has a name
+ set_fact:
+ notify_listen_ran_1: True
+ listen: notify_listen
+ # second listening handler does not
+ - set_fact:
+ notify_listen_ran_2: True
+ listen: notify_listen
+
+ - set_fact:
+ another_listen_ran: '{{ False if another_listen_ran is defined else True }}'
+ listen: another_listen
diff --git a/test/integration/targets/handlers/test_notify_included-handlers.yml b/test/integration/targets/handlers/test_notify_included-handlers.yml
new file mode 100644
index 0000000..61fce70
--- /dev/null
+++ b/test/integration/targets/handlers/test_notify_included-handlers.yml
@@ -0,0 +1,3 @@
+- name: handler_from_include
+ debug:
+ msg: I was included
diff --git a/test/integration/targets/handlers/test_notify_included.yml b/test/integration/targets/handlers/test_notify_included.yml
new file mode 100644
index 0000000..e612507
--- /dev/null
+++ b/test/integration/targets/handlers/test_notify_included.yml
@@ -0,0 +1,16 @@
+- name: This worked unintentionally, make sure it does not anymore
+ hosts: localhost
+ gather_facts: false
+ tasks:
+ - command: echo
+ notify:
+ - include_handler
+
+ - meta: flush_handlers
+
+ - command: echo
+ notify:
+ - handler_from_include
+ handlers:
+ - name: include_handler
+ include_tasks: test_notify_included-handlers.yml
diff --git a/test/integration/targets/handlers/test_role_as_handler.yml b/test/integration/targets/handlers/test_role_as_handler.yml
new file mode 100644
index 0000000..ae67427
--- /dev/null
+++ b/test/integration/targets/handlers/test_role_as_handler.yml
@@ -0,0 +1,11 @@
+- name: test include_role and import_role cannot be used as handlers
+ hosts: localhost
+ gather_facts: false
+ tasks:
+ - command: "echo"
+ notify:
+ - role_handler
+ handlers:
+ - name: role_handler
+ include_role:
+ name: doesnotmatter_fails_at_parse_time
diff --git a/test/integration/targets/handlers/test_role_handlers_including_tasks.yml b/test/integration/targets/handlers/test_role_handlers_including_tasks.yml
new file mode 100644
index 0000000..47d8f00
--- /dev/null
+++ b/test/integration/targets/handlers/test_role_handlers_including_tasks.yml
@@ -0,0 +1,18 @@
+---
+- name: Verify a role handler can include other tasks from handlers and tasks subdirs
+ hosts: testhost
+ roles:
+ - test_role_handlers_include_tasks
+
+ tasks:
+ - name: notify a role-based handler (include tasks from handler subdir)
+ debug:
+ msg: notifying role handler
+ changed_when: yes
+ notify: role-based handler from handler subdir
+
+ - name: notify a role-based handler (include tasks from tasks subdir)
+ debug:
+ msg: notifying another role handler
+ changed_when: yes
+ notify: role-based handler from tasks subdir
diff --git a/test/integration/targets/handlers/test_skip_flush.yml b/test/integration/targets/handlers/test_skip_flush.yml
new file mode 100644
index 0000000..5c1e82b
--- /dev/null
+++ b/test/integration/targets/handlers/test_skip_flush.yml
@@ -0,0 +1,13 @@
+- hosts: A
+ gather_facts: false
+ tasks:
+ - command: echo
+ notify:
+ - handler
+ - meta: flush_handlers
+ when: false
+ - fail:
+ handlers:
+ - name: handler
+ debug:
+ msg: handler ran
diff --git a/test/integration/targets/handlers/test_templating_in_handlers.yml b/test/integration/targets/handlers/test_templating_in_handlers.yml
new file mode 100644
index 0000000..662b8c1
--- /dev/null
+++ b/test/integration/targets/handlers/test_templating_in_handlers.yml
@@ -0,0 +1,62 @@
+- name: test templated values in handlers
+ hosts: localhost
+ gather_facts: no
+ vars:
+ handler1: name1
+ handler2: name2
+ handler3: name3
+ handler4: name4
+ handler_list:
+ - name5
+ - name6
+
+ handlers:
+ - name: name1
+ set_fact:
+ non_templated_name: True
+ - name: "{{ handler2 }}"
+ set_fact:
+ templated_name: True
+ - name: testlistener1
+ set_fact:
+ non_templated_listener: True
+ listen: name3
+ - name: testlistener2
+ set_fact:
+ templated_listener: True
+ listen: "{{ handler4 }}"
+ - name: name5
+ set_fact:
+ handler5: True
+ - set_fact:
+ handler6: True
+ listen: name6
+
+ tasks:
+ - command: echo Hello World
+ notify:
+ - "{{ handler1 }}"
+ - "{{ handler2 }}"
+ - "{{ handler3 }}"
+ - "{{ handler4 }}"
+
+ - meta: flush_handlers
+
+ - assert:
+ that:
+ - non_templated_name is defined
+ - templated_name is defined
+ - non_templated_listener is defined
+ - templated_listener is undefined
+
+ - command: echo
+ notify: "{{ handler_list }}"
+
+ - meta: flush_handlers
+
+ - assert:
+ that:
+ - handler5 is defined
+ - handler6 is defined
+
+ - include_role: name=test_templating_in_handlers
diff --git a/test/integration/targets/hardware_facts/aliases b/test/integration/targets/hardware_facts/aliases
new file mode 100644
index 0000000..a08519b
--- /dev/null
+++ b/test/integration/targets/hardware_facts/aliases
@@ -0,0 +1,4 @@
+destructive
+needs/privileged
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/hardware_facts/meta/main.yml b/test/integration/targets/hardware_facts/meta/main.yml
new file mode 100644
index 0000000..1810d4b
--- /dev/null
+++ b/test/integration/targets/hardware_facts/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/hardware_facts/tasks/Linux.yml b/test/integration/targets/hardware_facts/tasks/Linux.yml
new file mode 100644
index 0000000..885aa0e
--- /dev/null
+++ b/test/integration/targets/hardware_facts/tasks/Linux.yml
@@ -0,0 +1,92 @@
+- name: Test LVM facts
+ block:
+ - name: Install lvm2
+ package:
+ name: lvm2
+ state: present
+ register: lvm_pkg
+
+ - name: Create files to use as a disk devices
+ command: "dd if=/dev/zero of={{ remote_tmp_dir }}/img{{ item }} bs=1M count=10"
+ with_sequence: 'count=2'
+
+ - name: Create loop device for file
+ command: "losetup --show -f {{ remote_tmp_dir }}/img{{ item }}"
+ with_sequence: 'count=2'
+ register: loop_devices
+
+ - name: Get loop names
+ set_fact:
+ loop_device1: "{{ loop_devices.results[0].stdout }}"
+ loop_device2: "{{ loop_devices.results[1].stdout }}"
+
+ - name: Create pvs
+ command: pvcreate {{ item }}
+ with_items:
+ - "{{ loop_device1 }}"
+ - "{{ loop_device2 }}"
+
+ - name: Create vg
+ command: vgcreate first {{ loop_device1 }}
+
+ - name: Create another vg
+ command: vgcreate second {{ loop_device2 }}
+
+ - name: Create lv
+ command: lvcreate -L 4M first --name one
+
+ - name: Create another lv
+ command: lvcreate -L 4M first --name two
+
+ - name: Create yet another lv
+ command: lvcreate -L 4M second --name uno
+
+ - name: Gather facts
+ setup:
+
+ - assert:
+ that:
+ - ansible_lvm.pvs[loop_device1].vg == 'first'
+ - ansible_lvm.pvs[loop_device2].vg == 'second'
+ - ansible_lvm.lvs.one.vg == 'first'
+ - ansible_lvm.lvs.two.vg == 'first'
+ - ansible_lvm.lvs.uno.vg == 'second'
+ - ansible_lvm.vgs.first.num_lvs == "2"
+ - ansible_lvm.vgs.second.num_lvs == "1"
+
+ always:
+ - name: remove lvs
+ shell: "lvremove /dev/{{ item }}/* -f"
+ with_items:
+ - first
+ - second
+
+ - name: remove vgs
+ command: "vgremove {{ item }}"
+ with_items:
+ - first
+ - second
+
+ - name: remove pvs
+ command: "pvremove {{ item }}"
+ with_items:
+ - "{{ loop_device1 }}"
+ - "{{ loop_device2 }}"
+
+ - name: Detach loop device
+ command: "losetup -d {{ item }}"
+ with_items:
+ - "{{ loop_device1 }}"
+ - "{{ loop_device2 }}"
+
+ - name: Remove device files
+ file:
+ path: "{{ remote_tmp_dir }}/img{{ item }}"
+ state: absent
+ with_sequence: 'count={{ loop_devices.results|length }}'
+
+ - name: Remove lvm-tools
+ package:
+ name: lvm2
+ state: absent
+ when: lvm_pkg is changed
diff --git a/test/integration/targets/hardware_facts/tasks/main.yml b/test/integration/targets/hardware_facts/tasks/main.yml
new file mode 100644
index 0000000..e7059c6
--- /dev/null
+++ b/test/integration/targets/hardware_facts/tasks/main.yml
@@ -0,0 +1,2 @@
+- include_tasks: Linux.yml
+ when: ansible_system == 'Linux'
diff --git a/test/integration/targets/hash/aliases b/test/integration/targets/hash/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/hash/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/hash/group_vars/all b/test/integration/targets/hash/group_vars/all
new file mode 100644
index 0000000..805ac26
--- /dev/null
+++ b/test/integration/targets/hash/group_vars/all
@@ -0,0 +1,3 @@
+# variables used for hash merging behavior testing
+test_hash:
+ group_vars_all: "this is in group_vars/all"
diff --git a/test/integration/targets/hash/host_vars/testhost b/test/integration/targets/hash/host_vars/testhost
new file mode 100644
index 0000000..3a75ee6
--- /dev/null
+++ b/test/integration/targets/hash/host_vars/testhost
@@ -0,0 +1,2 @@
+test_hash:
+ host_vars_testhost: "this is in host_vars/testhost"
diff --git a/test/integration/targets/hash/roles/test_hash_behaviour/defaults/main.yml b/test/integration/targets/hash/roles/test_hash_behaviour/defaults/main.yml
new file mode 100644
index 0000000..10cc09f
--- /dev/null
+++ b/test/integration/targets/hash/roles/test_hash_behaviour/defaults/main.yml
@@ -0,0 +1,21 @@
+# test code for the hash variable behavior
+# (c) 2014, James Cammarata <jcammarata@ansible.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+---
+test_hash:
+ default_vars: "this is in role defaults/main.yml"
diff --git a/test/integration/targets/hash/roles/test_hash_behaviour/meta/main.yml b/test/integration/targets/hash/roles/test_hash_behaviour/meta/main.yml
new file mode 100644
index 0000000..59adf99
--- /dev/null
+++ b/test/integration/targets/hash/roles/test_hash_behaviour/meta/main.yml
@@ -0,0 +1,17 @@
+# test code for the hash variable behavior
+# (c) 2014, James Cammarata <jcammarata@ansible.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
diff --git a/test/integration/targets/hash/roles/test_hash_behaviour/tasks/main.yml b/test/integration/targets/hash/roles/test_hash_behaviour/tasks/main.yml
new file mode 100644
index 0000000..bc63549
--- /dev/null
+++ b/test/integration/targets/hash/roles/test_hash_behaviour/tasks/main.yml
@@ -0,0 +1,37 @@
+# test code for the hash variable behaviour
+# (c) 2014, James Cammarata <jcammarata@ansible.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: debug hash behaviour result
+ debug:
+ var: "{{ lookup('env', 'ANSIBLE_HASH_BEHAVIOUR') }}"
+ verbosity: 2
+
+- name: assert hash behaviour is merge or replace
+ assert:
+ that:
+ - lookup('env', 'ANSIBLE_HASH_BEHAVIOUR') in ('merge', 'replace')
+
+- name: debug test_hash var
+ debug:
+ var: test_hash
+ verbosity: 2
+
+- name: assert the dictionary values match
+ assert:
+ that:
+ - "lookup('env', 'ANSIBLE_HASH_BEHAVIOUR') == 'merge' and test_hash == merged_hash or lookup('env', 'ANSIBLE_HASH_BEHAVIOUR') == 'replace' and test_hash == replaced_hash"
diff --git a/test/integration/targets/hash/roles/test_hash_behaviour/vars/main.yml b/test/integration/targets/hash/roles/test_hash_behaviour/vars/main.yml
new file mode 100644
index 0000000..2068e9f
--- /dev/null
+++ b/test/integration/targets/hash/roles/test_hash_behaviour/vars/main.yml
@@ -0,0 +1,21 @@
+# test code for the hash variable behavior
+# (c) 2014, James Cammarata <jcammarata@ansible.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+---
+test_hash:
+ role_vars: "this is in role vars/main.yml"
diff --git a/test/integration/targets/hash/runme.sh b/test/integration/targets/hash/runme.sh
new file mode 100755
index 0000000..3689d83
--- /dev/null
+++ b/test/integration/targets/hash/runme.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+set -eux
+
+JSON_ARG='{"test_hash":{"extra_args":"this is an extra arg"}}'
+
+ANSIBLE_HASH_BEHAVIOUR=replace ansible-playbook test_hash.yml -i ../../inventory -v "$@" -e "${JSON_ARG}"
+ANSIBLE_HASH_BEHAVIOUR=merge ansible-playbook test_hash.yml -i ../../inventory -v "$@" -e "${JSON_ARG}"
+
+ANSIBLE_HASH_BEHAVIOUR=replace ansible-playbook test_inventory_hash.yml -i test_inv1.yml -i test_inv2.yml -v "$@"
+ANSIBLE_HASH_BEHAVIOUR=merge ansible-playbook test_inventory_hash.yml -i test_inv1.yml -i test_inv2.yml -v "$@"
diff --git a/test/integration/targets/hash/test_hash.yml b/test/integration/targets/hash/test_hash.yml
new file mode 100644
index 0000000..37b56e6
--- /dev/null
+++ b/test/integration/targets/hash/test_hash.yml
@@ -0,0 +1,21 @@
+- hosts: testhost
+ vars_files:
+ - vars/test_hash_vars.yml
+ vars:
+ test_hash:
+ playbook_vars: "this is a playbook variable"
+ replaced_hash:
+ extra_args: "this is an extra arg"
+ merged_hash:
+ default_vars: "this is in role defaults/main.yml"
+ extra_args: "this is an extra arg"
+ group_vars_all: "this is in group_vars/all"
+ host_vars_testhost: "this is in host_vars/testhost"
+ playbook_vars: "this is a playbook variable"
+ role_argument: "this is a role argument variable"
+ role_vars: "this is in role vars/main.yml"
+ vars_file: "this is in a vars_file"
+ roles:
+ - role: test_hash_behaviour
+ test_hash:
+ role_argument: 'this is a role argument variable'
diff --git a/test/integration/targets/hash/test_inv1.yml b/test/integration/targets/hash/test_inv1.yml
new file mode 100644
index 0000000..02bd017
--- /dev/null
+++ b/test/integration/targets/hash/test_inv1.yml
@@ -0,0 +1,10 @@
+all:
+ hosts:
+ host1:
+ test_inventory_host_hash:
+ host_var1: "inventory 1"
+ host_var2: "inventory 1"
+ vars:
+ test_inventory_group_hash:
+ group_var1: "inventory 1"
+ group_var2: "inventory 1"
diff --git a/test/integration/targets/hash/test_inv2.yml b/test/integration/targets/hash/test_inv2.yml
new file mode 100644
index 0000000..6529b93
--- /dev/null
+++ b/test/integration/targets/hash/test_inv2.yml
@@ -0,0 +1,8 @@
+all:
+ hosts:
+ host1:
+ test_inventory_host_hash:
+ host_var1: "inventory 2"
+ vars:
+ test_inventory_group_hash:
+ group_var1: "inventory 2"
diff --git a/test/integration/targets/hash/test_inventory_hash.yml b/test/integration/targets/hash/test_inventory_hash.yml
new file mode 100644
index 0000000..1091b13
--- /dev/null
+++ b/test/integration/targets/hash/test_inventory_hash.yml
@@ -0,0 +1,41 @@
+---
+- hosts: localhost
+ gather_facts: no
+ vars:
+ host_hash_merged: {'host_var1': 'inventory 2', 'host_var2': 'inventory 1'}
+ host_hash_replaced: {'host_var1': 'inventory 2'}
+ group_hash_merged: {'group_var1': 'inventory 2', 'group_var2': 'inventory 1'}
+ group_hash_replaced: {'group_var1': 'inventory 2'}
+ tasks:
+
+ - name: debug hash behaviour result
+ debug:
+ var: "{{ lookup('env', 'ANSIBLE_HASH_BEHAVIOUR') }}"
+ verbosity: 2
+
+ - name: assert hash behaviour is merge or replace
+ assert:
+ that:
+ - lookup('env', 'ANSIBLE_HASH_BEHAVIOUR') in ('merge', 'replace')
+
+ - name: debug test_inventory_host_hash
+ debug:
+ var: hostvars['host1']['test_inventory_host_hash']
+ verbosity: 2
+
+ - name: debug test_inventory_group_hash
+ debug:
+ var: test_inventory_group_hash
+ verbosity: 2
+
+ - assert:
+ that:
+ - hostvars['host1']['test_inventory_host_hash'] == host_hash_replaced
+ - test_inventory_group_hash == group_hash_replaced
+ when: "lookup('env', 'ANSIBLE_HASH_BEHAVIOUR') == 'replace'"
+
+ - assert:
+ that:
+ - hostvars['host1']['test_inventory_host_hash'] == host_hash_merged
+ - test_inventory_group_hash == group_hash_merged
+ when: "lookup('env', 'ANSIBLE_HASH_BEHAVIOUR') == 'merge'"
diff --git a/test/integration/targets/hash/vars/test_hash_vars.yml b/test/integration/targets/hash/vars/test_hash_vars.yml
new file mode 100644
index 0000000..e25f857
--- /dev/null
+++ b/test/integration/targets/hash/vars/test_hash_vars.yml
@@ -0,0 +1,3 @@
+---
+test_hash:
+ vars_file: "this is in a vars_file"
diff --git a/test/integration/targets/hostname/aliases b/test/integration/targets/hostname/aliases
new file mode 100644
index 0000000..6eae8bd
--- /dev/null
+++ b/test/integration/targets/hostname/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group1
+destructive
diff --git a/test/integration/targets/hostname/tasks/Debian.yml b/test/integration/targets/hostname/tasks/Debian.yml
new file mode 100644
index 0000000..dfa88fe
--- /dev/null
+++ b/test/integration/targets/hostname/tasks/Debian.yml
@@ -0,0 +1,20 @@
+---
+- name: Test DebianStrategy by setting hostname
+ become: 'yes'
+ hostname:
+ use: debian
+ name: "{{ ansible_distribution_release }}-bebop.ansible.example.com"
+
+- name: Test DebianStrategy by getting current hostname
+ command: hostname
+ register: get_hostname
+
+- name: Test DebianStrategy by verifying /etc/hostname content
+ command: grep -v '^#' /etc/hostname
+ register: grep_hostname
+
+- name: Test DebianStrategy using assertions
+ assert:
+ that:
+ - "'{{ ansible_distribution_release }}-bebop.ansible.example.com' in get_hostname.stdout"
+ - "'{{ ansible_distribution_release }}-bebop.ansible.example.com' in grep_hostname.stdout"
diff --git a/test/integration/targets/hostname/tasks/MacOSX.yml b/test/integration/targets/hostname/tasks/MacOSX.yml
new file mode 100644
index 0000000..912ced7
--- /dev/null
+++ b/test/integration/targets/hostname/tasks/MacOSX.yml
@@ -0,0 +1,52 @@
+- name: macOS | Set hostname
+ hostname:
+ name: bugs.acme.example.com
+
+# These tasks can be changed to a loop once https://github.com/ansible/ansible/issues/71031
+# is fixed
+- name: macOS | Set hostname specifiying macos strategy
+ hostname:
+ name: bugs.acme.example.com
+ use: macos
+
+- name: macOS | Set hostname specifiying macosx strategy
+ hostname:
+ name: bugs.acme.example.com
+ use: macosx
+
+- name: macOS | Set hostname specifiying darwin strategy
+ hostname:
+ name: bugs.acme.example.com
+ use: darwin
+
+- name: macOS | Get macOS hostname values
+ command: scutil --get {{ item }}
+ loop:
+ - HostName
+ - ComputerName
+ - LocalHostName
+ register: macos_scutil
+ ignore_errors: yes
+
+- name: macOS | Ensure all hostname values were set correctly
+ assert:
+ that:
+ - "['bugs.acme.example.com', 'bugs.acme.example.com', 'bugsacmeexamplecom'] == macos_scutil.results | map(attribute='stdout') | list"
+
+- name: macOS | Set to a hostname using spaces and punctuation
+ hostname:
+ name: The Dude's Computer
+
+- name: macOS | Get macOS hostname values
+ command: scutil --get {{ item }}
+ loop:
+ - HostName
+ - ComputerName
+ - LocalHostName
+ register: macos_scutil_complex
+ ignore_errors: yes
+
+- name: macOS | Ensure all hostname values were set correctly
+ assert:
+ that:
+ - "['The Dude\\'s Computer', 'The Dude\\'s Computer', 'The-Dudes-Computer'] == (macos_scutil_complex.results | map(attribute='stdout') | list)"
diff --git a/test/integration/targets/hostname/tasks/RedHat.yml b/test/integration/targets/hostname/tasks/RedHat.yml
new file mode 100644
index 0000000..1f61390
--- /dev/null
+++ b/test/integration/targets/hostname/tasks/RedHat.yml
@@ -0,0 +1,15 @@
+- name: Make sure we used SystemdStrategy...
+ lineinfile:
+ path: "{{ _hostname_file }}"
+ line: crocodile.ansible.test.doesthiswork.net.example.com
+ check_mode: true
+ register: etc_hostname
+ failed_when: etc_hostname is changed
+
+- name: ...and not RedhatStrategy
+ lineinfile:
+ path: /etc/sysconfig/network
+ line: HOSTNAME=crocodile.ansible.test.doesthiswork.net.example.com
+ check_mode: true
+ register: etc_sysconfig_network
+ failed_when: etc_sysconfig_network is not changed
diff --git a/test/integration/targets/hostname/tasks/check_mode.yml b/test/integration/targets/hostname/tasks/check_mode.yml
new file mode 100644
index 0000000..e25df97
--- /dev/null
+++ b/test/integration/targets/hostname/tasks/check_mode.yml
@@ -0,0 +1,20 @@
+# These are less useful (check_mode only) but run even in containers
+- block:
+ - name: Get current hostname
+ command: hostname
+ register: original
+
+ - name: Change hostname (check_mode)
+ hostname:
+ name: crocodile.ansible.test.doesthiswork.net.example.com
+ check_mode: true
+ register: hn
+
+ - name: Get current hostname again
+ command: hostname
+ register: after_hn
+
+ - assert:
+ that:
+ - hn is changed
+ - original.stdout == after_hn.stdout
diff --git a/test/integration/targets/hostname/tasks/default.yml b/test/integration/targets/hostname/tasks/default.yml
new file mode 100644
index 0000000..b308239
--- /dev/null
+++ b/test/integration/targets/hostname/tasks/default.yml
@@ -0,0 +1,2 @@
+- debug:
+ msg: No distro-specific tests defined for this distro.
diff --git a/test/integration/targets/hostname/tasks/main.yml b/test/integration/targets/hostname/tasks/main.yml
new file mode 100644
index 0000000..596dd89
--- /dev/null
+++ b/test/integration/targets/hostname/tasks/main.yml
@@ -0,0 +1,52 @@
+# Setting the hostname in our test containers doesn't work currently
+- when: ansible_facts.virtualization_type not in ('docker', 'container', 'containerd')
+ block:
+ - name: Include distribution specific variables
+ include_vars: "{{ lookup('first_found', params) }}"
+ vars:
+ params:
+ files:
+ - "{{ ansible_facts.distribution }}.yml"
+ - "{{ ansible_facts.os_family }}.yml"
+ - default.yml
+ paths:
+ - "{{ role_path }}/vars"
+
+ - name: Get current hostname
+ command: hostname
+ register: original
+
+ - import_tasks: test_check_mode.yml
+ - import_tasks: test_normal.yml
+
+ - name: Include distribution specific tasks
+ include_tasks:
+ file: "{{ lookup('first_found', files) }}"
+ vars:
+ files:
+ - "{{ ansible_facts.distribution }}.yml"
+ - default.yml
+
+ always:
+ # Reset back to original hostname
+ - name: Move back original file if it existed
+ become: 'yes'
+ command: mv -f {{ _hostname_file }}.orig {{ _hostname_file }}
+ when: hn_stat.stat.exists | default(False)
+
+ - name: Delete the file if it never existed
+ file:
+ path: "{{ _hostname_file }}"
+ state: absent
+ when: not hn_stat.stat.exists | default(True)
+
+ - name: Reset back to original hostname
+ become: 'yes'
+ hostname:
+ name: "{{ original.stdout }}"
+ register: revert
+
+ - name: Ensure original hostname was reset
+ assert:
+ that:
+ - revert is changed
diff --git a/test/integration/targets/hostname/tasks/test_check_mode.yml b/test/integration/targets/hostname/tasks/test_check_mode.yml
new file mode 100644
index 0000000..9ba1d65
--- /dev/null
+++ b/test/integration/targets/hostname/tasks/test_check_mode.yml
@@ -0,0 +1,50 @@
+- name: Run hostname module in check_mode
+ hostname:
+ name: crocodile.ansible.test.doesthiswork.net.example.com
+ check_mode: true
+ register: hn1
+
+- name: Get current hostname again
+ command: hostname
+ register: after_hn
+
+- name: Ensure hostname changed properly
+ assert:
+ that:
+ - hn1 is changed
+ - original.stdout == after_hn.stdout
+
+- when: _hostname_file is defined and _hostname_file
+ block:
+ - name: See if current hostname file exists
+ stat:
+ path: "{{ _hostname_file }}"
+ register: hn_stat
+
+ - name: Move the current hostname file if it exists
+ command: mv {{ _hostname_file }} {{ _hostname_file }}.orig
+ when: hn_stat.stat.exists
+
+ - name: Run hostname module in check_mode
+ hostname:
+ name: crocodile.ansible.test.doesthiswork.net.example.com
+ check_mode: true
+ register: hn
+
+ - stat:
+ path: /etc/rc.conf.d/hostname
+ register: hn_stat_checkmode
+
+ - assert:
+ that:
+ # TODO: This is a legitimate bug and will be fixed in another PR.
+ # - not hn_stat_checkmode.stat.exists
+ - hn is changed
+
+ - name: Get hostname again
+ command: hostname
+ register: current_after_cm
+
+ - assert:
+ that:
+ - original.stdout == current_after_cm.stdout
diff --git a/test/integration/targets/hostname/tasks/test_normal.yml b/test/integration/targets/hostname/tasks/test_normal.yml
new file mode 100644
index 0000000..9534d73
--- /dev/null
+++ b/test/integration/targets/hostname/tasks/test_normal.yml
@@ -0,0 +1,54 @@
+- name: Ensure hostname doesn't confuse NetworkManager
+ when: ansible_os_family == 'RedHat' and ansible_distribution_major_version is version('8')
+ block:
+ - name: slurp /var/log/messages
+ slurp:
+ path: /var/log/messages
+ become: yes
+ register: messages_before
+
+ - assert:
+ that:
+ - >
+ 'current hostname was changed outside NetworkManager' not in messages_before.content|b64decode
+
+- name: Run hostname module for real now
+ become: 'yes'
+ hostname:
+ name: crocodile.ansible.test.doesthiswork.net.example.com
+ register: hn2
+
+- name: Get hostname
+ command: hostname
+ register: current_after_hn2
+
+- name: Ensure hostname doesn't confuse NetworkManager
+ when: ansible_os_family == 'RedHat' and ansible_distribution_major_version is version('8')
+ block:
+ - name: slurp /var/log/messages
+ slurp:
+ path: /var/log/messages
+ become: yes
+ register: messages_after
+
+ - assert:
+ that:
+ - >
+ 'current hostname was changed outside NetworkManager' not in messages_after.content|b64decode
+
+- name: Run hostname again to ensure it does not change
+ become: 'yes'
+ hostname:
+ name: crocodile.ansible.test.doesthiswork.net.example.com
+ register: hn3
+
+- name: Get hostname
+ command: hostname
+ register: current_after_hn3
+
+- assert:
+ that:
+ - hn2 is changed
+ - hn3 is not changed
+ - current_after_hn2.stdout == 'crocodile.ansible.test.doesthiswork.net.example.com'
+ - current_after_hn2.stdout == current_after_hn2.stdout
diff --git a/test/integration/targets/hostname/vars/FreeBSD.yml b/test/integration/targets/hostname/vars/FreeBSD.yml
new file mode 100644
index 0000000..b63a16e
--- /dev/null
+++ b/test/integration/targets/hostname/vars/FreeBSD.yml
@@ -0,0 +1 @@
+_hostname_file: /etc/rc.conf.d/hostname
diff --git a/test/integration/targets/hostname/vars/RedHat.yml b/test/integration/targets/hostname/vars/RedHat.yml
new file mode 100644
index 0000000..08d883b
--- /dev/null
+++ b/test/integration/targets/hostname/vars/RedHat.yml
@@ -0,0 +1 @@
+_hostname_file: /etc/hostname
diff --git a/test/integration/targets/hostname/vars/default.yml b/test/integration/targets/hostname/vars/default.yml
new file mode 100644
index 0000000..a50b3f1
--- /dev/null
+++ b/test/integration/targets/hostname/vars/default.yml
@@ -0,0 +1 @@
+_hostname_file: ~
diff --git a/test/integration/targets/hosts_field/aliases b/test/integration/targets/hosts_field/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/hosts_field/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/hosts_field/inventory.hosts_field b/test/integration/targets/hosts_field/inventory.hosts_field
new file mode 100644
index 0000000..4664404
--- /dev/null
+++ b/test/integration/targets/hosts_field/inventory.hosts_field
@@ -0,0 +1 @@
+42 ansible_host=127.0.0.42 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
diff --git a/test/integration/targets/hosts_field/runme.sh b/test/integration/targets/hosts_field/runme.sh
new file mode 100755
index 0000000..1291933
--- /dev/null
+++ b/test/integration/targets/hosts_field/runme.sh
@@ -0,0 +1,49 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# Hosts in playbook has a list of strings consisting solely of digits
+ansible-playbook test_hosts_field.yml -i inventory.hosts_field -e 'target_kv=42' \
+ -e '{ "target_json_cli": "42", "target_json_cli_list": ["42", "localhost"] }' -e "@test_hosts_field.json" \
+ -t string_digit_host_in_list -v "$@" | tee test_hosts_field.out
+grep 'Running on 42' test_hosts_field.out 2>&1
+test "$(grep -c 'ok=1' test_hosts_field.out)" = 1
+
+# Hosts taken from kv extra_var on the CLI
+ansible-playbook test_hosts_field.yml -i inventory.hosts_field -e 'target_kv=42' \
+ -e '{ "target_json_cli": "42", "target_json_cli_list": ["42", "localhost"] }' -e "@test_hosts_field.json" \
+ -t hosts_from_kv_string -v "$@" | tee test_hosts_field.out
+grep 'Running on 42' test_hosts_field.out 2>&1
+test "$(grep -c 'ok=1' test_hosts_field.out)" = 1
+
+# hosts is taken from an all digit json extra_vars string on the CLI
+ansible-playbook test_hosts_field.yml -i inventory.hosts_field -e 'target_kv=42' \
+ -e '{ "target_json_cli": "42", "target_json_cli_list": ["42", "localhost"] }' -e "@test_hosts_field.json" \
+ -t hosts_from_cli_json_string -v "$@" | tee test_hosts_field.out
+grep 'Running on 42' test_hosts_field.out 2>&1
+test "$(grep -c 'ok=1' test_hosts_field.out)" = 1
+
+# hosts is taken from a json list in extra_vars on the CLI
+ansible-playbook test_hosts_field.yml -i inventory.hosts_field -e 'target_kv=42' \
+ -e '{ "target_json_cli": "42", "target_json_cli_list": ["42", "localhost"] }' -e "@test_hosts_field.json" \
+ -t hosts_from_cli_json_list -v "$@" | tee test_hosts_field.out
+grep 'Running on 42' test_hosts_field.out 2>&1
+grep 'Running on localhost' test_hosts_field.out 2>&1
+test "$(grep -c 'ok=1' test_hosts_field.out)" = 2
+
+# hosts is taken from a json string in an extra_vars file
+ansible-playbook test_hosts_field.yml -i inventory.hosts_field -e 'target_kv=42' \
+ -e '{ "target_json_cli": "42", "target_json_cli_list": ["42", "localhost"] }' -e "@test_hosts_field.json" \
+ -t hosts_from_json_file_string -v "$@" | tee test_hosts_field.out
+grep 'Running on 42' test_hosts_field.out 2>&1
+test "$(grep -c 'ok=1' test_hosts_field.out)" = 1
+
+# hosts is taken from a json list in an extra_vars file
+ansible-playbook test_hosts_field.yml -i inventory.hosts_field -e 'target_kv=42' \
+ -e '{ "target_json_cli": "42", "target_json_cli_list": ["42", "localhost"] }' -e "@test_hosts_field.json" \
+ -t hosts_from_json_file_list -v "$@" | tee test_hosts_field.out
+grep 'Running on 42' test_hosts_field.out 2>&1
+grep 'Running on localhost' test_hosts_field.out 2>&1
+test "$(grep -c 'ok=1' test_hosts_field.out)" = 2
+
+rm test_hosts_field.out
diff --git a/test/integration/targets/hosts_field/test_hosts_field.json b/test/integration/targets/hosts_field/test_hosts_field.json
new file mode 100644
index 0000000..2687556
--- /dev/null
+++ b/test/integration/targets/hosts_field/test_hosts_field.json
@@ -0,0 +1 @@
+{ "target_json_file": "42", "target_json_file_list": ["42", "localhost"] }
diff --git a/test/integration/targets/hosts_field/test_hosts_field.yml b/test/integration/targets/hosts_field/test_hosts_field.yml
new file mode 100644
index 0000000..568d702
--- /dev/null
+++ b/test/integration/targets/hosts_field/test_hosts_field.yml
@@ -0,0 +1,62 @@
+---
+#- name: Host in playbook is an integer
+# hosts: 42
+# tags: numeric_host
+# tasks:
+# - command: echo 'Running on {{ inventory_hostname }}'
+
+#- name: Host in playbook is a string of digits
+# hosts: "42"
+# tags: string_digit_host
+# tasks:
+# - command: echo 'Running on {{ inventory_hostname }}'
+
+#- name: Host in playbook is a list of integer
+# hosts:
+# - 42
+# tags: numeric_host_in_list
+# tasks:
+# - command: echo 'Running on {{ inventory_hostname }}'
+
+- name: Host in playbook is a list of strings of digits
+ hosts:
+ - "42"
+ gather_facts: False
+ tags: string_digit_host_in_list
+ tasks:
+ - command: echo 'Running on {{ inventory_hostname }}'
+
+- name: Hosts taken from kv extra_var on the CLI
+ hosts: "{{ target_kv }}"
+ gather_facts: False
+ tags: hosts_from_kv_string
+ tasks:
+ - command: echo 'Running on {{ inventory_hostname }}'
+
+- name: Hosts taken from a json string on the CLI
+ hosts: "{{ target_json_cli }}"
+ gather_facts: False
+ tags: hosts_from_cli_json_string
+ tasks:
+ - command: echo 'Running on {{ inventory_hostname }}'
+
+- name: Hosts taken from a json list on the CLI
+ hosts: "{{ target_json_cli_list }}"
+ gather_facts: False
+ tags: hosts_from_cli_json_list
+ tasks:
+ - command: echo 'Running on {{ inventory_hostname }}'
+
+- name: Hosts is taken from a json string in an extra_vars file
+ hosts: "{{ target_json_file }}"
+ gather_facts: False
+ tags: hosts_from_json_file_string
+ tasks:
+ - command: echo 'Running on {{ inventory_hostname }}'
+
+- name: Hosts is taken from a json list in an extra_vars file
+ hosts: "{{ target_json_file_list }}"
+ gather_facts: False
+ tags: hosts_from_json_file_list
+ tasks:
+ - command: echo 'Running on {{ inventory_hostname }}'
diff --git a/test/integration/targets/ignore_errors/aliases b/test/integration/targets/ignore_errors/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/ignore_errors/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/ignore_errors/meta/main.yml b/test/integration/targets/ignore_errors/meta/main.yml
new file mode 100644
index 0000000..07faa21
--- /dev/null
+++ b/test/integration/targets/ignore_errors/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - prepare_tests
diff --git a/test/integration/targets/ignore_errors/tasks/main.yml b/test/integration/targets/ignore_errors/tasks/main.yml
new file mode 100644
index 0000000..a6964e0
--- /dev/null
+++ b/test/integration/targets/ignore_errors/tasks/main.yml
@@ -0,0 +1,22 @@
+# test code
+# (c) 2014, Michael DeHaan <michael.dehaan@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: this will not stop the playbook
+ shell: /bin/false
+ register: failed
+ ignore_errors: True
diff --git a/test/integration/targets/ignore_unreachable/aliases b/test/integration/targets/ignore_unreachable/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/ignore_unreachable/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/ignore_unreachable/fake_connectors/bad_exec.py b/test/integration/targets/ignore_unreachable/fake_connectors/bad_exec.py
new file mode 100644
index 0000000..0d8c385
--- /dev/null
+++ b/test/integration/targets/ignore_unreachable/fake_connectors/bad_exec.py
@@ -0,0 +1,14 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import ansible.plugins.connection.local as ansible_local
+from ansible.errors import AnsibleConnectionFailure
+
+from ansible.utils.display import Display
+display = Display()
+
+
+class Connection(ansible_local.Connection):
+ def exec_command(self, cmd, in_data=None, sudoable=True):
+ display.debug('Intercepted call to exec remote command')
+ raise AnsibleConnectionFailure('BADLOCAL Error: this is supposed to fail')
diff --git a/test/integration/targets/ignore_unreachable/fake_connectors/bad_put_file.py b/test/integration/targets/ignore_unreachable/fake_connectors/bad_put_file.py
new file mode 100644
index 0000000..d4131f4
--- /dev/null
+++ b/test/integration/targets/ignore_unreachable/fake_connectors/bad_put_file.py
@@ -0,0 +1,14 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import ansible.plugins.connection.local as ansible_local
+from ansible.errors import AnsibleConnectionFailure
+
+from ansible.utils.display import Display
+display = Display()
+
+
+class Connection(ansible_local.Connection):
+ def put_file(self, in_path, out_path):
+ display.debug('Intercepted call to send data')
+ raise AnsibleConnectionFailure('BADLOCAL Error: this is supposed to fail')
diff --git a/test/integration/targets/ignore_unreachable/inventory b/test/integration/targets/ignore_unreachable/inventory
new file mode 100644
index 0000000..495a68c
--- /dev/null
+++ b/test/integration/targets/ignore_unreachable/inventory
@@ -0,0 +1,3 @@
+nonexistent ansible_host=169.254.199.200
+bad_put_file ansible_host=localhost ansible_connection=bad_put_file
+bad_exec ansible_host=localhost ansible_connection=bad_exec
diff --git a/test/integration/targets/ignore_unreachable/meta/main.yml b/test/integration/targets/ignore_unreachable/meta/main.yml
new file mode 100644
index 0000000..07faa21
--- /dev/null
+++ b/test/integration/targets/ignore_unreachable/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - prepare_tests
diff --git a/test/integration/targets/ignore_unreachable/runme.sh b/test/integration/targets/ignore_unreachable/runme.sh
new file mode 100755
index 0000000..5b0ef19
--- /dev/null
+++ b/test/integration/targets/ignore_unreachable/runme.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+set -eux
+
+export ANSIBLE_CONNECTION_PLUGINS=./fake_connectors
+# use fake connectors that raise srrors at different stages
+ansible-playbook test_with_bad_plugins.yml -i inventory -v "$@"
+unset ANSIBLE_CONNECTION_PLUGINS
+
+ansible-playbook test_cannot_connect.yml -i inventory -v "$@"
+
+if ansible-playbook test_base_cannot_connect.yml -i inventory -v "$@"; then
+ echo "Playbook intended to fail succeeded. Connection succeeded to nonexistent host"
+ exit 99
+else
+ echo "Connection to nonexistent hosts failed without using ignore_unreachable. Success!"
+fi
diff --git a/test/integration/targets/ignore_unreachable/test_base_cannot_connect.yml b/test/integration/targets/ignore_unreachable/test_base_cannot_connect.yml
new file mode 100644
index 0000000..931c82b
--- /dev/null
+++ b/test/integration/targets/ignore_unreachable/test_base_cannot_connect.yml
@@ -0,0 +1,5 @@
+- hosts: [localhost, nonexistent]
+ gather_facts: false
+ tasks:
+ - name: Hi
+ ping:
diff --git a/test/integration/targets/ignore_unreachable/test_cannot_connect.yml b/test/integration/targets/ignore_unreachable/test_cannot_connect.yml
new file mode 100644
index 0000000..64e2bfe
--- /dev/null
+++ b/test/integration/targets/ignore_unreachable/test_cannot_connect.yml
@@ -0,0 +1,29 @@
+---
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: Hi
+ ping:
+- hosts: [localhost, nonexistent]
+ ignore_unreachable: true
+ gather_facts: false
+ tasks:
+ - name: Hi
+ ping:
+- hosts: nonexistent
+ ignore_unreachable: true
+ gather_facts: false
+ tasks:
+ - name: Hi
+ ping:
+ - name: This should print anyway
+ debug:
+ msg: This should print worked even though host was unreachable
+ - name: Hi
+ ping:
+ register: should_fail
+ - assert:
+ that:
+ - 'should_fail is unreachable'
+ - 'not (should_fail is skipped)'
+ - 'not (should_fail is failed)'
diff --git a/test/integration/targets/ignore_unreachable/test_with_bad_plugins.yml b/test/integration/targets/ignore_unreachable/test_with_bad_plugins.yml
new file mode 100644
index 0000000..5d62f19
--- /dev/null
+++ b/test/integration/targets/ignore_unreachable/test_with_bad_plugins.yml
@@ -0,0 +1,24 @@
+- hosts: bad_put_file
+ gather_facts: false
+ ignore_unreachable: true
+ tasks:
+ - name: Hi
+ ping:
+- hosts: bad_put_file
+ gather_facts: true
+ ignore_unreachable: true
+ tasks:
+ - name: Hi
+ ping:
+- hosts: bad_exec
+ gather_facts: false
+ ignore_unreachable: true
+ tasks:
+ - name: Hi
+ ping:
+- hosts: bad_exec
+ gather_facts: true
+ ignore_unreachable: true
+ tasks:
+ - name: Hi
+ ping:
diff --git a/test/integration/targets/import_tasks/aliases b/test/integration/targets/import_tasks/aliases
new file mode 100644
index 0000000..2e198a7
--- /dev/null
+++ b/test/integration/targets/import_tasks/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller # this is a controller-only action, the module is just for documentation
diff --git a/test/integration/targets/import_tasks/inherit_notify.yml b/test/integration/targets/import_tasks/inherit_notify.yml
new file mode 100644
index 0000000..cf418f9
--- /dev/null
+++ b/test/integration/targets/import_tasks/inherit_notify.yml
@@ -0,0 +1,15 @@
+- hosts: localhost
+ gather_facts: false
+ pre_tasks:
+ - import_tasks: tasks/trigger_change.yml
+ notify: hello
+
+ handlers:
+ - name: hello
+ set_fact: hello=world
+
+ tasks:
+ - name: ensure handler ran
+ assert:
+ that:
+ - "hello is defined and hello == 'world'"
diff --git a/test/integration/targets/import_tasks/runme.sh b/test/integration/targets/import_tasks/runme.sh
new file mode 100755
index 0000000..ea3529b
--- /dev/null
+++ b/test/integration/targets/import_tasks/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook inherit_notify.yml "$@"
diff --git a/test/integration/targets/import_tasks/tasks/trigger_change.yml b/test/integration/targets/import_tasks/tasks/trigger_change.yml
new file mode 100644
index 0000000..6ee4551
--- /dev/null
+++ b/test/integration/targets/import_tasks/tasks/trigger_change.yml
@@ -0,0 +1,2 @@
+- debug: msg="I trigger changed!"
+ changed_when: true
diff --git a/test/integration/targets/incidental_ios_file/aliases b/test/integration/targets/incidental_ios_file/aliases
new file mode 100644
index 0000000..cbcfec6
--- /dev/null
+++ b/test/integration/targets/incidental_ios_file/aliases
@@ -0,0 +1,2 @@
+shippable/ios/incidental
+network/ios
diff --git a/test/integration/targets/incidental_ios_file/defaults/main.yaml b/test/integration/targets/incidental_ios_file/defaults/main.yaml
new file mode 100644
index 0000000..5f709c5
--- /dev/null
+++ b/test/integration/targets/incidental_ios_file/defaults/main.yaml
@@ -0,0 +1,2 @@
+---
+testcase: "*"
diff --git a/test/integration/targets/incidental_ios_file/ios1.cfg b/test/integration/targets/incidental_ios_file/ios1.cfg
new file mode 100644
index 0000000..120dd4c
--- /dev/null
+++ b/test/integration/targets/incidental_ios_file/ios1.cfg
@@ -0,0 +1,3 @@
+vlan 3
+ name ank_vlan3
+!
diff --git a/test/integration/targets/incidental_ios_file/nonascii.bin b/test/integration/targets/incidental_ios_file/nonascii.bin
new file mode 100644
index 0000000..14c6ddb
--- /dev/null
+++ b/test/integration/targets/incidental_ios_file/nonascii.bin
Binary files differ
diff --git a/test/integration/targets/incidental_ios_file/tasks/cli.yaml b/test/integration/targets/incidental_ios_file/tasks/cli.yaml
new file mode 100644
index 0000000..3eb5769
--- /dev/null
+++ b/test/integration/targets/incidental_ios_file/tasks/cli.yaml
@@ -0,0 +1,17 @@
+---
+- name: collect all cli test cases
+ find:
+ paths: "{{ role_path }}/tests/cli"
+ patterns: "{{ testcase }}.yaml"
+ register: test_cases
+ delegate_to: localhost
+
+- name: set test_items
+ set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
+
+- name: run test cases (connection=ansible.netcommon.network_cli)
+ include_tasks: "{{ test_case_to_run }}"
+ with_items: "{{ test_items }}"
+ loop_control:
+ loop_var: test_case_to_run
+ tags: connection_network_cli
diff --git a/test/integration/targets/incidental_ios_file/tasks/main.yaml b/test/integration/targets/incidental_ios_file/tasks/main.yaml
new file mode 100644
index 0000000..24ad94a
--- /dev/null
+++ b/test/integration/targets/incidental_ios_file/tasks/main.yaml
@@ -0,0 +1,2 @@
+---
+- { import_tasks: cli.yaml, tags: ['cli'] }
diff --git a/test/integration/targets/incidental_ios_file/tests/cli/net_get.yaml b/test/integration/targets/incidental_ios_file/tests/cli/net_get.yaml
new file mode 100644
index 0000000..5a7ebf0
--- /dev/null
+++ b/test/integration/targets/incidental_ios_file/tests/cli/net_get.yaml
@@ -0,0 +1,52 @@
+---
+- debug: msg="START ios cli/net_get.yaml on connection={{ ansible_connection }}"
+
+# Add minimal testcase to check args are passed correctly to
+# implementation module and module run is successful.
+
+- name: setup
+ cisco.ios.ios_config:
+ lines:
+ - ip ssh version 2
+ - ip scp server enable
+ - username {{ ansible_ssh_user }} privilege 15
+ match: none
+
+- name: setup (copy file to be fetched from device)
+ ansible.netcommon.net_put:
+ src: ios1.cfg
+ register: result
+
+- name: setup (remove file from localhost if present)
+ file:
+ path: ios_{{ inventory_hostname }}.cfg
+ state: absent
+ delegate_to: localhost
+
+- name: get the file from device with relative destination
+ ansible.netcommon.net_get:
+ src: ios1.cfg
+ dest: 'ios_{{ inventory_hostname }}.cfg'
+ register: result
+
+- assert:
+ that:
+ - result.changed == true
+
+- name: Idempotency check
+ ansible.netcommon.net_get:
+ src: ios1.cfg
+ dest: 'ios_{{ inventory_hostname }}.cfg'
+ register: result
+
+- assert:
+ that:
+ - result.changed == false
+
+- name: setup (remove file from localhost if present)
+ file:
+ path: ios_{{ inventory_hostname }}.cfg
+ state: absent
+ delegate_to: localhost
+
+- debug: msg="END ios cli/net_get.yaml on connection={{ ansible_connection }}"
diff --git a/test/integration/targets/incidental_ios_file/tests/cli/net_put.yaml b/test/integration/targets/incidental_ios_file/tests/cli/net_put.yaml
new file mode 100644
index 0000000..215b524
--- /dev/null
+++ b/test/integration/targets/incidental_ios_file/tests/cli/net_put.yaml
@@ -0,0 +1,73 @@
+---
+- debug:
+ msg: "START ios cli/net_put.yaml on connection={{ ansible_connection }}"
+
+# Add minimal testcase to check args are passed correctly to
+# implementation module and module run is successful.
+
+- name: setup
+ cisco.ios.ios_config:
+ lines:
+ - ip ssh version 2
+ - ip scp server enable
+ - username {{ ansible_ssh_user }} privilege 15
+ match: none
+
+- name: Delete existing files if present on remote host
+ cisco.ios.ios_command:
+ commands: "{{ item }}"
+ loop:
+ - delete /force ios1.cfg
+ - delete /force ios.cfg
+ - delete /force nonascii.bin
+ ignore_errors: true
+
+- name: copy file from controller to ios + scp (Default)
+ ansible.netcommon.net_put:
+ src: ios1.cfg
+ register: result
+
+- assert:
+ that:
+ - result.changed == true
+
+- name: Idempotency Check
+ ansible.netcommon.net_put:
+ src: ios1.cfg
+ register: result
+
+- assert:
+ that:
+ - result.changed == false
+
+- name: copy file from controller to ios + dest specified
+ ansible.netcommon.net_put:
+ src: ios1.cfg
+ dest: ios.cfg
+ register: result
+
+- assert:
+ that:
+ - result.changed == true
+
+- name: copy file with non-ascii characters to ios in template mode(Fail case)
+ ansible.netcommon.net_put:
+ src: nonascii.bin
+ mode: 'text'
+ register: result
+ ignore_errors: true
+
+- assert:
+ that:
+ - result.failed == true
+
+- name: copy file with non-ascii characters to ios in default mode(binary)
+ ansible.netcommon.net_put:
+ src: nonascii.bin
+ register: result
+
+- assert:
+ that:
+ - result.changed == true
+
+- debug: msg="END ios cli/net_put.yaml on connection={{ ansible_connection }}"
diff --git a/test/integration/targets/incidental_vyos_config/aliases b/test/integration/targets/incidental_vyos_config/aliases
new file mode 100644
index 0000000..fae06ba
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_config/aliases
@@ -0,0 +1,2 @@
+shippable/vyos/incidental
+network/vyos
diff --git a/test/integration/targets/incidental_vyos_config/defaults/main.yaml b/test/integration/targets/incidental_vyos_config/defaults/main.yaml
new file mode 100644
index 0000000..9ef5ba5
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_config/defaults/main.yaml
@@ -0,0 +1,3 @@
+---
+testcase: "*"
+test_items: []
diff --git a/test/integration/targets/incidental_vyos_config/tasks/cli.yaml b/test/integration/targets/incidental_vyos_config/tasks/cli.yaml
new file mode 100644
index 0000000..d601bb7
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_config/tasks/cli.yaml
@@ -0,0 +1,26 @@
+---
+- name: collect all cli test cases
+ find:
+ paths: "{{ role_path }}/tests/cli"
+ patterns: "{{ testcase }}.yaml"
+ register: test_cases
+ delegate_to: localhost
+
+- name: set test_items
+ set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
+
+- name: run test case (connection=ansible.netcommon.network_cli)
+ include_tasks: "file={{ test_case_to_run }}"
+ vars:
+ ansible_connection: ansible.netcommon.network_cli
+ with_items: "{{ test_items }}"
+ loop_control:
+ loop_var: test_case_to_run
+
+- name: run test case (connection=local)
+ include_tasks: "file={{ test_case_to_run }}"
+ vars:
+ ansible_connection: local
+ with_first_found: "{{ test_items }}"
+ loop_control:
+ loop_var: test_case_to_run
diff --git a/test/integration/targets/incidental_vyos_config/tasks/cli_config.yaml b/test/integration/targets/incidental_vyos_config/tasks/cli_config.yaml
new file mode 100644
index 0000000..7e67356
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_config/tasks/cli_config.yaml
@@ -0,0 +1,18 @@
+---
+- name: collect all cli_config test cases
+ find:
+ paths: "{{ role_path }}/tests/cli_config"
+ patterns: "{{ testcase }}.yaml"
+ register: test_cases
+ delegate_to: localhost
+
+- name: set test_items
+ set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
+
+- name: run test case (connection=ansible.netcommon.network_cli)
+ include_tasks: "file={{ test_case_to_run }}"
+ vars:
+ ansible_connection: ansible.netcommon.network_cli
+ with_items: "{{ test_items }}"
+ loop_control:
+ loop_var: test_case_to_run
diff --git a/test/integration/targets/incidental_vyos_config/tasks/main.yaml b/test/integration/targets/incidental_vyos_config/tasks/main.yaml
new file mode 100644
index 0000000..0d4e8fd
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_config/tasks/main.yaml
@@ -0,0 +1,3 @@
+---
+- {import_tasks: cli.yaml, tags: ['cli']}
+- {import_tasks: cli_config.yaml, tags: ['cli_config']}
diff --git a/test/integration/targets/incidental_vyos_config/tests/cli/backup.yaml b/test/integration/targets/incidental_vyos_config/tests/cli/backup.yaml
new file mode 100644
index 0000000..af6a772
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_config/tests/cli/backup.yaml
@@ -0,0 +1,113 @@
+---
+- debug: msg="START vyos/backup.yaml on connection={{ ansible_connection }}"
+
+- name: collect any backup files
+ find:
+ paths: "{{ role_path }}/backup"
+ pattern: "{{ inventory_hostname_short }}_config*"
+ register: backup_files
+ connection: local
+
+- name: delete backup files
+ file:
+ path: "{{ item.path }}"
+ state: absent
+ with_items: "{{backup_files.files|default([])}}"
+
+- name: take configure backup
+ vyos.vyos.vyos_config:
+ backup: true
+ register: result
+
+- assert:
+ that:
+ - "result.changed == true"
+
+- name: collect any backup files
+ find:
+ paths: "{{ role_path }}/backup"
+ pattern: "{{ inventory_hostname_short }}_config*"
+ register: backup_files
+ connection: local
+
+- assert:
+ that:
+ - "backup_files.files is defined"
+
+- name: delete configurable backup file path
+ file:
+ path: "{{ item }}"
+ state: absent
+ with_items:
+ - "{{ role_path }}/backup_test_dir/"
+ - "{{ role_path }}/backup/backup.cfg"
+
+- name: take configuration backup in custom filename and directory path
+ vyos.vyos.vyos_config:
+ backup: true
+ backup_options:
+ filename: backup.cfg
+ dir_path: "{{ role_path }}/backup_test_dir/{{ inventory_hostname_short }}"
+ become: true
+ register: result
+
+- assert:
+ that:
+ - "result.changed == true"
+
+- name: check if the backup file-1 exist
+ find:
+ paths: "{{ role_path }}/backup_test_dir/{{ inventory_hostname_short }}/backup.cfg"
+ register: backup_file
+ connection: local
+
+- assert:
+ that:
+ - "backup_file.files is defined"
+
+- name: take configuration backup in custom filename
+ vyos.vyos.vyos_config:
+ backup: true
+ backup_options:
+ filename: backup.cfg
+ become: true
+ register: result
+
+- assert:
+ that:
+ - "result.changed == true"
+
+- name: check if the backup file-2 exist
+ find:
+ paths: "{{ role_path }}/backup/backup.cfg"
+ register: backup_file
+ connection: local
+
+- assert:
+ that:
+ - "backup_file.files is defined"
+
+- name: take configuration backup in custom path and default filename
+ vyos.vyos.vyos_config:
+ backup: true
+ backup_options:
+ dir_path: "{{ role_path }}/backup_test_dir/{{ inventory_hostname_short }}"
+ become: true
+ register: result
+
+- assert:
+ that:
+ - "result.changed == true"
+
+- name: check if the backup file-3 exist
+ find:
+ paths: "{{ role_path }}/backup_test_dir/{{ inventory_hostname_short }}"
+ pattern: "{{ inventory_hostname_short }}_config*"
+ register: backup_file
+ connection: local
+
+- assert:
+ that:
+ - "backup_file.files is defined"
+
+- debug: msg="END vyos/backup.yaml on connection={{ ansible_connection }}"
diff --git a/test/integration/targets/incidental_vyos_config/tests/cli/check_config.yaml b/test/integration/targets/incidental_vyos_config/tests/cli/check_config.yaml
new file mode 100644
index 0000000..f1ddc71
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_config/tests/cli/check_config.yaml
@@ -0,0 +1,63 @@
+---
+- debug: msg="START cli/config_check.yaml on connection={{ ansible_connection }}"
+
+- name: setup- ensure interface is not present
+ vyos.vyos.vyos_config:
+ lines: delete interfaces loopback lo
+
+- name: setup- create interface
+ vyos.vyos.vyos_config:
+ lines:
+ - interfaces
+ - interfaces loopback lo
+ - interfaces loopback lo description test
+ register: result
+
+# note collapsing the duplicate lines doesn't work if
+# lines:
+# - interfaces loopback lo description test
+# - interfaces loopback lo
+# - interfaces
+
+- name: Check that multiple duplicate lines collapse into a single commands
+ assert:
+ that:
+ - "{{ result.commands|length }} == 1"
+
+- name: Check that set is correctly prepended
+ assert:
+ that:
+ - "result.commands[0] == 'set interfaces loopback lo description test'"
+
+- name: configure config_check config command
+ vyos.vyos.vyos_config:
+ lines: delete interfaces loopback lo
+ register: result
+
+- assert:
+ that:
+ - "result.changed == true"
+
+- name: check config_check config command idempontent
+ vyos.vyos.vyos_config:
+ lines: delete interfaces loopback lo
+ register: result
+
+- assert:
+ that:
+ - "result.changed == false"
+
+- name: check multiple line config filter is working
+ vyos.vyos.vyos_config:
+ lines:
+ - set system login user esa level admin
+ - set system login user esa authentication encrypted-password '!abc!'
+ - set system login user vyos level admin
+ - set system login user vyos authentication encrypted-password 'abc'
+ register: result
+
+- assert:
+ that:
+ - "{{ result.filtered|length }} == 2"
+
+- debug: msg="END cli/config_check.yaml on connection={{ ansible_connection }}"
diff --git a/test/integration/targets/incidental_vyos_config/tests/cli/comment.yaml b/test/integration/targets/incidental_vyos_config/tests/cli/comment.yaml
new file mode 100644
index 0000000..2cd1350
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_config/tests/cli/comment.yaml
@@ -0,0 +1,34 @@
+---
+- debug: msg="START cli/comment.yaml on connection={{ ansible_connection }}"
+
+- name: setup
+ vyos.vyos.vyos_config:
+ lines: set system host-name {{ inventory_hostname_short }}
+ match: none
+
+- name: configure using comment
+ vyos.vyos.vyos_config:
+ lines: set system host-name foo
+ comment: this is a test
+ register: result
+
+- assert:
+ that:
+ - "result.changed == true"
+ - "'set system host-name foo' in result.commands"
+
+- name: collect system commits
+ vyos.vyos.vyos_command:
+ commands: show system commit
+ register: result
+
+- assert:
+ that:
+ - "'this is a test' in result.stdout_lines[0][1]"
+
+- name: teardown
+ vyos.vyos.vyos_config:
+ lines: set system host-name {{ inventory_hostname_short }}
+ match: none
+
+- debug: msg="END cli/comment.yaml on connection={{ ansible_connection }}"
diff --git a/test/integration/targets/incidental_vyos_config/tests/cli/config.cfg b/test/integration/targets/incidental_vyos_config/tests/cli/config.cfg
new file mode 100644
index 0000000..36c98f1
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_config/tests/cli/config.cfg
@@ -0,0 +1,3 @@
+ set service lldp
+ set protocols static
+
diff --git a/test/integration/targets/incidental_vyos_config/tests/cli/save.yaml b/test/integration/targets/incidental_vyos_config/tests/cli/save.yaml
new file mode 100644
index 0000000..d8e45e2
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_config/tests/cli/save.yaml
@@ -0,0 +1,54 @@
+---
+- debug: msg="START cli/save.yaml on connection={{ ansible_connection }}"
+
+- name: setup
+ vyos.vyos.vyos_config:
+ lines: set system host-name {{ inventory_hostname_short }}
+ match: none
+
+- name: configure hostaname and save
+ vyos.vyos.vyos_config:
+ lines: set system host-name foo
+ save: true
+ register: result
+
+- assert:
+ that:
+ - "result.changed == true"
+ - "'set system host-name foo' in result.commands"
+
+- name: configure hostaname and don't save
+ vyos.vyos.vyos_config:
+ lines: set system host-name bar
+ register: result
+
+- assert:
+ that:
+ - "result.changed == true"
+ - "'set system host-name bar' in result.commands"
+
+- name: save config
+ vyos.vyos.vyos_config:
+ save: true
+ register: result
+
+- assert:
+ that:
+ - "result.changed == true"
+
+- name: save config again
+ vyos.vyos.vyos_config:
+ save: true
+ register: result
+
+- assert:
+ that:
+ - "result.changed == false"
+
+- name: teardown
+ vyos.vyos.vyos_config:
+ lines: set system host-name {{ inventory_hostname_short }}
+ match: none
+ save: true
+
+- debug: msg="END cli/simple.yaml on connection={{ ansible_connection }}"
diff --git a/test/integration/targets/incidental_vyos_config/tests/cli/simple.yaml b/test/integration/targets/incidental_vyos_config/tests/cli/simple.yaml
new file mode 100644
index 0000000..c082673
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_config/tests/cli/simple.yaml
@@ -0,0 +1,53 @@
+---
+- debug: msg="START cli/simple.yaml on connection={{ ansible_connection }}"
+
+- name: setup
+ vyos.vyos.vyos_config:
+ lines: set system host-name {{ inventory_hostname_short }}
+ match: none
+
+- name: configure simple config command
+ vyos.vyos.vyos_config:
+ lines: set system host-name foo
+ register: result
+
+- assert:
+ that:
+ - "result.changed == true"
+ - "'set system host-name foo' in result.commands"
+
+- name: check simple config command idempontent
+ vyos.vyos.vyos_config:
+ lines: set system host-name foo
+ register: result
+
+- assert:
+ that:
+ - "result.changed == false"
+
+- name: Delete services
+ vyos.vyos.vyos_config: &del
+ lines:
+ - delete service lldp
+ - delete protocols static
+
+- name: Configuring when commands starts with whitespaces
+ vyos.vyos.vyos_config:
+ src: "{{ role_path }}/tests/cli/config.cfg"
+ register: result
+
+- assert:
+ that:
+ - "result.changed == true"
+ - '"set service lldp" in result.commands'
+ - '"set protocols static" in result.commands'
+
+- name: Delete services
+ vyos.vyos.vyos_config: *del
+
+- name: teardown
+ vyos.vyos.vyos_config:
+ lines: set system host-name {{ inventory_hostname_short }}
+ match: none
+
+- debug: msg="END cli/simple.yaml on connection={{ ansible_connection }}"
diff --git a/test/integration/targets/incidental_vyos_config/tests/cli_config/cli_backup.yaml b/test/integration/targets/incidental_vyos_config/tests/cli_config/cli_backup.yaml
new file mode 100644
index 0000000..744bb7e
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_config/tests/cli_config/cli_backup.yaml
@@ -0,0 +1,114 @@
+---
+- debug: msg="END cli_config/backup.yaml on connection={{ ansible_connection }}"
+
+- name: delete configurable backup file path
+ file:
+ path: "{{ item }}"
+ state: absent
+ with_items:
+ - "{{ role_path }}/backup_test_dir/"
+ - "{{ role_path }}/backup/backup.cfg"
+
+- name: collect any backup files
+ find:
+ paths: "{{ role_path }}/backup"
+ pattern: "{{ inventory_hostname_short }}_config*"
+ register: backup_files
+ connection: local
+
+- name: delete backup files
+ file:
+ path: "{{ item.path }}"
+ state: absent
+ with_items: "{{backup_files.files|default([])}}"
+
+- name: take config backup
+ ansible.netcommon.cli_config:
+ backup: true
+ become: true
+ register: result
+
+- assert:
+ that:
+ - "result.changed == true"
+
+- name: collect any backup files
+ find:
+ paths: "{{ role_path }}/backup"
+ pattern: "{{ inventory_hostname_short }}_config*"
+ register: backup_files
+ connection: local
+
+- assert:
+ that:
+ - "backup_files.files is defined"
+
+- name: take configuration backup in custom filename and directory path
+ ansible.netcommon.cli_config:
+ backup: true
+ backup_options:
+ filename: backup.cfg
+ dir_path: "{{ role_path }}/backup_test_dir/{{ inventory_hostname_short }}"
+ become: true
+ register: result
+
+- assert:
+ that:
+ - "result.changed == true"
+
+- name: check if the backup file-1 exist
+ find:
+ paths: "{{ role_path }}/backup_test_dir/{{ inventory_hostname_short }}/backup.cfg"
+ register: backup_file
+ connection: local
+
+- assert:
+ that:
+ - "backup_file.files is defined"
+
+- name: take configuration backup in custom filename
+ ansible.netcommon.cli_config:
+ backup: true
+ backup_options:
+ filename: backup.cfg
+ become: true
+ register: result
+
+- assert:
+ that:
+ - "result.changed == true"
+
+- name: check if the backup file-2 exist
+ find:
+ paths: "{{ role_path }}/backup/backup.cfg"
+ register: backup_file
+ connection: local
+
+- assert:
+ that:
+ - "backup_file.files is defined"
+
+- name: take configuration backup in custom path and default filename
+ ansible.netcommon.cli_config:
+ backup: true
+ backup_options:
+ dir_path: "{{ role_path }}/backup_test_dir/{{ inventory_hostname_short }}"
+ become: true
+ register: result
+
+- assert:
+ that:
+ - "result.changed == true"
+
+- name: check if the backup file-3 exist
+ find:
+ paths: "{{ role_path }}/backup_test_dir/{{ inventory_hostname_short }}"
+ pattern: "{{ inventory_hostname_short }}_config*"
+ register: backup_file
+ connection: local
+
+- assert:
+ that:
+ - "backup_file.files is defined"
+
+- debug: msg="END cli_config/backup.yaml on connection={{ ansible_connection }}"
diff --git a/test/integration/targets/incidental_vyos_config/tests/cli_config/cli_basic.yaml b/test/integration/targets/incidental_vyos_config/tests/cli_config/cli_basic.yaml
new file mode 100644
index 0000000..c6c4f59
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_config/tests/cli_config/cli_basic.yaml
@@ -0,0 +1,28 @@
+---
+- debug: msg="START cli_config/cli_basic.yaml on connection={{ ansible_connection }}"
+
+- name: setup - remove interface description
+ ansible.netcommon.cli_config: &rm
+ config: delete interfaces loopback lo description
+
+- name: configure device with config
+ ansible.netcommon.cli_config: &conf
+ config: set interfaces loopback lo description 'this is a test'
+ register: result
+
+- assert:
+ that:
+ - "result.changed == true"
+
+- name: Idempotence
+ ansible.netcommon.cli_config: *conf
+ register: result
+
+- assert:
+ that:
+ - "result.changed == false"
+
+- name: teardown
+ ansible.netcommon.cli_config: *rm
+
+- debug: msg="END cli_config/cli_basic.yaml on connection={{ ansible_connection }}"
diff --git a/test/integration/targets/incidental_vyos_config/tests/cli_config/cli_comment.yaml b/test/integration/targets/incidental_vyos_config/tests/cli_config/cli_comment.yaml
new file mode 100644
index 0000000..90ee1c8
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_config/tests/cli_config/cli_comment.yaml
@@ -0,0 +1,30 @@
+---
+- debug: msg="START cli_config/cli_comment.yaml on connection={{ ansible_connection }}"
+
+- name: setup
+ ansible.netcommon.cli_config: &rm
+ config: set system host-name {{ inventory_hostname_short }}
+
+- name: configure using comment
+ ansible.netcommon.cli_config:
+ config: set system host-name foo
+ commit_comment: this is a test
+ register: result
+
+- assert:
+ that:
+ - "result.changed == true"
+
+- name: collect system commits
+ vyos.vyos.vyos_command:
+ commands: show system commit
+ register: result
+
+- assert:
+ that:
+ - "'this is a test' in result.stdout_lines[0][1]"
+
+- name: teardown
+ ansible.netcommon.cli_config: *rm
+
+- debug: msg="END cli_config/cli_comment.yaml on connection={{ ansible_connection }}"
diff --git a/test/integration/targets/incidental_vyos_lldp_interfaces/aliases b/test/integration/targets/incidental_vyos_lldp_interfaces/aliases
new file mode 100644
index 0000000..fae06ba
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_lldp_interfaces/aliases
@@ -0,0 +1,2 @@
+shippable/vyos/incidental
+network/vyos
diff --git a/test/integration/targets/incidental_vyos_lldp_interfaces/defaults/main.yaml b/test/integration/targets/incidental_vyos_lldp_interfaces/defaults/main.yaml
new file mode 100644
index 0000000..164afea
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_lldp_interfaces/defaults/main.yaml
@@ -0,0 +1,3 @@
+---
+testcase: "[^_].*"
+test_items: []
diff --git a/test/integration/targets/incidental_vyos_lldp_interfaces/meta/main.yaml b/test/integration/targets/incidental_vyos_lldp_interfaces/meta/main.yaml
new file mode 100644
index 0000000..ee1fa01
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_lldp_interfaces/meta/main.yaml
@@ -0,0 +1,3 @@
+---
+dependencies:
+ - incidental_vyos_prepare_tests
diff --git a/test/integration/targets/incidental_vyos_lldp_interfaces/tasks/cli.yaml b/test/integration/targets/incidental_vyos_lldp_interfaces/tasks/cli.yaml
new file mode 100644
index 0000000..c6923f3
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_lldp_interfaces/tasks/cli.yaml
@@ -0,0 +1,19 @@
+---
+- name: Collect all cli test cases
+ find:
+ paths: "{{ role_path }}/tests/cli"
+ patterns: "{{ testcase }}.yaml"
+ use_regex: true
+ register: test_cases
+ delegate_to: localhost
+
+- name: Set test_items
+ set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
+
+- name: Run test case (connection=ansible.netcommon.network_cli)
+ include_tasks: "{{ test_case_to_run }}"
+ vars:
+ ansible_connection: ansible.netcommon.network_cli
+ with_items: "{{ test_items }}"
+ loop_control:
+ loop_var: test_case_to_run
diff --git a/test/integration/targets/incidental_vyos_lldp_interfaces/tasks/main.yaml b/test/integration/targets/incidental_vyos_lldp_interfaces/tasks/main.yaml
new file mode 100644
index 0000000..a6d418b
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_lldp_interfaces/tasks/main.yaml
@@ -0,0 +1,2 @@
+---
+- {import_tasks: cli.yaml, tags: ['cli']}
diff --git a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/_populate.yaml b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/_populate.yaml
new file mode 100644
index 0000000..3acded6
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/_populate.yaml
@@ -0,0 +1,14 @@
+---
+- name: Setup
+ ansible.netcommon.cli_config:
+ config: "{{ lines }}"
+ vars:
+ lines: |
+ set service lldp interface eth1
+ set service lldp interface eth1 location civic-based country-code US
+ set service lldp interface eth1 location civic-based ca-type 0 ca-value ENGLISH
+ set service lldp interface eth2
+ set service lldp interface eth2 location coordinate-based latitude 33.524449N
+ set service lldp interface eth2 location coordinate-based altitude 2200
+ set service lldp interface eth2 location coordinate-based datum WGS84
+ set service lldp interface eth2 location coordinate-based longitude 222.267255W
diff --git a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/_populate_intf.yaml b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/_populate_intf.yaml
new file mode 100644
index 0000000..c7ab1ae
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/_populate_intf.yaml
@@ -0,0 +1,10 @@
+---
+- name: Setup
+ ansible.netcommon.cli_config:
+ config: "{{ lines }}"
+ vars:
+ lines: |
+ set service lldp interface eth2
+ set service lldp interface eth2 location civic-based country-code US
+ set service lldp interface eth2 location civic-based ca-type 0 ca-value ENGLISH
+ set service lldp interface eth2 disable
diff --git a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/_remove_config.yaml b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/_remove_config.yaml
new file mode 100644
index 0000000..1b1a3b3
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/_remove_config.yaml
@@ -0,0 +1,8 @@
+---
+- name: Remove Config
+ ansible.netcommon.cli_config:
+ config: "{{ lines }}"
+ vars:
+ lines: |
+ delete service lldp interface
+ delete service lldp
diff --git a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/deleted.yaml b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/deleted.yaml
new file mode 100644
index 0000000..7b2d53a
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/deleted.yaml
@@ -0,0 +1,46 @@
+---
+- debug:
+ msg: "Start vyos_lldp_interfaces deleted integration tests ansible_connection={{ ansible_connection }}"
+
+- include_tasks: _populate.yaml
+
+- block:
+ - name: Delete attributes of given LLDP interfaces.
+ vyos.vyos.vyos_lldp_interfaces: &deleted
+ config:
+ - name: 'eth1'
+ - name: 'eth2'
+ state: deleted
+ register: result
+
+ - name: Assert that the before dicts were correctly generated
+ assert:
+ that:
+ - "{{ populate | symmetric_difference(result['before']) |length == 0 }}"
+
+ - name: Assert that the correct set of commands were generated
+ assert:
+ that:
+ - "{{ deleted['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
+
+ - name: Assert that the after dicts were correctly generated
+ assert:
+ that:
+ - "{{ deleted['after'] | symmetric_difference(result['after']) |length == 0 }}"
+
+ - name: Delete attributes of given interfaces (IDEMPOTENT)
+ vyos.vyos.vyos_lldp_interfaces: *deleted
+ register: result
+
+ - name: Assert that the previous task was idempotent
+ assert:
+ that:
+ - "result.changed == false"
+ - "result.commands|length == 0"
+
+ - name: Assert that the before dicts were correctly generated
+ assert:
+ that:
+ - "{{ deleted['after'] | symmetric_difference(result['before']) |length == 0 }}"
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/empty_config.yaml b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/empty_config.yaml
new file mode 100644
index 0000000..44c0b89
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/empty_config.yaml
@@ -0,0 +1,36 @@
+---
+- debug:
+ msg: "START vyos_lldp_interfaces empty_config integration tests on connection={{ ansible_connection }}"
+
+- name: Merged with empty config should give appropriate error message
+ vyos.vyos.vyos_lldp_interfaces:
+ config:
+ state: merged
+ register: result
+ ignore_errors: true
+
+- assert:
+ that:
+ - result.msg == 'value of config parameter must not be empty for state merged'
+
+- name: Replaced with empty config should give appropriate error message
+ vyos.vyos.vyos_lldp_interfaces:
+ config:
+ state: replaced
+ register: result
+ ignore_errors: true
+
+- assert:
+ that:
+ - result.msg == 'value of config parameter must not be empty for state replaced'
+
+- name: Overridden with empty config should give appropriate error message
+ vyos.vyos.vyos_lldp_interfaces:
+ config:
+ state: overridden
+ register: result
+ ignore_errors: true
+
+- assert:
+ that:
+ - result.msg == 'value of config parameter must not be empty for state overridden'
diff --git a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/merged.yaml b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/merged.yaml
new file mode 100644
index 0000000..bf968b2
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/merged.yaml
@@ -0,0 +1,58 @@
+---
+- debug:
+ msg: "START vyos_lldp_interfaces merged integration tests on connection={{ ansible_connection }}"
+
+- include_tasks: _remove_config.yaml
+
+- block:
+ - name: Merge the provided configuration with the exisiting running configuration
+ vyos.vyos.vyos_lldp_interfaces: &merged
+ config:
+ - name: 'eth1'
+ location:
+ civic_based:
+ country_code: 'US'
+ ca_info:
+ - ca_type: 0
+ ca_value: 'ENGLISH'
+
+ - name: 'eth2'
+ location:
+ coordinate_based:
+ altitude: 2200
+ datum: 'WGS84'
+ longitude: '222.267255W'
+ latitude: '33.524449N'
+ state: merged
+ register: result
+
+ - name: Assert that before dicts were correctly generated
+ assert:
+ that: "{{ merged['before'] | symmetric_difference(result['before']) |length == 0 }}"
+
+ - name: Assert that correct set of commands were generated
+ assert:
+ that:
+ - "{{ merged['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
+
+ - name: Assert that after dicts was correctly generated
+ assert:
+ that:
+ - "{{ merged['after'] | symmetric_difference(result['after']) |length == 0 }}"
+
+ - name: Merge the provided configuration with the existing running configuration (IDEMPOTENT)
+ vyos.vyos.vyos_lldp_interfaces: *merged
+ register: result
+
+ - name: Assert that the previous task was idempotent
+ assert:
+ that:
+ - "result['changed'] == false"
+
+ - name: Assert that before dicts were correctly generated
+ assert:
+ that:
+ - "{{ merged['after'] | symmetric_difference(result['before']) |length == 0 }}"
+
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/overridden.yaml b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/overridden.yaml
new file mode 100644
index 0000000..8cf038c
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/overridden.yaml
@@ -0,0 +1,49 @@
+---
+- debug:
+ msg: "START vyos_lldp_interfaces overridden integration tests on connection={{ ansible_connection }}"
+
+- include_tasks: _remove_config.yaml
+
+- include_tasks: _populate_intf.yaml
+
+- block:
+ - name: Overrides all device configuration with provided configuration
+ vyos.vyos.vyos_lldp_interfaces: &overridden
+ config:
+ - name: 'eth2'
+ location:
+ elin: '0000000911'
+ state: overridden
+ register: result
+
+ - name: Assert that before dicts were correctly generated
+ assert:
+ that:
+ - "{{ populate_intf | symmetric_difference(result['before']) |length == 0 }}"
+
+ - name: Assert that correct commands were generated
+ assert:
+ that:
+ - "{{ overridden['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
+
+ - name: Assert that after dicts were correctly generated
+ assert:
+ that:
+ - "{{ overridden['after'] | symmetric_difference(result['after']) |length == 0 }}"
+
+ - name: Overrides all device configuration with provided configurations (IDEMPOTENT)
+ vyos.vyos.vyos_lldp_interfaces: *overridden
+ register: result
+
+ - name: Assert that the previous task was idempotent
+ assert:
+ that:
+ - "result['changed'] == false"
+
+ - name: Assert that before dicts were correctly generated
+ assert:
+ that:
+ - "{{ overridden['after'] | symmetric_difference(result['before']) |length == 0 }}"
+
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/replaced.yaml b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/replaced.yaml
new file mode 100644
index 0000000..17acf06
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/replaced.yaml
@@ -0,0 +1,63 @@
+---
+- debug:
+ msg: "START vyos_lldp_interfaces replaced integration tests on connection={{ ansible_connection }}"
+
+- include_tasks: _remove_config.yaml
+
+- include_tasks: _populate.yaml
+
+- block:
+ - name: Replace device configurations of listed LLDP interfaces with provided configurations
+ vyos.vyos.vyos_lldp_interfaces: &replaced
+ config:
+ - name: 'eth2'
+ enable: false
+ location:
+ civic_based:
+ country_code: 'US'
+ ca_info:
+ - ca_type: 0
+ ca_value: 'ENGLISH'
+
+ - name: 'eth1'
+ enable: false
+ location:
+ coordinate_based:
+ altitude: 2200
+ datum: 'WGS84'
+ longitude: '222.267255W'
+ latitude: '33.524449N'
+ state: replaced
+ register: result
+
+ - name: Assert that correct set of commands were generated
+ assert:
+ that:
+ - "{{ replaced['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
+
+ - name: Assert that before dicts are correctly generated
+ assert:
+ that:
+ - "{{ populate | symmetric_difference(result['before']) |length == 0 }}"
+
+ - name: Assert that after dict is correctly generated
+ assert:
+ that:
+ - "{{ replaced['after'] | symmetric_difference(result['after']) |length == 0 }}"
+
+ - name: Replace device configurations of listed LLDP interfaces with provided configurarions (IDEMPOTENT)
+ vyos.vyos.vyos_lldp_interfaces: *replaced
+ register: result
+
+ - name: Assert that task was idempotent
+ assert:
+ that:
+ - "result['changed'] == false"
+
+ - name: Assert that before dict is correctly generated
+ assert:
+ that:
+ - "{{ replaced['after'] | symmetric_difference(result['before']) |length == 0 }}"
+
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/rtt.yaml b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/rtt.yaml
new file mode 100644
index 0000000..4d4cf82
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/rtt.yaml
@@ -0,0 +1,57 @@
+---
+- debug:
+ msg: "START vyos_lldp_interfaces round trip integration tests on connection={{ ansible_connection }}"
+
+- include_tasks: _remove_config.yaml
+
+- block:
+ - name: Apply the provided configuration (base config)
+ vyos.vyos.vyos_lldp_interfaces:
+ config:
+ - name: 'eth1'
+ location:
+ civic_based:
+ country_code: 'US'
+ ca_info:
+ - ca_type: 0
+ ca_value: 'ENGLISH'
+
+ state: merged
+ register: base_config
+
+ - name: Gather lldp_interfaces facts
+ vyos.vyos.vyos_facts:
+ gather_subset:
+ - default
+ gather_network_resources:
+ - lldp_interfaces
+
+ - name: Apply the provided configuration (config to be reverted)
+ vyos.vyos.vyos_lldp_interfaces:
+ config:
+ - name: 'eth2'
+ location:
+ coordinate_based:
+ altitude: 2200
+ datum: 'WGS84'
+ longitude: '222.267255W'
+ latitude: '33.524449N'
+ state: merged
+ register: result
+
+ - name: Assert that changes were applied
+ assert:
+ that: "{{ round_trip['after'] | symmetric_difference(result['after']) |length == 0 }}"
+
+ - name: Revert back to base config using facts round trip
+ vyos.vyos.vyos_lldp_interfaces:
+ config: "{{ ansible_facts['network_resources']['lldp_interfaces'] }}"
+ state: overridden
+ register: revert
+
+ - name: Assert that config was reverted
+ assert:
+ that: "{{ base_config['after'] | symmetric_difference(revert['after']) |length == 0 }}"
+
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/test/integration/targets/incidental_vyos_lldp_interfaces/vars/main.yaml b/test/integration/targets/incidental_vyos_lldp_interfaces/vars/main.yaml
new file mode 100644
index 0000000..169b0d5
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_lldp_interfaces/vars/main.yaml
@@ -0,0 +1,130 @@
+---
+merged:
+ before: []
+
+
+ commands:
+ - "set service lldp interface eth1 location civic-based country-code 'US'"
+ - "set service lldp interface eth1 location civic-based ca-type 0 ca-value 'ENGLISH'"
+ - "set service lldp interface eth1"
+ - "set service lldp interface eth2 location coordinate-based latitude '33.524449N'"
+ - "set service lldp interface eth2 location coordinate-based altitude '2200'"
+ - "set service lldp interface eth2 location coordinate-based datum 'WGS84'"
+ - "set service lldp interface eth2 location coordinate-based longitude '222.267255W'"
+ - "set service lldp interface eth2 location coordinate-based latitude '33.524449N'"
+ - "set service lldp interface eth2 location coordinate-based altitude '2200'"
+ - "set service lldp interface eth2 location coordinate-based datum 'WGS84'"
+ - "set service lldp interface eth2 location coordinate-based longitude '222.267255W'"
+ - "set service lldp interface eth2"
+
+ after:
+ - name: 'eth1'
+ location:
+ civic_based:
+ country_code: 'US'
+ ca_info:
+ - ca_type: 0
+ ca_value: 'ENGLISH'
+
+ - name: 'eth2'
+ location:
+ coordinate_based:
+ altitude: 2200
+ datum: 'WGS84'
+ longitude: '222.267255W'
+ latitude: '33.524449N'
+
+populate:
+ - name: 'eth1'
+ location:
+ civic_based:
+ country_code: 'US'
+ ca_info:
+ - ca_type: 0
+ ca_value: 'ENGLISH'
+
+ - name: 'eth2'
+ location:
+ coordinate_based:
+ altitude: 2200
+ datum: 'WGS84'
+ longitude: '222.267255W'
+ latitude: '33.524449N'
+
+replaced:
+ commands:
+ - "delete service lldp interface eth2 location"
+ - "set service lldp interface eth2 'disable'"
+ - "set service lldp interface eth2 location civic-based country-code 'US'"
+ - "set service lldp interface eth2 location civic-based ca-type 0 ca-value 'ENGLISH'"
+ - "delete service lldp interface eth1 location"
+ - "set service lldp interface eth1 'disable'"
+ - "set service lldp interface eth1 location coordinate-based latitude '33.524449N'"
+ - "set service lldp interface eth1 location coordinate-based altitude '2200'"
+ - "set service lldp interface eth1 location coordinate-based datum 'WGS84'"
+ - "set service lldp interface eth1 location coordinate-based longitude '222.267255W'"
+
+ after:
+ - name: 'eth2'
+ enable: false
+ location:
+ civic_based:
+ country_code: 'US'
+ ca_info:
+ - ca_type: 0
+ ca_value: 'ENGLISH'
+
+ - name: 'eth1'
+ enable: false
+ location:
+ coordinate_based:
+ altitude: 2200
+ datum: 'WGS84'
+ longitude: '222.267255W'
+ latitude: '33.524449N'
+
+populate_intf:
+ - name: 'eth2'
+ enable: false
+ location:
+ civic_based:
+ country_code: 'US'
+ ca_info:
+ - ca_type: 0
+ ca_value: 'ENGLISH'
+
+overridden:
+ commands:
+ - "delete service lldp interface eth2 location"
+ - "delete service lldp interface eth2 'disable'"
+ - "set service lldp interface eth2 location elin '0000000911'"
+
+ after:
+ - name: 'eth2'
+ location:
+ elin: 0000000911
+
+deleted:
+ commands:
+ - "delete service lldp interface eth1"
+ - "delete service lldp interface eth2"
+
+ after: []
+
+round_trip:
+ after:
+ - name: 'eth1'
+ location:
+ civic_based:
+ country_code: 'US'
+ ca_info:
+ - ca_type: 0
+ ca_value: 'ENGLISH'
+
+ - name: 'eth2'
+ location:
+ coordinate_based:
+ altitude: 2200
+ datum: 'WGS84'
+ longitude: '222.267255W'
+ latitude: '33.524449N'
diff --git a/test/integration/targets/incidental_vyos_prepare_tests/aliases b/test/integration/targets/incidental_vyos_prepare_tests/aliases
new file mode 100644
index 0000000..136c05e
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_prepare_tests/aliases
@@ -0,0 +1 @@
+hidden
diff --git a/test/integration/targets/incidental_vyos_prepare_tests/tasks/main.yaml b/test/integration/targets/incidental_vyos_prepare_tests/tasks/main.yaml
new file mode 100644
index 0000000..ac0b492
--- /dev/null
+++ b/test/integration/targets/incidental_vyos_prepare_tests/tasks/main.yaml
@@ -0,0 +1,13 @@
+---
+- name: Ensure required interfaces are present in running-config
+ ansible.netcommon.cli_config:
+ config: "{{ lines }}"
+ vars:
+ lines: |
+ set interfaces ethernet eth0 address dhcp
+ set interfaces ethernet eth0 speed auto
+ set interfaces ethernet eth0 duplex auto
+ set interfaces ethernet eth1
+ set interfaces ethernet eth2
+ delete interfaces loopback lo
+ ignore_errors: true
diff --git a/test/integration/targets/incidental_win_reboot/aliases b/test/integration/targets/incidental_win_reboot/aliases
new file mode 100644
index 0000000..a5fc90d
--- /dev/null
+++ b/test/integration/targets/incidental_win_reboot/aliases
@@ -0,0 +1,2 @@
+shippable/windows/incidental
+windows
diff --git a/test/integration/targets/incidental_win_reboot/tasks/main.yml b/test/integration/targets/incidental_win_reboot/tasks/main.yml
new file mode 100644
index 0000000..7757e08
--- /dev/null
+++ b/test/integration/targets/incidental_win_reboot/tasks/main.yml
@@ -0,0 +1,70 @@
+---
+- name: make sure win output dir exists
+ win_file:
+ path: "{{win_output_dir}}"
+ state: directory
+
+- name: reboot with defaults
+ win_reboot:
+
+- name: test with negative values for delays
+ win_reboot:
+ post_reboot_delay: -0.5
+ pre_reboot_delay: -61
+
+- name: schedule a reboot for sometime in the future
+ win_command: shutdown.exe /r /t 599
+
+- name: reboot with a shutdown already scheduled
+ win_reboot:
+
+# test a reboot that reboots again during the test_command phase
+- name: create test file
+ win_file:
+ path: '{{win_output_dir}}\win_reboot_test'
+ state: touch
+
+- name: reboot with secondary reboot stage
+ win_reboot:
+ test_command: '{{ lookup("template", "post_reboot.ps1") }}'
+
+- name: reboot with test command that fails
+ win_reboot:
+ test_command: 'FAIL'
+ reboot_timeout: 120
+ register: reboot_fail_test
+ failed_when: "reboot_fail_test.msg != 'Timed out waiting for post-reboot test command (timeout=120)'"
+
+- name: remove SeRemoteShutdownPrivilege
+ win_user_right:
+ name: SeRemoteShutdownPrivilege
+ users: []
+ action: set
+ register: removed_shutdown_privilege
+
+- block:
+ - name: try and reboot without required privilege
+ win_reboot:
+ register: fail_privilege
+ failed_when:
+ - "'Reboot command failed, error was:' not in fail_privilege.msg"
+ - "'Access is denied.(5)' not in fail_privilege.msg"
+
+ always:
+ - name: reset the SeRemoteShutdownPrivilege
+ win_user_right:
+ name: SeRemoteShutdownPrivilege
+ users: '{{ removed_shutdown_privilege.removed }}'
+ action: add
+
+- name: Use invalid parameter
+ reboot:
+ foo: bar
+ ignore_errors: true
+ register: invalid_parameter
+
+- name: Ensure task fails with error
+ assert:
+ that:
+ - invalid_parameter is failed
+ - "invalid_parameter.msg == 'Invalid options for reboot: foo'"
diff --git a/test/integration/targets/incidental_win_reboot/templates/post_reboot.ps1 b/test/integration/targets/incidental_win_reboot/templates/post_reboot.ps1
new file mode 100644
index 0000000..e4a99a7
--- /dev/null
+++ b/test/integration/targets/incidental_win_reboot/templates/post_reboot.ps1
@@ -0,0 +1,8 @@
+if (Test-Path -Path '{{win_output_dir}}\win_reboot_test') {
+ New-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' `
+ -Name PendingFileRenameOperations `
+ -Value @("\??\{{win_output_dir}}\win_reboot_test`0") `
+ -PropertyType MultiString
+ Restart-Computer -Force
+ exit 1
+}
diff --git a/test/integration/targets/include_import/aliases b/test/integration/targets/include_import/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/include_import/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/include_import/apply/import_apply.yml b/test/integration/targets/include_import/apply/import_apply.yml
new file mode 100644
index 0000000..27a4086
--- /dev/null
+++ b/test/integration/targets/include_import/apply/import_apply.yml
@@ -0,0 +1,31 @@
+---
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - import_tasks:
+ file: import_tasks.yml
+ apply:
+ tags:
+ - foo
+ tags:
+ - always
+
+ - assert:
+ that:
+ - include_tasks_result is defined
+ tags:
+ - always
+
+ - import_role:
+ name: import_role
+ apply:
+ tags:
+ - foo
+ tags:
+ - always
+
+ - assert:
+ that:
+ - include_role_result is defined
+ tags:
+ - always
diff --git a/test/integration/targets/include_import/apply/include_apply.yml b/test/integration/targets/include_import/apply/include_apply.yml
new file mode 100644
index 0000000..32c6e5e
--- /dev/null
+++ b/test/integration/targets/include_import/apply/include_apply.yml
@@ -0,0 +1,50 @@
+---
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - include_tasks:
+ file: include_tasks.yml
+ apply:
+ tags:
+ - foo
+ tags:
+ - always
+
+ - assert:
+ that:
+ - include_tasks_result is defined
+ tags:
+ - always
+
+ - include_role:
+ name: include_role
+ apply:
+ tags:
+ - foo
+ tags:
+ - always
+
+ - assert:
+ that:
+ - include_role_result is defined
+ tags:
+ - always
+
+ - include_role:
+ name: include_role2
+ apply:
+ tags:
+ - foo
+ tags:
+ - not_specified_on_purpose
+
+ - assert:
+ that:
+ - include_role2_result is undefined
+ tags:
+ - always
+
+ - include_role:
+ name: include_role
+ apply:
+ delegate_to: testhost2
diff --git a/test/integration/targets/include_import/apply/include_apply_65710.yml b/test/integration/targets/include_import/apply/include_apply_65710.yml
new file mode 100644
index 0000000..457aab8
--- /dev/null
+++ b/test/integration/targets/include_import/apply/include_apply_65710.yml
@@ -0,0 +1,11 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - include_tasks:
+ file: include_tasks.yml
+ apply:
+ tags: always
+
+ - assert:
+ that:
+ - include_tasks_result is defined
diff --git a/test/integration/targets/include_import/apply/include_tasks.yml b/test/integration/targets/include_import/apply/include_tasks.yml
new file mode 100644
index 0000000..be511d1
--- /dev/null
+++ b/test/integration/targets/include_import/apply/include_tasks.yml
@@ -0,0 +1,2 @@
+- set_fact:
+ include_tasks_result: true
diff --git a/test/integration/targets/include_import/apply/roles/include_role/tasks/main.yml b/test/integration/targets/include_import/apply/roles/include_role/tasks/main.yml
new file mode 100644
index 0000000..7f86b26
--- /dev/null
+++ b/test/integration/targets/include_import/apply/roles/include_role/tasks/main.yml
@@ -0,0 +1,2 @@
+- set_fact:
+ include_role_result: true
diff --git a/test/integration/targets/include_import/apply/roles/include_role2/tasks/main.yml b/test/integration/targets/include_import/apply/roles/include_role2/tasks/main.yml
new file mode 100644
index 0000000..028c30d
--- /dev/null
+++ b/test/integration/targets/include_import/apply/roles/include_role2/tasks/main.yml
@@ -0,0 +1,2 @@
+- set_fact:
+ include_role2_result: true
diff --git a/test/integration/targets/include_import/empty_group_warning/playbook.yml b/test/integration/targets/include_import/empty_group_warning/playbook.yml
new file mode 100644
index 0000000..6da5b7c
--- /dev/null
+++ b/test/integration/targets/include_import/empty_group_warning/playbook.yml
@@ -0,0 +1,13 @@
+---
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: Group
+ group_by:
+ key: test_{{ inventory_hostname }}
+
+- hosts: test_localhost
+ gather_facts: false
+ tasks:
+ - name: Print
+ import_tasks: tasks.yml
diff --git a/test/integration/targets/include_import/empty_group_warning/tasks.yml b/test/integration/targets/include_import/empty_group_warning/tasks.yml
new file mode 100644
index 0000000..2fbad77
--- /dev/null
+++ b/test/integration/targets/include_import/empty_group_warning/tasks.yml
@@ -0,0 +1,3 @@
+- name: test
+ debug:
+ msg: hello
diff --git a/test/integration/targets/include_import/grandchild/block_include_tasks.yml b/test/integration/targets/include_import/grandchild/block_include_tasks.yml
new file mode 100644
index 0000000..f8addcf
--- /dev/null
+++ b/test/integration/targets/include_import/grandchild/block_include_tasks.yml
@@ -0,0 +1,2 @@
+- command: "true"
+ register: block_include_result
diff --git a/test/integration/targets/include_import/grandchild/import.yml b/test/integration/targets/include_import/grandchild/import.yml
new file mode 100644
index 0000000..ef6990e
--- /dev/null
+++ b/test/integration/targets/include_import/grandchild/import.yml
@@ -0,0 +1 @@
+- include_tasks: include_level_1.yml
diff --git a/test/integration/targets/include_import/grandchild/import_include_include_tasks.yml b/test/integration/targets/include_import/grandchild/import_include_include_tasks.yml
new file mode 100644
index 0000000..dae3a24
--- /dev/null
+++ b/test/integration/targets/include_import/grandchild/import_include_include_tasks.yml
@@ -0,0 +1,2 @@
+- command: "true"
+ register: import_include_include_result
diff --git a/test/integration/targets/include_import/grandchild/include_level_1.yml b/test/integration/targets/include_import/grandchild/include_level_1.yml
new file mode 100644
index 0000000..e323511
--- /dev/null
+++ b/test/integration/targets/include_import/grandchild/include_level_1.yml
@@ -0,0 +1 @@
+- include_tasks: import_include_include_tasks.yml
diff --git a/test/integration/targets/include_import/handler_addressing/playbook.yml b/test/integration/targets/include_import/handler_addressing/playbook.yml
new file mode 100644
index 0000000..7515dc9
--- /dev/null
+++ b/test/integration/targets/include_import/handler_addressing/playbook.yml
@@ -0,0 +1,11 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - import_role:
+ name: include_handler_test
+
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - import_role:
+ name: import_handler_test
diff --git a/test/integration/targets/include_import/handler_addressing/roles/import_handler_test/handlers/main.yml b/test/integration/targets/include_import/handler_addressing/roles/import_handler_test/handlers/main.yml
new file mode 100644
index 0000000..95524ed
--- /dev/null
+++ b/test/integration/targets/include_import/handler_addressing/roles/import_handler_test/handlers/main.yml
@@ -0,0 +1,2 @@
+- name: do_import
+ import_tasks: tasks/handlers.yml
diff --git a/test/integration/targets/include_import/handler_addressing/roles/import_handler_test/tasks/handlers.yml b/test/integration/targets/include_import/handler_addressing/roles/import_handler_test/tasks/handlers.yml
new file mode 100644
index 0000000..eeb49ff
--- /dev/null
+++ b/test/integration/targets/include_import/handler_addressing/roles/import_handler_test/tasks/handlers.yml
@@ -0,0 +1,2 @@
+- debug:
+ msg: import handler task
diff --git a/test/integration/targets/include_import/handler_addressing/roles/import_handler_test/tasks/main.yml b/test/integration/targets/include_import/handler_addressing/roles/import_handler_test/tasks/main.yml
new file mode 100644
index 0000000..b0312cc
--- /dev/null
+++ b/test/integration/targets/include_import/handler_addressing/roles/import_handler_test/tasks/main.yml
@@ -0,0 +1,3 @@
+- command: "true"
+ notify:
+ - do_import
diff --git a/test/integration/targets/include_import/handler_addressing/roles/include_handler_test/handlers/main.yml b/test/integration/targets/include_import/handler_addressing/roles/include_handler_test/handlers/main.yml
new file mode 100644
index 0000000..7f24b9d
--- /dev/null
+++ b/test/integration/targets/include_import/handler_addressing/roles/include_handler_test/handlers/main.yml
@@ -0,0 +1,2 @@
+- name: do_include
+ include_tasks: tasks/handlers.yml
diff --git a/test/integration/targets/include_import/handler_addressing/roles/include_handler_test/tasks/handlers.yml b/test/integration/targets/include_import/handler_addressing/roles/include_handler_test/tasks/handlers.yml
new file mode 100644
index 0000000..2bf07f2
--- /dev/null
+++ b/test/integration/targets/include_import/handler_addressing/roles/include_handler_test/tasks/handlers.yml
@@ -0,0 +1,2 @@
+- debug:
+ msg: include handler task
diff --git a/test/integration/targets/include_import/handler_addressing/roles/include_handler_test/tasks/main.yml b/test/integration/targets/include_import/handler_addressing/roles/include_handler_test/tasks/main.yml
new file mode 100644
index 0000000..c29a787
--- /dev/null
+++ b/test/integration/targets/include_import/handler_addressing/roles/include_handler_test/tasks/main.yml
@@ -0,0 +1,3 @@
+- command: "true"
+ notify:
+ - do_include
diff --git a/test/integration/targets/include_import/include_role_omit/playbook.yml b/test/integration/targets/include_import/include_role_omit/playbook.yml
new file mode 100644
index 0000000..a036906
--- /dev/null
+++ b/test/integration/targets/include_import/include_role_omit/playbook.yml
@@ -0,0 +1,12 @@
+- hosts: localhost
+ gather_facts: false
+ vars:
+ include_role_omit: false
+ tasks:
+ - include_role:
+ name: foo
+ tasks_from: '{{ omit }}'
+
+ - assert:
+ that:
+ - include_role_omit is sameas(true)
diff --git a/test/integration/targets/include_import/include_role_omit/roles/foo/tasks/main.yml b/test/integration/targets/include_import/include_role_omit/roles/foo/tasks/main.yml
new file mode 100644
index 0000000..e27ca5b
--- /dev/null
+++ b/test/integration/targets/include_import/include_role_omit/roles/foo/tasks/main.yml
@@ -0,0 +1,2 @@
+- set_fact:
+ include_role_omit: true
diff --git a/test/integration/targets/include_import/inventory b/test/integration/targets/include_import/inventory
new file mode 100644
index 0000000..3ae8d9c
--- /dev/null
+++ b/test/integration/targets/include_import/inventory
@@ -0,0 +1,6 @@
+[local]
+testhost ansible_connection=local host_var_role_name=role3
+testhost2 ansible_connection=local host_var_role_name=role2
+
+[local:vars]
+ansible_python_interpreter="{{ ansible_playbook_python }}"
diff --git a/test/integration/targets/include_import/issue73657.yml b/test/integration/targets/include_import/issue73657.yml
new file mode 100644
index 0000000..b692ccb
--- /dev/null
+++ b/test/integration/targets/include_import/issue73657.yml
@@ -0,0 +1,8 @@
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - block:
+ - include_tasks: issue73657_tasks.yml
+ rescue:
+ - debug:
+ msg: SHOULD_NOT_EXECUTE
diff --git a/test/integration/targets/include_import/issue73657_tasks.yml b/test/integration/targets/include_import/issue73657_tasks.yml
new file mode 100644
index 0000000..7247d76
--- /dev/null
+++ b/test/integration/targets/include_import/issue73657_tasks.yml
@@ -0,0 +1,2 @@
+- wrong.wrong.wrong:
+ parser: error
diff --git a/test/integration/targets/include_import/nestedtasks/nested/nested.yml b/test/integration/targets/include_import/nestedtasks/nested/nested.yml
new file mode 100644
index 0000000..95fe266
--- /dev/null
+++ b/test/integration/targets/include_import/nestedtasks/nested/nested.yml
@@ -0,0 +1,2 @@
+---
+- include_role: {name: nested_include_task}
diff --git a/test/integration/targets/include_import/parent_templating/playbook.yml b/test/integration/targets/include_import/parent_templating/playbook.yml
new file mode 100644
index 0000000..b733020
--- /dev/null
+++ b/test/integration/targets/include_import/parent_templating/playbook.yml
@@ -0,0 +1,11 @@
+# https://github.com/ansible/ansible/issues/49969
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - include_role:
+ name: test
+ public: true
+
+ - assert:
+ that:
+ - included_other is defined
diff --git a/test/integration/targets/include_import/parent_templating/roles/test/tasks/localhost.yml b/test/integration/targets/include_import/parent_templating/roles/test/tasks/localhost.yml
new file mode 100644
index 0000000..e5b281e
--- /dev/null
+++ b/test/integration/targets/include_import/parent_templating/roles/test/tasks/localhost.yml
@@ -0,0 +1 @@
+- include_tasks: other.yml
diff --git a/test/integration/targets/include_import/parent_templating/roles/test/tasks/main.yml b/test/integration/targets/include_import/parent_templating/roles/test/tasks/main.yml
new file mode 100644
index 0000000..16fba69
--- /dev/null
+++ b/test/integration/targets/include_import/parent_templating/roles/test/tasks/main.yml
@@ -0,0 +1 @@
+- include_tasks: "{{ lookup('first_found', inventory_hostname ~ '.yml') }}"
diff --git a/test/integration/targets/include_import/parent_templating/roles/test/tasks/other.yml b/test/integration/targets/include_import/parent_templating/roles/test/tasks/other.yml
new file mode 100644
index 0000000..c3bae1a
--- /dev/null
+++ b/test/integration/targets/include_import/parent_templating/roles/test/tasks/other.yml
@@ -0,0 +1,2 @@
+- set_fact:
+ included_other: true
diff --git a/test/integration/targets/include_import/playbook/group_vars/all.yml b/test/integration/targets/include_import/playbook/group_vars/all.yml
new file mode 100644
index 0000000..9acd8c6
--- /dev/null
+++ b/test/integration/targets/include_import/playbook/group_vars/all.yml
@@ -0,0 +1 @@
+group_var1: set in group_vars/all.yml
diff --git a/test/integration/targets/include_import/playbook/playbook1.yml b/test/integration/targets/include_import/playbook/playbook1.yml
new file mode 100644
index 0000000..55c66d8
--- /dev/null
+++ b/test/integration/targets/include_import/playbook/playbook1.yml
@@ -0,0 +1,9 @@
+- name: Playbook 1
+ hosts: testhost2
+
+ tasks:
+ - name: Set fact in playbook 1
+ set_fact:
+ canary_var1: playbook1 imported
+ tags:
+ - canary1
diff --git a/test/integration/targets/include_import/playbook/playbook2.yml b/test/integration/targets/include_import/playbook/playbook2.yml
new file mode 100644
index 0000000..c986165
--- /dev/null
+++ b/test/integration/targets/include_import/playbook/playbook2.yml
@@ -0,0 +1,9 @@
+- name: Playbook 2
+ hosts: testhost2
+
+ tasks:
+ - name: Set fact in playbook 2
+ set_fact:
+ canary_var2: playbook2 imported
+ tags:
+ - canary2
diff --git a/test/integration/targets/include_import/playbook/playbook3.yml b/test/integration/targets/include_import/playbook/playbook3.yml
new file mode 100644
index 0000000..b62b96c
--- /dev/null
+++ b/test/integration/targets/include_import/playbook/playbook3.yml
@@ -0,0 +1,10 @@
+- name: Playbook 3
+ hosts: testhost2
+
+ tasks:
+ - name: Set fact in playbook 3
+ set_fact:
+ canary_var3: playbook3 imported
+ include_next_playbook: yes
+ tags:
+ - canary3
diff --git a/test/integration/targets/include_import/playbook/playbook4.yml b/test/integration/targets/include_import/playbook/playbook4.yml
new file mode 100644
index 0000000..330612a
--- /dev/null
+++ b/test/integration/targets/include_import/playbook/playbook4.yml
@@ -0,0 +1,9 @@
+- name: Playbook 4
+ hosts: testhost2
+
+ tasks:
+ - name: Set fact in playbook 4
+ set_fact:
+ canary_var4: playbook4 imported
+ tags:
+ - canary4
diff --git a/test/integration/targets/include_import/playbook/playbook_needing_vars.yml b/test/integration/targets/include_import/playbook/playbook_needing_vars.yml
new file mode 100644
index 0000000..6454502
--- /dev/null
+++ b/test/integration/targets/include_import/playbook/playbook_needing_vars.yml
@@ -0,0 +1,6 @@
+---
+- hosts: testhost
+ gather_facts: no
+ tasks:
+ - import_role:
+ name: "{{ import_playbook_role_name }}"
diff --git a/test/integration/targets/include_import/playbook/roles/import_playbook_role/tasks/main.yml b/test/integration/targets/include_import/playbook/roles/import_playbook_role/tasks/main.yml
new file mode 100644
index 0000000..7755439
--- /dev/null
+++ b/test/integration/targets/include_import/playbook/roles/import_playbook_role/tasks/main.yml
@@ -0,0 +1,2 @@
+- debug:
+ msg: in import_playbook_role
diff --git a/test/integration/targets/include_import/playbook/sub_playbook/library/helloworld.py b/test/integration/targets/include_import/playbook/sub_playbook/library/helloworld.py
new file mode 100644
index 0000000..0ebe690
--- /dev/null
+++ b/test/integration/targets/include_import/playbook/sub_playbook/library/helloworld.py
@@ -0,0 +1,30 @@
+#!/usr/bin/python
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(argument_spec={})
+
+ module.exit_json(msg='Hello, World!')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/include_import/playbook/sub_playbook/sub_playbook.yml b/test/integration/targets/include_import/playbook/sub_playbook/sub_playbook.yml
new file mode 100644
index 0000000..4399d93
--- /dev/null
+++ b/test/integration/targets/include_import/playbook/sub_playbook/sub_playbook.yml
@@ -0,0 +1,4 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - helloworld:
diff --git a/test/integration/targets/include_import/playbook/test_import_playbook.yml b/test/integration/targets/include_import/playbook/test_import_playbook.yml
new file mode 100644
index 0000000..4fcdb10
--- /dev/null
+++ b/test/integration/targets/include_import/playbook/test_import_playbook.yml
@@ -0,0 +1,22 @@
+# Test and validate playbook import
+- import_playbook: playbook1.yml
+- import_playbook: validate1.yml
+
+# Test and validate conditional import
+- import_playbook: playbook2.yml
+ when: no
+
+- import_playbook: validate2.yml
+
+- import_playbook: playbook3.yml
+- import_playbook: playbook4.yml
+ when: include_next_playbook
+
+- import_playbook: validate34.yml
+
+- import_playbook: playbook_needing_vars.yml
+ vars:
+ import_playbook_role_name: import_playbook_role
+
+# https://github.com/ansible/ansible/issues/59548
+- import_playbook: sub_playbook/sub_playbook.yml
diff --git a/test/integration/targets/include_import/playbook/test_import_playbook_tags.yml b/test/integration/targets/include_import/playbook/test_import_playbook_tags.yml
new file mode 100644
index 0000000..46136f6
--- /dev/null
+++ b/test/integration/targets/include_import/playbook/test_import_playbook_tags.yml
@@ -0,0 +1,10 @@
+- import_playbook: playbook1.yml # Test tag in tasks in included play
+- import_playbook: playbook2.yml # Test tag added to import_playbook
+ tags:
+ - canary22
+
+- import_playbook: playbook3.yml # Test skipping tags added to import_playbook
+ tags:
+ - skipme
+
+- import_playbook: validate_tags.yml # Validate
diff --git a/test/integration/targets/include_import/playbook/test_templated_filenames.yml b/test/integration/targets/include_import/playbook/test_templated_filenames.yml
new file mode 100644
index 0000000..2f78ab0
--- /dev/null
+++ b/test/integration/targets/include_import/playbook/test_templated_filenames.yml
@@ -0,0 +1,47 @@
+- name: test templating import_playbook with extra vars
+ import_playbook: "{{ pb }}"
+
+- name: test templating import_playbook with vars
+ import_playbook: "{{ test_var }}"
+ vars:
+ test_var: validate_templated_playbook.yml
+
+- name: test templating import_tasks
+ hosts: localhost
+ gather_facts: no
+ vars:
+ play_var: validate_templated_tasks.yml
+ tasks:
+ - name: test templating import_tasks with play vars
+ import_tasks: "{{ play_var }}"
+
+ - name: test templating import_tasks with task vars
+ import_tasks: "{{ task_var }}"
+ vars:
+ task_var: validate_templated_tasks.yml
+
+ - name: test templating import_tasks with extra vars
+ import_tasks: "{{ tasks }}"
+
+- name: test templating import_role from_files
+ hosts: localhost
+ gather_facts: no
+ vars:
+ play_var: templated.yml
+ tasks:
+ - name: test templating import_role tasks_from with play vars
+ import_role:
+ name: role1
+ tasks_from: "{{ play_var }}"
+
+ - name: test templating import_role tasks_from with task vars
+ import_role:
+ name: role1
+ tasks_from: "{{ task_var }}"
+ vars:
+ task_var: templated.yml
+
+ - name: test templating import_role tasks_from with extra vars
+ import_role:
+ name: role1
+ tasks_from: "{{ tasks_from }}"
diff --git a/test/integration/targets/include_import/playbook/validate1.yml b/test/integration/targets/include_import/playbook/validate1.yml
new file mode 100644
index 0000000..0018344
--- /dev/null
+++ b/test/integration/targets/include_import/playbook/validate1.yml
@@ -0,0 +1,10 @@
+- hosts: testhost2
+
+ tasks:
+ - name: Assert that variable was set in playbook1.yml
+ assert:
+ that:
+ - canary_var1 == 'playbook1 imported'
+ tags:
+ - validate
+ - validate1
diff --git a/test/integration/targets/include_import/playbook/validate2.yml b/test/integration/targets/include_import/playbook/validate2.yml
new file mode 100644
index 0000000..f22bcb6
--- /dev/null
+++ b/test/integration/targets/include_import/playbook/validate2.yml
@@ -0,0 +1,10 @@
+- hosts: testhost2
+
+ tasks:
+ - name: Assert that playbook2.yml was skipeed
+ assert:
+ that:
+ - canary_var2 is not defined
+ tags:
+ - validate
+ - validate2
diff --git a/test/integration/targets/include_import/playbook/validate34.yml b/test/integration/targets/include_import/playbook/validate34.yml
new file mode 100644
index 0000000..fd53a30
--- /dev/null
+++ b/test/integration/targets/include_import/playbook/validate34.yml
@@ -0,0 +1,11 @@
+- hosts: testhost2
+
+ tasks:
+ - name: Assert that playbook3.yml and playbook4.yml were imported
+ assert:
+ that:
+ - canary_var3 == 'playbook3 imported'
+ - canary_var4 == 'playbook4 imported'
+ tags:
+ - validate
+ - validate34
diff --git a/test/integration/targets/include_import/playbook/validate_tags.yml b/test/integration/targets/include_import/playbook/validate_tags.yml
new file mode 100644
index 0000000..acdcb1f
--- /dev/null
+++ b/test/integration/targets/include_import/playbook/validate_tags.yml
@@ -0,0 +1,11 @@
+- hosts: testhost2
+
+ tasks:
+ - name: Assert that only tasks with tags were run
+ assert:
+ that:
+ - canary_var1 == 'playbook1 imported'
+ - canary_var2 == 'playbook2 imported'
+ - canary_var3 is not defined
+ tags:
+ - validate
diff --git a/test/integration/targets/include_import/playbook/validate_templated_playbook.yml b/test/integration/targets/include_import/playbook/validate_templated_playbook.yml
new file mode 100644
index 0000000..631ee9b
--- /dev/null
+++ b/test/integration/targets/include_import/playbook/validate_templated_playbook.yml
@@ -0,0 +1,5 @@
+---
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - debug: msg="In imported playbook"
diff --git a/test/integration/targets/include_import/playbook/validate_templated_tasks.yml b/test/integration/targets/include_import/playbook/validate_templated_tasks.yml
new file mode 100644
index 0000000..16d682d
--- /dev/null
+++ b/test/integration/targets/include_import/playbook/validate_templated_tasks.yml
@@ -0,0 +1 @@
+- debug: msg="In imported tasks"
diff --git a/test/integration/targets/include_import/public_exposure/no_bleeding.yml b/test/integration/targets/include_import/public_exposure/no_bleeding.yml
new file mode 100644
index 0000000..b9db713
--- /dev/null
+++ b/test/integration/targets/include_import/public_exposure/no_bleeding.yml
@@ -0,0 +1,25 @@
+---
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - name: Static imports should expose vars at parse time, not at execution time
+ assert:
+ that:
+ - static_defaults_var == 'static_defaults'
+ - static_vars_var == 'static_vars'
+ - import_role:
+ name: static
+ - assert:
+ that:
+ - static_tasks_var == 'static_tasks'
+ - static_defaults_var == 'static_defaults'
+ - static_vars_var == 'static_vars'
+
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - name: Ensure vars from import_roles do not bleed between plays
+ assert:
+ that:
+ - static_defaults_var is undefined
+ - static_vars_var is undefined
diff --git a/test/integration/targets/include_import/public_exposure/no_overwrite_roles.yml b/test/integration/targets/include_import/public_exposure/no_overwrite_roles.yml
new file mode 100644
index 0000000..6a1d9bf
--- /dev/null
+++ b/test/integration/targets/include_import/public_exposure/no_overwrite_roles.yml
@@ -0,0 +1,4 @@
+- hosts: localhost
+ gather_facts: false
+ roles:
+ - call_import
diff --git a/test/integration/targets/include_import/public_exposure/playbook.yml b/test/integration/targets/include_import/public_exposure/playbook.yml
new file mode 100644
index 0000000..11735e7
--- /dev/null
+++ b/test/integration/targets/include_import/public_exposure/playbook.yml
@@ -0,0 +1,56 @@
+---
+- hosts: testhost
+ gather_facts: false
+ roles:
+ - regular
+ tasks:
+ - debug:
+ msg: start tasks
+
+ - name: Static imports should expose vars at parse time, not at execution time
+ assert:
+ that:
+ - static_defaults_var == 'static_defaults'
+ - static_vars_var == 'static_vars'
+ - import_role:
+ name: static
+ - assert:
+ that:
+ - static_tasks_var == 'static_tasks'
+ - static_defaults_var == 'static_defaults'
+ - static_vars_var == 'static_vars'
+
+ - include_role:
+ name: dynamic_private
+ - assert:
+ that:
+ - private_tasks_var == 'private_tasks'
+ - private_defaults_var is undefined
+ - private_vars_var is undefined
+
+ - name: Dynamic include should not expose vars until execution time
+ assert:
+ that:
+ - dynamic_tasks_var is undefined
+ - dynamic_defaults_var is undefined
+ - dynamic_vars_var is undefined
+ - include_role:
+ name: dynamic
+ public: true
+ - assert:
+ that:
+ - dynamic_tasks_var == 'dynamic_tasks'
+ - dynamic_defaults_var == 'dynamic_defaults'
+ - dynamic_vars_var == 'dynamic_vars'
+
+ - include_role:
+ name: from
+ public: true
+ tasks_from: from.yml
+ vars_from: from.yml
+ defaults_from: from.yml
+ - assert:
+ that:
+ - from_tasks_var == 'from_tasks'
+ - from_defaults_var == 'from_defaults'
+ - from_vars_var == 'from_vars'
diff --git a/test/integration/targets/include_import/public_exposure/roles/call_import/tasks/main.yml b/test/integration/targets/include_import/public_exposure/roles/call_import/tasks/main.yml
new file mode 100644
index 0000000..d6b28f0
--- /dev/null
+++ b/test/integration/targets/include_import/public_exposure/roles/call_import/tasks/main.yml
@@ -0,0 +1,6 @@
+- import_role:
+ name: regular
+
+- assert:
+ that:
+ - regular_defaults_var is defined
diff --git a/test/integration/targets/include_import/public_exposure/roles/dynamic/defaults/main.yml b/test/integration/targets/include_import/public_exposure/roles/dynamic/defaults/main.yml
new file mode 100644
index 0000000..099ac29
--- /dev/null
+++ b/test/integration/targets/include_import/public_exposure/roles/dynamic/defaults/main.yml
@@ -0,0 +1 @@
+dynamic_defaults_var: dynamic_defaults
diff --git a/test/integration/targets/include_import/public_exposure/roles/dynamic/tasks/main.yml b/test/integration/targets/include_import/public_exposure/roles/dynamic/tasks/main.yml
new file mode 100644
index 0000000..e9b9ad3
--- /dev/null
+++ b/test/integration/targets/include_import/public_exposure/roles/dynamic/tasks/main.yml
@@ -0,0 +1,5 @@
+- debug:
+ msg: dynamic
+
+- set_fact:
+ dynamic_tasks_var: dynamic_tasks
diff --git a/test/integration/targets/include_import/public_exposure/roles/dynamic/vars/main.yml b/test/integration/targets/include_import/public_exposure/roles/dynamic/vars/main.yml
new file mode 100644
index 0000000..b33c12d
--- /dev/null
+++ b/test/integration/targets/include_import/public_exposure/roles/dynamic/vars/main.yml
@@ -0,0 +1 @@
+dynamic_vars_var: dynamic_vars
diff --git a/test/integration/targets/include_import/public_exposure/roles/dynamic_private/defaults/main.yml b/test/integration/targets/include_import/public_exposure/roles/dynamic_private/defaults/main.yml
new file mode 100644
index 0000000..b19ef72
--- /dev/null
+++ b/test/integration/targets/include_import/public_exposure/roles/dynamic_private/defaults/main.yml
@@ -0,0 +1 @@
+private_defaults_var: private_defaults
diff --git a/test/integration/targets/include_import/public_exposure/roles/dynamic_private/tasks/main.yml b/test/integration/targets/include_import/public_exposure/roles/dynamic_private/tasks/main.yml
new file mode 100644
index 0000000..1c7f653
--- /dev/null
+++ b/test/integration/targets/include_import/public_exposure/roles/dynamic_private/tasks/main.yml
@@ -0,0 +1,5 @@
+- debug:
+ msg: private
+
+- set_fact:
+ private_tasks_var: private_tasks
diff --git a/test/integration/targets/include_import/public_exposure/roles/dynamic_private/vars/main.yml b/test/integration/targets/include_import/public_exposure/roles/dynamic_private/vars/main.yml
new file mode 100644
index 0000000..60f7ca8
--- /dev/null
+++ b/test/integration/targets/include_import/public_exposure/roles/dynamic_private/vars/main.yml
@@ -0,0 +1 @@
+private_vars_var: private_vars
diff --git a/test/integration/targets/include_import/public_exposure/roles/from/defaults/from.yml b/test/integration/targets/include_import/public_exposure/roles/from/defaults/from.yml
new file mode 100644
index 0000000..6729c4b
--- /dev/null
+++ b/test/integration/targets/include_import/public_exposure/roles/from/defaults/from.yml
@@ -0,0 +1 @@
+from_defaults_var: from_defaults
diff --git a/test/integration/targets/include_import/public_exposure/roles/from/tasks/from.yml b/test/integration/targets/include_import/public_exposure/roles/from/tasks/from.yml
new file mode 100644
index 0000000..932efc9
--- /dev/null
+++ b/test/integration/targets/include_import/public_exposure/roles/from/tasks/from.yml
@@ -0,0 +1,5 @@
+- debug:
+ msg: from
+
+- set_fact:
+ from_tasks_var: from_tasks
diff --git a/test/integration/targets/include_import/public_exposure/roles/from/vars/from.yml b/test/integration/targets/include_import/public_exposure/roles/from/vars/from.yml
new file mode 100644
index 0000000..98b2ad4
--- /dev/null
+++ b/test/integration/targets/include_import/public_exposure/roles/from/vars/from.yml
@@ -0,0 +1 @@
+from_vars_var: from_vars
diff --git a/test/integration/targets/include_import/public_exposure/roles/regular/defaults/main.yml b/test/integration/targets/include_import/public_exposure/roles/regular/defaults/main.yml
new file mode 100644
index 0000000..21a6967
--- /dev/null
+++ b/test/integration/targets/include_import/public_exposure/roles/regular/defaults/main.yml
@@ -0,0 +1 @@
+regular_defaults_var: regular_defaults
diff --git a/test/integration/targets/include_import/public_exposure/roles/regular/tasks/main.yml b/test/integration/targets/include_import/public_exposure/roles/regular/tasks/main.yml
new file mode 100644
index 0000000..eafa141
--- /dev/null
+++ b/test/integration/targets/include_import/public_exposure/roles/regular/tasks/main.yml
@@ -0,0 +1,5 @@
+- debug:
+ msg: regular
+
+- set_fact:
+ regular_tasks_var: regular_tasks
diff --git a/test/integration/targets/include_import/public_exposure/roles/regular/vars/main.yml b/test/integration/targets/include_import/public_exposure/roles/regular/vars/main.yml
new file mode 100644
index 0000000..3d06546
--- /dev/null
+++ b/test/integration/targets/include_import/public_exposure/roles/regular/vars/main.yml
@@ -0,0 +1 @@
+regular_vars_var: regular_vars
diff --git a/test/integration/targets/include_import/public_exposure/roles/static/defaults/main.yml b/test/integration/targets/include_import/public_exposure/roles/static/defaults/main.yml
new file mode 100644
index 0000000..d88f555
--- /dev/null
+++ b/test/integration/targets/include_import/public_exposure/roles/static/defaults/main.yml
@@ -0,0 +1 @@
+static_defaults_var: static_defaults
diff --git a/test/integration/targets/include_import/public_exposure/roles/static/tasks/main.yml b/test/integration/targets/include_import/public_exposure/roles/static/tasks/main.yml
new file mode 100644
index 0000000..5a6488c
--- /dev/null
+++ b/test/integration/targets/include_import/public_exposure/roles/static/tasks/main.yml
@@ -0,0 +1,5 @@
+- debug:
+ msg: static
+
+- set_fact:
+ static_tasks_var: static_tasks
diff --git a/test/integration/targets/include_import/public_exposure/roles/static/vars/main.yml b/test/integration/targets/include_import/public_exposure/roles/static/vars/main.yml
new file mode 100644
index 0000000..982e34d
--- /dev/null
+++ b/test/integration/targets/include_import/public_exposure/roles/static/vars/main.yml
@@ -0,0 +1 @@
+static_vars_var: static_vars
diff --git a/test/integration/targets/include_import/role/test_import_role.yml b/test/integration/targets/include_import/role/test_import_role.yml
new file mode 100644
index 0000000..d45ff79
--- /dev/null
+++ b/test/integration/targets/include_import/role/test_import_role.yml
@@ -0,0 +1,139 @@
+- name: Test import_role
+ hosts: testhost
+
+ vars:
+ run_role: yes
+ do_not_run_role: no
+ role_name: role1
+ test_var: templating test in playbook
+ role_vars:
+ where_am_i_defined: in the playbook
+ entire_task:
+ include_role:
+ name: role1
+
+ tasks:
+ - name: Test basic role import
+ import_role:
+ name: role1
+
+ - name: Assert that basic include works
+ assert:
+ that:
+ - _role1_result.msg == 'In role1'
+
+ - name: Test conditional role include
+ import_role:
+ name: role1
+ tasks_from: canary1.yml
+ when: run_role
+
+ - name: Assert that role ran
+ assert:
+ that:
+ - role1_canary1 == 'r1c1'
+
+ - name: Test conditional role import that should be skipped
+ import_role:
+ name: role1
+ tasks_from: canary2.yml
+ when: do_not_run_role
+
+ - name: Assert that role did not run
+ assert:
+ that:
+ - role1_canary2 is not defined
+
+ # FIXME We expect this to fail, but I'm not sure how best to test for
+ # syntax level failures.
+ #
+ # - name: Test role import with a loop
+ # import_role:
+ # name: "{{ item }}"
+ # register: loop_test
+ # with_items:
+ # - role1
+ # - role3
+ # - role2
+
+ - name: Test importing a task file from a role
+ import_role:
+ name: role1
+ tasks_from: tasks.yml
+
+ - name: Test importing vars file and tasks file from a role
+ import_role:
+ name: role3
+ tasks_from: vartest.yml
+ vars_from: role3vars.yml
+
+ - name: Assert that variables defined in previous task are available to play
+ assert:
+ that:
+ - role3_default == 'defined in role3/defaults/main.yml'
+ - role3_main == 'defined in role3/vars/main.yml'
+ - role3_var == 'defined in role3/vars/role3vars.yml'
+ ignore_errors: yes
+
+ - name: Test using a play variable for role name
+ import_role:
+ name: "{{ role_name }}"
+
+ # FIXME Trying to use a host_var here causes play execution to fail because
+ # the variable is undefined.
+ #
+ # - name: Test using a host variable for role name
+ # import_role:
+ # name: "{{ host_var_role_name }}"
+
+ - name: Pass variable to role
+ import_role:
+ name: role1
+ tasks_from: vartest.yml
+ vars:
+ where_am_i_defined: in the task
+
+ ## FIXME Currently failing
+ ## ERROR! Vars in a IncludeRole must be specified as a dictionary, or a list of dictionaries
+ # - name: Pass all variables in a variable to role
+ # import_role:
+ # name: role1
+ # tasks_from: vartest.yml
+ # vars: "{{ role_vars }}"
+
+ - name: Pass templated variable to a role
+ import_role:
+ name: role1
+ tasks_from: vartest.yml
+ vars:
+ where_am_i_defined: "{{ test_var }}"
+
+ # FIXME This fails with the following error:
+ # The module {u'import_role': {u'name': u'role1'}} was not found in configured module paths.
+ #
+ - name: Include an entire task
+ action:
+ module: "{{ entire_task }}"
+ tags:
+ - never
+
+ - block:
+ - name: Include a role that will fail
+ import_role:
+ name: role1
+ tasks_from: fail.yml
+
+ rescue:
+ - name: Include a role inside rescue
+ import_role:
+ name: role2
+
+ always:
+ - name: Include role inside always
+ import_role:
+ name: role3
+
+ - name: Test delegate_to handler is delegated
+ import_role:
+ name: delegated_handler
+ delegate_to: localhost
diff --git a/test/integration/targets/include_import/role/test_include_role.yml b/test/integration/targets/include_import/role/test_include_role.yml
new file mode 100644
index 0000000..e120bd8
--- /dev/null
+++ b/test/integration/targets/include_import/role/test_include_role.yml
@@ -0,0 +1,166 @@
+- name: Test include_role
+ hosts: testhost
+
+ vars:
+ run_role: yes
+ do_not_run_role: no
+ role_name: role1
+ test_var: templating test in playbook
+ role_vars:
+ where_am_i_defined: in the playbook
+ entire_task:
+ include_role:
+ name: role1
+
+ tasks:
+ - name: Test basic role include
+ include_role:
+ name: role1
+
+ - name: Assert that basic include works
+ assert:
+ that:
+ - _role1_result.msg == 'In role1'
+
+ - name: Test conditional role include
+ include_role:
+ name: role1
+ tasks_from: canary1.yml
+ when: run_role
+
+ - name: Assert that role ran
+ assert:
+ that:
+ - role1_canary1 == 'r1c1'
+
+ - name: Test conditional role include that should be skipped
+ include_role:
+ name: role1
+ tasks_from: canary2.yml
+ when: do_not_run_role
+
+ - name: Assert that role did not run
+ assert:
+ that:
+ - role1_canary2 is not defined
+
+ - name: Test role include with a loop
+ include_role:
+ name: "{{ item }}"
+ with_items:
+ - role1
+ - role3
+ - role2
+
+ - name: Assert that roles run with_items
+ assert:
+ that:
+ - _role1_result.msg == 'In role1'
+ - _role2_result.msg == 'In role2'
+ - _role3_result.msg == 'In role3'
+
+ - name: Test including a task file from a role
+ include_role:
+ name: role1
+ tasks_from: tasks.yml
+
+ - name: Test including vars file and tasks file from a role
+ include_role:
+ name: role3
+ tasks_from: vartest.yml
+ vars_from: role3vars.yml
+
+ - name: Assert that variables defined in previous task are available to play
+ assert:
+ that:
+ - role3_default == 'defined in role3/defaults/main.yml'
+ - role3_main == 'defined in role3/vars/main.yml'
+ - role3_var == 'defined in role3/vars/role3vars.yml'
+ ignore_errors: yes
+
+ - name: Test using a play variable for role name
+ include_role:
+ name: "{{ role_name }}"
+
+ - name: Test using a host variable for role name
+ include_role:
+ name: "{{ host_var_role_name }}"
+
+ - name: Pass variable to role
+ include_role:
+ name: role1
+ tasks_from: vartest.yml
+ vars:
+ where_am_i_defined: in the task
+
+ ## FIXME Currently failing with
+ ## ERROR! Vars in a IncludeRole must be specified as a dictionary, or a list of dictionaries
+ # - name: Pass all variables in a variable to role
+ # include_role:
+ # name: role1
+ # tasks_from: vartest.yml
+ # vars: "{{ role_vars }}"
+
+ - name: Pass templated variable to a role
+ include_role:
+ name: role1
+ tasks_from: vartest.yml
+ vars:
+ where_am_i_defined: "{{ test_var }}"
+
+ - name: Use a variable in tasks_from field
+ include_role:
+ name: role1
+ tasks_from: "{{ tasks_file_name }}.yml"
+ vars:
+ tasks_file_name: canary3
+
+ - name: Assert that tasks file was included
+ assert:
+ that:
+ - role1_canary3 == 'r1c3'
+
+ ## FIXME This fails with the following error:
+ ## The module {u'include_role': {u'name': u'role1'}} was not found in configured module paths.
+ # - name: Include an entire task
+ # action:
+ # module: "{{ entire_task }}"
+
+ - block:
+ - name: Include a role that will fail
+ include_role:
+ name: role1
+ tasks_from: fail.yml
+
+ rescue:
+ - name: Include a role inside rescue
+ include_role:
+ name: role2
+
+ always:
+ - name: Include role inside always
+ include_role:
+ name: role3
+
+- hosts: testhost,testhost2
+ tasks:
+ - name: wipe role results
+ set_fact:
+ _role2_result: ~
+ _role3_result: ~
+
+ - name: Test using a host variable for role name
+ include_role:
+ name: "{{ host_var_role_name }}"
+
+ - name: assert that host variable for role name calls 2 diff roles
+ assert:
+ that:
+ - _role2_result is not none
+ when: inventory_hostname == 'testhost2'
+
+ - name: assert that host variable for role name calls 2 diff roles
+ assert:
+ that:
+ - _role3_result is not none
+ when: inventory_hostname == 'testhost'
diff --git a/test/integration/targets/include_import/role/test_include_role_vars_from.yml b/test/integration/targets/include_import/role/test_include_role_vars_from.yml
new file mode 100644
index 0000000..f7bb4d7
--- /dev/null
+++ b/test/integration/targets/include_import/role/test_include_role_vars_from.yml
@@ -0,0 +1,10 @@
+- name: Test include_role vars_from
+ hosts: testhost
+ vars:
+ role_name: role1
+ tasks:
+ - name: Test vars_from
+ include_role:
+ name: role1
+ vars_from:
+ - vars_1.yml
diff --git a/test/integration/targets/include_import/roles/delegated_handler/handlers/main.yml b/test/integration/targets/include_import/roles/delegated_handler/handlers/main.yml
new file mode 100644
index 0000000..550ddc2
--- /dev/null
+++ b/test/integration/targets/include_import/roles/delegated_handler/handlers/main.yml
@@ -0,0 +1,4 @@
+- name: delegated assert handler
+ assert:
+ that:
+ - ansible_delegated_vars is defined
diff --git a/test/integration/targets/include_import/roles/delegated_handler/tasks/main.yml b/test/integration/targets/include_import/roles/delegated_handler/tasks/main.yml
new file mode 100644
index 0000000..9d2ef61
--- /dev/null
+++ b/test/integration/targets/include_import/roles/delegated_handler/tasks/main.yml
@@ -0,0 +1,3 @@
+- command: "true"
+ notify:
+ - delegated assert handler
diff --git a/test/integration/targets/include_import/roles/dup_allowed_role/meta/main.yml b/test/integration/targets/include_import/roles/dup_allowed_role/meta/main.yml
new file mode 100644
index 0000000..61d3ffe
--- /dev/null
+++ b/test/integration/targets/include_import/roles/dup_allowed_role/meta/main.yml
@@ -0,0 +1,2 @@
+---
+allow_duplicates: true
diff --git a/test/integration/targets/include_import/roles/dup_allowed_role/tasks/main.yml b/test/integration/targets/include_import/roles/dup_allowed_role/tasks/main.yml
new file mode 100644
index 0000000..cad935e
--- /dev/null
+++ b/test/integration/targets/include_import/roles/dup_allowed_role/tasks/main.yml
@@ -0,0 +1,3 @@
+---
+- debug:
+ msg: "Tasks file inside role"
diff --git a/test/integration/targets/include_import/roles/loop_name_assert/tasks/main.yml b/test/integration/targets/include_import/roles/loop_name_assert/tasks/main.yml
new file mode 100644
index 0000000..9bb3db5
--- /dev/null
+++ b/test/integration/targets/include_import/roles/loop_name_assert/tasks/main.yml
@@ -0,0 +1,4 @@
+- assert:
+ that:
+ - name == 'name_from_loop_var'
+ - name != 'loop_name_assert'
diff --git a/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/defaults/main.yml b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/defaults/main.yml
new file mode 100644
index 0000000..aba24bb
--- /dev/null
+++ b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/defaults/main.yml
@@ -0,0 +1,3 @@
+---
+testnesteddep2_defvar1: foobar
+testnesteddep2_varvar1: foobar
diff --git a/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/meta/main.yml b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/meta/main.yml
new file mode 100644
index 0000000..31afcaa
--- /dev/null
+++ b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+- role: nested/nested/nested_dep_role2a
diff --git a/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/tasks/main.yml b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/tasks/main.yml
new file mode 100644
index 0000000..1f2ee7f
--- /dev/null
+++ b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/tasks/main.yml
@@ -0,0 +1,2 @@
+---
+- include_tasks: ./rund.yml
diff --git a/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/tasks/rund.yml b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/tasks/rund.yml
new file mode 100644
index 0000000..523e579
--- /dev/null
+++ b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/tasks/rund.yml
@@ -0,0 +1,2 @@
+---
+- shell: echo from deprole2a
diff --git a/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/vars/main.yml b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/vars/main.yml
new file mode 100644
index 0000000..c89b697
--- /dev/null
+++ b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/vars/main.yml
@@ -0,0 +1,2 @@
+---
+testnesteddep2_varvar1: muche
diff --git a/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/defaults/main.yml b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/defaults/main.yml
new file mode 100644
index 0000000..aba24bb
--- /dev/null
+++ b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/defaults/main.yml
@@ -0,0 +1,3 @@
+---
+testnesteddep2_defvar1: foobar
+testnesteddep2_varvar1: foobar
diff --git a/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/meta/main.yml b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/meta/main.yml
new file mode 100644
index 0000000..6fc8ab0
--- /dev/null
+++ b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+- role: nested/nested/nested_dep_role2b
diff --git a/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/tasks/main.yml b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/tasks/main.yml
new file mode 100644
index 0000000..729582c
--- /dev/null
+++ b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/tasks/main.yml
@@ -0,0 +1,2 @@
+---
+- include_tasks: ./rune.yml
diff --git a/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/tasks/rune.yml b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/tasks/rune.yml
new file mode 100644
index 0000000..e77882b
--- /dev/null
+++ b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/tasks/rune.yml
@@ -0,0 +1,2 @@
+---
+- shell: echo from deprole2
diff --git a/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/vars/main.yml b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/vars/main.yml
new file mode 100644
index 0000000..c89b697
--- /dev/null
+++ b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/vars/main.yml
@@ -0,0 +1,2 @@
+---
+testnesteddep2_varvar1: muche
diff --git a/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/defaults/main.yml b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/defaults/main.yml
new file mode 100644
index 0000000..aba24bb
--- /dev/null
+++ b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/defaults/main.yml
@@ -0,0 +1,3 @@
+---
+testnesteddep2_defvar1: foobar
+testnesteddep2_varvar1: foobar
diff --git a/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/meta/main.yml b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/meta/main.yml
new file mode 100644
index 0000000..32cf5dd
--- /dev/null
+++ b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/meta/main.yml
@@ -0,0 +1 @@
+dependencies: []
diff --git a/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/tasks/main.yml b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/tasks/main.yml
new file mode 100644
index 0000000..5fbb04f
--- /dev/null
+++ b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/tasks/main.yml
@@ -0,0 +1,2 @@
+---
+- include_tasks: ./runf.yml
diff --git a/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/tasks/runf.yml b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/tasks/runf.yml
new file mode 100644
index 0000000..694005f
--- /dev/null
+++ b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/tasks/runf.yml
@@ -0,0 +1,2 @@
+---
+- shell: echo from deprole2b
diff --git a/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/vars/main.yml b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/vars/main.yml
new file mode 100644
index 0000000..c89b697
--- /dev/null
+++ b/test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/vars/main.yml
@@ -0,0 +1,2 @@
+---
+testnesteddep2_varvar1: muche
diff --git a/test/integration/targets/include_import/roles/nested/nested_dep_role/defaults/main.yml b/test/integration/targets/include_import/roles/nested/nested_dep_role/defaults/main.yml
new file mode 100644
index 0000000..536745e
--- /dev/null
+++ b/test/integration/targets/include_import/roles/nested/nested_dep_role/defaults/main.yml
@@ -0,0 +1,3 @@
+---
+testnesteddep_defvar1: foobar
+testnesteddep_varvar1: foobar
diff --git a/test/integration/targets/include_import/roles/nested/nested_dep_role/meta/main.yml b/test/integration/targets/include_import/roles/nested/nested_dep_role/meta/main.yml
new file mode 100644
index 0000000..23d65c7
--- /dev/null
+++ b/test/integration/targets/include_import/roles/nested/nested_dep_role/meta/main.yml
@@ -0,0 +1,2 @@
+---
+dependencies: []
diff --git a/test/integration/targets/include_import/roles/nested/nested_dep_role/tasks/main.yml b/test/integration/targets/include_import/roles/nested/nested_dep_role/tasks/main.yml
new file mode 100644
index 0000000..d86604b
--- /dev/null
+++ b/test/integration/targets/include_import/roles/nested/nested_dep_role/tasks/main.yml
@@ -0,0 +1,2 @@
+---
+- include_tasks: ./runc.yml
diff --git a/test/integration/targets/include_import/roles/nested/nested_dep_role/tasks/runc.yml b/test/integration/targets/include_import/roles/nested/nested_dep_role/tasks/runc.yml
new file mode 100644
index 0000000..76682f5
--- /dev/null
+++ b/test/integration/targets/include_import/roles/nested/nested_dep_role/tasks/runc.yml
@@ -0,0 +1,4 @@
+---
+- debug:
+ msg: from test_nested_dep_role
+- include_role: {name: nested/nested/nested_dep_role2}
diff --git a/test/integration/targets/include_import/roles/nested/nested_dep_role/vars/main.yml b/test/integration/targets/include_import/roles/nested/nested_dep_role/vars/main.yml
new file mode 100644
index 0000000..b80b5de
--- /dev/null
+++ b/test/integration/targets/include_import/roles/nested/nested_dep_role/vars/main.yml
@@ -0,0 +1,2 @@
+---
+testnesteddep_varvar1: muche
diff --git a/test/integration/targets/include_import/roles/nested_include_task/meta/main.yml b/test/integration/targets/include_import/roles/nested_include_task/meta/main.yml
new file mode 100644
index 0000000..9410b7d
--- /dev/null
+++ b/test/integration/targets/include_import/roles/nested_include_task/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+- role: nested/nested_dep_role
diff --git a/test/integration/targets/include_import/roles/nested_include_task/tasks/main.yml b/test/integration/targets/include_import/roles/nested_include_task/tasks/main.yml
new file mode 100644
index 0000000..15a8e9f
--- /dev/null
+++ b/test/integration/targets/include_import/roles/nested_include_task/tasks/main.yml
@@ -0,0 +1,2 @@
+---
+- include_tasks: ./runa.yml
diff --git a/test/integration/targets/include_import/roles/nested_include_task/tasks/runa.yml b/test/integration/targets/include_import/roles/nested_include_task/tasks/runa.yml
new file mode 100644
index 0000000..643fdd2
--- /dev/null
+++ b/test/integration/targets/include_import/roles/nested_include_task/tasks/runa.yml
@@ -0,0 +1,3 @@
+---
+- debug:
+ msg: from nested_include_task
diff --git a/test/integration/targets/include_import/roles/role1/tasks/canary1.yml b/test/integration/targets/include_import/roles/role1/tasks/canary1.yml
new file mode 100644
index 0000000..9f202ba
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role1/tasks/canary1.yml
@@ -0,0 +1,2 @@
+- set_fact:
+ role1_canary1: r1c1
diff --git a/test/integration/targets/include_import/roles/role1/tasks/canary2.yml b/test/integration/targets/include_import/roles/role1/tasks/canary2.yml
new file mode 100644
index 0000000..80e18b8
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role1/tasks/canary2.yml
@@ -0,0 +1,2 @@
+- set_fact:
+ role1_canary2: r1c2
diff --git a/test/integration/targets/include_import/roles/role1/tasks/canary3.yml b/test/integration/targets/include_import/roles/role1/tasks/canary3.yml
new file mode 100644
index 0000000..40014e3
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role1/tasks/canary3.yml
@@ -0,0 +1,2 @@
+- set_fact:
+ role1_canary3: r1c3
diff --git a/test/integration/targets/include_import/roles/role1/tasks/fail.yml b/test/integration/targets/include_import/roles/role1/tasks/fail.yml
new file mode 100644
index 0000000..b1b5f15
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role1/tasks/fail.yml
@@ -0,0 +1,3 @@
+- name: EXPECTED FAILURE
+ fail:
+ msg: This command should always fail
diff --git a/test/integration/targets/include_import/roles/role1/tasks/main.yml b/test/integration/targets/include_import/roles/role1/tasks/main.yml
new file mode 100644
index 0000000..a8b641e
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role1/tasks/main.yml
@@ -0,0 +1,3 @@
+- debug:
+ msg: In role1
+ register: _role1_result
diff --git a/test/integration/targets/include_import/roles/role1/tasks/r1t01.yml b/test/integration/targets/include_import/roles/role1/tasks/r1t01.yml
new file mode 100644
index 0000000..e4a1e63
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role1/tasks/r1t01.yml
@@ -0,0 +1 @@
+- import_tasks: r1t02.yml
diff --git a/test/integration/targets/include_import/roles/role1/tasks/r1t02.yml b/test/integration/targets/include_import/roles/role1/tasks/r1t02.yml
new file mode 100644
index 0000000..d3d3750
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role1/tasks/r1t02.yml
@@ -0,0 +1 @@
+- import_tasks: r1t03.yml
diff --git a/test/integration/targets/include_import/roles/role1/tasks/r1t03.yml b/test/integration/targets/include_import/roles/role1/tasks/r1t03.yml
new file mode 100644
index 0000000..1d3330a
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role1/tasks/r1t03.yml
@@ -0,0 +1 @@
+- import_tasks: r1t04.yml
diff --git a/test/integration/targets/include_import/roles/role1/tasks/r1t04.yml b/test/integration/targets/include_import/roles/role1/tasks/r1t04.yml
new file mode 100644
index 0000000..f3eece2
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role1/tasks/r1t04.yml
@@ -0,0 +1 @@
+- import_tasks: r1t05.yml
diff --git a/test/integration/targets/include_import/roles/role1/tasks/r1t05.yml b/test/integration/targets/include_import/roles/role1/tasks/r1t05.yml
new file mode 100644
index 0000000..4c7371e
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role1/tasks/r1t05.yml
@@ -0,0 +1 @@
+- import_tasks: r1t06.yml
diff --git a/test/integration/targets/include_import/roles/role1/tasks/r1t06.yml b/test/integration/targets/include_import/roles/role1/tasks/r1t06.yml
new file mode 100644
index 0000000..96d5660
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role1/tasks/r1t06.yml
@@ -0,0 +1 @@
+- import_tasks: r1t07.yml
diff --git a/test/integration/targets/include_import/roles/role1/tasks/r1t07.yml b/test/integration/targets/include_import/roles/role1/tasks/r1t07.yml
new file mode 100644
index 0000000..ee8d325
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role1/tasks/r1t07.yml
@@ -0,0 +1 @@
+- import_tasks: r1t08.yml
diff --git a/test/integration/targets/include_import/roles/role1/tasks/r1t08.yml b/test/integration/targets/include_import/roles/role1/tasks/r1t08.yml
new file mode 100644
index 0000000..33b8109
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role1/tasks/r1t08.yml
@@ -0,0 +1 @@
+- import_tasks: r1t09.yml
diff --git a/test/integration/targets/include_import/roles/role1/tasks/r1t09.yml b/test/integration/targets/include_import/roles/role1/tasks/r1t09.yml
new file mode 100644
index 0000000..8973c29
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role1/tasks/r1t09.yml
@@ -0,0 +1 @@
+- import_tasks: r1t10.yml
diff --git a/test/integration/targets/include_import/roles/role1/tasks/r1t10.yml b/test/integration/targets/include_import/roles/role1/tasks/r1t10.yml
new file mode 100644
index 0000000..eafdca2
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role1/tasks/r1t10.yml
@@ -0,0 +1 @@
+- import_tasks: r1t11.yml
diff --git a/test/integration/targets/include_import/roles/role1/tasks/r1t11.yml b/test/integration/targets/include_import/roles/role1/tasks/r1t11.yml
new file mode 100644
index 0000000..9ab828f
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role1/tasks/r1t11.yml
@@ -0,0 +1 @@
+- import_tasks: r1t12.yml
diff --git a/test/integration/targets/include_import/roles/role1/tasks/r1t12.yml b/test/integration/targets/include_import/roles/role1/tasks/r1t12.yml
new file mode 100644
index 0000000..8828486
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role1/tasks/r1t12.yml
@@ -0,0 +1,2 @@
+- debug:
+ msg: r1t12
diff --git a/test/integration/targets/include_import/roles/role1/tasks/tasks.yml b/test/integration/targets/include_import/roles/role1/tasks/tasks.yml
new file mode 100644
index 0000000..45430bc
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role1/tasks/tasks.yml
@@ -0,0 +1,2 @@
+- debug:
+ msg: Tasks file inside role1
diff --git a/test/integration/targets/include_import/roles/role1/tasks/templated.yml b/test/integration/targets/include_import/roles/role1/tasks/templated.yml
new file mode 100644
index 0000000..eb9a997
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role1/tasks/templated.yml
@@ -0,0 +1 @@
+- debug: msg="In imported role"
diff --git a/test/integration/targets/include_import/roles/role1/tasks/vartest.yml b/test/integration/targets/include_import/roles/role1/tasks/vartest.yml
new file mode 100644
index 0000000..5a49d8d
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role1/tasks/vartest.yml
@@ -0,0 +1,2 @@
+- debug:
+ var: where_am_i_defined
diff --git a/test/integration/targets/include_import/roles/role1/vars/main.yml b/test/integration/targets/include_import/roles/role1/vars/main.yml
new file mode 100644
index 0000000..57d31cf
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role1/vars/main.yml
@@ -0,0 +1 @@
+where_am_i_defined: role1 vars/main.yml
diff --git a/test/integration/targets/include_import/roles/role1/vars/role1vars.yml b/test/integration/targets/include_import/roles/role1/vars/role1vars.yml
new file mode 100644
index 0000000..57d31cf
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role1/vars/role1vars.yml
@@ -0,0 +1 @@
+where_am_i_defined: role1 vars/main.yml
diff --git a/test/integration/targets/include_import/roles/role2/tasks/main.yml b/test/integration/targets/include_import/roles/role2/tasks/main.yml
new file mode 100644
index 0000000..82934f6
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role2/tasks/main.yml
@@ -0,0 +1,3 @@
+- debug:
+ msg: In role2
+ register: _role2_result
diff --git a/test/integration/targets/include_import/roles/role3/defaults/main.yml b/test/integration/targets/include_import/roles/role3/defaults/main.yml
new file mode 100644
index 0000000..c3464c4
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role3/defaults/main.yml
@@ -0,0 +1,2 @@
+where_am_i_defined: defaults in role3
+role3_default: defined in role3/defaults/main.yml
diff --git a/test/integration/targets/include_import/roles/role3/handlers/main.yml b/test/integration/targets/include_import/roles/role3/handlers/main.yml
new file mode 100644
index 0000000..c8baa27
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role3/handlers/main.yml
@@ -0,0 +1,3 @@
+- name: runme
+ debug:
+ msg: role3 handler
diff --git a/test/integration/targets/include_import/roles/role3/tasks/main.yml b/test/integration/targets/include_import/roles/role3/tasks/main.yml
new file mode 100644
index 0000000..bb70dad
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role3/tasks/main.yml
@@ -0,0 +1,3 @@
+- debug:
+ msg: In role3
+ register: _role3_result
diff --git a/test/integration/targets/include_import/roles/role3/tasks/tasks.yml b/test/integration/targets/include_import/roles/role3/tasks/tasks.yml
new file mode 100644
index 0000000..0e82269
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role3/tasks/tasks.yml
@@ -0,0 +1,2 @@
+- debug:
+ msg: Tasks file inside role3
diff --git a/test/integration/targets/include_import/roles/role3/tasks/vartest.yml b/test/integration/targets/include_import/roles/role3/tasks/vartest.yml
new file mode 100644
index 0000000..cb21c53
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role3/tasks/vartest.yml
@@ -0,0 +1,2 @@
+- debug:
+ var: role3_var
diff --git a/test/integration/targets/include_import/roles/role3/vars/main.yml b/test/integration/targets/include_import/roles/role3/vars/main.yml
new file mode 100644
index 0000000..9adac6b
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role3/vars/main.yml
@@ -0,0 +1 @@
+role3_main: defined in role3/vars/main.yml
diff --git a/test/integration/targets/include_import/roles/role3/vars/role3vars.yml b/test/integration/targets/include_import/roles/role3/vars/role3vars.yml
new file mode 100644
index 0000000..f324d56
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role3/vars/role3vars.yml
@@ -0,0 +1,2 @@
+where_am_i_defined: role3vars.yml
+role3_var: defined in role3/vars/role3vars.yml
diff --git a/test/integration/targets/include_import/roles/role_with_deps/meta/main.yml b/test/integration/targets/include_import/roles/role_with_deps/meta/main.yml
new file mode 100644
index 0000000..a2446bb
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role_with_deps/meta/main.yml
@@ -0,0 +1,3 @@
+dependencies:
+ - role1
+ - role2
diff --git a/test/integration/targets/include_import/roles/role_with_deps/tasks/main.yml b/test/integration/targets/include_import/roles/role_with_deps/tasks/main.yml
new file mode 100644
index 0000000..060fe42
--- /dev/null
+++ b/test/integration/targets/include_import/roles/role_with_deps/tasks/main.yml
@@ -0,0 +1,2 @@
+- debug:
+ msg: In role_with_deps
diff --git a/test/integration/targets/include_import/run_once/include_me.yml b/test/integration/targets/include_import/run_once/include_me.yml
new file mode 100644
index 0000000..e92128a
--- /dev/null
+++ b/test/integration/targets/include_import/run_once/include_me.yml
@@ -0,0 +1,2 @@
+- set_fact:
+ lola: wiseman
diff --git a/test/integration/targets/include_import/run_once/playbook.yml b/test/integration/targets/include_import/run_once/playbook.yml
new file mode 100644
index 0000000..cc1e265
--- /dev/null
+++ b/test/integration/targets/include_import/run_once/playbook.yml
@@ -0,0 +1,61 @@
+# This playbook exists to document the behavior of how run_once when
+# applied to a dynamic include works
+#
+# As with other uses of keywords on dynamic includes, it only affects the include.
+# In this case it causes the include to only be processed for ansible_play_hosts[0]
+# which has the side effect of only running the tasks on ansible_play_hosts[0]
+# and would only delegate facts of the include itself, not the tasks contained within
+
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - add_host:
+ name: "{{ item }}"
+ ansible_connection: local
+ groups:
+ - all
+ loop:
+ - localhost0
+ - localhost1
+
+ - add_host:
+ name: "{{ item }}"
+ groups:
+ - testing
+ ansible_connection: local
+ loop:
+ - localhost2
+ - localhost3
+
+- hosts: all:!testing
+ gather_facts: false
+ vars:
+ lola: untouched
+ tasks:
+ - include_tasks:
+ file: include_me.yml
+ apply:
+ run_once: true
+ run_once: true
+
+ - assert:
+ that:
+ - lola == 'wiseman'
+
+- hosts: testing
+ gather_facts: false
+ vars:
+ lola: untouched
+ tasks:
+ - include_tasks: include_me.yml
+ run_once: true
+
+ - assert:
+ that:
+ - lola == 'wiseman'
+ when: inventory_hostname == ansible_play_hosts[0]
+
+ - assert:
+ that:
+ - lola == 'untouched'
+ when: inventory_hostname != ansible_play_hosts[0]
diff --git a/test/integration/targets/include_import/runme.sh b/test/integration/targets/include_import/runme.sh
new file mode 100755
index 0000000..d384a12
--- /dev/null
+++ b/test/integration/targets/include_import/runme.sh
@@ -0,0 +1,141 @@
+#!/usr/bin/env bash
+
+set -eux
+
+export ANSIBLE_ROLES_PATH=./roles
+
+function gen_task_files() {
+ for i in $(printf "%03d " {1..39}); do
+ echo -e "- name: Hello Message\n debug:\n msg: Task file ${i}" > "tasks/hello/tasks-file-${i}.yml"
+ done
+}
+
+## Adhoc
+
+ansible -m include_role -a name=role1 localhost
+
+## Import (static)
+
+# Playbook
+ansible-playbook playbook/test_import_playbook.yml -i inventory "$@"
+
+ANSIBLE_STRATEGY='linear' ansible-playbook playbook/test_import_playbook_tags.yml -i inventory "$@" --tags canary1,canary22,validate --skip-tags skipme
+
+# Tasks
+ANSIBLE_STRATEGY='linear' ansible-playbook tasks/test_import_tasks.yml -i inventory "$@"
+ANSIBLE_STRATEGY='free' ansible-playbook tasks/test_import_tasks.yml -i inventory "$@"
+ANSIBLE_STRATEGY='free' ansible-playbook tasks/test_import_tasks_tags.yml -i inventory "$@" --tags tasks1,canary1,validate
+
+# Role
+ANSIBLE_STRATEGY='linear' ansible-playbook role/test_import_role.yml -i inventory "$@"
+ANSIBLE_STRATEGY='free' ansible-playbook role/test_import_role.yml -i inventory "$@"
+
+
+## Include (dynamic)
+
+# Tasks
+ANSIBLE_STRATEGY='linear' ansible-playbook tasks/test_include_tasks.yml -i inventory "$@"
+ANSIBLE_STRATEGY='free' ansible-playbook tasks/test_include_tasks.yml -i inventory "$@"
+ANSIBLE_STRATEGY='free' ansible-playbook tasks/test_include_tasks_tags.yml -i inventory "$@" --tags tasks1,canary1,validate
+
+# Role
+ANSIBLE_STRATEGY='linear' ansible-playbook role/test_include_role.yml -i inventory "$@"
+ANSIBLE_STRATEGY='free' ansible-playbook role/test_include_role.yml -i inventory "$@"
+
+# https://github.com/ansible/ansible/issues/68515
+ansible-playbook -v role/test_include_role_vars_from.yml 2>&1 | tee test_include_role_vars_from.out
+test "$(grep -E -c 'Expected a string for vars_from but got' test_include_role_vars_from.out)" = 1
+
+## Max Recursion Depth
+# https://github.com/ansible/ansible/issues/23609
+ANSIBLE_STRATEGY='linear' ansible-playbook test_role_recursion.yml -i inventory "$@"
+ANSIBLE_STRATEGY='linear' ansible-playbook test_role_recursion_fqcn.yml -i inventory "$@"
+
+## Nested tasks
+# https://github.com/ansible/ansible/issues/34782
+ANSIBLE_STRATEGY='linear' ansible-playbook test_nested_tasks.yml -i inventory "$@"
+ANSIBLE_STRATEGY='linear' ansible-playbook test_nested_tasks_fqcn.yml -i inventory "$@"
+ANSIBLE_STRATEGY='free' ansible-playbook test_nested_tasks.yml -i inventory "$@"
+ANSIBLE_STRATEGY='free' ansible-playbook test_nested_tasks_fqcn.yml -i inventory "$@"
+
+## Tons of top level include_tasks
+# https://github.com/ansible/ansible/issues/36053
+# Fixed by https://github.com/ansible/ansible/pull/36075
+gen_task_files
+ANSIBLE_STRATEGY='linear' ansible-playbook test_copious_include_tasks.yml -i inventory "$@"
+ANSIBLE_STRATEGY='linear' ansible-playbook test_copious_include_tasks_fqcn.yml -i inventory "$@"
+ANSIBLE_STRATEGY='free' ansible-playbook test_copious_include_tasks.yml -i inventory "$@"
+ANSIBLE_STRATEGY='free' ansible-playbook test_copious_include_tasks_fqcn.yml -i inventory "$@"
+rm -f tasks/hello/*.yml
+
+# Inlcuded tasks should inherit attrs from non-dynamic blocks in parent chain
+# https://github.com/ansible/ansible/pull/38827
+ANSIBLE_STRATEGY='linear' ansible-playbook test_grandparent_inheritance.yml -i inventory "$@"
+ANSIBLE_STRATEGY='linear' ansible-playbook test_grandparent_inheritance_fqcn.yml -i inventory "$@"
+
+# undefined_var
+ANSIBLE_STRATEGY='linear' ansible-playbook undefined_var/playbook.yml -i inventory "$@"
+ANSIBLE_STRATEGY='free' ansible-playbook undefined_var/playbook.yml -i inventory "$@"
+
+# include_ + apply (explicit inheritance)
+ANSIBLE_STRATEGY='linear' ansible-playbook apply/include_apply.yml -i inventory "$@" --tags foo
+set +e
+OUT=$(ANSIBLE_STRATEGY='linear' ansible-playbook apply/import_apply.yml -i inventory "$@" --tags foo 2>&1 | grep 'ERROR! Invalid options for import_tasks: apply')
+set -e
+if [[ -z "$OUT" ]]; then
+ echo "apply on import_tasks did not cause error"
+ exit 1
+fi
+
+ANSIBLE_STRATEGY='linear' ANSIBLE_PLAYBOOK_VARS_ROOT=all ansible-playbook apply/include_apply_65710.yml -i inventory "$@"
+ANSIBLE_STRATEGY='free' ANSIBLE_PLAYBOOK_VARS_ROOT=all ansible-playbook apply/include_apply_65710.yml -i inventory "$@"
+
+# Test that duplicate items in loop are not deduped
+ANSIBLE_STRATEGY='linear' ansible-playbook tasks/test_include_dupe_loop.yml -i inventory "$@" | tee test_include_dupe_loop.out
+test "$(grep -c '"item=foo"' test_include_dupe_loop.out)" = 3
+ANSIBLE_STRATEGY='free' ansible-playbook tasks/test_include_dupe_loop.yml -i inventory "$@" | tee test_include_dupe_loop.out
+test "$(grep -c '"item=foo"' test_include_dupe_loop.out)" = 3
+
+ansible-playbook public_exposure/playbook.yml -i inventory "$@"
+ansible-playbook public_exposure/no_bleeding.yml -i inventory "$@"
+ansible-playbook public_exposure/no_overwrite_roles.yml -i inventory "$@"
+
+# https://github.com/ansible/ansible/pull/48068
+ANSIBLE_HOST_PATTERN_MISMATCH=warning ansible-playbook run_once/playbook.yml "$@"
+
+# https://github.com/ansible/ansible/issues/48936
+ansible-playbook -v handler_addressing/playbook.yml 2>&1 | tee test_handler_addressing.out
+test "$(grep -E -c 'include handler task|ERROR! The requested handler '"'"'do_import'"'"' was not found' test_handler_addressing.out)" = 2
+
+# https://github.com/ansible/ansible/issues/49969
+ansible-playbook -v parent_templating/playbook.yml 2>&1 | tee test_parent_templating.out
+test "$(grep -E -c 'Templating the path of the parent include_tasks failed.' test_parent_templating.out)" = 0
+
+# https://github.com/ansible/ansible/issues/54618
+ansible-playbook test_loop_var_bleed.yaml "$@"
+
+# https://github.com/ansible/ansible/issues/56580
+ansible-playbook valid_include_keywords/playbook.yml "$@"
+
+# https://github.com/ansible/ansible/issues/64902
+ansible-playbook tasks/test_allow_single_role_dup.yml 2>&1 | tee test_allow_single_role_dup.out
+test "$(grep -c 'ok=3' test_allow_single_role_dup.out)" = 1
+
+# https://github.com/ansible/ansible/issues/66764
+ANSIBLE_HOST_PATTERN_MISMATCH=error ansible-playbook empty_group_warning/playbook.yml
+
+ansible-playbook test_include_loop.yml "$@"
+ansible-playbook test_include_loop_fqcn.yml "$@"
+
+ansible-playbook include_role_omit/playbook.yml "$@"
+
+# Test templating import_playbook, import_tasks, and import_role files
+ansible-playbook playbook/test_templated_filenames.yml -e "pb=validate_templated_playbook.yml tasks=validate_templated_tasks.yml tasks_from=templated.yml" "$@" | tee out.txt
+cat out.txt
+test "$(grep out.txt -ce 'In imported playbook')" = 2
+test "$(grep out.txt -ce 'In imported tasks')" = 3
+test "$(grep out.txt -ce 'In imported role')" = 3
+
+# https://github.com/ansible/ansible/issues/73657
+ansible-playbook issue73657.yml 2>&1 | tee issue73657.out
+test "$(grep -c 'SHOULD_NOT_EXECUTE' issue73657.out)" = 0
diff --git a/test/integration/targets/include_import/tasks/debug_item.yml b/test/integration/targets/include_import/tasks/debug_item.yml
new file mode 100644
index 0000000..025e132
--- /dev/null
+++ b/test/integration/targets/include_import/tasks/debug_item.yml
@@ -0,0 +1,2 @@
+- debug:
+ msg: "item={{ item }}"
diff --git a/test/integration/targets/include_import/tasks/hello/.gitignore b/test/integration/targets/include_import/tasks/hello/.gitignore
new file mode 100644
index 0000000..b4602e7
--- /dev/null
+++ b/test/integration/targets/include_import/tasks/hello/.gitignore
@@ -0,0 +1 @@
+tasks-file-*
diff --git a/test/integration/targets/include_import/tasks/hello/keep b/test/integration/targets/include_import/tasks/hello/keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/include_import/tasks/hello/keep
diff --git a/test/integration/targets/include_import/tasks/nested/nested.yml b/test/integration/targets/include_import/tasks/nested/nested.yml
new file mode 100644
index 0000000..0bfcdee
--- /dev/null
+++ b/test/integration/targets/include_import/tasks/nested/nested.yml
@@ -0,0 +1,2 @@
+---
+- include_tasks: ../../nestedtasks/nested/nested.yml
diff --git a/test/integration/targets/include_import/tasks/tasks1.yml b/test/integration/targets/include_import/tasks/tasks1.yml
new file mode 100644
index 0000000..e1d83d9
--- /dev/null
+++ b/test/integration/targets/include_import/tasks/tasks1.yml
@@ -0,0 +1,5 @@
+- name: Set variable inside tasks1.yml
+ set_fact:
+ set_in_tasks1: yes
+ tags:
+ - tasks1
diff --git a/test/integration/targets/include_import/tasks/tasks2.yml b/test/integration/targets/include_import/tasks/tasks2.yml
new file mode 100644
index 0000000..1b4c86f
--- /dev/null
+++ b/test/integration/targets/include_import/tasks/tasks2.yml
@@ -0,0 +1,5 @@
+- name: Set variable inside tasks2.yml
+ set_fact:
+ set_in_tasks2: yes
+ tags:
+ - tasks2
diff --git a/test/integration/targets/include_import/tasks/tasks3.yml b/test/integration/targets/include_import/tasks/tasks3.yml
new file mode 100644
index 0000000..6da3719
--- /dev/null
+++ b/test/integration/targets/include_import/tasks/tasks3.yml
@@ -0,0 +1,5 @@
+- name: Set variable inside tasks3.yml
+ set_fact:
+ set_in_tasks3: yes
+ tags:
+ - tasks3
diff --git a/test/integration/targets/include_import/tasks/tasks4.yml b/test/integration/targets/include_import/tasks/tasks4.yml
new file mode 100644
index 0000000..fc2eb6c
--- /dev/null
+++ b/test/integration/targets/include_import/tasks/tasks4.yml
@@ -0,0 +1,5 @@
+- name: Set variable inside tasks4.yml
+ set_fact:
+ set_in_tasks4: yes
+ tags:
+ - tasks4
diff --git a/test/integration/targets/include_import/tasks/tasks5.yml b/test/integration/targets/include_import/tasks/tasks5.yml
new file mode 100644
index 0000000..f2ee6b9
--- /dev/null
+++ b/test/integration/targets/include_import/tasks/tasks5.yml
@@ -0,0 +1,6 @@
+- name: Set variable inside tasks5.yml
+ set_fact:
+ set_in_tasks5: yes
+ tags:
+ - tasks5
+ - canary1
diff --git a/test/integration/targets/include_import/tasks/tasks6.yml b/test/integration/targets/include_import/tasks/tasks6.yml
new file mode 100644
index 0000000..fa03079
--- /dev/null
+++ b/test/integration/targets/include_import/tasks/tasks6.yml
@@ -0,0 +1,5 @@
+- name: Set variable inside tasks6.yml
+ set_fact:
+ set_in_tasks6: yes
+ tags:
+ - tasks6
diff --git a/test/integration/targets/include_import/tasks/test_allow_single_role_dup.yml b/test/integration/targets/include_import/tasks/test_allow_single_role_dup.yml
new file mode 100644
index 0000000..3a6992f
--- /dev/null
+++ b/test/integration/targets/include_import/tasks/test_allow_single_role_dup.yml
@@ -0,0 +1,8 @@
+---
+- name: test for allow_duplicates with single role
+ hosts: localhost
+ gather_facts: false
+ roles:
+ - dup_allowed_role
+ - dup_allowed_role
+ - dup_allowed_role
diff --git a/test/integration/targets/include_import/tasks/test_import_tasks.yml b/test/integration/targets/include_import/tasks/test_import_tasks.yml
new file mode 100644
index 0000000..8f07bb9
--- /dev/null
+++ b/test/integration/targets/include_import/tasks/test_import_tasks.yml
@@ -0,0 +1,41 @@
+- name: Test import_tasks
+ hosts: testhost
+
+ tasks:
+ - name: Test basic task import
+ import_tasks: tasks1.yml
+
+ - name: Assert that fact was set in import
+ assert:
+ that:
+ - set_in_tasks1
+
+ - name: Test conditional task import
+ import_tasks: tasks2.yml
+ when: no
+
+ - name: Assert that tasks were skipped
+ assert:
+ that:
+ - set_in_tasks2 is not defined
+
+ - block:
+ - name: Import tasks inside a block
+ import_tasks: tasks3.yml
+
+ - name: Assert that task3 was included
+ assert:
+ that:
+ - set_in_tasks3
+
+ always:
+ - name: Import task inside always
+ import_tasks: tasks4.yml
+
+ - name: Validate that variables set in previously improted tasks are passed down.
+ import_tasks: validate3.yml
+
+ - name: Assert that tasks4 was included
+ assert:
+ that:
+ - set_in_tasks4
diff --git a/test/integration/targets/include_import/tasks/test_import_tasks_tags.yml b/test/integration/targets/include_import/tasks/test_import_tasks_tags.yml
new file mode 100644
index 0000000..3b1d68f
--- /dev/null
+++ b/test/integration/targets/include_import/tasks/test_import_tasks_tags.yml
@@ -0,0 +1,23 @@
+- name: Test import_tasks using tags
+ hosts: testhost
+
+ tasks:
+ - name: Import tasks1.yml
+ import_tasks: tasks1.yml
+
+ - name: Import tasks4.yml using tag on import task
+ import_tasks: tasks4.yml
+ tags:
+ - canary1
+
+ - name: Import tasks2.yml
+ import_tasks: tasks2.yml
+
+ - name: Assert that appropriate tasks were run
+ assert:
+ that:
+ - set_in_tasks1
+ - set_in_tasks4
+ - set_in_tasks2 is not defined
+ tags:
+ - validate
diff --git a/test/integration/targets/include_import/tasks/test_include_dupe_loop.yml b/test/integration/targets/include_import/tasks/test_include_dupe_loop.yml
new file mode 100644
index 0000000..b7b9301
--- /dev/null
+++ b/test/integration/targets/include_import/tasks/test_include_dupe_loop.yml
@@ -0,0 +1,8 @@
+- name: Test Include Duplicate Loop Items
+ hosts: testhost
+ tasks:
+ - include_tasks: debug_item.yml
+ loop:
+ - foo
+ - foo
+ - foo
diff --git a/test/integration/targets/include_import/tasks/test_include_tasks.yml b/test/integration/targets/include_import/tasks/test_include_tasks.yml
new file mode 100644
index 0000000..ebe2273
--- /dev/null
+++ b/test/integration/targets/include_import/tasks/test_include_tasks.yml
@@ -0,0 +1,44 @@
+- name: Test include_tasks
+ hosts: testhost
+
+ tasks:
+ - name: Test basic task include
+ include_tasks: tasks1.yml
+
+ - name: Assert that fact was set in include
+ assert:
+ that:
+ - set_in_tasks1
+
+ - name: Test conditional task include
+ include_tasks: tasks2.yml
+ when: no
+
+ - name: Assert that tasks were skipped
+ assert:
+ that:
+ - set_in_tasks2 is not defined
+
+ - block:
+ - name: Include tasks inside a block
+ include_tasks: tasks3.yml
+
+ - name: Assert that task3 was included
+ assert:
+ that:
+ - set_in_tasks3
+
+ always:
+ - name: Include task inside always
+ include_tasks: tasks4.yml
+
+ - name: Validate that variables set in previously improted tasks are passed down
+ include_tasks: validate3.yml
+
+ - name: Assert that tasks4 was included
+ assert:
+ that:
+ - set_in_tasks4
+
+ - name: include_tasks + action
+ action: include_tasks tasks1.yml
diff --git a/test/integration/targets/include_import/tasks/test_include_tasks_tags.yml b/test/integration/targets/include_import/tasks/test_include_tasks_tags.yml
new file mode 100644
index 0000000..3fe4380
--- /dev/null
+++ b/test/integration/targets/include_import/tasks/test_include_tasks_tags.yml
@@ -0,0 +1,25 @@
+- name: Test include_tasks using tags
+ hosts: testhost
+
+ tasks:
+ # This should not be included
+ - name: Include tasks1.yml
+ include_tasks: tasks1.yml
+
+ # This should be included but tasks inside should not run because they do not have
+ # the canary1 tag and tasks2 is not in the list of tags for the ansible-playbook command
+ - name: Include tasks2.yml
+ include_tasks: tasks2.yml
+ tags:
+ - canary1
+
+ # This should be included and tasks inside should be run
+ - name: Include tasks5.yml using tag on include task
+ include_tasks: tasks5.yml
+ tags:
+ - canary1
+
+ - name: Include validate_tags.yml
+ include_tasks: validate_tags.yml
+ tags:
+ - validate
diff --git a/test/integration/targets/include_import/tasks/test_recursion.yml b/test/integration/targets/include_import/tasks/test_recursion.yml
new file mode 100644
index 0000000..96754ec
--- /dev/null
+++ b/test/integration/targets/include_import/tasks/test_recursion.yml
@@ -0,0 +1,6 @@
+- hosts: testhost
+
+ tasks:
+ - include_role:
+ name: role
+ tasks_from: r1t1.yml
diff --git a/test/integration/targets/include_import/tasks/validate3.yml b/test/integration/targets/include_import/tasks/validate3.yml
new file mode 100644
index 0000000..e3166aa
--- /dev/null
+++ b/test/integration/targets/include_import/tasks/validate3.yml
@@ -0,0 +1,4 @@
+- name: Assert than variable set in previously included task is defined
+ assert:
+ that:
+ - set_in_tasks3
diff --git a/test/integration/targets/include_import/tasks/validate_tags.yml b/test/integration/targets/include_import/tasks/validate_tags.yml
new file mode 100644
index 0000000..e2f3377
--- /dev/null
+++ b/test/integration/targets/include_import/tasks/validate_tags.yml
@@ -0,0 +1,8 @@
+- name: Assert that appropriate tasks were run
+ assert:
+ that:
+ - set_in_tasks1 is undefined
+ - set_in_tasks2 is undefined
+ - set_in_tasks5
+ tags:
+ - validate
diff --git a/test/integration/targets/include_import/test_copious_include_tasks.yml b/test/integration/targets/include_import/test_copious_include_tasks.yml
new file mode 100644
index 0000000..4564c76
--- /dev/null
+++ b/test/integration/targets/include_import/test_copious_include_tasks.yml
@@ -0,0 +1,44 @@
+- name: Test many include_tasks
+ hosts: testhost
+ gather_facts: no
+
+ tasks:
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-001.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-002.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-003.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-004.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-005.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-006.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-007.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-008.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-009.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-010.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-011.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-012.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-013.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-014.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-015.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-016.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-017.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-018.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-019.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-020.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-021.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-022.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-023.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-024.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-025.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-026.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-027.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-028.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-029.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-030.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-031.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-032.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-033.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-034.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-035.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-036.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-037.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-038.yml"
+ - include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-039.yml"
diff --git a/test/integration/targets/include_import/test_copious_include_tasks_fqcn.yml b/test/integration/targets/include_import/test_copious_include_tasks_fqcn.yml
new file mode 100644
index 0000000..32fa9ab
--- /dev/null
+++ b/test/integration/targets/include_import/test_copious_include_tasks_fqcn.yml
@@ -0,0 +1,44 @@
+- name: Test many ansible.builtin.include_tasks
+ hosts: testhost
+ gather_facts: no
+
+ tasks:
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-001.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-002.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-003.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-004.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-005.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-006.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-007.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-008.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-009.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-010.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-011.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-012.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-013.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-014.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-015.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-016.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-017.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-018.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-019.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-020.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-021.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-022.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-023.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-024.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-025.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-026.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-027.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-028.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-029.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-030.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-031.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-032.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-033.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-034.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-035.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-036.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-037.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-038.yml"
+ - ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/hello/tasks-file-039.yml"
diff --git a/test/integration/targets/include_import/test_grandparent_inheritance.yml b/test/integration/targets/include_import/test_grandparent_inheritance.yml
new file mode 100644
index 0000000..45a3d83
--- /dev/null
+++ b/test/integration/targets/include_import/test_grandparent_inheritance.yml
@@ -0,0 +1,29 @@
+---
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - debug:
+ var: inventory_hostname
+
+ - name: Test included tasks inherit from block
+ check_mode: true
+ block:
+ - include_tasks: grandchild/block_include_tasks.yml
+
+ - debug:
+ var: block_include_result
+
+ - assert:
+ that:
+ - block_include_result is skipped
+
+ - name: Test included tasks inherit deeply from import
+ import_tasks: grandchild/import.yml
+ check_mode: true
+
+ - debug:
+ var: import_include_include_result
+
+ - assert:
+ that:
+ - import_include_include_result is skipped
diff --git a/test/integration/targets/include_import/test_grandparent_inheritance_fqcn.yml b/test/integration/targets/include_import/test_grandparent_inheritance_fqcn.yml
new file mode 100644
index 0000000..37a0ad0
--- /dev/null
+++ b/test/integration/targets/include_import/test_grandparent_inheritance_fqcn.yml
@@ -0,0 +1,29 @@
+---
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - debug:
+ var: inventory_hostname
+
+ - name: Test included tasks inherit from block
+ check_mode: true
+ block:
+ - ansible.builtin.include_tasks: grandchild/block_include_tasks.yml
+
+ - debug:
+ var: block_include_result
+
+ - assert:
+ that:
+ - block_include_result is skipped
+
+ - name: Test included tasks inherit deeply from import
+ ansible.builtin.import_tasks: grandchild/import.yml
+ check_mode: true
+
+ - debug:
+ var: import_include_include_result
+
+ - assert:
+ that:
+ - import_include_include_result is skipped
diff --git a/test/integration/targets/include_import/test_include_loop.yml b/test/integration/targets/include_import/test_include_loop.yml
new file mode 100644
index 0000000..33775d1
--- /dev/null
+++ b/test/integration/targets/include_import/test_include_loop.yml
@@ -0,0 +1,17 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: skipped include undefined loop
+ include_tasks: doesnt_matter.yml
+ loop: '{{ lkjsdflkjsdlfkjsdlfkjsdf }}'
+ when: false
+ register: skipped_include
+
+ - debug:
+ var: skipped_include
+
+ - assert:
+ that:
+ - skipped_include.results is undefined
+ - skipped_include.skip_reason is defined
+ - skipped_include is skipped
diff --git a/test/integration/targets/include_import/test_include_loop_fqcn.yml b/test/integration/targets/include_import/test_include_loop_fqcn.yml
new file mode 100644
index 0000000..62d91f2
--- /dev/null
+++ b/test/integration/targets/include_import/test_include_loop_fqcn.yml
@@ -0,0 +1,17 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: skipped include undefined loop
+ ansible.builtin.include_tasks: doesnt_matter.yml
+ loop: '{{ lkjsdflkjsdlfkjsdlfkjsdf }}'
+ when: false
+ register: skipped_include
+
+ - debug:
+ var: skipped_include
+
+ - assert:
+ that:
+ - skipped_include.results is undefined
+ - skipped_include.skip_reason is defined
+ - skipped_include is skipped
diff --git a/test/integration/targets/include_import/test_loop_var_bleed.yaml b/test/integration/targets/include_import/test_loop_var_bleed.yaml
new file mode 100644
index 0000000..a5146f3
--- /dev/null
+++ b/test/integration/targets/include_import/test_loop_var_bleed.yaml
@@ -0,0 +1,9 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - include_role:
+ name: loop_name_assert
+ loop:
+ - name_from_loop_var
+ loop_control:
+ loop_var: name
diff --git a/test/integration/targets/include_import/test_nested_tasks.yml b/test/integration/targets/include_import/test_nested_tasks.yml
new file mode 100644
index 0000000..7451ec4
--- /dev/null
+++ b/test/integration/targets/include_import/test_nested_tasks.yml
@@ -0,0 +1,6 @@
+- name: >-
+ verify that multiple level of nested statements and
+ include+meta doesnt mess included files mecanisms
+ hosts: testhost
+ tasks:
+ - include_tasks: ./tasks/nested/nested.yml
diff --git a/test/integration/targets/include_import/test_nested_tasks_fqcn.yml b/test/integration/targets/include_import/test_nested_tasks_fqcn.yml
new file mode 100644
index 0000000..14e72ee
--- /dev/null
+++ b/test/integration/targets/include_import/test_nested_tasks_fqcn.yml
@@ -0,0 +1,6 @@
+- name: >-
+ verify that multiple level of nested statements and
+ include+meta doesnt mess included files mecanisms
+ hosts: testhost
+ tasks:
+ - ansible.builtin.include_tasks: ./tasks/nested/nested.yml
diff --git a/test/integration/targets/include_import/test_role_recursion.yml b/test/integration/targets/include_import/test_role_recursion.yml
new file mode 100644
index 0000000..ad2489a
--- /dev/null
+++ b/test/integration/targets/include_import/test_role_recursion.yml
@@ -0,0 +1,7 @@
+- name: Test max recursion depth
+ hosts: testhost
+
+ tasks:
+ - import_role:
+ name: role1
+ tasks_from: r1t01.yml
diff --git a/test/integration/targets/include_import/test_role_recursion_fqcn.yml b/test/integration/targets/include_import/test_role_recursion_fqcn.yml
new file mode 100644
index 0000000..13d8d2c
--- /dev/null
+++ b/test/integration/targets/include_import/test_role_recursion_fqcn.yml
@@ -0,0 +1,7 @@
+- name: Test max recursion depth
+ hosts: testhost
+
+ tasks:
+ - ansible.builtin.import_role:
+ name: role1
+ tasks_from: r1t01.yml
diff --git a/test/integration/targets/include_import/undefined_var/include_tasks.yml b/test/integration/targets/include_import/undefined_var/include_tasks.yml
new file mode 100644
index 0000000..56f06c9
--- /dev/null
+++ b/test/integration/targets/include_import/undefined_var/include_tasks.yml
@@ -0,0 +1,5 @@
+---
+
+- debug:
+ msg: "This message comes from an 'include_tasks'-task! :-)"
+ register: "_include_tasks_task_result"
diff --git a/test/integration/targets/include_import/undefined_var/include_that_defines_var.yml b/test/integration/targets/include_import/undefined_var/include_that_defines_var.yml
new file mode 100644
index 0000000..7f24a43
--- /dev/null
+++ b/test/integration/targets/include_import/undefined_var/include_that_defines_var.yml
@@ -0,0 +1,5 @@
+- vars:
+ _undefined: 'yes'
+ block:
+ - set_fact:
+ _include_defined_result: 'good'
diff --git a/test/integration/targets/include_import/undefined_var/playbook.yml b/test/integration/targets/include_import/undefined_var/playbook.yml
new file mode 100644
index 0000000..6576d50
--- /dev/null
+++ b/test/integration/targets/include_import/undefined_var/playbook.yml
@@ -0,0 +1,35 @@
+---
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - include_tasks: "include_tasks.yml"
+ ignore_errors: True
+ register: "_include_tasks_result"
+ when:
+ - "_undefined == 'yes'"
+
+ - assert:
+ that:
+ - "_include_tasks_result is failed"
+ - "_include_tasks_task_result is not defined"
+ msg: "'include_tasks' did not evaluate it's attached condition and failed"
+
+ - include_role:
+ name: "no_log"
+ ignore_errors: True
+ register: "_include_role_result"
+ when:
+ - "_undefined == 'yes'"
+
+ - assert:
+ that:
+ - "_include_role_result is failed"
+ msg: "'include_role' did not evaluate it's attached condition and failed"
+
+ - import_tasks: include_that_defines_var.yml
+ when:
+ - "_undefined == 'yes'"
+
+ - assert:
+ that:
+ - _include_defined_result == 'good'
diff --git a/test/integration/targets/include_import/valid_include_keywords/include_me.yml b/test/integration/targets/include_import/valid_include_keywords/include_me.yml
new file mode 100644
index 0000000..ab5c6a9
--- /dev/null
+++ b/test/integration/targets/include_import/valid_include_keywords/include_me.yml
@@ -0,0 +1,6 @@
+- debug:
+ msg: include_me
+- assert:
+ that:
+ - loopy == 1
+ - baz == 'qux'
diff --git a/test/integration/targets/include_import/valid_include_keywords/include_me_listen.yml b/test/integration/targets/include_import/valid_include_keywords/include_me_listen.yml
new file mode 100644
index 0000000..47b424a
--- /dev/null
+++ b/test/integration/targets/include_import/valid_include_keywords/include_me_listen.yml
@@ -0,0 +1,2 @@
+- debug:
+ msg: listen
diff --git a/test/integration/targets/include_import/valid_include_keywords/include_me_notify.yml b/test/integration/targets/include_import/valid_include_keywords/include_me_notify.yml
new file mode 100644
index 0000000..4501e38
--- /dev/null
+++ b/test/integration/targets/include_import/valid_include_keywords/include_me_notify.yml
@@ -0,0 +1,2 @@
+- debug:
+ msg: notify
diff --git a/test/integration/targets/include_import/valid_include_keywords/playbook.yml b/test/integration/targets/include_import/valid_include_keywords/playbook.yml
new file mode 100644
index 0000000..c70ec81
--- /dev/null
+++ b/test/integration/targets/include_import/valid_include_keywords/playbook.yml
@@ -0,0 +1,40 @@
+- hosts: localhost
+ gather_facts: false
+ handlers:
+ - include_tasks:
+ file: include_me_listen.yml
+ listen:
+ - include_me_listen
+
+ - name: Include Me Notify
+ include_tasks: include_me_notify.yml
+
+ tasks:
+ - name: Include me
+ include_tasks: include_me.yml
+ args:
+ apply:
+ tags:
+ - bar
+ debugger: ~
+ ignore_errors: false
+ loop:
+ - 1
+ loop_control:
+ loop_var: loopy
+ no_log: false
+ register: this_isnt_useful
+ run_once: true
+ tags:
+ - foo
+ vars:
+ baz: qux
+ when: true
+
+ - command: "true"
+ notify:
+ - include_me_listen
+
+ - command: "true"
+ notify:
+ - Include Me Notify
diff --git a/test/integration/targets/include_import_tasks_nested/aliases b/test/integration/targets/include_import_tasks_nested/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/include_import_tasks_nested/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/include_import_tasks_nested/tasks/main.yml b/test/integration/targets/include_import_tasks_nested/tasks/main.yml
new file mode 100644
index 0000000..5d67267
--- /dev/null
+++ b/test/integration/targets/include_import_tasks_nested/tasks/main.yml
@@ -0,0 +1,11 @@
+- include_tasks: nested/nested_include.yml
+
+- assert:
+ that:
+ - nested_adjacent_count|int == 1
+
+- import_tasks: nested/nested_import.yml
+
+- assert:
+ that:
+ - nested_adjacent_count|int == 2
diff --git a/test/integration/targets/include_import_tasks_nested/tasks/nested/nested_adjacent.yml b/test/integration/targets/include_import_tasks_nested/tasks/nested/nested_adjacent.yml
new file mode 100644
index 0000000..89c5c93
--- /dev/null
+++ b/test/integration/targets/include_import_tasks_nested/tasks/nested/nested_adjacent.yml
@@ -0,0 +1,2 @@
+- set_fact:
+ nested_adjacent_count: '{{ nested_adjacent_count|default(0)|int + 1 }}'
diff --git a/test/integration/targets/include_import_tasks_nested/tasks/nested/nested_import.yml b/test/integration/targets/include_import_tasks_nested/tasks/nested/nested_import.yml
new file mode 100644
index 0000000..a3b0930
--- /dev/null
+++ b/test/integration/targets/include_import_tasks_nested/tasks/nested/nested_import.yml
@@ -0,0 +1 @@
+- import_tasks: nested_adjacent.yml
diff --git a/test/integration/targets/include_import_tasks_nested/tasks/nested/nested_include.yml b/test/integration/targets/include_import_tasks_nested/tasks/nested/nested_include.yml
new file mode 100644
index 0000000..8d4bfb0
--- /dev/null
+++ b/test/integration/targets/include_import_tasks_nested/tasks/nested/nested_include.yml
@@ -0,0 +1 @@
+- include_tasks: nested_adjacent.yml
diff --git a/test/integration/targets/include_parent_role_vars/aliases b/test/integration/targets/include_parent_role_vars/aliases
new file mode 100644
index 0000000..23abb8d
--- /dev/null
+++ b/test/integration/targets/include_parent_role_vars/aliases
@@ -0,0 +1,2 @@
+# Continuation of special_vars integration tests to test special variables set on role inclusion.
+hidden \ No newline at end of file
diff --git a/test/integration/targets/include_parent_role_vars/tasks/included_by_other_role.yml b/test/integration/targets/include_parent_role_vars/tasks/included_by_other_role.yml
new file mode 100644
index 0000000..79b7b1c
--- /dev/null
+++ b/test/integration/targets/include_parent_role_vars/tasks/included_by_other_role.yml
@@ -0,0 +1,37 @@
+# Copyright 2019 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+- name: ensure our parent role tree to contain only our direct parent item
+ assert:
+ that:
+ - "ansible_parent_role_names == ['special_vars']"
+
+- name: ensure that ansible_parent_role_paths has the same length as ansible_parent_role_names
+ assert:
+ that:
+ - "ansible_parent_role_names|length == ansible_parent_role_paths|length"
+
+- name: attempt to import ourselves
+ import_role:
+ name: "include_parent_role_vars"
+ tasks_from: "included_by_ourselves.yml"
+
+- name: ensure our parent role tree to contain only our direct parent item after importing
+ assert:
+ that:
+ - "ansible_parent_role_names == ['special_vars']"
+
+- name: attempt to include ourselves
+ include_role:
+ name: "include_parent_role_vars"
+ tasks_from: "included_by_ourselves.yml"
+
+- name: ensure our parent role tree to contain only our direct parent item after including
+ assert:
+ that:
+ - "ansible_parent_role_names == ['special_vars']"
+
+- name: ensure that ansible_parent_role_paths has the same length as ansible_parent_role_names
+ assert:
+ that:
+ - "ansible_parent_role_names|length == ansible_parent_role_paths|length"
diff --git a/test/integration/targets/include_parent_role_vars/tasks/included_by_ourselves.yml b/test/integration/targets/include_parent_role_vars/tasks/included_by_ourselves.yml
new file mode 100644
index 0000000..3ea9300
--- /dev/null
+++ b/test/integration/targets/include_parent_role_vars/tasks/included_by_ourselves.yml
@@ -0,0 +1,14 @@
+# Copyright 2019 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+- name: check if the inclusion tree shows ourself twice as well as our initial parent
+ assert:
+ that:
+ - "ansible_parent_role_names|length == 2"
+ - "ansible_parent_role_names[0] == 'include_parent_role_vars'" # Since we included ourselves, we're the top level
+ - "ansible_parent_role_names[1] == 'special_vars'"
+
+- name: ensure that ansible_parent_role_paths has the same length as ansible_parent_role_names
+ assert:
+ that:
+ - "ansible_parent_role_names|length == ansible_parent_role_paths|length"
diff --git a/test/integration/targets/include_parent_role_vars/tasks/main.yml b/test/integration/targets/include_parent_role_vars/tasks/main.yml
new file mode 100644
index 0000000..56a485b
--- /dev/null
+++ b/test/integration/targets/include_parent_role_vars/tasks/main.yml
@@ -0,0 +1,21 @@
+# Copyright 2019 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+
+- name: ensure our parent role tree to contain only our direct parent item
+ assert:
+ that:
+ - "ansible_parent_role_names == ['special_vars']"
+
+- name: ensure that ansible_parent_role_paths has the same length as ansible_parent_role_names
+ assert:
+ that:
+ - "ansible_parent_role_names|length == ansible_parent_role_paths|length"
+
+# task importing should not affect ansible_parent_role_names
+- name: test task-importing after we've been included by another role
+ import_tasks: "included_by_other_role.yml"
+
+# task inclusion should not affect ansible_parent_role_names
+- name: test task-inclusion after we've been included by another role
+ include_tasks: "included_by_other_role.yml"
diff --git a/test/integration/targets/include_vars-ad-hoc/aliases b/test/integration/targets/include_vars-ad-hoc/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/include_vars-ad-hoc/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/include_vars-ad-hoc/dir/inc.yml b/test/integration/targets/include_vars-ad-hoc/dir/inc.yml
new file mode 100644
index 0000000..c1d24c8
--- /dev/null
+++ b/test/integration/targets/include_vars-ad-hoc/dir/inc.yml
@@ -0,0 +1 @@
+porter: cable
diff --git a/test/integration/targets/include_vars-ad-hoc/runme.sh b/test/integration/targets/include_vars-ad-hoc/runme.sh
new file mode 100755
index 0000000..51b68d2
--- /dev/null
+++ b/test/integration/targets/include_vars-ad-hoc/runme.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible testhost -i ../../inventory -m include_vars -a 'dir/inc.yml' "$@"
+ansible testhost -i ../../inventory -m include_vars -a 'dir=dir' "$@"
diff --git a/test/integration/targets/include_vars/aliases b/test/integration/targets/include_vars/aliases
new file mode 100644
index 0000000..765b70d
--- /dev/null
+++ b/test/integration/targets/include_vars/aliases
@@ -0,0 +1 @@
+shippable/posix/group2
diff --git a/test/integration/targets/include_vars/defaults/main.yml b/test/integration/targets/include_vars/defaults/main.yml
new file mode 100644
index 0000000..901fb22
--- /dev/null
+++ b/test/integration/targets/include_vars/defaults/main.yml
@@ -0,0 +1,3 @@
+---
+testing: 1
+base_dir: defaults
diff --git a/test/integration/targets/include_vars/tasks/main.yml b/test/integration/targets/include_vars/tasks/main.yml
new file mode 100644
index 0000000..db15ba3
--- /dev/null
+++ b/test/integration/targets/include_vars/tasks/main.yml
@@ -0,0 +1,217 @@
+---
+- name: verify that the default value is indeed 1
+ assert:
+ that:
+ - "testing == 1"
+ - "base_dir == 'defaults'"
+
+- name: include the vars/environments/development/all.yml
+ include_vars:
+ file: environments/development/all.yml
+ register: included_one_file
+
+- name: verify that the correct file has been loaded and default value is indeed 789
+ assert:
+ that:
+ - "testing == 789"
+ - "base_dir == 'environments/development'"
+ - "{{ included_one_file.ansible_included_var_files | length }} == 1"
+ - "'vars/environments/development/all.yml' in included_one_file.ansible_included_var_files[0]"
+
+- name: include the vars/environments/development/all.yml and save results in all
+ include_vars:
+ file: environments/development/all.yml
+ name: all
+
+- name: verify that the values are stored in the all variable
+ assert:
+ that:
+ - "all['testing'] == 789"
+ - "all['base_dir'] == 'environments/development'"
+
+- name: include the all directory in vars
+ include_vars:
+ dir: all
+ depth: 1
+
+- name: verify that the default value is indeed 123
+ assert:
+ that:
+ - "testing == 123"
+ - "base_dir == 'all'"
+
+- name: include var files with extension only
+ include_vars:
+ dir: webapp
+ ignore_unknown_extensions: True
+ extensions: ['', 'yaml', 'yml', 'json']
+ register: include_without_file_extension
+
+- name: verify that only files with valid extensions are loaded
+ assert:
+ that:
+ - webapp_version is defined
+ - "'file_without_extension' in '{{ include_without_file_extension.ansible_included_var_files | join(' ') }}'"
+
+- name: include every directory in vars
+ include_vars:
+ dir: vars
+ extensions: ['', 'yaml', 'yml', 'json']
+ ignore_files:
+ - no_auto_unsafe.yml
+ register: include_every_dir
+
+- name: verify that the correct files have been loaded and overwrite based on alphabetical order
+ assert:
+ that:
+ - "testing == 456"
+ - "base_dir == 'services'"
+ - "webapp_containers == 10"
+ - "{{ include_every_dir.ansible_included_var_files | length }} == 7"
+ - "'vars/all/all.yml' in include_every_dir.ansible_included_var_files[0]"
+ - "'vars/environments/development/all.yml' in include_every_dir.ansible_included_var_files[1]"
+ - "'vars/environments/development/services/webapp.yml' in include_every_dir.ansible_included_var_files[2]"
+ - "'vars/services/webapp.yml' in include_every_dir.ansible_included_var_files[5]"
+ - "'vars/webapp/file_without_extension' in include_every_dir.ansible_included_var_files[6]"
+
+- name: include every directory in vars except files matching webapp.yml
+ include_vars:
+ dir: vars
+ ignore_files:
+ - webapp.yml
+ - file_without_extension
+ - no_auto_unsafe.yml
+ register: include_without_webapp
+
+- name: verify that the webapp.yml file was not included
+ assert:
+ that:
+ - "testing == 789"
+ - "base_dir == 'environments/development'"
+ - "{{ include_without_webapp.ansible_included_var_files | length }} == 4"
+ - "'webapp.yml' not in '{{ include_without_webapp.ansible_included_var_files | join(' ') }}'"
+ - "'file_without_extension' not in '{{ include_without_webapp.ansible_included_var_files | join(' ') }}'"
+
+- name: include only files matching webapp.yml
+ include_vars:
+ dir: environments
+ files_matching: webapp.yml
+ register: include_match_webapp
+
+- name: verify that only files matching webapp.yml and in the environments directory get loaded.
+ assert:
+ that:
+ - "testing == 101112"
+ - "base_dir == 'development/services'"
+ - "webapp_containers == 20"
+ - "{{ include_match_webapp.ansible_included_var_files | length }} == 1"
+ - "'vars/environments/development/services/webapp.yml' in include_match_webapp.ansible_included_var_files[0]"
+ - "'all.yml' not in '{{ include_match_webapp.ansible_included_var_files | join(' ') }}'"
+
+- name: include only files matching webapp.yml and store results in webapp
+ include_vars:
+ dir: environments
+ files_matching: webapp.yml
+ name: webapp
+
+- name: verify that only files matching webapp.yml and in the environments directory get loaded into stored variable webapp.
+ assert:
+ that:
+ - "webapp['testing'] == 101112"
+ - "webapp['base_dir'] == 'development/services'"
+ - "webapp['webapp_containers'] == 20"
+
+- name: include var files without extension
+ include_vars:
+ dir: webapp
+ ignore_unknown_extensions: False
+ register: include_with_unknown_file_extension
+ ignore_errors: True
+
+- name: verify that files without valid extensions are loaded
+ assert:
+ that:
+ - "'a valid extension' in include_with_unknown_file_extension.message"
+
+- name: include var with raw params
+ include_vars: >
+ services/service_vars.yml
+
+- name: Verify that files with raw params is include without new line character
+ assert:
+ that:
+ - "service_name == 'my_custom_service'"
+
+- name: Check NoneType for raw params and file
+ include_vars:
+ file: "{{ lookup('first_found', possible_files, errors='ignore') }}"
+ vars:
+ possible_files:
+ - "does_not_exist.yml"
+ ignore_errors: True
+ register: include_with_non_existent_file
+
+- name: Verify that file and raw_params provide correct error message to user
+ assert:
+ that:
+ - "'Could not find file' in include_with_non_existent_file.message"
+
+- name: include var (FQCN) with raw params
+ ansible.builtin.include_vars: >
+ services/service_vars_fqcn.yml
+
+- name: Verify that FQCN of include_vars works
+ assert:
+ that:
+ - "'my_custom_service' == service_name_fqcn"
+ - "'my_custom_service' == service_name_tmpl_fqcn"
+
+- name: Include a vars file with a hash variable
+ include_vars:
+ file: vars2/hashes/hash1.yml
+
+- name: Verify the hash variable
+ assert:
+ that:
+ - "{{ config | length }} == 3"
+ - "config.key0 == 0"
+ - "config.key1 == 0"
+ - "{{ config.key2 | length }} == 1"
+ - "config.key2.a == 21"
+
+- name: Include the second file to merge the hash variable
+ include_vars:
+ file: vars2/hashes/hash2.yml
+ hash_behaviour: merge
+
+- name: Verify that the hash is merged
+ assert:
+ that:
+ - "{{ config | length }} == 4"
+ - "config.key0 == 0"
+ - "config.key1 == 1"
+ - "{{ config.key2 | length }} == 2"
+ - "config.key2.a == 21"
+ - "config.key2.b == 22"
+ - "config.key3 == 3"
+
+- name: Include the second file again without hash_behaviour option
+ include_vars:
+ file: vars2/hashes/hash2.yml
+
+- name: Verify that the properties from the first file is cleared
+ assert:
+ that:
+ - "{{ config | length }} == 3"
+ - "config.key1 == 1"
+ - "{{ config.key2 | length }} == 1"
+ - "config.key2.b == 22"
+ - "config.key3 == 3"
+
+- include_vars:
+ file: no_auto_unsafe.yml
+ register: baz
+
+- assert:
+ that:
+ - baz.ansible_facts.foo|type_debug != "AnsibleUnsafeText"
diff --git a/test/integration/targets/include_vars/vars/all/all.yml b/test/integration/targets/include_vars/vars/all/all.yml
new file mode 100644
index 0000000..14c3e92
--- /dev/null
+++ b/test/integration/targets/include_vars/vars/all/all.yml
@@ -0,0 +1,3 @@
+---
+testing: 123
+base_dir: all
diff --git a/test/integration/targets/include_vars/vars/environments/development/all.yml b/test/integration/targets/include_vars/vars/environments/development/all.yml
new file mode 100644
index 0000000..9f370de
--- /dev/null
+++ b/test/integration/targets/include_vars/vars/environments/development/all.yml
@@ -0,0 +1,3 @@
+---
+testing: 789
+base_dir: 'environments/development'
diff --git a/test/integration/targets/include_vars/vars/environments/development/services/webapp.yml b/test/integration/targets/include_vars/vars/environments/development/services/webapp.yml
new file mode 100644
index 0000000..a0a809c
--- /dev/null
+++ b/test/integration/targets/include_vars/vars/environments/development/services/webapp.yml
@@ -0,0 +1,4 @@
+---
+testing: 101112
+base_dir: 'development/services'
+webapp_containers: 20
diff --git a/test/integration/targets/include_vars/vars/no_auto_unsafe.yml b/test/integration/targets/include_vars/vars/no_auto_unsafe.yml
new file mode 100644
index 0000000..20e9ff3
--- /dev/null
+++ b/test/integration/targets/include_vars/vars/no_auto_unsafe.yml
@@ -0,0 +1 @@
+foo: bar
diff --git a/test/integration/targets/include_vars/vars/services/service_vars.yml b/test/integration/targets/include_vars/vars/services/service_vars.yml
new file mode 100644
index 0000000..96b05d6
--- /dev/null
+++ b/test/integration/targets/include_vars/vars/services/service_vars.yml
@@ -0,0 +1,2 @@
+---
+service_name: 'my_custom_service' \ No newline at end of file
diff --git a/test/integration/targets/include_vars/vars/services/service_vars_fqcn.yml b/test/integration/targets/include_vars/vars/services/service_vars_fqcn.yml
new file mode 100644
index 0000000..2c04fee
--- /dev/null
+++ b/test/integration/targets/include_vars/vars/services/service_vars_fqcn.yml
@@ -0,0 +1,3 @@
+---
+service_name_fqcn: 'my_custom_service'
+service_name_tmpl_fqcn: '{{ service_name_fqcn }}' \ No newline at end of file
diff --git a/test/integration/targets/include_vars/vars/services/webapp.yml b/test/integration/targets/include_vars/vars/services/webapp.yml
new file mode 100644
index 0000000..f0dcc8b
--- /dev/null
+++ b/test/integration/targets/include_vars/vars/services/webapp.yml
@@ -0,0 +1,4 @@
+---
+testing: 456
+base_dir: services
+webapp_containers: 10
diff --git a/test/integration/targets/include_vars/vars/webapp/file_without_extension b/test/integration/targets/include_vars/vars/webapp/file_without_extension
new file mode 100644
index 0000000..9cfb60f
--- /dev/null
+++ b/test/integration/targets/include_vars/vars/webapp/file_without_extension
@@ -0,0 +1,2 @@
+---
+webapp_version: "1"
diff --git a/test/integration/targets/include_vars/vars2/hashes/hash1.yml b/test/integration/targets/include_vars/vars2/hashes/hash1.yml
new file mode 100644
index 0000000..b0706f8
--- /dev/null
+++ b/test/integration/targets/include_vars/vars2/hashes/hash1.yml
@@ -0,0 +1,5 @@
+---
+config:
+ key0: 0
+ key1: 0
+ key2: { a: 21 }
diff --git a/test/integration/targets/include_vars/vars2/hashes/hash2.yml b/test/integration/targets/include_vars/vars2/hashes/hash2.yml
new file mode 100644
index 0000000..1f2a963
--- /dev/null
+++ b/test/integration/targets/include_vars/vars2/hashes/hash2.yml
@@ -0,0 +1,5 @@
+---
+config:
+ key1: 1
+ key2: { b: 22 }
+ key3: 3
diff --git a/test/integration/targets/include_when_parent_is_dynamic/aliases b/test/integration/targets/include_when_parent_is_dynamic/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/include_when_parent_is_dynamic/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/include_when_parent_is_dynamic/playbook.yml b/test/integration/targets/include_when_parent_is_dynamic/playbook.yml
new file mode 100644
index 0000000..afdbc54
--- /dev/null
+++ b/test/integration/targets/include_when_parent_is_dynamic/playbook.yml
@@ -0,0 +1,4 @@
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - include_tasks: tasks.yml
diff --git a/test/integration/targets/include_when_parent_is_dynamic/runme.sh b/test/integration/targets/include_when_parent_is_dynamic/runme.sh
new file mode 100755
index 0000000..fa7a345
--- /dev/null
+++ b/test/integration/targets/include_when_parent_is_dynamic/runme.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook playbook.yml "$@" > output.log 2>&1 || true
+
+if grep "task should always execute" output.log >/dev/null; then
+ echo "Test passed (playbook failed with expected output, output not shown)."
+ exit 0
+fi
+
+cat output.log
+exit 1
diff --git a/test/integration/targets/include_when_parent_is_dynamic/syntax_error.yml b/test/integration/targets/include_when_parent_is_dynamic/syntax_error.yml
new file mode 100644
index 0000000..101a18a
--- /dev/null
+++ b/test/integration/targets/include_when_parent_is_dynamic/syntax_error.yml
@@ -0,0 +1 @@
+intentional syntax error which should NOT be encountered
diff --git a/test/integration/targets/include_when_parent_is_dynamic/tasks.yml b/test/integration/targets/include_when_parent_is_dynamic/tasks.yml
new file mode 100644
index 0000000..6831245
--- /dev/null
+++ b/test/integration/targets/include_when_parent_is_dynamic/tasks.yml
@@ -0,0 +1,12 @@
+# intentionally stop execution of the play before reaching the include below
+# if the include is dynamic as expected it will not trigger a syntax error
+# however, if the include is static a syntax error will occur
+- name: EXPECTED FAILURE
+ fail:
+ msg:
+ This task should always execute.
+ The playbook would have failed due to a syntax error in 'syntax_error.yml' when attempting a static include of that file.
+
+# perform an include task which should be static if all of the task's parents are static, otherwise it should be dynamic
+# this file was loaded using include_tasks, which is dynamic, so this include should also be dynamic
+- include: syntax_error.yml
diff --git a/test/integration/targets/include_when_parent_is_static/aliases b/test/integration/targets/include_when_parent_is_static/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/include_when_parent_is_static/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/include_when_parent_is_static/playbook.yml b/test/integration/targets/include_when_parent_is_static/playbook.yml
new file mode 100644
index 0000000..6189873
--- /dev/null
+++ b/test/integration/targets/include_when_parent_is_static/playbook.yml
@@ -0,0 +1,4 @@
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - import_tasks: tasks.yml
diff --git a/test/integration/targets/include_when_parent_is_static/runme.sh b/test/integration/targets/include_when_parent_is_static/runme.sh
new file mode 100755
index 0000000..0b66f6b
--- /dev/null
+++ b/test/integration/targets/include_when_parent_is_static/runme.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook playbook.yml "$@" > output.log 2>&1 || true
+
+if grep "intentional syntax error" output.log >/dev/null; then
+ echo "Test passed (playbook failed with expected output, output not shown)."
+ exit 0
+fi
+
+cat output.log
+exit 1
diff --git a/test/integration/targets/include_when_parent_is_static/syntax_error.yml b/test/integration/targets/include_when_parent_is_static/syntax_error.yml
new file mode 100644
index 0000000..e1a629c
--- /dev/null
+++ b/test/integration/targets/include_when_parent_is_static/syntax_error.yml
@@ -0,0 +1 @@
+intentional syntax error which SHOULD be encountered
diff --git a/test/integration/targets/include_when_parent_is_static/tasks.yml b/test/integration/targets/include_when_parent_is_static/tasks.yml
new file mode 100644
index 0000000..a234a3d
--- /dev/null
+++ b/test/integration/targets/include_when_parent_is_static/tasks.yml
@@ -0,0 +1,12 @@
+# intentionally stop execution of the play before reaching the include below
+# if the include is static as expected it will trigger a syntax error
+# however, if the include is dynamic a syntax error will not occur
+- name: EXPECTED SUCCESS
+ fail:
+ msg:
+ This task should never execute.
+ The playbook should have failed due to a syntax error in 'syntax_error.yml' when attempting a static include of that file.
+
+# perform an include task which should be static if all of the task's parents are static, otherwise it should be dynamic
+# this file was loaded using import_tasks, which is static, so this include should also be static
+- include: syntax_error.yml
diff --git a/test/integration/targets/includes/aliases b/test/integration/targets/includes/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/includes/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/includes/include_on_playbook_should_fail.yml b/test/integration/targets/includes/include_on_playbook_should_fail.yml
new file mode 100644
index 0000000..953459d
--- /dev/null
+++ b/test/integration/targets/includes/include_on_playbook_should_fail.yml
@@ -0,0 +1 @@
+- include: test_includes3.yml
diff --git a/test/integration/targets/includes/includes_loop_rescue.yml b/test/integration/targets/includes/includes_loop_rescue.yml
new file mode 100644
index 0000000..af2743a
--- /dev/null
+++ b/test/integration/targets/includes/includes_loop_rescue.yml
@@ -0,0 +1,29 @@
+- name: "Test rescue/always sections with includes in a loop, strategy={{ strategy }}"
+ hosts: localhost
+ gather_facts: false
+ strategy: "{{ strategy }}"
+ tasks:
+ - block:
+ - include_role:
+ name: "{{ item }}"
+ loop:
+ - a
+ - b
+ rescue:
+ - debug:
+ msg: rescue include_role in a loop
+ always:
+ - debug:
+ msg: always include_role in a loop
+
+ - block:
+ - include_tasks: "{{ item }}"
+ loop:
+ - a
+ - b
+ rescue:
+ - debug:
+ msg: rescue include_tasks in a loop
+ always:
+ - debug:
+ msg: always include_tasks in a loop
diff --git a/test/integration/targets/includes/inherit_notify.yml b/test/integration/targets/includes/inherit_notify.yml
new file mode 100644
index 0000000..f868be1
--- /dev/null
+++ b/test/integration/targets/includes/inherit_notify.yml
@@ -0,0 +1,18 @@
+- hosts: localhost
+ gather_facts: false
+ pre_tasks:
+ - include_tasks:
+ file: tasks/trigger_change.yml
+ apply:
+ notify: hello
+
+ handlers:
+ - name: hello
+ set_fact: hello=world
+
+ tasks:
+ - name: ensure handler ran
+ assert:
+ that:
+ - hello is defined
+ - "hello == 'world'"
diff --git a/test/integration/targets/includes/roles/test_includes/handlers/main.yml b/test/integration/targets/includes/roles/test_includes/handlers/main.yml
new file mode 100644
index 0000000..7d3e625
--- /dev/null
+++ b/test/integration/targets/includes/roles/test_includes/handlers/main.yml
@@ -0,0 +1 @@
+- include: more_handlers.yml
diff --git a/test/integration/targets/includes/roles/test_includes/handlers/more_handlers.yml b/test/integration/targets/includes/roles/test_includes/handlers/more_handlers.yml
new file mode 100644
index 0000000..c85d53c
--- /dev/null
+++ b/test/integration/targets/includes/roles/test_includes/handlers/more_handlers.yml
@@ -0,0 +1,12 @@
+- name: included_handler
+ set_fact:
+ ca: 4001
+ cb: 4002
+ cc: 4003
+
+- name: verify_handler
+ assert:
+ that:
+ - "ca == 4001"
+ - "cb == 4002"
+ - "cc == 4003"
diff --git a/test/integration/targets/includes/roles/test_includes/tasks/branch_toplevel.yml b/test/integration/targets/includes/roles/test_includes/tasks/branch_toplevel.yml
new file mode 100644
index 0000000..30cd6f2
--- /dev/null
+++ b/test/integration/targets/includes/roles/test_includes/tasks/branch_toplevel.yml
@@ -0,0 +1,11 @@
+# 'canary2' used instead of 'canary', otherwise a "recursive loop detected in
+# template string" occurs when both includes use static=yes
+- import_tasks: leaf_sublevel.yml
+ vars:
+ canary2: '{{ canary }}'
+ when: 'nested_include_static|bool' # value for 'static' can not be a variable, hence use 'when'
+
+- include_tasks: leaf_sublevel.yml
+ vars:
+ canary2: '{{ canary }}'
+ when: 'not nested_include_static|bool'
diff --git a/test/integration/targets/includes/roles/test_includes/tasks/empty.yml b/test/integration/targets/includes/roles/test_includes/tasks/empty.yml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/includes/roles/test_includes/tasks/empty.yml
diff --git a/test/integration/targets/includes/roles/test_includes/tasks/included_task1.yml b/test/integration/targets/includes/roles/test_includes/tasks/included_task1.yml
new file mode 100644
index 0000000..6f4c048
--- /dev/null
+++ b/test/integration/targets/includes/roles/test_includes/tasks/included_task1.yml
@@ -0,0 +1,9 @@
+- set_fact:
+ ca: "{{ a }}"
+- debug: var=ca
+- set_fact:
+ cb: "{{b}}"
+- debug: var=cb
+- set_fact:
+ cc: "{{ c }}"
+- debug: var=cc
diff --git a/test/integration/targets/includes/roles/test_includes/tasks/leaf_sublevel.yml b/test/integration/targets/includes/roles/test_includes/tasks/leaf_sublevel.yml
new file mode 100644
index 0000000..0663201
--- /dev/null
+++ b/test/integration/targets/includes/roles/test_includes/tasks/leaf_sublevel.yml
@@ -0,0 +1,2 @@
+- set_fact:
+ canary_fact: '{{ canary2 }}'
diff --git a/test/integration/targets/includes/roles/test_includes/tasks/main.yml b/test/integration/targets/includes/roles/test_includes/tasks/main.yml
new file mode 100644
index 0000000..83ca468
--- /dev/null
+++ b/test/integration/targets/includes/roles/test_includes/tasks/main.yml
@@ -0,0 +1,114 @@
+# test code for the ping module
+# (c) 2014, James Cammarata <jcammarata@ansible.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+
+- include: included_task1.yml a=1 b=2 c=3
+
+- name: verify non-variable include params
+ assert:
+ that:
+ - "ca == '1'"
+ - "cb == '2'"
+ - "cc == '3'"
+
+- set_fact:
+ a: 101
+ b: 102
+ c: 103
+
+- include: included_task1.yml a={{a}} b={{b}} c=103
+
+- name: verify variable include params
+ assert:
+ that:
+ - "ca == 101"
+ - "cb == 102"
+ - "cc == 103"
+
+# Test that strings are not turned into numbers
+- set_fact:
+ a: "101"
+ b: "102"
+ c: "103"
+
+- include: included_task1.yml a={{a}} b={{b}} c=103
+
+- name: verify variable include params
+ assert:
+ that:
+ - "ca == '101'"
+ - "cb == '102'"
+ - "cc == '103'"
+
+# now try long form includes
+
+- include: included_task1.yml
+ vars:
+ a: 201
+ b: 202
+ c: 203
+
+- debug: var=a
+- debug: var=b
+- debug: var=c
+
+- name: verify long-form include params
+ assert:
+ that:
+ - "ca == 201"
+ - "cb == 202"
+ - "cc == 203"
+
+- name: test handlers with includes
+ shell: echo 1
+ notify:
+ # both these via a handler include
+ - included_handler
+ - verify_handler
+
+- include_tasks: branch_toplevel.yml
+ vars:
+ canary: value1
+ nested_include_static: 'no'
+- assert:
+ that:
+ - 'canary_fact == "value1"'
+
+- include_tasks: branch_toplevel.yml
+ vars:
+ canary: value2
+ nested_include_static: 'yes'
+- assert:
+ that:
+ - 'canary_fact == "value2"'
+
+- import_tasks: branch_toplevel.yml
+ vars:
+ canary: value3
+ nested_include_static: 'no'
+- assert:
+ that:
+ - 'canary_fact == "value3"'
+
+- import_tasks: branch_toplevel.yml
+ vars:
+ canary: value4
+ nested_include_static: 'yes'
+- assert:
+ that:
+ - 'canary_fact == "value4"'
diff --git a/test/integration/targets/includes/roles/test_includes/tasks/not_a_role_task.yml b/test/integration/targets/includes/roles/test_includes/tasks/not_a_role_task.yml
new file mode 100644
index 0000000..862b051
--- /dev/null
+++ b/test/integration/targets/includes/roles/test_includes/tasks/not_a_role_task.yml
@@ -0,0 +1,4 @@
+- set_fact:
+ ca: 33000
+ cb: 33001
+ cc: 33002
diff --git a/test/integration/targets/includes/roles/test_includes_free/tasks/inner.yml b/test/integration/targets/includes/roles/test_includes_free/tasks/inner.yml
new file mode 100644
index 0000000..d9c32f4
--- /dev/null
+++ b/test/integration/targets/includes/roles/test_includes_free/tasks/inner.yml
@@ -0,0 +1,2 @@
+- set_fact:
+ inner: "reached"
diff --git a/test/integration/targets/includes/roles/test_includes_free/tasks/inner_fqcn.yml b/test/integration/targets/includes/roles/test_includes_free/tasks/inner_fqcn.yml
new file mode 100644
index 0000000..5b4ce04
--- /dev/null
+++ b/test/integration/targets/includes/roles/test_includes_free/tasks/inner_fqcn.yml
@@ -0,0 +1,2 @@
+- set_fact:
+ inner_fqcn: "reached"
diff --git a/test/integration/targets/includes/roles/test_includes_free/tasks/main.yml b/test/integration/targets/includes/roles/test_includes_free/tasks/main.yml
new file mode 100644
index 0000000..5ae7882
--- /dev/null
+++ b/test/integration/targets/includes/roles/test_includes_free/tasks/main.yml
@@ -0,0 +1,9 @@
+- name: this needs to be here
+ debug:
+ msg: "hello"
+- include: inner.yml
+ with_items:
+ - '1'
+- ansible.builtin.include: inner_fqcn.yml
+ with_items:
+ - '1'
diff --git a/test/integration/targets/includes/roles/test_includes_host_pinned/tasks/inner.yml b/test/integration/targets/includes/roles/test_includes_host_pinned/tasks/inner.yml
new file mode 100644
index 0000000..fa4ec93
--- /dev/null
+++ b/test/integration/targets/includes/roles/test_includes_host_pinned/tasks/inner.yml
@@ -0,0 +1,2 @@
+- set_fact:
+ inner_host_pinned: "reached"
diff --git a/test/integration/targets/includes/roles/test_includes_host_pinned/tasks/main.yml b/test/integration/targets/includes/roles/test_includes_host_pinned/tasks/main.yml
new file mode 100644
index 0000000..7bc19fa
--- /dev/null
+++ b/test/integration/targets/includes/roles/test_includes_host_pinned/tasks/main.yml
@@ -0,0 +1,6 @@
+- name: this needs to be here
+ debug:
+ msg: "hello"
+- include: inner.yml
+ with_items:
+ - '1'
diff --git a/test/integration/targets/includes/runme.sh b/test/integration/targets/includes/runme.sh
new file mode 100755
index 0000000..e619fea
--- /dev/null
+++ b/test/integration/targets/includes/runme.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook test_includes.yml -i ../../inventory "$@"
+
+ansible-playbook inherit_notify.yml "$@"
+
+echo "EXPECTED ERROR: Ensure we fail if using 'include' to include a playbook."
+set +e
+result="$(ansible-playbook -i ../../inventory include_on_playbook_should_fail.yml -v "$@" 2>&1)"
+set -e
+grep -q "ERROR! 'include' is not a valid attribute for a Play" <<< "$result"
+
+ansible-playbook includes_loop_rescue.yml --extra-vars strategy=linear "$@"
+ansible-playbook includes_loop_rescue.yml --extra-vars strategy=free "$@"
diff --git a/test/integration/targets/includes/tasks/trigger_change.yml b/test/integration/targets/includes/tasks/trigger_change.yml
new file mode 100644
index 0000000..6ee4551
--- /dev/null
+++ b/test/integration/targets/includes/tasks/trigger_change.yml
@@ -0,0 +1,2 @@
+- debug: msg="I trigger changed!"
+ changed_when: true
diff --git a/test/integration/targets/includes/test_include_free.yml b/test/integration/targets/includes/test_include_free.yml
new file mode 100644
index 0000000..dedad73
--- /dev/null
+++ b/test/integration/targets/includes/test_include_free.yml
@@ -0,0 +1,10 @@
+- hosts: testhost
+ gather_facts: no
+ strategy: free
+ roles:
+ - test_includes_free
+ tasks:
+ - assert:
+ that:
+ - "inner == 'reached'"
+ - "inner_fqcn == 'reached'"
diff --git a/test/integration/targets/includes/test_include_host_pinned.yml b/test/integration/targets/includes/test_include_host_pinned.yml
new file mode 100644
index 0000000..6ff92c6
--- /dev/null
+++ b/test/integration/targets/includes/test_include_host_pinned.yml
@@ -0,0 +1,9 @@
+- hosts: testhost
+ gather_facts: no
+ strategy: host_pinned
+ roles:
+ - test_includes_host_pinned
+ tasks:
+ - assert:
+ that:
+ - "inner_host_pinned == 'reached'"
diff --git a/test/integration/targets/includes/test_includes.yml b/test/integration/targets/includes/test_includes.yml
new file mode 100644
index 0000000..80b009c
--- /dev/null
+++ b/test/integration/targets/includes/test_includes.yml
@@ -0,0 +1,10 @@
+- import_playbook: test_includes2.yml
+ vars:
+ parameter1: asdf
+ parameter2: jkl
+
+- import_playbook: test_includes3.yml
+
+- import_playbook: test_include_free.yml
+
+- import_playbook: test_include_host_pinned.yml
diff --git a/test/integration/targets/includes/test_includes2.yml b/test/integration/targets/includes/test_includes2.yml
new file mode 100644
index 0000000..a32e851
--- /dev/null
+++ b/test/integration/targets/includes/test_includes2.yml
@@ -0,0 +1,22 @@
+- name: verify playbook includes can take parameters
+ hosts: testhost
+ tasks:
+ - assert:
+ that:
+ - "parameter1 == 'asdf'"
+ - "parameter2 == 'jkl'"
+
+- name: verify task include logic
+ hosts: testhost
+ gather_facts: True
+ roles:
+ - role: test_includes
+ tags: test_includes
+ tasks:
+ - include: roles/test_includes/tasks/not_a_role_task.yml
+ - include: roles/test_includes/tasks/empty.yml
+ - assert:
+ that:
+ - "ca == 33000"
+ - "cb == 33001"
+ - "cc == 33002"
diff --git a/test/integration/targets/includes/test_includes3.yml b/test/integration/targets/includes/test_includes3.yml
new file mode 100644
index 0000000..0b4c631
--- /dev/null
+++ b/test/integration/targets/includes/test_includes3.yml
@@ -0,0 +1,6 @@
+- hosts: testhost
+ tasks:
+ - include: test_includes4.yml
+ with_items: ["a"]
+ loop_control:
+ loop_var: r
diff --git a/test/integration/targets/includes/test_includes4.yml b/test/integration/targets/includes/test_includes4.yml
new file mode 100644
index 0000000..bee906b
--- /dev/null
+++ b/test/integration/targets/includes/test_includes4.yml
@@ -0,0 +1,2 @@
+- set_fact:
+ p: 1
diff --git a/test/integration/targets/includes_race/aliases b/test/integration/targets/includes_race/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/includes_race/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/includes_race/inventory b/test/integration/targets/includes_race/inventory
new file mode 100644
index 0000000..8787929
--- /dev/null
+++ b/test/integration/targets/includes_race/inventory
@@ -0,0 +1,30 @@
+host001 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host002 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host003 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host004 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host005 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host006 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host007 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host008 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host009 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host010 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host011 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host012 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host013 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host014 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host015 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host016 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host017 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host018 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host019 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host020 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host021 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host022 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host023 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host024 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host025 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host026 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host027 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host028 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host029 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+host030 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
diff --git a/test/integration/targets/includes_race/roles/random_sleep/tasks/main.yml b/test/integration/targets/includes_race/roles/random_sleep/tasks/main.yml
new file mode 100644
index 0000000..cee459a
--- /dev/null
+++ b/test/integration/targets/includes_race/roles/random_sleep/tasks/main.yml
@@ -0,0 +1,8 @@
+---
+# tasks file for random_sleep
+- name: Generate sleep time
+ set_fact:
+ sleep_time: "{{ 3 | random }}"
+
+- name: Do random sleep
+ shell: sleep "{{ sleep_time }}"
diff --git a/test/integration/targets/includes_race/roles/set_a_fact/tasks/fact1.yml b/test/integration/targets/includes_race/roles/set_a_fact/tasks/fact1.yml
new file mode 100644
index 0000000..36b08dc
--- /dev/null
+++ b/test/integration/targets/includes_race/roles/set_a_fact/tasks/fact1.yml
@@ -0,0 +1,4 @@
+---
+- name: Set fact1
+ set_fact:
+ fact1: yay
diff --git a/test/integration/targets/includes_race/roles/set_a_fact/tasks/fact2.yml b/test/integration/targets/includes_race/roles/set_a_fact/tasks/fact2.yml
new file mode 100644
index 0000000..865f130
--- /dev/null
+++ b/test/integration/targets/includes_race/roles/set_a_fact/tasks/fact2.yml
@@ -0,0 +1,4 @@
+---
+- name: Set fact2
+ set_fact:
+ fact2: yay
diff --git a/test/integration/targets/includes_race/runme.sh b/test/integration/targets/includes_race/runme.sh
new file mode 100755
index 0000000..2261d27
--- /dev/null
+++ b/test/integration/targets/includes_race/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook test_includes_race.yml -i inventory -v "$@"
diff --git a/test/integration/targets/includes_race/test_includes_race.yml b/test/integration/targets/includes_race/test_includes_race.yml
new file mode 100644
index 0000000..20f7ddd
--- /dev/null
+++ b/test/integration/targets/includes_race/test_includes_race.yml
@@ -0,0 +1,19 @@
+- hosts: all
+ strategy: free
+ gather_facts: false
+ tasks:
+ - include_role:
+ name: random_sleep
+ - block:
+ - name: set a fact (1)
+ include_role:
+ name: set_a_fact
+ tasks_from: fact1.yml
+ - name: set a fact (2)
+ include_role:
+ name: set_a_fact
+ tasks_from: fact2.yml
+ - name: include didn't run
+ fail:
+ msg: "set_a_fact didn't run fact1 {{ fact1 | default('not defined')}} fact2: {{ fact2 | default('not defined') }}"
+ when: (fact1 is not defined or fact2 is not defined)
diff --git a/test/integration/targets/infra/aliases b/test/integration/targets/infra/aliases
new file mode 100644
index 0000000..af16cd4
--- /dev/null
+++ b/test/integration/targets/infra/aliases
@@ -0,0 +1,4 @@
+shippable/posix/group5
+needs/file/hacking/test-module.py
+needs/file/lib/ansible/modules/ping.py
+context/controller
diff --git a/test/integration/targets/infra/inventory.local b/test/integration/targets/infra/inventory.local
new file mode 100644
index 0000000..2baa1f8
--- /dev/null
+++ b/test/integration/targets/infra/inventory.local
@@ -0,0 +1,2 @@
+testhost ansible_connection=local
+
diff --git a/test/integration/targets/infra/library/test.py b/test/integration/targets/infra/library/test.py
new file mode 100644
index 0000000..dbc4b61
--- /dev/null
+++ b/test/integration/targets/infra/library/test.py
@@ -0,0 +1,24 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(),
+ )
+ result = {
+ 'selinux_special_fs': module._selinux_special_fs,
+ 'tmpdir': module._tmpdir,
+ 'keep_remote_files': module._keep_remote_files,
+ 'version': module.ansible_version,
+ }
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/infra/runme.sh b/test/integration/targets/infra/runme.sh
new file mode 100755
index 0000000..9e348b8
--- /dev/null
+++ b/test/integration/targets/infra/runme.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+
+set -ux
+
+# ensure fail/assert work locally and can stop execution with non-zero exit code
+PB_OUT=$(ansible-playbook -i inventory.local test_test_infra.yml)
+APB_RC=$?
+echo "$PB_OUT"
+echo "rc was $APB_RC (must be non-zero)"
+[ ${APB_RC} -ne 0 ]
+echo "ensure playbook output shows assert/fail works (True)"
+echo "$PB_OUT" | grep -F "fail works (True)" || exit 1
+echo "$PB_OUT" | grep -F "assert works (True)" || exit 1
+
+# ensure we work using all specified test args, overridden inventory, etc
+PB_OUT=$(ansible-playbook -i ../../inventory test_test_infra.yml "$@")
+APB_RC=$?
+echo "$PB_OUT"
+echo "rc was $APB_RC (must be non-zero)"
+[ ${APB_RC} -ne 0 ]
+echo "ensure playbook output shows assert/fail works (True)"
+echo "$PB_OUT" | grep -F "fail works (True)" || exit 1
+echo "$PB_OUT" | grep -F "assert works (True)" || exit 1
+
+set -e
+
+PING_MODULE_PATH="../../../../lib/ansible/modules/ping.py"
+
+# ensure test-module.py script works without passing Python interpreter path
+../../../../hacking/test-module.py -m "$PING_MODULE_PATH"
+
+# ensure test-module.py script works well
+../../../../hacking/test-module.py -m "$PING_MODULE_PATH" -I ansible_python_interpreter="${ANSIBLE_TEST_PYTHON_INTERPRETER}"
+
+# ensure module.ansible_version is defined when using test-module.py
+../../../../hacking/test-module.py -m library/test.py -I ansible_python_interpreter="${ANSIBLE_TEST_PYTHON_INTERPRETER}" <<< '{"ANSIBLE_MODULE_ARGS": {}}'
+
+# ensure exercising module code locally works
+python -m ansible.modules.file <<< '{"ANSIBLE_MODULE_ARGS": {"path": "/path/to/file", "state": "absent"}}'
diff --git a/test/integration/targets/infra/test_test_infra.yml b/test/integration/targets/infra/test_test_infra.yml
new file mode 100644
index 0000000..706f9b8
--- /dev/null
+++ b/test/integration/targets/infra/test_test_infra.yml
@@ -0,0 +1,25 @@
+- hosts: testhost
+ gather_facts: no
+ tags:
+ - always
+ tasks:
+ - name: ensure fail action produces a failing result
+ fail:
+ ignore_errors: yes
+ register: fail_out
+
+ - debug:
+ msg: fail works ({{ fail_out.failed }})
+
+ - name: ensure assert produces a failing result
+ assert:
+ that: false
+ ignore_errors: yes
+ register: assert_out
+
+ - debug:
+ msg: assert works ({{ assert_out.failed }})
+
+ - name: EXPECTED FAILURE ensure fail action stops execution
+ fail:
+ msg: fail actually failed (this is expected)
diff --git a/test/integration/targets/interpreter_discovery_python/aliases b/test/integration/targets/interpreter_discovery_python/aliases
new file mode 100644
index 0000000..0dfc90e
--- /dev/null
+++ b/test/integration/targets/interpreter_discovery_python/aliases
@@ -0,0 +1,3 @@
+shippable/posix/group1
+non_local # workaround to allow override of ansible_python_interpreter; disables coverage on this integration target
+context/target
diff --git a/test/integration/targets/interpreter_discovery_python/library/test_echo_module.py b/test/integration/targets/interpreter_discovery_python/library/test_echo_module.py
new file mode 100644
index 0000000..7317921
--- /dev/null
+++ b/test/integration/targets/interpreter_discovery_python/library/test_echo_module.py
@@ -0,0 +1,29 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# (c) 2016, Toshio Kuratomi <tkuratomi@ansible.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import sys
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ result = dict(changed=False)
+
+ module = AnsibleModule(argument_spec=dict(
+ facts=dict(type=dict, default={})
+ ))
+
+ result['ansible_facts'] = module.params['facts']
+ result['running_python_interpreter'] = sys.executable
+
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/interpreter_discovery_python/tasks/main.yml b/test/integration/targets/interpreter_discovery_python/tasks/main.yml
new file mode 100644
index 0000000..7e9b2e8
--- /dev/null
+++ b/test/integration/targets/interpreter_discovery_python/tasks/main.yml
@@ -0,0 +1,183 @@
+- name: ensure we can override ansible_python_interpreter
+ vars:
+ ansible_python_interpreter: overriddenpython
+ assert:
+ that:
+ - ansible_python_interpreter == 'overriddenpython'
+ fail_msg: "'ansible_python_interpreter' appears to be set at a high precedence to {{ ansible_python_interpreter }},
+ which breaks this test."
+
+- name: snag some facts to validate for later
+ set_fact:
+ distro: '{{ ansible_distribution | default("unknown") | lower }}'
+ distro_version: '{{ ansible_distribution_version | default("unknown") }}'
+ distro_major_version: '{{ ansible_distribution_major_version | default("unknown") }}'
+ os_family: '{{ ansible_os_family | default("unknown") }}'
+
+- name: test that python discovery is working and that fact persistence makes it only run once
+ block:
+ - name: clear facts to force interpreter discovery to run
+ meta: clear_facts
+
+ - name: trigger discovery with auto
+ vars:
+ ansible_python_interpreter: auto
+ ping:
+ register: auto_out
+
+ - name: get the interpreter being used on the target to execute modules
+ vars:
+ # keep this set so we can verify we didn't repeat discovery
+ ansible_python_interpreter: auto
+ test_echo_module:
+ register: echoout
+
+ - name: clear facts to force interpreter discovery to run again
+ meta: clear_facts
+
+ - name: get the interpreter being used on the target to execute modules with ansible_facts
+ vars:
+ # keep this set so we can verify we didn't repeat discovery
+ ansible_python_interpreter: auto
+ test_echo_module:
+ facts:
+ sandwich: ham
+ register: echoout_with_facts
+
+ - when: distro == 'macosx'
+ block:
+ - name: Get the sys.executable for the macos discovered interpreter, as it may be different than the actual path
+ raw: '{{ auto_out.ansible_facts.discovered_interpreter_python }} -c "import sys; print(sys.executable)"'
+ register: discovered_sys_executable
+
+ - set_fact:
+ normalized_discovered_interpreter: '{{ discovered_sys_executable.stdout_lines[0] }}'
+
+ - set_fact:
+ normalized_discovered_interpreter: '{{ auto_out.ansible_facts.discovered_interpreter_python }}'
+ when: distro != 'macosx'
+
+ - assert:
+ that:
+ - auto_out.ansible_facts.discovered_interpreter_python is defined
+ - echoout.running_python_interpreter == normalized_discovered_interpreter
+ # verify that discovery didn't run again (if it did, we'd have the fact in the result)
+ - echoout.ansible_facts is not defined or echoout.ansible_facts.discovered_interpreter_python is not defined
+ - echoout_with_facts.ansible_facts is defined
+ - echoout_with_facts.running_python_interpreter == normalized_discovered_interpreter
+
+- name: test that auto_legacy gives a dep warning when /usr/bin/python present but != auto result
+ block:
+ - name: clear facts to force interpreter discovery to run
+ meta: clear_facts
+
+ - name: trigger discovery with auto_legacy
+ vars:
+ ansible_python_interpreter: auto_legacy
+ ping:
+ register: legacy
+
+ - name: check for warning (only on platforms where auto result is not /usr/bin/python and legacy is)
+ assert:
+ that:
+ - legacy.warnings | default([]) | length > 0
+ # only check for a dep warning if legacy returned /usr/bin/python and auto didn't
+ when: legacy.ansible_facts.discovered_interpreter_python == '/usr/bin/python' and
+ auto_out.ansible_facts.discovered_interpreter_python != '/usr/bin/python'
+
+
+- name: test that auto_silent never warns and got the same answer as auto
+ block:
+ - name: clear facts to force interpreter discovery to run
+ meta: clear_facts
+
+ - name: initial task to trigger discovery
+ vars:
+ ansible_python_interpreter: auto_silent
+ ping:
+ register: auto_silent_out
+
+ - assert:
+ that:
+ - auto_silent_out.warnings is not defined
+ - auto_silent_out.ansible_facts.discovered_interpreter_python == auto_out.ansible_facts.discovered_interpreter_python
+
+
+- name: test that auto_legacy_silent never warns and got the same answer as auto_legacy
+ block:
+ - name: clear facts to force interpreter discovery to run
+ meta: clear_facts
+
+ - name: trigger discovery with auto_legacy_silent
+ vars:
+ ansible_python_interpreter: auto_legacy_silent
+ ping:
+ register: legacy_silent
+
+ - assert:
+ that:
+ - legacy_silent.warnings is not defined
+ - legacy_silent.ansible_facts.discovered_interpreter_python == legacy.ansible_facts.discovered_interpreter_python
+
+- name: ensure modules can't set discovered_interpreter_X or ansible_X_interpreter
+ block:
+ - test_echo_module:
+ facts:
+ ansible_discovered_interpreter_bogus: from module
+ discovered_interpreter_bogus: from_module
+ ansible_bogus_interpreter: from_module
+ test_fact: from_module
+ register: echoout
+
+ - assert:
+ that:
+ - test_fact == 'from_module'
+ - discovered_interpreter_bogus | default('nope') == 'nope'
+ - ansible_bogus_interpreter | default('nope') == 'nope'
+ # this one will exist in facts, but with its prefix removed
+ - ansible_facts['ansible_bogus_interpreter'] | default('nope') == 'nope'
+ - ansible_facts['discovered_interpreter_bogus'] | default('nope') == 'nope'
+
+ - name: debian assertions
+ assert:
+ that:
+ # Debian 8 and older
+ - auto_out.ansible_facts.discovered_interpreter_python == '/usr/bin/python' and distro_version is version('8', '<=') or distro_version is version('8', '>')
+ # Debian 10 and newer
+ - auto_out.ansible_facts.discovered_interpreter_python == '/usr/bin/python3' and distro_version is version('10', '>=') or distro_version is version('10', '<')
+ when: distro == 'debian'
+
+ - name: fedora assertions
+ assert:
+ that:
+ - auto_out.ansible_facts.discovered_interpreter_python == '/usr/bin/python3'
+ when: distro == 'fedora' and distro_version is version('23', '>=')
+
+ - name: rhel assertions
+ assert:
+ that:
+ # rhel 6/7
+ - (auto_out.ansible_facts.discovered_interpreter_python == '/usr/bin/python' and distro_major_version is version('8','<')) or distro_major_version is version('8','>=')
+ # rhel 8
+ - (auto_out.ansible_facts.discovered_interpreter_python == '/usr/libexec/platform-python' and distro_major_version is version('8','==')) or distro_major_version is version('8','!=')
+ # rhel 9
+ - (auto_out.ansible_facts.discovered_interpreter_python == '/usr/bin/python3' and distro_major_version is version('9','==')) or distro_major_version is version('9','!=')
+ when: distro == 'redhat'
+
+ - name: ubuntu assertions
+ assert:
+ that:
+ # ubuntu < 16
+ - (auto_out.ansible_facts.discovered_interpreter_python == '/usr/bin/python' and distro_version is version('16.04','<')) or distro_version is version('16.04','>=')
+ # ubuntu >= 16
+ - (auto_out.ansible_facts.discovered_interpreter_python == '/usr/bin/python3' and distro_version is version('16.04','>=')) or distro_version is version('16.04','<')
+ when: distro == 'ubuntu'
+
+ - name: mac assertions
+ assert:
+ that:
+ - auto_out.ansible_facts.discovered_interpreter_python == '/usr/bin/python'
+ when: os_family == 'darwin'
+
+ always:
+ - meta: clear_facts
diff --git a/test/integration/targets/interpreter_discovery_python_delegate_facts/aliases b/test/integration/targets/interpreter_discovery_python_delegate_facts/aliases
new file mode 100644
index 0000000..7f3d63f
--- /dev/null
+++ b/test/integration/targets/interpreter_discovery_python_delegate_facts/aliases
@@ -0,0 +1,3 @@
+shippable/posix/group3
+non_local # this test requires interpreter discovery, which means code coverage must be disabled
+context/controller
diff --git a/test/integration/targets/interpreter_discovery_python_delegate_facts/delegate_facts.yml b/test/integration/targets/interpreter_discovery_python_delegate_facts/delegate_facts.yml
new file mode 100644
index 0000000..535269d
--- /dev/null
+++ b/test/integration/targets/interpreter_discovery_python_delegate_facts/delegate_facts.yml
@@ -0,0 +1,10 @@
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - name: Test python interpreter discovery with delegate_to without delegate_facts
+ ping:
+ delegate_to: testhost
+ - name: Test python interpreter discovery with delegate_to with delegate_facts
+ ping:
+ delegate_to: testhost
+ delegate_facts: yes
diff --git a/test/integration/targets/interpreter_discovery_python_delegate_facts/inventory b/test/integration/targets/interpreter_discovery_python_delegate_facts/inventory
new file mode 100644
index 0000000..350f3e8
--- /dev/null
+++ b/test/integration/targets/interpreter_discovery_python_delegate_facts/inventory
@@ -0,0 +1,2 @@
+[local]
+testhost ansible_connection=local ansible_python_interpreter=auto # interpreter discovery required
diff --git a/test/integration/targets/interpreter_discovery_python_delegate_facts/runme.sh b/test/integration/targets/interpreter_discovery_python_delegate_facts/runme.sh
new file mode 100755
index 0000000..ca2caa1
--- /dev/null
+++ b/test/integration/targets/interpreter_discovery_python_delegate_facts/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook delegate_facts.yml -i inventory "$@"
diff --git a/test/integration/targets/inventory-invalid-group/aliases b/test/integration/targets/inventory-invalid-group/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/inventory-invalid-group/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/inventory-invalid-group/inventory.ini b/test/integration/targets/inventory-invalid-group/inventory.ini
new file mode 100644
index 0000000..762c9da
--- /dev/null
+++ b/test/integration/targets/inventory-invalid-group/inventory.ini
@@ -0,0 +1,5 @@
+[local-]
+testhost ansible_connection=local
+
+[all:vars]
+ansible_python_interpreter="{{ ansible_playbook_python }}"
diff --git a/test/integration/targets/inventory-invalid-group/runme.sh b/test/integration/targets/inventory-invalid-group/runme.sh
new file mode 100755
index 0000000..8c40554
--- /dev/null
+++ b/test/integration/targets/inventory-invalid-group/runme.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+set -eux
+set -o pipefail
+
+command=(ansible-playbook -v -i inventory.ini test.yml)
+ never='Invalid characters were found in group names but not replaced'
+always='Invalid characters were found in group names and automatically'
+
+ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS=never "${command[@]}" -l "local-" 2>&1 | grep -c -e "${never}"
+ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS=always "${command[@]}" -l "local_" 2>&1 | grep -c -e "${always}"
+ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS=ignore "${command[@]}" -l "local-" 2>&1 | grep -cv -e "${never}" -e "${always}"
+ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS=silently "${command[@]}" -l "local_" 2>&1 | grep -cv -e "${never}" -e "${always}"
diff --git a/test/integration/targets/inventory-invalid-group/test.yml b/test/integration/targets/inventory-invalid-group/test.yml
new file mode 100644
index 0000000..2208f9d
--- /dev/null
+++ b/test/integration/targets/inventory-invalid-group/test.yml
@@ -0,0 +1,3 @@
+- hosts: testhost
+ gather_facts: no
+ tasks: []
diff --git a/test/integration/targets/inventory/1/2/3/extra_vars_relative.yml b/test/integration/targets/inventory/1/2/3/extra_vars_relative.yml
new file mode 100644
index 0000000..fa50a5a
--- /dev/null
+++ b/test/integration/targets/inventory/1/2/3/extra_vars_relative.yml
@@ -0,0 +1,19 @@
+- hosts: localhost
+ gather_facts: false
+ vars:
+ conditions:
+ - my is defined
+ - my == 'var'
+ - "'webservers' in groups"
+ - "'web_host.example.com' in groups['webservers']"
+ tasks:
+ - name: Make sure all is loaded
+ assert:
+ that: '{{conditions}}'
+
+ - name: Reload inventory, forces extra vars re-eval from diff basedir
+ meta: refresh_inventory
+
+ - name: Make sure all is loaded, again
+ assert:
+ that: '{{conditions}}'
diff --git a/test/integration/targets/inventory/1/2/inventory.yml b/test/integration/targets/inventory/1/2/inventory.yml
new file mode 100644
index 0000000..b6c31ad
--- /dev/null
+++ b/test/integration/targets/inventory/1/2/inventory.yml
@@ -0,0 +1,3 @@
+plugin: ansible.builtin.constructed
+groups:
+ webservers: inventory_hostname.startswith('web')
diff --git a/test/integration/targets/inventory/1/vars.yml b/test/integration/targets/inventory/1/vars.yml
new file mode 100644
index 0000000..c114584
--- /dev/null
+++ b/test/integration/targets/inventory/1/vars.yml
@@ -0,0 +1 @@
+my: var
diff --git a/test/integration/targets/inventory/aliases b/test/integration/targets/inventory/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/inventory/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/inventory/extra_vars_constructed.yml b/test/integration/targets/inventory/extra_vars_constructed.yml
new file mode 100644
index 0000000..ee6f5fd
--- /dev/null
+++ b/test/integration/targets/inventory/extra_vars_constructed.yml
@@ -0,0 +1,5 @@
+plugin: ansible.builtin.constructed
+strict: true
+use_extra_vars: True
+compose:
+ example: " 'hello' + from_extras"
diff --git a/test/integration/targets/inventory/host_vars_constructed.yml b/test/integration/targets/inventory/host_vars_constructed.yml
new file mode 100644
index 0000000..eec5250
--- /dev/null
+++ b/test/integration/targets/inventory/host_vars_constructed.yml
@@ -0,0 +1,6 @@
+plugin: ansible.legacy.contructed_with_hostvars
+groups:
+ host_var1_defined: host_var1 is defined
+keyed_groups:
+ - key: host_var2
+ prefix: host_var2
diff --git a/test/integration/targets/inventory/inv_with_host_vars.yml b/test/integration/targets/inventory/inv_with_host_vars.yml
new file mode 100644
index 0000000..7403505
--- /dev/null
+++ b/test/integration/targets/inventory/inv_with_host_vars.yml
@@ -0,0 +1,5 @@
+all:
+ hosts:
+ host1:
+ host_var1: 'defined'
+ host_var2: 'defined'
diff --git a/test/integration/targets/inventory/inv_with_int.yml b/test/integration/targets/inventory/inv_with_int.yml
new file mode 100644
index 0000000..5b2f21d
--- /dev/null
+++ b/test/integration/targets/inventory/inv_with_int.yml
@@ -0,0 +1,6 @@
+all:
+ hosts:
+ testing123:
+ x:
+ a: 1
+ 0: 2
diff --git a/test/integration/targets/inventory/inventory_plugins/contructed_with_hostvars.py b/test/integration/targets/inventory/inventory_plugins/contructed_with_hostvars.py
new file mode 100644
index 0000000..7ca445a
--- /dev/null
+++ b/test/integration/targets/inventory/inventory_plugins/contructed_with_hostvars.py
@@ -0,0 +1,44 @@
+# Copyright (c) 2022 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ name: constructed_with_hostvars
+ options:
+ plugin:
+ description: the load name of the plugin
+ extends_documentation_fragment:
+ - constructed
+'''
+
+from ansible.errors import AnsibleParserError
+from ansible.module_utils._text import to_native
+from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
+
+
+class InventoryModule(BaseInventoryPlugin, Constructable):
+
+ NAME = 'constructed_with_hostvars'
+
+ def verify_file(self, path):
+ return super(InventoryModule, self).verify_file(path) and path.endswith(('constructed.yml', 'constructed.yaml'))
+
+ def parse(self, inventory, loader, path, cache=True):
+ super(InventoryModule, self).parse(inventory, loader, path, cache)
+ config = self._read_config_data(path)
+
+ strict = self.get_option('strict')
+ try:
+ for host in inventory.hosts:
+ hostvars = {}
+
+ # constructed groups based on conditionals
+ self._add_host_to_composed_groups(self.get_option('groups'), hostvars, host, strict=strict, fetch_hostvars=True)
+
+ # constructed groups based variable values
+ self._add_host_to_keyed_groups(self.get_option('keyed_groups'), hostvars, host, strict=strict, fetch_hostvars=True)
+
+ except Exception as e:
+ raise AnsibleParserError("failed to parse %s: %s " % (to_native(path), to_native(e)), orig_exc=e)
diff --git a/test/integration/targets/inventory/playbook.yml b/test/integration/targets/inventory/playbook.yml
new file mode 100644
index 0000000..5e07361
--- /dev/null
+++ b/test/integration/targets/inventory/playbook.yml
@@ -0,0 +1,4 @@
+- hosts: all
+ gather_facts: false
+ tasks:
+ - ping:
diff --git a/test/integration/targets/inventory/runme.sh b/test/integration/targets/inventory/runme.sh
new file mode 100755
index 0000000..8dcac40
--- /dev/null
+++ b/test/integration/targets/inventory/runme.sh
@@ -0,0 +1,114 @@
+#!/usr/bin/env bash
+
+set -eux
+
+empty_limit_file="$(mktemp)"
+touch "${empty_limit_file}"
+
+tmpdir="$(mktemp -d)"
+
+cleanup() {
+ if [[ -f "${empty_limit_file}" ]]; then
+ rm -rf "${empty_limit_file}"
+ fi
+ rm -rf "$tmpdir"
+}
+
+trap 'cleanup' EXIT
+
+# https://github.com/ansible/ansible/issues/52152
+# Ensure that non-matching limit causes failure with rc 1
+if ansible-playbook -i ../../inventory --limit foo playbook.yml; then
+ echo "Non-matching limit should cause failure"
+ exit 1
+fi
+
+# Ensure that non-existing limit file causes failure with rc 1
+if ansible-playbook -i ../../inventory --limit @foo playbook.yml; then
+ echo "Non-existing limit file should cause failure"
+ exit 1
+fi
+
+if ! ansible-playbook -i ../../inventory --limit @"$tmpdir" playbook.yml 2>&1 | grep 'must be a file'; then
+ echo "Using a directory as a limit file should throw proper AnsibleError"
+ exit 1
+fi
+
+# Ensure that empty limit file does not cause IndexError #59695
+ansible-playbook -i ../../inventory --limit @"${empty_limit_file}" playbook.yml
+
+ansible-playbook -i ../../inventory "$@" strategy.yml
+ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS=always ansible-playbook -i ../../inventory "$@" strategy.yml
+ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS=never ansible-playbook -i ../../inventory "$@" strategy.yml
+
+# test extra vars
+ansible-inventory -i testhost, -i ./extra_vars_constructed.yml --list -e 'from_extras=hey ' "$@"|grep '"example": "hellohey"'
+
+# test host vars from previous inventory sources
+ansible-inventory -i ./inv_with_host_vars.yml -i ./host_vars_constructed.yml --graph "$@" | tee out.txt
+if [[ "$(grep out.txt -ce '.*host_var[1|2]_defined')" != 2 ]]; then
+ cat out.txt
+ echo "Expected groups host_var1_defined and host_var2_defined to both exist"
+ exit 1
+fi
+
+# Do not fail when all inventories fail to parse.
+# Do not fail when any inventory fails to parse.
+ANSIBLE_INVENTORY_UNPARSED_FAILED=False ANSIBLE_INVENTORY_ANY_UNPARSED_IS_FAILED=False ansible -m ping localhost -i /idontexist "$@"
+
+# Fail when all inventories fail to parse.
+# Do not fail when just one inventory fails to parse.
+if ANSIBLE_INVENTORY_UNPARSED_FAILED=True ANSIBLE_INVENTORY_ANY_UNPARSED_IS_FAILED=False ansible -m ping localhost -i /idontexist; then
+ echo "All inventories failed/did not exist, should cause failure"
+ echo "ran with: ANSIBLE_INVENTORY_UNPARSED_FAILED=True ANSIBLE_INVENTORY_ANY_UNPARSED_IS_FAILED=False"
+ exit 1
+fi
+
+# Same as above but ensuring no failure we *only* fail when all inventories fail to parse.
+# Fail when all inventories fail to parse.
+# Do not fail when just one inventory fails to parse.
+ANSIBLE_INVENTORY_UNPARSED_FAILED=True ANSIBLE_INVENTORY_ANY_UNPARSED_IS_FAILED=False ansible -m ping localhost -i /idontexist -i ../../inventory "$@"
+# Fail when all inventories fail to parse.
+# Do not fail when just one inventory fails to parse.
+
+# Fail when any inventories fail to parse.
+if ANSIBLE_INVENTORY_ANY_UNPARSED_IS_FAILED=True ansible -m ping localhost -i /idontexist -i ../../inventory; then
+ echo "One inventory failed/did not exist, should NOT cause failure"
+ echo "ran with: ANSIBLE_INVENTORY_UNPARSED_FAILED=True ANSIBLE_INVENTORY_ANY_UNPARSED_IS_FAILED=False"
+ exit 1
+fi
+
+# Test parsing an empty config
+set +e
+ANSIBLE_INVENTORY_UNPARSED_FAILED=True ANSIBLE_INVENTORY_ENABLED=constructed ansible-inventory -i ./test_empty.yml --list "$@"
+rc_failed_inventory="$?"
+set -e
+if [[ "$rc_failed_inventory" != 1 ]]; then
+ echo "Config was empty so inventory was not parsed, should cause failure"
+ exit 1
+fi
+
+# Ensure we don't throw when an empty directory is used as inventory
+ansible-playbook -i "$tmpdir" playbook.yml
+
+# Ensure we can use a directory of inventories
+cp ../../inventory "$tmpdir"
+ansible-playbook -i "$tmpdir" playbook.yml
+
+# ... even if it contains another empty directory
+mkdir "$tmpdir/empty"
+ansible-playbook -i "$tmpdir" playbook.yml
+
+if ANSIBLE_INVENTORY_ANY_UNPARSED_IS_FAILED=True ansible -m ping localhost -i "$tmpdir"; then
+ echo "Empty directory should cause failure when ANSIBLE_INVENTORY_ANY_UNPARSED_IS_FAILED=True"
+ exit 1
+fi
+
+# ensure we don't traceback on inventory due to variables with int as key
+ansible-inventory -i inv_with_int.yml --list "$@"
+
+# test in subshell relative paths work mid play for extra vars in inventory refresh
+{
+ cd 1/2
+ ansible-playbook -e @../vars.yml -i 'web_host.example.com,' -i inventory.yml 3/extra_vars_relative.yml "$@"
+}
diff --git a/test/integration/targets/inventory/strategy.yml b/test/integration/targets/inventory/strategy.yml
new file mode 100644
index 0000000..5c1cbd2
--- /dev/null
+++ b/test/integration/targets/inventory/strategy.yml
@@ -0,0 +1,12 @@
+- name: Check that 'invalid' group works, problem exposed in #58980
+ hosts: localhost
+ tasks:
+ - name: add a host to a group, that has - to trigger substitution
+ add_host:
+ name: localhost
+ groups: Not-Working
+
+ - name: group hosts by distribution, with dash to trigger substitution
+ group_by:
+ key: "{{ ansible_distribution }}-{{ ansible_distribution_version }}"
+ changed_when: false
diff --git a/test/integration/targets/inventory/test_empty.yml b/test/integration/targets/inventory/test_empty.yml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/inventory/test_empty.yml
diff --git a/test/integration/targets/inventory_advanced_host_list/aliases b/test/integration/targets/inventory_advanced_host_list/aliases
new file mode 100644
index 0000000..b598321
--- /dev/null
+++ b/test/integration/targets/inventory_advanced_host_list/aliases
@@ -0,0 +1 @@
+shippable/posix/group3
diff --git a/test/integration/targets/inventory_advanced_host_list/runme.sh b/test/integration/targets/inventory_advanced_host_list/runme.sh
new file mode 100755
index 0000000..41b1f8b
--- /dev/null
+++ b/test/integration/targets/inventory_advanced_host_list/runme.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+
+set -eux
+
+export ANSIBLE_INVENTORY_ENABLED=advanced_host_list
+
+# A few things to make it easier to grep against adhoc
+export ANSIBLE_LOAD_CALLBACK_PLUGINS=True
+export ANSIBLE_STDOUT_CALLBACK=oneline
+
+adhoc="$(ansible -i 'local[0:10],' -m ping --connection=local -e ansible_python_interpreter="{{ ansible_playbook_python }}" all -v)"
+
+for i in $(seq 0 10); do
+ grep -qE "local${i} \| SUCCESS.*\"ping\": \"pong\"" <<< "$adhoc"
+done
+
+set +e
+parse_fail="$(ansible -i 'local[1:j],' -m ping --connection=local all -v 2>&1)"
+set -e
+
+grep -q "Failed to parse local\[1:j\], with advanced_host_list" <<< "$parse_fail"
+
+# Intentionally missing comma, ensure we don't fatal.
+no_comma="$(ansible -i 'local[1:5]' -m ping --connection=local all -v 2>&1)"
+grep -q "No inventory was parsed" <<< "$no_comma"
+
+# Intentionally botched range (missing end number), ensure we don't fatal.
+no_end="$(ansible -i 'local[1:],' -m ping --connection=local -e ansible_python_interpreter="{{ ansible_playbook_python }}" all -vvv 2>&1)"
+grep -q "Unable to parse address from hostname, leaving unchanged:" <<< "$no_end"
+grep -q "host range must specify end value" <<< "$no_end"
+grep -q "local\[3:\] \| SUCCESS" <<< "$no_end"
+
+# Unset adhoc stuff
+unset ANSIBLE_LOAD_CALLBACK_PLUGINS ANSIBLE_STDOUT_CALLBACK
+
+ansible-playbook -i 'local100,local[100:110:2]' test_advanced_host_list.yml -v "$@"
diff --git a/test/integration/targets/inventory_advanced_host_list/test_advanced_host_list.yml b/test/integration/targets/inventory_advanced_host_list/test_advanced_host_list.yml
new file mode 100644
index 0000000..918078a
--- /dev/null
+++ b/test/integration/targets/inventory_advanced_host_list/test_advanced_host_list.yml
@@ -0,0 +1,9 @@
+- hosts: all
+ connection: local
+ vars:
+ ansible_python_interpreter: "{{ ansible_playbook_python }}"
+ tasks:
+ - assert:
+ that:
+ - inventory_hostname in ["local100", "local102", "local104", "local106", "local108", "local110", "local118"]
+ - inventory_hostname not in ["local101", "local103", "local105", "local107", "local109", "local111"]
diff --git a/test/integration/targets/inventory_cache/aliases b/test/integration/targets/inventory_cache/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/inventory_cache/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/inventory_cache/cache/.keep b/test/integration/targets/inventory_cache/cache/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/inventory_cache/cache/.keep
diff --git a/test/integration/targets/inventory_cache/cache_host.yml b/test/integration/targets/inventory_cache/cache_host.yml
new file mode 100644
index 0000000..3630641
--- /dev/null
+++ b/test/integration/targets/inventory_cache/cache_host.yml
@@ -0,0 +1,4 @@
+plugin: cache_host
+cache: true
+cache_plugin: jsonfile
+cache_connection: ./cache
diff --git a/test/integration/targets/inventory_cache/exercise_cache.yml b/test/integration/targets/inventory_cache/exercise_cache.yml
new file mode 100644
index 0000000..6fd8712
--- /dev/null
+++ b/test/integration/targets/inventory_cache/exercise_cache.yml
@@ -0,0 +1,4 @@
+plugin: exercise_cache
+cache: true
+cache_plugin: jsonfile
+cache_connection: ./cache
diff --git a/test/integration/targets/inventory_cache/plugins/inventory/cache_host.py b/test/integration/targets/inventory_cache/plugins/inventory/cache_host.py
new file mode 100644
index 0000000..628aba1
--- /dev/null
+++ b/test/integration/targets/inventory_cache/plugins/inventory/cache_host.py
@@ -0,0 +1,56 @@
+# Copyright (c) 2021 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ inventory: cache_host
+ short_description: add a host to inventory and cache it
+ description: add a host to inventory and cache it
+ extends_documentation_fragment:
+ - inventory_cache
+ options:
+ plugin:
+ required: true
+ description: name of the plugin (cache_host)
+'''
+
+from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable
+import random
+
+
+class InventoryModule(BaseInventoryPlugin, Cacheable):
+
+ NAME = 'cache_host'
+
+ def verify_file(self, path):
+ if not path.endswith(('cache_host.yml', 'cache_host.yaml',)):
+ return False
+ return super(InventoryModule, self).verify_file(path)
+
+ def parse(self, inventory, loader, path, cache=None):
+ super(InventoryModule, self).parse(inventory, loader, path)
+ self._read_config_data(path)
+
+ cache_key = self.get_cache_key(path)
+ # user has enabled cache and the cache is not being flushed
+ read_cache = self.get_option('cache') and cache
+ # user has enabled cache and the cache is being flushed
+ update_cache = self.get_option('cache') and not cache
+
+ host = None
+ if read_cache:
+ try:
+ host = self._cache[cache_key]
+ except KeyError:
+ # cache expired
+ update_cache = True
+
+ if host is None:
+ host = 'testhost{0}'.format(random.randint(0, 50))
+
+ self.inventory.add_host(host, 'all')
+
+ if update_cache:
+ self._cache[cache_key] = host
diff --git a/test/integration/targets/inventory_cache/plugins/inventory/exercise_cache.py b/test/integration/targets/inventory_cache/plugins/inventory/exercise_cache.py
new file mode 100644
index 0000000..cca2aa0
--- /dev/null
+++ b/test/integration/targets/inventory_cache/plugins/inventory/exercise_cache.py
@@ -0,0 +1,344 @@
+# Copyright (c) 2022 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ inventory: exercise_cache
+ short_description: run tests against the specified cache plugin
+ description:
+ - This plugin doesn't modify inventory.
+ - Load a cache plugin and test the inventory cache interface is dict-like.
+ - Most inventory cache write methods only apply to the in-memory cache.
+ - The 'flush' and 'set_cache' methods should be used to apply changes to the backing cache plugin.
+ - The inventory cache read methods prefer the in-memory cache, and fall back to reading from the cache plugin.
+ extends_documentation_fragment:
+ - inventory_cache
+ options:
+ plugin:
+ required: true
+ description: name of the plugin (exercise_cache)
+ cache_timeout:
+ ini: []
+ env: []
+ cli: []
+ default: 0 # never expire
+'''
+
+from ansible.errors import AnsibleError
+from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable
+from ansible.utils.display import Display
+
+from time import sleep
+
+
+display = Display()
+
+
+class InventoryModule(BaseInventoryPlugin, Cacheable):
+
+ NAME = 'exercise_cache'
+
+ test_cache_methods = [
+ 'test_plugin_name',
+ 'test_update_cache_if_changed',
+ 'test_set_cache',
+ 'test_load_whole_cache',
+ 'test_iter',
+ 'test_len',
+ 'test_get_missing_key',
+ 'test_get_expired_key',
+ 'test_initial_get',
+ 'test_get',
+ 'test_items',
+ 'test_keys',
+ 'test_values',
+ 'test_pop',
+ 'test_del',
+ 'test_set',
+ 'test_update',
+ 'test_flush',
+ ]
+
+ def verify_file(self, path):
+ if not path.endswith(('exercise_cache.yml', 'exercise_cache.yaml',)):
+ return False
+ return super(InventoryModule, self).verify_file(path)
+
+ def parse(self, inventory, loader, path, cache=None):
+ super(InventoryModule, self).parse(inventory, loader, path)
+ self._read_config_data(path)
+
+ try:
+ self.exercise_test_cache()
+ except AnsibleError:
+ raise
+ except Exception as e:
+ raise AnsibleError("Failed to run cache tests: {0}".format(e)) from e
+
+ def exercise_test_cache(self):
+ failed = []
+ for test_name in self.test_cache_methods:
+ try:
+ getattr(self, test_name)()
+ except AssertionError:
+ failed.append(test_name)
+ finally:
+ self.cache.flush()
+ self.cache.update_cache_if_changed()
+
+ if failed:
+ raise AnsibleError(f"Cache tests failed: {', '.join(failed)}")
+
+ def test_equal(self, a, b):
+ try:
+ assert a == b
+ except AssertionError:
+ display.warning(f"Assertion {a} == {b} failed")
+ raise
+
+ def test_plugin_name(self):
+ self.test_equal(self.cache._plugin_name, self.get_option('cache_plugin'))
+
+ def test_update_cache_if_changed(self):
+ self.cache._retrieved = {}
+ self.cache._cache = {'foo': 'bar'}
+
+ self.cache.update_cache_if_changed()
+
+ self.test_equal(self.cache._retrieved, {'foo': 'bar'})
+ self.test_equal(self.cache._cache, {'foo': 'bar'})
+
+ def test_set_cache(self):
+ cache_key1 = 'key1'
+ cache1 = {'hosts': {'h1': {'foo': 'bar'}}}
+ cache_key2 = 'key2'
+ cache2 = {'hosts': {'h2': {}}}
+
+ self.cache._cache = {cache_key1: cache1, cache_key2: cache2}
+ self.cache.set_cache()
+
+ self.test_equal(self.cache._plugin.contains(cache_key1), True)
+ self.test_equal(self.cache._plugin.get(cache_key1), cache1)
+ self.test_equal(self.cache._plugin.contains(cache_key2), True)
+ self.test_equal(self.cache._plugin.get(cache_key2), cache2)
+
+ def test_load_whole_cache(self):
+ cache_data = {
+ 'key1': {'hosts': {'h1': {'foo': 'bar'}}},
+ 'key2': {'hosts': {'h2': {}}},
+ }
+ self.cache._cache = cache_data
+ self.cache.set_cache()
+ self.cache._cache = {}
+
+ self.cache.load_whole_cache()
+ self.test_equal(self.cache._cache, cache_data)
+
+ def test_iter(self):
+ cache_data = {
+ 'key1': {'hosts': {'h1': {'foo': 'bar'}}},
+ 'key2': {'hosts': {'h2': {}}},
+ }
+ self.cache._cache = cache_data
+ self.test_equal(sorted(list(self.cache)), ['key1', 'key2'])
+
+ def test_len(self):
+ cache_data = {
+ 'key1': {'hosts': {'h1': {'foo': 'bar'}}},
+ 'key2': {'hosts': {'h2': {}}},
+ }
+ self.cache._cache = cache_data
+ self.test_equal(len(self.cache), 2)
+
+ def test_get_missing_key(self):
+ # cache should behave like a dictionary
+ # a missing key with __getitem__ should raise a KeyError
+ try:
+ self.cache['keyerror']
+ except KeyError:
+ pass
+ else:
+ assert False
+
+ # get should return the default instead
+ self.test_equal(self.cache.get('missing'), None)
+ self.test_equal(self.cache.get('missing', 'default'), 'default')
+
+ def _setup_expired(self):
+ self.cache._cache = {'expired': True}
+ self.cache.set_cache()
+
+ # empty the in-memory info to test loading the key
+ # keys that expire mid-use do not cause errors
+ self.cache._cache = {}
+ self.cache._retrieved = {}
+ self.cache._plugin._cache = {}
+
+ self.cache._plugin.set_option('timeout', 1)
+ self.cache._plugin._timeout = 1
+ sleep(2)
+
+ def _cleanup_expired(self):
+ # Set cache timeout back to never
+ self.cache._plugin.set_option('timeout', 0)
+ self.cache._plugin._timeout = 0
+
+ def test_get_expired_key(self):
+ if not hasattr(self.cache._plugin, '_timeout'):
+ # DB-backed caches do not have a standard timeout interface
+ return
+
+ self._setup_expired()
+ try:
+ self.cache['expired']
+ except KeyError:
+ pass
+ else:
+ assert False
+ finally:
+ self._cleanup_expired()
+
+ self._setup_expired()
+ try:
+ self.test_equal(self.cache.get('expired'), None)
+ self.test_equal(self.cache.get('expired', 'default'), 'default')
+ finally:
+ self._cleanup_expired()
+
+ def test_initial_get(self):
+ # test cache behaves like a dictionary
+
+ # set the cache to test getting a key that exists
+ k1 = {'hosts': {'h1': {'foo': 'bar'}}}
+ k2 = {'hosts': {'h2': {}}}
+ self.cache._cache = {'key1': k1, 'key2': k2}
+ self.cache.set_cache()
+
+ # empty the in-memory info to test loading the key from the plugin
+ self.cache._cache = {}
+ self.cache._retrieved = {}
+ self.cache._plugin._cache = {}
+
+ self.test_equal(self.cache['key1'], k1)
+
+ # empty the in-memory info to test loading the key from the plugin
+ self.cache._cache = {}
+ self.cache._retrieved = {}
+ self.cache._plugin._cache = {}
+
+ self.test_equal(self.cache.get('key1'), k1)
+
+ def test_get(self):
+ # test cache behaves like a dictionary
+
+ # set the cache to test getting a key that exists
+ k1 = {'hosts': {'h1': {'foo': 'bar'}}}
+ k2 = {'hosts': {'h2': {}}}
+ self.cache._cache = {'key1': k1, 'key2': k2}
+ self.cache.set_cache()
+
+ self.test_equal(self.cache['key1'], k1)
+ self.test_equal(self.cache.get('key1'), k1)
+
+ def test_items(self):
+ self.test_equal(self.cache.items(), {}.items())
+
+ test_items = {'hosts': {'host1': {'foo': 'bar'}}}
+ self.cache._cache = test_items
+ self.test_equal(self.cache.items(), test_items.items())
+
+ def test_keys(self):
+ self.test_equal(self.cache.keys(), {}.keys())
+
+ test_items = {'hosts': {'host1': {'foo': 'bar'}}}
+ self.cache._cache = test_items
+ self.test_equal(self.cache.keys(), test_items.keys())
+
+ def test_values(self):
+ self.test_equal(list(self.cache.values()), list({}.values()))
+
+ test_items = {'hosts': {'host1': {'foo': 'bar'}}}
+ self.cache._cache = test_items
+ self.test_equal(list(self.cache.values()), list(test_items.values()))
+
+ def test_pop(self):
+ try:
+ self.cache.pop('missing')
+ except KeyError:
+ pass
+ else:
+ assert False
+
+ self.test_equal(self.cache.pop('missing', 'default'), 'default')
+
+ self.cache._cache = {'cache_key': 'cache'}
+ self.test_equal(self.cache.pop('cache_key'), 'cache')
+
+ # test backing plugin cache isn't modified
+ cache_key1 = 'key1'
+ cache1 = {'hosts': {'h1': {'foo': 'bar'}}}
+ cache_key2 = 'key2'
+ cache2 = {'hosts': {'h2': {}}}
+
+ self.cache._cache = {cache_key1: cache1, cache_key2: cache2}
+ self.cache.set_cache()
+
+ self.test_equal(self.cache.pop('key1'), cache1)
+ self.test_equal(self.cache._cache, {cache_key2: cache2})
+ self.test_equal(self.cache._plugin._cache, {cache_key1: cache1, cache_key2: cache2})
+
+ def test_del(self):
+ try:
+ del self.cache['missing']
+ except KeyError:
+ pass
+ else:
+ assert False
+
+ cache_key1 = 'key1'
+ cache1 = {'hosts': {'h1': {'foo': 'bar'}}}
+ cache_key2 = 'key2'
+ cache2 = {'hosts': {'h2': {}}}
+
+ self.cache._cache = {cache_key1: cache1, cache_key2: cache2}
+ self.cache.set_cache()
+
+ del self.cache['key1']
+
+ self.test_equal(self.cache._cache, {cache_key2: cache2})
+ self.test_equal(self.cache._plugin._cache, {cache_key1: cache1, cache_key2: cache2})
+
+ def test_set(self):
+ cache_key = 'key1'
+ hosts = {'hosts': {'h1': {'foo': 'bar'}}}
+ self.cache[cache_key] = hosts
+
+ self.test_equal(self.cache._cache, {cache_key: hosts})
+ self.test_equal(self.cache._plugin._cache, {})
+
+ def test_update(self):
+ cache_key1 = 'key1'
+ cache1 = {'hosts': {'h1': {'foo': 'bar'}}}
+ cache_key2 = 'key2'
+ cache2 = {'hosts': {'h2': {}}}
+
+ self.cache._cache = {cache_key1: cache1}
+ self.cache.update({cache_key2: cache2})
+ self.test_equal(self.cache._cache, {cache_key1: cache1, cache_key2: cache2})
+
+ def test_flush(self):
+ cache_key1 = 'key1'
+ cache1 = {'hosts': {'h1': {'foo': 'bar'}}}
+ cache_key2 = 'key2'
+ cache2 = {'hosts': {'h2': {}}}
+
+ self.cache._cache = {cache_key1: cache1, cache_key2: cache2}
+ self.cache.set_cache()
+
+ # Unlike the dict write methods, cache.flush() flushes the backing plugin
+ self.cache.flush()
+
+ self.test_equal(self.cache._cache, {})
+ self.test_equal(self.cache._plugin._cache, {})
diff --git a/test/integration/targets/inventory_cache/runme.sh b/test/integration/targets/inventory_cache/runme.sh
new file mode 100755
index 0000000..942eb82
--- /dev/null
+++ b/test/integration/targets/inventory_cache/runme.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+
+set -eux
+
+export ANSIBLE_INVENTORY_PLUGINS=./plugins/inventory
+
+cleanup() {
+ for f in ./cache/ansible_inventory*; do
+ if [ -f "$f" ]; then rm -rf "$f"; fi
+ done
+}
+
+trap 'cleanup' EXIT
+
+# Test no warning when writing to the cache for the first time
+test "$(ansible-inventory -i cache_host.yml --graph 2>&1 | tee out.txt | grep -c '\[WARNING\]')" = 0
+writehost="$(grep "testhost[0-9]\{1,2\}" out.txt)"
+
+# Test reading from the cache
+test "$(ansible-inventory -i cache_host.yml --graph 2>&1 | tee out.txt | grep -c '\[WARNING\]')" = 0
+readhost="$(grep 'testhost[0-9]\{1,2\}' out.txt)"
+
+test "$readhost" = "$writehost"
+
+ansible-inventory -i exercise_cache.yml --graph
diff --git a/test/integration/targets/inventory_constructed/aliases b/test/integration/targets/inventory_constructed/aliases
new file mode 100644
index 0000000..70a7b7a
--- /dev/null
+++ b/test/integration/targets/inventory_constructed/aliases
@@ -0,0 +1 @@
+shippable/posix/group5
diff --git a/test/integration/targets/inventory_constructed/constructed.yml b/test/integration/targets/inventory_constructed/constructed.yml
new file mode 100644
index 0000000..be02858
--- /dev/null
+++ b/test/integration/targets/inventory_constructed/constructed.yml
@@ -0,0 +1,19 @@
+plugin: ansible.builtin.constructed
+keyed_groups:
+ - key: hostvar0
+ - key: hostvar1
+ - key: hostvar2
+
+ - key: hostvar0
+ separator: 'separator'
+ - key: hostvar1
+ separator: 'separator'
+ - key: hostvar2
+ separator: 'separator'
+
+ - key: hostvar0
+ prefix: 'prefix'
+ - key: hostvar1
+ prefix: 'prefix'
+ - key: hostvar2
+ prefix: 'prefix'
diff --git a/test/integration/targets/inventory_constructed/invs/1/group_vars/stuff.yml b/test/integration/targets/inventory_constructed/invs/1/group_vars/stuff.yml
new file mode 100644
index 0000000..a67b90f
--- /dev/null
+++ b/test/integration/targets/inventory_constructed/invs/1/group_vars/stuff.yml
@@ -0,0 +1 @@
+iamdefined: group4testing
diff --git a/test/integration/targets/inventory_constructed/invs/1/host_vars/testing.yml b/test/integration/targets/inventory_constructed/invs/1/host_vars/testing.yml
new file mode 100644
index 0000000..0ffe382
--- /dev/null
+++ b/test/integration/targets/inventory_constructed/invs/1/host_vars/testing.yml
@@ -0,0 +1 @@
+hola: lola
diff --git a/test/integration/targets/inventory_constructed/invs/1/one.yml b/test/integration/targets/inventory_constructed/invs/1/one.yml
new file mode 100644
index 0000000..ad5a5e0
--- /dev/null
+++ b/test/integration/targets/inventory_constructed/invs/1/one.yml
@@ -0,0 +1,5 @@
+all:
+ children:
+ stuff:
+ hosts:
+ testing:
diff --git a/test/integration/targets/inventory_constructed/invs/2/constructed.yml b/test/integration/targets/inventory_constructed/invs/2/constructed.yml
new file mode 100644
index 0000000..ca26e2c
--- /dev/null
+++ b/test/integration/targets/inventory_constructed/invs/2/constructed.yml
@@ -0,0 +1,7 @@
+plugin: ansible.builtin.constructed
+use_vars_plugins: true
+keyed_groups:
+ - key: iamdefined
+ prefix: c
+ - key: hola
+ prefix: c
diff --git a/test/integration/targets/inventory_constructed/keyed_group_default_value.yml b/test/integration/targets/inventory_constructed/keyed_group_default_value.yml
new file mode 100644
index 0000000..d69e8ec
--- /dev/null
+++ b/test/integration/targets/inventory_constructed/keyed_group_default_value.yml
@@ -0,0 +1,5 @@
+plugin: ansible.builtin.constructed
+keyed_groups:
+ - key: tags
+ prefix: tag
+ default_value: "running"
diff --git a/test/integration/targets/inventory_constructed/keyed_group_list_default_value.yml b/test/integration/targets/inventory_constructed/keyed_group_list_default_value.yml
new file mode 100644
index 0000000..4481db3
--- /dev/null
+++ b/test/integration/targets/inventory_constructed/keyed_group_list_default_value.yml
@@ -0,0 +1,5 @@
+plugin: ansible.builtin.constructed
+keyed_groups:
+ - key: roles
+ default_value: storage
+ prefix: host
diff --git a/test/integration/targets/inventory_constructed/keyed_group_str_default_value.yml b/test/integration/targets/inventory_constructed/keyed_group_str_default_value.yml
new file mode 100644
index 0000000..256d330
--- /dev/null
+++ b/test/integration/targets/inventory_constructed/keyed_group_str_default_value.yml
@@ -0,0 +1,5 @@
+plugin: ansible.builtin.constructed
+keyed_groups:
+ - key: os
+ default_value: "fedora"
+ prefix: host
diff --git a/test/integration/targets/inventory_constructed/keyed_group_trailing_separator.yml b/test/integration/targets/inventory_constructed/keyed_group_trailing_separator.yml
new file mode 100644
index 0000000..d69899d
--- /dev/null
+++ b/test/integration/targets/inventory_constructed/keyed_group_trailing_separator.yml
@@ -0,0 +1,5 @@
+plugin: ansible.builtin.constructed
+keyed_groups:
+ - key: tags
+ prefix: tag
+ trailing_separator: False
diff --git a/test/integration/targets/inventory_constructed/no_leading_separator_constructed.yml b/test/integration/targets/inventory_constructed/no_leading_separator_constructed.yml
new file mode 100644
index 0000000..5ff8f93
--- /dev/null
+++ b/test/integration/targets/inventory_constructed/no_leading_separator_constructed.yml
@@ -0,0 +1,20 @@
+plugin: ansible.builtin.constructed
+keyed_groups:
+ - key: hostvar0
+ - key: hostvar1
+ - key: hostvar2
+
+ - key: hostvar0
+ separator: 'separator'
+ - key: hostvar1
+ separator: 'separator'
+ - key: hostvar2
+ separator: 'separator'
+
+ - key: hostvar0
+ prefix: 'prefix'
+ - key: hostvar1
+ prefix: 'prefix'
+ - key: hostvar2
+ prefix: 'prefix'
+leading_separator: False
diff --git a/test/integration/targets/inventory_constructed/runme.sh b/test/integration/targets/inventory_constructed/runme.sh
new file mode 100755
index 0000000..91bbd66
--- /dev/null
+++ b/test/integration/targets/inventory_constructed/runme.sh
@@ -0,0 +1,59 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-inventory -i static_inventory.yml -i constructed.yml --graph | tee out.txt
+
+grep '@_hostvalue1' out.txt
+grep '@_item0' out.txt
+grep '@_key0_value0' out.txt
+grep '@prefix_hostvalue1' out.txt
+grep '@prefix_item0' out.txt
+grep '@prefix_key0_value0' out.txt
+grep '@separatorhostvalue1' out.txt
+grep '@separatoritem0' out.txt
+grep '@separatorkey0separatorvalue0' out.txt
+
+ansible-inventory -i static_inventory.yml -i no_leading_separator_constructed.yml --graph | tee out.txt
+
+grep '@hostvalue1' out.txt
+grep '@item0' out.txt
+grep '@key0_value0' out.txt
+grep '@key0separatorvalue0' out.txt
+grep '@prefix_hostvalue1' out.txt
+grep '@prefix_item0' out.txt
+grep '@prefix_key0_value0' out.txt
+
+# keyed group with default value for key's value empty (dict)
+ansible-inventory -i tag_inventory.yml -i keyed_group_default_value.yml --graph | tee out.txt
+
+grep '@tag_name_host0' out.txt
+grep '@tag_environment_test' out.txt
+grep '@tag_status_running' out.txt
+
+# keyed group with default value for key's value empty (list)
+ansible-inventory -i tag_inventory.yml -i keyed_group_list_default_value.yml --graph | tee out.txt
+
+grep '@host_db' out.txt
+grep '@host_web' out.txt
+grep '@host_storage' out.txt
+
+# keyed group with default value for key's value empty (str)
+ansible-inventory -i tag_inventory.yml -i keyed_group_str_default_value.yml --graph | tee out.txt
+
+grep '@host_fedora' out.txt
+
+
+# keyed group with 'trailing_separator' set to 'False' for key's value empty
+ansible-inventory -i tag_inventory.yml -i keyed_group_trailing_separator.yml --graph | tee out.txt
+
+grep '@tag_name_host0' out.txt
+grep '@tag_environment_test' out.txt
+grep '@tag_status' out.txt
+
+
+# test using use_vars_plugins
+ansible-inventory -i invs/1/one.yml -i invs/2/constructed.yml --graph | tee out.txt
+
+grep '@c_lola' out.txt
+grep '@c_group4testing' out.txt
diff --git a/test/integration/targets/inventory_constructed/static_inventory.yml b/test/integration/targets/inventory_constructed/static_inventory.yml
new file mode 100644
index 0000000..d050df4
--- /dev/null
+++ b/test/integration/targets/inventory_constructed/static_inventory.yml
@@ -0,0 +1,8 @@
+all:
+ hosts:
+ host0:
+ hostvar0:
+ key0: value0
+ hostvar1: hostvalue1
+ hostvar2:
+ - item0
diff --git a/test/integration/targets/inventory_constructed/tag_inventory.yml b/test/integration/targets/inventory_constructed/tag_inventory.yml
new file mode 100644
index 0000000..acf810e
--- /dev/null
+++ b/test/integration/targets/inventory_constructed/tag_inventory.yml
@@ -0,0 +1,12 @@
+all:
+ hosts:
+ host0:
+ tags:
+ name: "host0"
+ environment: "test"
+ status: ""
+ os: ""
+ roles:
+ - db
+ - web
+ - ""
diff --git a/test/integration/targets/inventory_ini/aliases b/test/integration/targets/inventory_ini/aliases
new file mode 100644
index 0000000..3005e4b
--- /dev/null
+++ b/test/integration/targets/inventory_ini/aliases
@@ -0,0 +1 @@
+shippable/posix/group4
diff --git a/test/integration/targets/inventory_ini/inventory.ini b/test/integration/targets/inventory_ini/inventory.ini
new file mode 100644
index 0000000..a0c99ad
--- /dev/null
+++ b/test/integration/targets/inventory_ini/inventory.ini
@@ -0,0 +1,5 @@
+[local]
+testhost ansible_connection=local ansible_become=no ansible_become_user=ansibletest1
+
+[all:vars]
+ansible_python_interpreter="{{ ansible_playbook_python }}"
diff --git a/test/integration/targets/inventory_ini/runme.sh b/test/integration/targets/inventory_ini/runme.sh
new file mode 100755
index 0000000..81bf147
--- /dev/null
+++ b/test/integration/targets/inventory_ini/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook -v -i inventory.ini test_ansible_become.yml
diff --git a/test/integration/targets/inventory_ini/test_ansible_become.yml b/test/integration/targets/inventory_ini/test_ansible_become.yml
new file mode 100644
index 0000000..55bbe7d
--- /dev/null
+++ b/test/integration/targets/inventory_ini/test_ansible_become.yml
@@ -0,0 +1,11 @@
+- hosts: testhost
+ gather_facts: no
+ tasks:
+ - name: Test proper bool evaluation of ansible_become (issue #70476)
+ shell: whoami
+ register: output
+
+ - name: Assert we are NOT the become user specified
+ assert:
+ that:
+ - "output.stdout != 'ansibletest1'"
diff --git a/test/integration/targets/inventory_script/aliases b/test/integration/targets/inventory_script/aliases
new file mode 100644
index 0000000..b598321
--- /dev/null
+++ b/test/integration/targets/inventory_script/aliases
@@ -0,0 +1 @@
+shippable/posix/group3
diff --git a/test/integration/targets/inventory_script/inventory.json b/test/integration/targets/inventory_script/inventory.json
new file mode 100644
index 0000000..69ba547
--- /dev/null
+++ b/test/integration/targets/inventory_script/inventory.json
@@ -0,0 +1,1045 @@
+{
+ "None": {
+ "hosts": [
+ "DC0_C0_RP0_VM0_cd0681bf-2f18-5c00-9b9b-8197c0095348",
+ "DC0_C0_RP0_VM1_f7c371d6-2003-5a48-9859-3bc9a8b08908",
+ "DC0_H0_VM0_265104de-1472-547c-b873-6dc7883fb6cb",
+ "DC0_H0_VM1_39365506-5a0a-5fd0-be10-9586ad53aaad"
+ ]
+ },
+ "_meta": {
+ "hostvars": {
+ "DC0_C0_RP0_VM0_cd0681bf-2f18-5c00-9b9b-8197c0095348": {
+ "alarmactionsenabled": null,
+ "ansible_host": "None",
+ "ansible_ssh_host": "None",
+ "ansible_uuid": "239fb366-6d93-430e-939a-0b6ab272d98f",
+ "availablefield": [],
+ "capability": {
+ "bootoptionssupported": false,
+ "bootretryoptionssupported": false,
+ "changetrackingsupported": false,
+ "consolepreferencessupported": false,
+ "cpufeaturemasksupported": false,
+ "disablesnapshotssupported": false,
+ "diskonlysnapshotonsuspendedvmsupported": null,
+ "disksharessupported": false,
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "featurerequirementsupported": false,
+ "guestautolocksupported": false,
+ "hostbasedreplicationsupported": false,
+ "locksnapshotssupported": false,
+ "memoryreservationlocksupported": false,
+ "memorysnapshotssupported": false,
+ "multiplecorespersocketsupported": false,
+ "multiplesnapshotssupported": false,
+ "nestedhvsupported": false,
+ "npivwwnonnonrdmvmsupported": false,
+ "pervmevcsupported": null,
+ "poweredoffsnapshotssupported": false,
+ "poweredonmonitortypechangesupported": false,
+ "quiescedsnapshotssupported": false,
+ "recordreplaysupported": false,
+ "reverttosnapshotsupported": false,
+ "s1acpimanagementsupported": false,
+ "securebootsupported": null,
+ "sesparsedisksupported": false,
+ "settingdisplaytopologysupported": false,
+ "settingscreenresolutionsupported": false,
+ "settingvideoramsizesupported": false,
+ "snapshotconfigsupported": false,
+ "snapshotoperationssupported": false,
+ "swapplacementsupported": false,
+ "toolsautoupdatesupported": false,
+ "toolssynctimesupported": false,
+ "virtualexecusageignored": null,
+ "virtualmmuusageignored": null,
+ "virtualmmuusagesupported": false,
+ "vmnpivwwndisablesupported": false,
+ "vmnpivwwnsupported": false,
+ "vmnpivwwnupdatesupported": false,
+ "vpmcsupported": false
+ },
+ "config": {
+ "alternateguestname": "",
+ "annotation": null,
+ "bootoptions": null,
+ "changetrackingenabled": null,
+ "changeversion": "",
+ "consolepreferences": null,
+ "contentlibiteminfo": null,
+ "cpuaffinity": null,
+ "cpuallocation": {},
+ "cpufeaturemask": [],
+ "cpuhotaddenabled": null,
+ "cpuhotremoveenabled": null,
+ "createdate": null,
+ "datastoreurl": [],
+ "defaultpowerops": {},
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "extraconfig": [],
+ "files": {},
+ "firmware": null,
+ "flags": {},
+ "forkconfiginfo": null,
+ "ftinfo": null,
+ "guestautolockenabled": null,
+ "guestfullname": "otherGuest",
+ "guestid": "otherGuest",
+ "guestintegrityinfo": null,
+ "guestmonitoringmodeinfo": null,
+ "hardware": {},
+ "hotplugmemoryincrementsize": null,
+ "hotplugmemorylimit": null,
+ "initialoverhead": null,
+ "instanceuuid": "bfff331f-7f07-572d-951e-edd3701dc061",
+ "keyid": null,
+ "latencysensitivity": null,
+ "locationid": null,
+ "managedby": null,
+ "maxmksconnections": null,
+ "memoryaffinity": null,
+ "memoryallocation": {},
+ "memoryhotaddenabled": null,
+ "memoryreservationlockedtomax": null,
+ "messagebustunnelenabled": null,
+ "migrateencryption": null,
+ "modified": {},
+ "name": "DC0_C0_RP0_VM0",
+ "nestedhvenabled": null,
+ "networkshaper": null,
+ "npivdesirednodewwns": null,
+ "npivdesiredportwwns": null,
+ "npivnodeworldwidename": [],
+ "npivonnonrdmdisks": null,
+ "npivportworldwidename": [],
+ "npivtemporarydisabled": null,
+ "npivworldwidenametype": null,
+ "repconfig": null,
+ "scheduledhardwareupgradeinfo": null,
+ "sgxinfo": null,
+ "swapplacement": null,
+ "swapstorageobjectid": null,
+ "template": false,
+ "tools": {},
+ "uuid": "cd0681bf-2f18-5c00-9b9b-8197c0095348",
+ "vappconfig": null,
+ "vassertsenabled": null,
+ "vcpuconfig": [],
+ "version": "vmx-13",
+ "vflashcachereservation": null,
+ "vmstorageobjectid": null,
+ "vmxconfigchecksum": null,
+ "vpmcenabled": null
+ },
+ "configissue": [],
+ "configstatus": "green",
+ "customvalue": [],
+ "datastore": [
+ {
+ "_moId": "/tmp/govcsim-DC0-LocalDS_0-949174843@folder-5",
+ "name": "LocalDS_0"
+ }
+ ],
+ "effectiverole": [
+ -1
+ ],
+ "guest": {
+ "appheartbeatstatus": null,
+ "appstate": null,
+ "disk": [],
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "generationinfo": [],
+ "guestfamily": null,
+ "guestfullname": null,
+ "guestid": null,
+ "guestkernelcrashed": null,
+ "guestoperationsready": null,
+ "gueststate": "",
+ "gueststatechangesupported": null,
+ "hostname": null,
+ "hwversion": null,
+ "interactiveguestoperationsready": null,
+ "ipaddress": null,
+ "ipstack": [],
+ "net": [],
+ "screen": null,
+ "toolsinstalltype": null,
+ "toolsrunningstatus": "guestToolsNotRunning",
+ "toolsstatus": "toolsNotInstalled",
+ "toolsversion": "0",
+ "toolsversionstatus": null,
+ "toolsversionstatus2": null
+ },
+ "guestheartbeatstatus": null,
+ "layout": {
+ "configfile": [],
+ "disk": [],
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "logfile": [],
+ "snapshot": [],
+ "swapfile": null
+ },
+ "layoutex": {
+ "disk": [],
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "file": [],
+ "snapshot": [],
+ "timestamp": {}
+ },
+ "name": "DC0_C0_RP0_VM0",
+ "network": [],
+ "overallstatus": "green",
+ "parentvapp": null,
+ "permission": [],
+ "recenttask": [],
+ "resourcepool": {
+ "_moId": "resgroup-26",
+ "name": "Resources"
+ },
+ "rootsnapshot": [],
+ "runtime": {
+ "boottime": null,
+ "cleanpoweroff": null,
+ "connectionstate": "connected",
+ "consolidationneeded": false,
+ "cryptostate": null,
+ "dasvmprotection": null,
+ "device": [],
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "faulttolerancestate": null,
+ "featuremask": [],
+ "featurerequirement": [],
+ "host": {
+ "_moId": "host-47",
+ "name": "DC0_C0_H2"
+ },
+ "instantclonefrozen": null,
+ "maxcpuusage": null,
+ "maxmemoryusage": null,
+ "memoryoverhead": null,
+ "minrequiredevcmodekey": null,
+ "needsecondaryreason": null,
+ "nummksconnections": 0,
+ "offlinefeaturerequirement": [],
+ "onlinestandby": false,
+ "paused": null,
+ "powerstate": "poweredOn",
+ "question": null,
+ "quiescedforkparent": null,
+ "recordreplaystate": null,
+ "snapshotinbackground": null,
+ "suspendinterval": null,
+ "suspendtime": null,
+ "toolsinstallermounted": false,
+ "vflashcacheallocation": null
+ },
+ "snapshot": null,
+ "storage": {
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "perdatastoreusage": [],
+ "timestamp": {}
+ },
+ "summary": {
+ "config": {},
+ "customvalue": [],
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "guest": {},
+ "overallstatus": "green",
+ "quickstats": {},
+ "runtime": {},
+ "storage": {},
+ "vm": {}
+ },
+ "tag": [],
+ "triggeredalarmstate": [],
+ "value": []
+ },
+ "DC0_C0_RP0_VM1_f7c371d6-2003-5a48-9859-3bc9a8b08908": {
+ "alarmactionsenabled": null,
+ "ansible_host": "None",
+ "ansible_ssh_host": "None",
+ "ansible_uuid": "64b6ca93-f35f-4749-abeb-fc1fabae6c79",
+ "availablefield": [],
+ "capability": {
+ "bootoptionssupported": false,
+ "bootretryoptionssupported": false,
+ "changetrackingsupported": false,
+ "consolepreferencessupported": false,
+ "cpufeaturemasksupported": false,
+ "disablesnapshotssupported": false,
+ "diskonlysnapshotonsuspendedvmsupported": null,
+ "disksharessupported": false,
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "featurerequirementsupported": false,
+ "guestautolocksupported": false,
+ "hostbasedreplicationsupported": false,
+ "locksnapshotssupported": false,
+ "memoryreservationlocksupported": false,
+ "memorysnapshotssupported": false,
+ "multiplecorespersocketsupported": false,
+ "multiplesnapshotssupported": false,
+ "nestedhvsupported": false,
+ "npivwwnonnonrdmvmsupported": false,
+ "pervmevcsupported": null,
+ "poweredoffsnapshotssupported": false,
+ "poweredonmonitortypechangesupported": false,
+ "quiescedsnapshotssupported": false,
+ "recordreplaysupported": false,
+ "reverttosnapshotsupported": false,
+ "s1acpimanagementsupported": false,
+ "securebootsupported": null,
+ "sesparsedisksupported": false,
+ "settingdisplaytopologysupported": false,
+ "settingscreenresolutionsupported": false,
+ "settingvideoramsizesupported": false,
+ "snapshotconfigsupported": false,
+ "snapshotoperationssupported": false,
+ "swapplacementsupported": false,
+ "toolsautoupdatesupported": false,
+ "toolssynctimesupported": false,
+ "virtualexecusageignored": null,
+ "virtualmmuusageignored": null,
+ "virtualmmuusagesupported": false,
+ "vmnpivwwndisablesupported": false,
+ "vmnpivwwnsupported": false,
+ "vmnpivwwnupdatesupported": false,
+ "vpmcsupported": false
+ },
+ "config": {
+ "alternateguestname": "",
+ "annotation": null,
+ "bootoptions": null,
+ "changetrackingenabled": null,
+ "changeversion": "",
+ "consolepreferences": null,
+ "contentlibiteminfo": null,
+ "cpuaffinity": null,
+ "cpuallocation": {},
+ "cpufeaturemask": [],
+ "cpuhotaddenabled": null,
+ "cpuhotremoveenabled": null,
+ "createdate": null,
+ "datastoreurl": [],
+ "defaultpowerops": {},
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "extraconfig": [],
+ "files": {},
+ "firmware": null,
+ "flags": {},
+ "forkconfiginfo": null,
+ "ftinfo": null,
+ "guestautolockenabled": null,
+ "guestfullname": "otherGuest",
+ "guestid": "otherGuest",
+ "guestintegrityinfo": null,
+ "guestmonitoringmodeinfo": null,
+ "hardware": {},
+ "hotplugmemoryincrementsize": null,
+ "hotplugmemorylimit": null,
+ "initialoverhead": null,
+ "instanceuuid": "6132d223-1566-5921-bc3b-df91ece09a4d",
+ "keyid": null,
+ "latencysensitivity": null,
+ "locationid": null,
+ "managedby": null,
+ "maxmksconnections": null,
+ "memoryaffinity": null,
+ "memoryallocation": {},
+ "memoryhotaddenabled": null,
+ "memoryreservationlockedtomax": null,
+ "messagebustunnelenabled": null,
+ "migrateencryption": null,
+ "modified": {},
+ "name": "DC0_C0_RP0_VM1",
+ "nestedhvenabled": null,
+ "networkshaper": null,
+ "npivdesirednodewwns": null,
+ "npivdesiredportwwns": null,
+ "npivnodeworldwidename": [],
+ "npivonnonrdmdisks": null,
+ "npivportworldwidename": [],
+ "npivtemporarydisabled": null,
+ "npivworldwidenametype": null,
+ "repconfig": null,
+ "scheduledhardwareupgradeinfo": null,
+ "sgxinfo": null,
+ "swapplacement": null,
+ "swapstorageobjectid": null,
+ "template": false,
+ "tools": {},
+ "uuid": "f7c371d6-2003-5a48-9859-3bc9a8b08908",
+ "vappconfig": null,
+ "vassertsenabled": null,
+ "vcpuconfig": [],
+ "version": "vmx-13",
+ "vflashcachereservation": null,
+ "vmstorageobjectid": null,
+ "vmxconfigchecksum": null,
+ "vpmcenabled": null
+ },
+ "configissue": [],
+ "configstatus": "green",
+ "customvalue": [],
+ "datastore": [
+ {
+ "_moId": "/tmp/govcsim-DC0-LocalDS_0-949174843@folder-5",
+ "name": "LocalDS_0"
+ }
+ ],
+ "effectiverole": [
+ -1
+ ],
+ "guest": {
+ "appheartbeatstatus": null,
+ "appstate": null,
+ "disk": [],
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "generationinfo": [],
+ "guestfamily": null,
+ "guestfullname": null,
+ "guestid": null,
+ "guestkernelcrashed": null,
+ "guestoperationsready": null,
+ "gueststate": "",
+ "gueststatechangesupported": null,
+ "hostname": null,
+ "hwversion": null,
+ "interactiveguestoperationsready": null,
+ "ipaddress": null,
+ "ipstack": [],
+ "net": [],
+ "screen": null,
+ "toolsinstalltype": null,
+ "toolsrunningstatus": "guestToolsNotRunning",
+ "toolsstatus": "toolsNotInstalled",
+ "toolsversion": "0",
+ "toolsversionstatus": null,
+ "toolsversionstatus2": null
+ },
+ "guestheartbeatstatus": null,
+ "layout": {
+ "configfile": [],
+ "disk": [],
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "logfile": [],
+ "snapshot": [],
+ "swapfile": null
+ },
+ "layoutex": {
+ "disk": [],
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "file": [],
+ "snapshot": [],
+ "timestamp": {}
+ },
+ "name": "DC0_C0_RP0_VM1",
+ "network": [],
+ "overallstatus": "green",
+ "parentvapp": null,
+ "permission": [],
+ "recenttask": [],
+ "resourcepool": {
+ "_moId": "resgroup-26",
+ "name": "Resources"
+ },
+ "rootsnapshot": [],
+ "runtime": {
+ "boottime": null,
+ "cleanpoweroff": null,
+ "connectionstate": "connected",
+ "consolidationneeded": false,
+ "cryptostate": null,
+ "dasvmprotection": null,
+ "device": [],
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "faulttolerancestate": null,
+ "featuremask": [],
+ "featurerequirement": [],
+ "host": {
+ "_moId": "host-33",
+ "name": "DC0_C0_H0"
+ },
+ "instantclonefrozen": null,
+ "maxcpuusage": null,
+ "maxmemoryusage": null,
+ "memoryoverhead": null,
+ "minrequiredevcmodekey": null,
+ "needsecondaryreason": null,
+ "nummksconnections": 0,
+ "offlinefeaturerequirement": [],
+ "onlinestandby": false,
+ "paused": null,
+ "powerstate": "poweredOn",
+ "question": null,
+ "quiescedforkparent": null,
+ "recordreplaystate": null,
+ "snapshotinbackground": null,
+ "suspendinterval": null,
+ "suspendtime": null,
+ "toolsinstallermounted": false,
+ "vflashcacheallocation": null
+ },
+ "snapshot": null,
+ "storage": {
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "perdatastoreusage": [],
+ "timestamp": {}
+ },
+ "summary": {
+ "config": {},
+ "customvalue": [],
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "guest": {},
+ "overallstatus": "green",
+ "quickstats": {},
+ "runtime": {},
+ "storage": {},
+ "vm": {}
+ },
+ "tag": [],
+ "triggeredalarmstate": [],
+ "value": []
+ },
+ "DC0_H0_VM0_265104de-1472-547c-b873-6dc7883fb6cb": {
+ "alarmactionsenabled": null,
+ "ansible_host": "None",
+ "ansible_ssh_host": "None",
+ "ansible_uuid": "6616671b-16b0-494c-8201-737ca506790b",
+ "availablefield": [],
+ "capability": {
+ "bootoptionssupported": false,
+ "bootretryoptionssupported": false,
+ "changetrackingsupported": false,
+ "consolepreferencessupported": false,
+ "cpufeaturemasksupported": false,
+ "disablesnapshotssupported": false,
+ "diskonlysnapshotonsuspendedvmsupported": null,
+ "disksharessupported": false,
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "featurerequirementsupported": false,
+ "guestautolocksupported": false,
+ "hostbasedreplicationsupported": false,
+ "locksnapshotssupported": false,
+ "memoryreservationlocksupported": false,
+ "memorysnapshotssupported": false,
+ "multiplecorespersocketsupported": false,
+ "multiplesnapshotssupported": false,
+ "nestedhvsupported": false,
+ "npivwwnonnonrdmvmsupported": false,
+ "pervmevcsupported": null,
+ "poweredoffsnapshotssupported": false,
+ "poweredonmonitortypechangesupported": false,
+ "quiescedsnapshotssupported": false,
+ "recordreplaysupported": false,
+ "reverttosnapshotsupported": false,
+ "s1acpimanagementsupported": false,
+ "securebootsupported": null,
+ "sesparsedisksupported": false,
+ "settingdisplaytopologysupported": false,
+ "settingscreenresolutionsupported": false,
+ "settingvideoramsizesupported": false,
+ "snapshotconfigsupported": false,
+ "snapshotoperationssupported": false,
+ "swapplacementsupported": false,
+ "toolsautoupdatesupported": false,
+ "toolssynctimesupported": false,
+ "virtualexecusageignored": null,
+ "virtualmmuusageignored": null,
+ "virtualmmuusagesupported": false,
+ "vmnpivwwndisablesupported": false,
+ "vmnpivwwnsupported": false,
+ "vmnpivwwnupdatesupported": false,
+ "vpmcsupported": false
+ },
+ "config": {
+ "alternateguestname": "",
+ "annotation": null,
+ "bootoptions": null,
+ "changetrackingenabled": null,
+ "changeversion": "",
+ "consolepreferences": null,
+ "contentlibiteminfo": null,
+ "cpuaffinity": null,
+ "cpuallocation": {},
+ "cpufeaturemask": [],
+ "cpuhotaddenabled": null,
+ "cpuhotremoveenabled": null,
+ "createdate": null,
+ "datastoreurl": [],
+ "defaultpowerops": {},
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "extraconfig": [],
+ "files": {},
+ "firmware": null,
+ "flags": {},
+ "forkconfiginfo": null,
+ "ftinfo": null,
+ "guestautolockenabled": null,
+ "guestfullname": "otherGuest",
+ "guestid": "otherGuest",
+ "guestintegrityinfo": null,
+ "guestmonitoringmodeinfo": null,
+ "hardware": {},
+ "hotplugmemoryincrementsize": null,
+ "hotplugmemorylimit": null,
+ "initialoverhead": null,
+ "instanceuuid": "b4689bed-97f0-5bcd-8a4c-07477cc8f06f",
+ "keyid": null,
+ "latencysensitivity": null,
+ "locationid": null,
+ "managedby": null,
+ "maxmksconnections": null,
+ "memoryaffinity": null,
+ "memoryallocation": {},
+ "memoryhotaddenabled": null,
+ "memoryreservationlockedtomax": null,
+ "messagebustunnelenabled": null,
+ "migrateencryption": null,
+ "modified": {},
+ "name": "DC0_H0_VM0",
+ "nestedhvenabled": null,
+ "networkshaper": null,
+ "npivdesirednodewwns": null,
+ "npivdesiredportwwns": null,
+ "npivnodeworldwidename": [],
+ "npivonnonrdmdisks": null,
+ "npivportworldwidename": [],
+ "npivtemporarydisabled": null,
+ "npivworldwidenametype": null,
+ "repconfig": null,
+ "scheduledhardwareupgradeinfo": null,
+ "sgxinfo": null,
+ "swapplacement": null,
+ "swapstorageobjectid": null,
+ "template": false,
+ "tools": {},
+ "uuid": "265104de-1472-547c-b873-6dc7883fb6cb",
+ "vappconfig": null,
+ "vassertsenabled": null,
+ "vcpuconfig": [],
+ "version": "vmx-13",
+ "vflashcachereservation": null,
+ "vmstorageobjectid": null,
+ "vmxconfigchecksum": null,
+ "vpmcenabled": null
+ },
+ "configissue": [],
+ "configstatus": "green",
+ "customvalue": [],
+ "datastore": [
+ {
+ "_moId": "/tmp/govcsim-DC0-LocalDS_0-949174843@folder-5",
+ "name": "LocalDS_0"
+ }
+ ],
+ "effectiverole": [
+ -1
+ ],
+ "guest": {
+ "appheartbeatstatus": null,
+ "appstate": null,
+ "disk": [],
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "generationinfo": [],
+ "guestfamily": null,
+ "guestfullname": null,
+ "guestid": null,
+ "guestkernelcrashed": null,
+ "guestoperationsready": null,
+ "gueststate": "",
+ "gueststatechangesupported": null,
+ "hostname": null,
+ "hwversion": null,
+ "interactiveguestoperationsready": null,
+ "ipaddress": null,
+ "ipstack": [],
+ "net": [],
+ "screen": null,
+ "toolsinstalltype": null,
+ "toolsrunningstatus": "guestToolsNotRunning",
+ "toolsstatus": "toolsNotInstalled",
+ "toolsversion": "0",
+ "toolsversionstatus": null,
+ "toolsversionstatus2": null
+ },
+ "guestheartbeatstatus": null,
+ "layout": {
+ "configfile": [],
+ "disk": [],
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "logfile": [],
+ "snapshot": [],
+ "swapfile": null
+ },
+ "layoutex": {
+ "disk": [],
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "file": [],
+ "snapshot": [],
+ "timestamp": {}
+ },
+ "name": "DC0_H0_VM0",
+ "network": [],
+ "overallstatus": "green",
+ "parentvapp": null,
+ "permission": [],
+ "recenttask": [],
+ "resourcepool": {
+ "_moId": "resgroup-22",
+ "name": "Resources"
+ },
+ "rootsnapshot": [],
+ "runtime": {
+ "boottime": null,
+ "cleanpoweroff": null,
+ "connectionstate": "connected",
+ "consolidationneeded": false,
+ "cryptostate": null,
+ "dasvmprotection": null,
+ "device": [],
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "faulttolerancestate": null,
+ "featuremask": [],
+ "featurerequirement": [],
+ "host": {
+ "_moId": "host-21",
+ "name": "DC0_H0"
+ },
+ "instantclonefrozen": null,
+ "maxcpuusage": null,
+ "maxmemoryusage": null,
+ "memoryoverhead": null,
+ "minrequiredevcmodekey": null,
+ "needsecondaryreason": null,
+ "nummksconnections": 0,
+ "offlinefeaturerequirement": [],
+ "onlinestandby": false,
+ "paused": null,
+ "powerstate": "poweredOn",
+ "question": null,
+ "quiescedforkparent": null,
+ "recordreplaystate": null,
+ "snapshotinbackground": null,
+ "suspendinterval": null,
+ "suspendtime": null,
+ "toolsinstallermounted": false,
+ "vflashcacheallocation": null
+ },
+ "snapshot": null,
+ "storage": {
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "perdatastoreusage": [],
+ "timestamp": {}
+ },
+ "summary": {
+ "config": {},
+ "customvalue": [],
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "guest": {},
+ "overallstatus": "green",
+ "quickstats": {},
+ "runtime": {},
+ "storage": {},
+ "vm": {}
+ },
+ "tag": [],
+ "triggeredalarmstate": [],
+ "value": []
+ },
+ "DC0_H0_VM1_39365506-5a0a-5fd0-be10-9586ad53aaad": {
+ "alarmactionsenabled": null,
+ "ansible_host": "None",
+ "ansible_ssh_host": "None",
+ "ansible_uuid": "50401ff9-720a-4166-b9e6-d7cd0d9a4dc9",
+ "availablefield": [],
+ "capability": {
+ "bootoptionssupported": false,
+ "bootretryoptionssupported": false,
+ "changetrackingsupported": false,
+ "consolepreferencessupported": false,
+ "cpufeaturemasksupported": false,
+ "disablesnapshotssupported": false,
+ "diskonlysnapshotonsuspendedvmsupported": null,
+ "disksharessupported": false,
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "featurerequirementsupported": false,
+ "guestautolocksupported": false,
+ "hostbasedreplicationsupported": false,
+ "locksnapshotssupported": false,
+ "memoryreservationlocksupported": false,
+ "memorysnapshotssupported": false,
+ "multiplecorespersocketsupported": false,
+ "multiplesnapshotssupported": false,
+ "nestedhvsupported": false,
+ "npivwwnonnonrdmvmsupported": false,
+ "pervmevcsupported": null,
+ "poweredoffsnapshotssupported": false,
+ "poweredonmonitortypechangesupported": false,
+ "quiescedsnapshotssupported": false,
+ "recordreplaysupported": false,
+ "reverttosnapshotsupported": false,
+ "s1acpimanagementsupported": false,
+ "securebootsupported": null,
+ "sesparsedisksupported": false,
+ "settingdisplaytopologysupported": false,
+ "settingscreenresolutionsupported": false,
+ "settingvideoramsizesupported": false,
+ "snapshotconfigsupported": false,
+ "snapshotoperationssupported": false,
+ "swapplacementsupported": false,
+ "toolsautoupdatesupported": false,
+ "toolssynctimesupported": false,
+ "virtualexecusageignored": null,
+ "virtualmmuusageignored": null,
+ "virtualmmuusagesupported": false,
+ "vmnpivwwndisablesupported": false,
+ "vmnpivwwnsupported": false,
+ "vmnpivwwnupdatesupported": false,
+ "vpmcsupported": false
+ },
+ "config": {
+ "alternateguestname": "",
+ "annotation": null,
+ "bootoptions": null,
+ "changetrackingenabled": null,
+ "changeversion": "",
+ "consolepreferences": null,
+ "contentlibiteminfo": null,
+ "cpuaffinity": null,
+ "cpuallocation": {},
+ "cpufeaturemask": [],
+ "cpuhotaddenabled": null,
+ "cpuhotremoveenabled": null,
+ "createdate": null,
+ "datastoreurl": [],
+ "defaultpowerops": {},
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "extraconfig": [],
+ "files": {},
+ "firmware": null,
+ "flags": {},
+ "forkconfiginfo": null,
+ "ftinfo": null,
+ "guestautolockenabled": null,
+ "guestfullname": "otherGuest",
+ "guestid": "otherGuest",
+ "guestintegrityinfo": null,
+ "guestmonitoringmodeinfo": null,
+ "hardware": {},
+ "hotplugmemoryincrementsize": null,
+ "hotplugmemorylimit": null,
+ "initialoverhead": null,
+ "instanceuuid": "12f8928d-f144-5c57-89db-dd2d0902c9fa",
+ "keyid": null,
+ "latencysensitivity": null,
+ "locationid": null,
+ "managedby": null,
+ "maxmksconnections": null,
+ "memoryaffinity": null,
+ "memoryallocation": {},
+ "memoryhotaddenabled": null,
+ "memoryreservationlockedtomax": null,
+ "messagebustunnelenabled": null,
+ "migrateencryption": null,
+ "modified": {},
+ "name": "DC0_H0_VM1",
+ "nestedhvenabled": null,
+ "networkshaper": null,
+ "npivdesirednodewwns": null,
+ "npivdesiredportwwns": null,
+ "npivnodeworldwidename": [],
+ "npivonnonrdmdisks": null,
+ "npivportworldwidename": [],
+ "npivtemporarydisabled": null,
+ "npivworldwidenametype": null,
+ "repconfig": null,
+ "scheduledhardwareupgradeinfo": null,
+ "sgxinfo": null,
+ "swapplacement": null,
+ "swapstorageobjectid": null,
+ "template": false,
+ "tools": {},
+ "uuid": "39365506-5a0a-5fd0-be10-9586ad53aaad",
+ "vappconfig": null,
+ "vassertsenabled": null,
+ "vcpuconfig": [],
+ "version": "vmx-13",
+ "vflashcachereservation": null,
+ "vmstorageobjectid": null,
+ "vmxconfigchecksum": null,
+ "vpmcenabled": null
+ },
+ "configissue": [],
+ "configstatus": "green",
+ "customvalue": [],
+ "datastore": [
+ {
+ "_moId": "/tmp/govcsim-DC0-LocalDS_0-949174843@folder-5",
+ "name": "LocalDS_0"
+ }
+ ],
+ "effectiverole": [
+ -1
+ ],
+ "guest": {
+ "appheartbeatstatus": null,
+ "appstate": null,
+ "disk": [],
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "generationinfo": [],
+ "guestfamily": null,
+ "guestfullname": null,
+ "guestid": null,
+ "guestkernelcrashed": null,
+ "guestoperationsready": null,
+ "gueststate": "",
+ "gueststatechangesupported": null,
+ "hostname": null,
+ "hwversion": null,
+ "interactiveguestoperationsready": null,
+ "ipaddress": null,
+ "ipstack": [],
+ "net": [],
+ "screen": null,
+ "toolsinstalltype": null,
+ "toolsrunningstatus": "guestToolsNotRunning",
+ "toolsstatus": "toolsNotInstalled",
+ "toolsversion": "0",
+ "toolsversionstatus": null,
+ "toolsversionstatus2": null
+ },
+ "guestheartbeatstatus": null,
+ "layout": {
+ "configfile": [],
+ "disk": [],
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "logfile": [],
+ "snapshot": [],
+ "swapfile": null
+ },
+ "layoutex": {
+ "disk": [],
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "file": [],
+ "snapshot": [],
+ "timestamp": {}
+ },
+ "name": "DC0_H0_VM1",
+ "network": [],
+ "overallstatus": "green",
+ "parentvapp": null,
+ "permission": [],
+ "recenttask": [],
+ "resourcepool": {
+ "_moId": "resgroup-22",
+ "name": "Resources"
+ },
+ "rootsnapshot": [],
+ "runtime": {
+ "boottime": null,
+ "cleanpoweroff": null,
+ "connectionstate": "connected",
+ "consolidationneeded": false,
+ "cryptostate": null,
+ "dasvmprotection": null,
+ "device": [],
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "faulttolerancestate": null,
+ "featuremask": [],
+ "featurerequirement": [],
+ "host": {
+ "_moId": "host-21",
+ "name": "DC0_H0"
+ },
+ "instantclonefrozen": null,
+ "maxcpuusage": null,
+ "maxmemoryusage": null,
+ "memoryoverhead": null,
+ "minrequiredevcmodekey": null,
+ "needsecondaryreason": null,
+ "nummksconnections": 0,
+ "offlinefeaturerequirement": [],
+ "onlinestandby": false,
+ "paused": null,
+ "powerstate": "poweredOn",
+ "question": null,
+ "quiescedforkparent": null,
+ "recordreplaystate": null,
+ "snapshotinbackground": null,
+ "suspendinterval": null,
+ "suspendtime": null,
+ "toolsinstallermounted": false,
+ "vflashcacheallocation": null
+ },
+ "snapshot": null,
+ "storage": {
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "perdatastoreusage": [],
+ "timestamp": {}
+ },
+ "summary": {
+ "config": {},
+ "customvalue": [],
+ "dynamicproperty": [],
+ "dynamictype": null,
+ "guest": {},
+ "overallstatus": "green",
+ "quickstats": {},
+ "runtime": {},
+ "storage": {},
+ "vm": {}
+ },
+ "tag": [],
+ "triggeredalarmstate": [],
+ "value": []
+ }
+ }
+ },
+ "all": {
+ "children": [
+ "ungrouped",
+ "None",
+ "guests"
+ ]
+ },
+ "guests": {
+ "hosts": [
+ "DC0_C0_RP0_VM0_cd0681bf-2f18-5c00-9b9b-8197c0095348",
+ "DC0_C0_RP0_VM1_f7c371d6-2003-5a48-9859-3bc9a8b08908",
+ "DC0_H0_VM0_265104de-1472-547c-b873-6dc7883fb6cb",
+ "DC0_H0_VM1_39365506-5a0a-5fd0-be10-9586ad53aaad"
+ ]
+ }
+}
diff --git a/test/integration/targets/inventory_script/inventory.sh b/test/integration/targets/inventory_script/inventory.sh
new file mode 100755
index 0000000..b3f1d03
--- /dev/null
+++ b/test/integration/targets/inventory_script/inventory.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+# This script mimics the output from what the contrib/inventory/vmware_inventory.py
+# dynamic inventory script produced.
+# This ensures we are still covering the same code that the original tests gave us
+# and subsequently ensures that ansible-inventory produces output consistent with
+# that of a dynamic inventory script
+cat inventory.json
diff --git a/test/integration/targets/inventory_script/runme.sh b/test/integration/targets/inventory_script/runme.sh
new file mode 100755
index 0000000..bb4fcea
--- /dev/null
+++ b/test/integration/targets/inventory_script/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eux
+
+diff -uw <(ansible-inventory -i inventory.sh --list --export) inventory.json
diff --git a/test/integration/targets/inventory_yaml/aliases b/test/integration/targets/inventory_yaml/aliases
new file mode 100644
index 0000000..b598321
--- /dev/null
+++ b/test/integration/targets/inventory_yaml/aliases
@@ -0,0 +1 @@
+shippable/posix/group3
diff --git a/test/integration/targets/inventory_yaml/empty.json b/test/integration/targets/inventory_yaml/empty.json
new file mode 100644
index 0000000..e1ae068
--- /dev/null
+++ b/test/integration/targets/inventory_yaml/empty.json
@@ -0,0 +1,10 @@
+{
+ "_meta": {
+ "hostvars": {}
+ },
+ "all": {
+ "children": [
+ "ungrouped"
+ ]
+ }
+}
diff --git a/test/integration/targets/inventory_yaml/runme.sh b/test/integration/targets/inventory_yaml/runme.sh
new file mode 100755
index 0000000..a8818dd
--- /dev/null
+++ b/test/integration/targets/inventory_yaml/runme.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+# handle empty/commented out group keys correctly https://github.com/ansible/ansible/issues/47254
+ANSIBLE_VERBOSITY=0 diff -w <(ansible-inventory -i ./test.yml --list) success.json
+
+ansible-inventory -i ./test_int_hostname.yml --list 2>&1 | grep 'Host pattern 1234 must be a string'
diff --git a/test/integration/targets/inventory_yaml/success.json b/test/integration/targets/inventory_yaml/success.json
new file mode 100644
index 0000000..a8b15f9
--- /dev/null
+++ b/test/integration/targets/inventory_yaml/success.json
@@ -0,0 +1,61 @@
+{
+ "_meta": {
+ "hostvars": {
+ "alice": {
+ "status": "single"
+ },
+ "bobby": {
+ "in_trouble": true,
+ "popular": false
+ },
+ "cindy": {
+ "in_trouble": true,
+ "popular": true
+ },
+ "greg": {
+ "in_trouble": true,
+ "popular": true
+ },
+ "jan": {
+ "in_trouble": true,
+ "popular": false
+ },
+ "marcia": {
+ "in_trouble": true,
+ "popular": true
+ },
+ "peter": {
+ "in_trouble": true,
+ "popular": false
+ }
+ }
+ },
+ "all": {
+ "children": [
+ "cousins",
+ "kids",
+ "the-maid",
+ "ungrouped"
+ ]
+ },
+ "cousins": {
+ "children": [
+ "redheads"
+ ]
+ },
+ "kids": {
+ "hosts": [
+ "bobby",
+ "cindy",
+ "greg",
+ "jan",
+ "marcia",
+ "peter"
+ ]
+ },
+ "the-maid": {
+ "hosts": [
+ "alice"
+ ]
+ }
+}
diff --git a/test/integration/targets/inventory_yaml/test.yml b/test/integration/targets/inventory_yaml/test.yml
new file mode 100644
index 0000000..9755396
--- /dev/null
+++ b/test/integration/targets/inventory_yaml/test.yml
@@ -0,0 +1,27 @@
+all:
+ children:
+ kids:
+ hosts:
+ marcia:
+ popular: True
+ jan:
+ popular: False
+ cindy:
+ popular: True
+ greg:
+ popular: True
+ peter:
+ popular: False
+ bobby:
+ popular: False
+ vars:
+ in_trouble: True
+ cousins:
+ children:
+ redheads:
+ hosts:
+ #oliver: # this used to cause an error and deliver incomplete inventory
+ the-maid:
+ hosts:
+ alice:
+ status: single
diff --git a/test/integration/targets/inventory_yaml/test_int_hostname.yml b/test/integration/targets/inventory_yaml/test_int_hostname.yml
new file mode 100644
index 0000000..d2285cb
--- /dev/null
+++ b/test/integration/targets/inventory_yaml/test_int_hostname.yml
@@ -0,0 +1,5 @@
+all:
+ children:
+ kids:
+ hosts:
+ 1234: {}
diff --git a/test/integration/targets/iptables/aliases b/test/integration/targets/iptables/aliases
new file mode 100644
index 0000000..7d66ecf
--- /dev/null
+++ b/test/integration/targets/iptables/aliases
@@ -0,0 +1,5 @@
+shippable/posix/group2
+skip/freebsd
+skip/osx
+skip/macos
+skip/docker
diff --git a/test/integration/targets/iptables/tasks/chain_management.yml b/test/integration/targets/iptables/tasks/chain_management.yml
new file mode 100644
index 0000000..0355122
--- /dev/null
+++ b/test/integration/targets/iptables/tasks/chain_management.yml
@@ -0,0 +1,71 @@
+# test code for the iptables module
+# (c) 2021, Éloi Rivard <eloi@yaal.coop>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+---
+- name: get the state of the iptable rules
+ shell: "{{ iptables_bin }} -L"
+ become: true
+ register: result
+
+- name: assert the rule is absent
+ assert:
+ that:
+ - result is not failed
+ - '"FOOBAR-CHAIN" not in result.stdout'
+
+- name: create the foobar chain
+ become: true
+ iptables:
+ chain: FOOBAR-CHAIN
+ chain_management: true
+ state: present
+
+- name: get the state of the iptable rules after chain is created
+ become: true
+ shell: "{{ iptables_bin }} -L"
+ register: result
+
+- name: assert the rule is present
+ assert:
+ that:
+ - result is not failed
+ - '"FOOBAR-CHAIN" in result.stdout'
+
+- name: flush the foobar chain
+ become: true
+ iptables:
+ chain: FOOBAR-CHAIN
+ flush: true
+
+- name: delete the foobar chain
+ become: true
+ iptables:
+ chain: FOOBAR-CHAIN
+ chain_management: true
+ state: absent
+
+- name: get the state of the iptable rules after chain is deleted
+ become: true
+ shell: "{{ iptables_bin }} -L"
+ register: result
+
+- name: assert the rule is absent
+ assert:
+ that:
+ - result is not failed
+ - '"FOOBAR-CHAIN" not in result.stdout'
+ - '"FOOBAR-RULE" not in result.stdout'
diff --git a/test/integration/targets/iptables/tasks/main.yml b/test/integration/targets/iptables/tasks/main.yml
new file mode 100644
index 0000000..eb2674a
--- /dev/null
+++ b/test/integration/targets/iptables/tasks/main.yml
@@ -0,0 +1,36 @@
+# test code for the iptables module
+# (c) 2021, Éloi Rivard <eloi@yaal.coop>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+---
+- name: Include distribution specific variables
+ include_vars: "{{ lookup('first_found', search) }}"
+ vars:
+ search:
+ files:
+ - '{{ ansible_distribution | lower }}.yml'
+ - '{{ ansible_os_family | lower }}.yml'
+ - '{{ ansible_system | lower }}.yml'
+ - default.yml
+ paths:
+ - vars
+
+- name: install dependencies for iptables test
+ package:
+ name: iptables
+ state: present
+
+- import_tasks: chain_management.yml
diff --git a/test/integration/targets/iptables/vars/alpine.yml b/test/integration/targets/iptables/vars/alpine.yml
new file mode 100644
index 0000000..7bdd1a0
--- /dev/null
+++ b/test/integration/targets/iptables/vars/alpine.yml
@@ -0,0 +1,2 @@
+---
+iptables_bin: /sbin/iptables
diff --git a/test/integration/targets/iptables/vars/centos.yml b/test/integration/targets/iptables/vars/centos.yml
new file mode 100644
index 0000000..7bdd1a0
--- /dev/null
+++ b/test/integration/targets/iptables/vars/centos.yml
@@ -0,0 +1,2 @@
+---
+iptables_bin: /sbin/iptables
diff --git a/test/integration/targets/iptables/vars/default.yml b/test/integration/targets/iptables/vars/default.yml
new file mode 100644
index 0000000..0c5f877
--- /dev/null
+++ b/test/integration/targets/iptables/vars/default.yml
@@ -0,0 +1,2 @@
+---
+iptables_bin: /usr/sbin/iptables
diff --git a/test/integration/targets/iptables/vars/fedora.yml b/test/integration/targets/iptables/vars/fedora.yml
new file mode 100644
index 0000000..7bdd1a0
--- /dev/null
+++ b/test/integration/targets/iptables/vars/fedora.yml
@@ -0,0 +1,2 @@
+---
+iptables_bin: /sbin/iptables
diff --git a/test/integration/targets/iptables/vars/redhat.yml b/test/integration/targets/iptables/vars/redhat.yml
new file mode 100644
index 0000000..7bdd1a0
--- /dev/null
+++ b/test/integration/targets/iptables/vars/redhat.yml
@@ -0,0 +1,2 @@
+---
+iptables_bin: /sbin/iptables
diff --git a/test/integration/targets/iptables/vars/suse.yml b/test/integration/targets/iptables/vars/suse.yml
new file mode 100644
index 0000000..7bdd1a0
--- /dev/null
+++ b/test/integration/targets/iptables/vars/suse.yml
@@ -0,0 +1,2 @@
+---
+iptables_bin: /sbin/iptables
diff --git a/test/integration/targets/jinja2_native_types/aliases b/test/integration/targets/jinja2_native_types/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/jinja2_native_types/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/jinja2_native_types/nested_undefined.yml b/test/integration/targets/jinja2_native_types/nested_undefined.yml
new file mode 100644
index 0000000..b60a871
--- /dev/null
+++ b/test/integration/targets/jinja2_native_types/nested_undefined.yml
@@ -0,0 +1,23 @@
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - block:
+ - name: Test nested undefined var fails, single node
+ debug:
+ msg: "{{ [{ 'key': nested_and_undefined }] }}"
+ register: result
+ ignore_errors: yes
+
+ - assert:
+ that:
+ - "\"'nested_and_undefined' is undefined\" in result.msg"
+
+ - name: Test nested undefined var fails, multiple nodes
+ debug:
+ msg: "{{ [{ 'key': nested_and_undefined}] }} second_node"
+ register: result
+ ignore_errors: yes
+
+ - assert:
+ that:
+ - "\"'nested_and_undefined' is undefined\" in result.msg"
diff --git a/test/integration/targets/jinja2_native_types/runme.sh b/test/integration/targets/jinja2_native_types/runme.sh
new file mode 100755
index 0000000..a6c2bef
--- /dev/null
+++ b/test/integration/targets/jinja2_native_types/runme.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+set -eux
+
+export ANSIBLE_JINJA2_NATIVE=1
+ansible-playbook runtests.yml -v "$@"
+ansible-playbook --vault-password-file test_vault_pass test_vault.yml -v "$@"
+ansible-playbook test_hostvars.yml -v "$@"
+ansible-playbook nested_undefined.yml -v "$@"
+ansible-playbook test_preserving_quotes.yml -v "$@"
+unset ANSIBLE_JINJA2_NATIVE
diff --git a/test/integration/targets/jinja2_native_types/runtests.yml b/test/integration/targets/jinja2_native_types/runtests.yml
new file mode 100644
index 0000000..422ef57
--- /dev/null
+++ b/test/integration/targets/jinja2_native_types/runtests.yml
@@ -0,0 +1,40 @@
+- name: Test jinja2 native types
+ hosts: localhost
+ gather_facts: no
+ vars:
+ i_one: 1
+ i_two: 2
+ i_three: 3
+ s_one: "1"
+ s_two: "2"
+ s_three: "3"
+ dict_one:
+ foo: bar
+ baz: bang
+ dict_two:
+ bar: foo
+ foobar: barfoo
+ list_one:
+ - one
+ - two
+ list_two:
+ - three
+ - four
+ list_ints:
+ - 4
+ - 2
+ list_one_int:
+ - 1
+ b_true: True
+ b_false: False
+ s_true: "True"
+ s_false: "False"
+ yaml_none: ~
+ tasks:
+ - import_tasks: test_casting.yml
+ - import_tasks: test_concatentation.yml
+ - import_tasks: test_bool.yml
+ - import_tasks: test_dunder.yml
+ - import_tasks: test_types.yml
+ - import_tasks: test_none.yml
+ - import_tasks: test_template.yml
diff --git a/test/integration/targets/jinja2_native_types/test_bool.yml b/test/integration/targets/jinja2_native_types/test_bool.yml
new file mode 100644
index 0000000..f3b5e8c
--- /dev/null
+++ b/test/integration/targets/jinja2_native_types/test_bool.yml
@@ -0,0 +1,53 @@
+- name: test bool True
+ set_fact:
+ bool_var_true: "{{ b_true }}"
+
+- assert:
+ that:
+ - 'bool_var_true is sameas true'
+ - 'bool_var_true|type_debug == "bool"'
+
+- name: test bool False
+ set_fact:
+ bool_var_false: "{{ b_false }}"
+
+- assert:
+ that:
+ - 'bool_var_false is sameas false'
+ - 'bool_var_false|type_debug == "bool"'
+
+- name: test bool expr True
+ set_fact:
+ bool_var_expr_true: "{{ 1 == 1 }}"
+
+- assert:
+ that:
+ - 'bool_var_expr_true is sameas true'
+ - 'bool_var_expr_true|type_debug == "bool"'
+
+- name: test bool expr False
+ set_fact:
+ bool_var_expr_false: "{{ 2 + 2 == 5 }}"
+
+- assert:
+ that:
+ - 'bool_var_expr_false is sameas false'
+ - 'bool_var_expr_false|type_debug == "bool"'
+
+- name: test bool expr with None, True
+ set_fact:
+ bool_var_none_expr_true: "{{ None == None }}"
+
+- assert:
+ that:
+ - 'bool_var_none_expr_true is sameas true'
+ - 'bool_var_none_expr_true|type_debug == "bool"'
+
+- name: test bool expr with None, False
+ set_fact:
+ bool_var_none_expr_false: "{{ '' == None }}"
+
+- assert:
+ that:
+ - 'bool_var_none_expr_false is sameas false'
+ - 'bool_var_none_expr_false|type_debug == "bool"'
diff --git a/test/integration/targets/jinja2_native_types/test_casting.yml b/test/integration/targets/jinja2_native_types/test_casting.yml
new file mode 100644
index 0000000..5e9c76d
--- /dev/null
+++ b/test/integration/targets/jinja2_native_types/test_casting.yml
@@ -0,0 +1,31 @@
+- name: cast things to other things
+ set_fact:
+ int_to_str: "'{{ i_two }}'"
+ int_to_str2: "{{ i_two | string }}"
+ str_to_int: "{{ s_two|int }}"
+ dict_to_str: "'{{ dict_one }}'"
+ list_to_str: "'{{ list_one }}'"
+ int_to_bool: "{{ i_one|bool }}"
+ str_true_to_bool: "{{ s_true|bool }}"
+ str_false_to_bool: "{{ s_false|bool }}"
+ list_to_json_str: "{{ list_one | to_json }}"
+ list_to_yaml_str: "{{ list_one | to_yaml }}"
+
+- assert:
+ that:
+ - int_to_str == "'2'"
+ - 'int_to_str|type_debug in ["str", "unicode"]'
+ - 'int_to_str2 == "2"'
+ - 'int_to_str2|type_debug in ["NativeJinjaText"]'
+ - 'str_to_int == 2'
+ - 'str_to_int|type_debug == "int"'
+ - 'dict_to_str|type_debug in ["str", "unicode"]'
+ - 'list_to_str|type_debug in ["str", "unicode"]'
+ - 'int_to_bool is sameas true'
+ - 'int_to_bool|type_debug == "bool"'
+ - 'str_true_to_bool is sameas true'
+ - 'str_true_to_bool|type_debug == "bool"'
+ - 'str_false_to_bool is sameas false'
+ - 'str_false_to_bool|type_debug == "bool"'
+ - 'list_to_json_str|type_debug in ["NativeJinjaText"]'
+ - 'list_to_yaml_str|type_debug in ["NativeJinjaText"]'
diff --git a/test/integration/targets/jinja2_native_types/test_concatentation.yml b/test/integration/targets/jinja2_native_types/test_concatentation.yml
new file mode 100644
index 0000000..24a9038
--- /dev/null
+++ b/test/integration/targets/jinja2_native_types/test_concatentation.yml
@@ -0,0 +1,88 @@
+- name: add two ints
+ set_fact:
+ integer_sum: "{{ i_one + i_two }}"
+
+- assert:
+ that:
+ - 'integer_sum == 3'
+ - 'integer_sum|type_debug == "int"'
+
+- name: add casted string and int
+ set_fact:
+ integer_sum2: "{{ s_one|int + i_two }}"
+
+- assert:
+ that:
+ - 'integer_sum2 == 3'
+ - 'integer_sum2|type_debug == "int"'
+
+- name: concatenate int and string
+ set_fact:
+ string_sum: "'{{ [i_one, s_two]|join('') }}'"
+
+- assert:
+ that:
+ - string_sum == "'12'"
+ - 'string_sum|type_debug in ["str", "unicode"]'
+
+- name: add two lists
+ set_fact:
+ list_sum: "{{ list_one + list_two }}"
+
+- assert:
+ that:
+ - 'list_sum == ["one", "two", "three", "four"]'
+ - 'list_sum|type_debug == "list"'
+
+- name: add two lists, multi expression
+ set_fact:
+ list_sum_multi: "{{ list_one }} + {{ list_two }}"
+
+- assert:
+ that:
+ - 'list_sum_multi|type_debug in ["str", "unicode"]'
+
+- name: add two dicts
+ set_fact:
+ dict_sum: "{{ dict_one + dict_two }}"
+ ignore_errors: yes
+
+- assert:
+ that:
+ - 'dict_sum is undefined'
+
+- name: loop through list with strings
+ set_fact:
+ list_for_strings: "{% for x in list_one %}{{ x }}{% endfor %}"
+
+- assert:
+ that:
+ - 'list_for_strings == "onetwo"'
+ - 'list_for_strings|type_debug in ["str", "unicode"]'
+
+- name: loop through list with int
+ set_fact:
+ list_for_int: "{% for x in list_one_int %}{{ x }}{% endfor %}"
+
+- assert:
+ that:
+ - 'list_for_int == 1'
+ - 'list_for_int|type_debug == "int"'
+
+- name: loop through list with ints
+ set_fact:
+ list_for_ints: "{% for x in list_ints %}{{ x }}{% endfor %}"
+
+- assert:
+ that:
+ - 'list_for_ints == 42'
+ - 'list_for_ints|type_debug == "int"'
+
+- name: loop through list to create a new list
+ set_fact:
+ list_from_list: "[{% for x in list_ints %}{{ x }},{% endfor %}]"
+
+- assert:
+ that:
+ - 'list_from_list == [4, 2]'
+ - 'list_from_list|type_debug == "list"'
diff --git a/test/integration/targets/jinja2_native_types/test_dunder.yml b/test/integration/targets/jinja2_native_types/test_dunder.yml
new file mode 100644
index 0000000..df5ea92
--- /dev/null
+++ b/test/integration/targets/jinja2_native_types/test_dunder.yml
@@ -0,0 +1,23 @@
+- name: test variable dunder
+ set_fact:
+ var_dunder: "{{ b_true.__class__ }}"
+
+- assert:
+ that:
+ - 'var_dunder|type_debug == "type"'
+
+- name: test constant dunder
+ set_fact:
+ const_dunder: "{{ true.__class__ }}"
+
+- assert:
+ that:
+ - 'const_dunder|type_debug == "type"'
+
+- name: test constant dunder to string
+ set_fact:
+ const_dunder: "{{ true.__class__|string }}"
+
+- assert:
+ that:
+ - 'const_dunder|type_debug in ["str", "unicode", "NativeJinjaText"]'
diff --git a/test/integration/targets/jinja2_native_types/test_hostvars.yml b/test/integration/targets/jinja2_native_types/test_hostvars.yml
new file mode 100644
index 0000000..ef0047b
--- /dev/null
+++ b/test/integration/targets/jinja2_native_types/test_hostvars.yml
@@ -0,0 +1,10 @@
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - name: Print vars
+ debug:
+ var: vars
+
+ - name: Print hostvars
+ debug:
+ var: hostvars
diff --git a/test/integration/targets/jinja2_native_types/test_none.yml b/test/integration/targets/jinja2_native_types/test_none.yml
new file mode 100644
index 0000000..1d26154
--- /dev/null
+++ b/test/integration/targets/jinja2_native_types/test_none.yml
@@ -0,0 +1,11 @@
+- name: test none
+ set_fact:
+ none_var: "{{ yaml_none }}"
+ none_var_direct: "{{ None }}"
+
+- assert:
+ that:
+ - 'none_var is sameas none'
+ - 'none_var|type_debug == "NoneType"'
+ - 'none_var_direct is sameas none'
+ - 'none_var_direct|type_debug == "NoneType"'
diff --git a/test/integration/targets/jinja2_native_types/test_preserving_quotes.yml b/test/integration/targets/jinja2_native_types/test_preserving_quotes.yml
new file mode 100644
index 0000000..d6fea10
--- /dev/null
+++ b/test/integration/targets/jinja2_native_types/test_preserving_quotes.yml
@@ -0,0 +1,14 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - assert:
+ that:
+ - quoted_str == '"hello"'
+ - empty_quoted_str == '""'
+ vars:
+ third_nested_lvl: '"hello"'
+ second_nested_lvl: "{{ third_nested_lvl }}"
+ first_nested_lvl: "{{ second_nested_lvl }}"
+ quoted_str: "{{ first_nested_lvl }}"
+ empty_quoted_str: "{{ empty_str }}"
+ empty_str: '""'
diff --git a/test/integration/targets/jinja2_native_types/test_template.yml b/test/integration/targets/jinja2_native_types/test_template.yml
new file mode 100644
index 0000000..0896ac1
--- /dev/null
+++ b/test/integration/targets/jinja2_native_types/test_template.yml
@@ -0,0 +1,27 @@
+- block:
+ - name: Template file with newlines
+ template:
+ src: test_template_newlines.j2
+ dest: test_template_newlines.res
+
+ - name: Dump template file
+ stat:
+ path: test_template_newlines.j2
+ get_checksum: yes
+ register: template_stat
+
+ - name: Dump result file
+ stat:
+ path: test_template_newlines.res
+ get_checksum: yes
+ register: result_stat
+
+ - name: Check that number of newlines from original template are preserved
+ assert:
+ that:
+ - template_stat.stat.checksum == result_stat.stat.checksum
+ always:
+ - name: Clean up
+ file:
+ path: test_template_newlines.res
+ state: absent
diff --git a/test/integration/targets/jinja2_native_types/test_template_newlines.j2 b/test/integration/targets/jinja2_native_types/test_template_newlines.j2
new file mode 100644
index 0000000..ca887ef
--- /dev/null
+++ b/test/integration/targets/jinja2_native_types/test_template_newlines.j2
@@ -0,0 +1,4 @@
+First line.
+
+
+
diff --git a/test/integration/targets/jinja2_native_types/test_types.yml b/test/integration/targets/jinja2_native_types/test_types.yml
new file mode 100644
index 0000000..f5659d4
--- /dev/null
+++ b/test/integration/targets/jinja2_native_types/test_types.yml
@@ -0,0 +1,20 @@
+- assert:
+ that:
+ - 'i_one|type_debug == "int"'
+ - 's_one|type_debug == "AnsibleUnicode"'
+ - 'dict_one|type_debug == "dict"'
+ - 'dict_one is mapping'
+ - 'list_one|type_debug == "list"'
+ - 'b_true|type_debug == "bool"'
+ - 's_true|type_debug == "AnsibleUnicode"'
+
+- set_fact:
+ a_list: "{{[i_one, s_two]}}"
+
+- assert:
+ that:
+ - 'a_list|type_debug == "list"'
+ - 'a_list[0] == 1'
+ - 'a_list[0]|type_debug == "int"'
+ - 'a_list[1] == "2"'
+ - 'a_list[1]|type_debug == "AnsibleUnicode"'
diff --git a/test/integration/targets/jinja2_native_types/test_vault.yml b/test/integration/targets/jinja2_native_types/test_vault.yml
new file mode 100644
index 0000000..2daa3c5
--- /dev/null
+++ b/test/integration/targets/jinja2_native_types/test_vault.yml
@@ -0,0 +1,16 @@
+- hosts: localhost
+ gather_facts: no
+ vars:
+ # ansible-vault encrypt_string root
+ # vault_password_file = test_vault_pass
+ vaulted_root_string: !vault |
+ $ANSIBLE_VAULT;1.1;AES256
+ 39333565666430306232343266346635373235626564396332323838613063646132653436303239
+ 3133363232306334393863343563366131373565616338380a666339383162333838653631663131
+ 36633637303862353435643930393664386365323164643831363332666435303436373365393162
+ 6535383134323539380a613663366631626534313837313565666665336164353362373431666366
+ 3464
+ tasks:
+ - name: make sure group root exists
+ group:
+ name: "{{ vaulted_root_string }}"
diff --git a/test/integration/targets/jinja2_native_types/test_vault_pass b/test/integration/targets/jinja2_native_types/test_vault_pass
new file mode 100644
index 0000000..9daeafb
--- /dev/null
+++ b/test/integration/targets/jinja2_native_types/test_vault_pass
@@ -0,0 +1 @@
+test
diff --git a/test/integration/targets/jinja_plugins/aliases b/test/integration/targets/jinja_plugins/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/jinja_plugins/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/filter/bad_collection_filter.py b/test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/filter/bad_collection_filter.py
new file mode 100644
index 0000000..3666953
--- /dev/null
+++ b/test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/filter/bad_collection_filter.py
@@ -0,0 +1,11 @@
+# Copyright (c) 2021 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+class FilterModule:
+ def filters(self):
+ raise TypeError('bad_collection_filter')
diff --git a/test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/filter/bad_collection_filter2.py b/test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/filter/bad_collection_filter2.py
new file mode 100644
index 0000000..96e726a
--- /dev/null
+++ b/test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/filter/bad_collection_filter2.py
@@ -0,0 +1,10 @@
+# Copyright (c) 2021 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+class FilterModule:
+ pass
diff --git a/test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/filter/good_collection_filter.py b/test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/filter/good_collection_filter.py
new file mode 100644
index 0000000..e2e7ffc
--- /dev/null
+++ b/test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/filter/good_collection_filter.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2021 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+class FilterModule:
+ def filters(self):
+ return {
+ 'hello': lambda x: 'Hello, %s!' % x,
+ }
diff --git a/test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/test/bad_collection_test.py b/test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/test/bad_collection_test.py
new file mode 100644
index 0000000..9fce558
--- /dev/null
+++ b/test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/test/bad_collection_test.py
@@ -0,0 +1,11 @@
+# Copyright (c) 2021 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+class TestModule:
+ def tests(self):
+ raise TypeError('bad_collection_test')
diff --git a/test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/test/good_collection_test.py b/test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/test/good_collection_test.py
new file mode 100644
index 0000000..a4ca2ff
--- /dev/null
+++ b/test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/test/good_collection_test.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2021 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+class TestModule:
+ def tests(self):
+ return {
+ 'world': lambda x: x.lower() == 'world',
+ }
diff --git a/test/integration/targets/jinja_plugins/filter_plugins/bad_filter.py b/test/integration/targets/jinja_plugins/filter_plugins/bad_filter.py
new file mode 100644
index 0000000..eebf39c
--- /dev/null
+++ b/test/integration/targets/jinja_plugins/filter_plugins/bad_filter.py
@@ -0,0 +1,11 @@
+# Copyright (c) 2021 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+class FilterModule:
+ def filters(self):
+ raise TypeError('bad_filter')
diff --git a/test/integration/targets/jinja_plugins/filter_plugins/good_filter.py b/test/integration/targets/jinja_plugins/filter_plugins/good_filter.py
new file mode 100644
index 0000000..e2e7ffc
--- /dev/null
+++ b/test/integration/targets/jinja_plugins/filter_plugins/good_filter.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2021 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+class FilterModule:
+ def filters(self):
+ return {
+ 'hello': lambda x: 'Hello, %s!' % x,
+ }
diff --git a/test/integration/targets/jinja_plugins/playbook.yml b/test/integration/targets/jinja_plugins/playbook.yml
new file mode 100644
index 0000000..789be65
--- /dev/null
+++ b/test/integration/targets/jinja_plugins/playbook.yml
@@ -0,0 +1,10 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - assert:
+ that:
+ - '"World"|hello == "Hello, World!"'
+ - '"World" is world'
+
+ - '"World"|foo.bar.hello == "Hello, World!"'
+ - '"World" is foo.bar.world'
diff --git a/test/integration/targets/jinja_plugins/tasks/main.yml b/test/integration/targets/jinja_plugins/tasks/main.yml
new file mode 100644
index 0000000..d3d6e2e
--- /dev/null
+++ b/test/integration/targets/jinja_plugins/tasks/main.yml
@@ -0,0 +1,23 @@
+- shell: ansible-playbook {{ verbosity }} playbook.yml
+ environment:
+ ANSIBLE_FORCE_COLOR: no
+ args:
+ chdir: '{{ role_path }}'
+ vars:
+ verbosity: "{{ '' if not ansible_verbosity else '-' ~ ('v' * ansible_verbosity) }}"
+ register: result
+
+- debug:
+ var: result
+
+- set_fact:
+ # NOTE: This will cram words together that were manually wrapped, which should be OK for this test.
+ stderr: "{{ result.stderr | replace('\n', '') }}"
+
+- assert:
+ that:
+ - '"[WARNING]: Skipping filter plugin" in stderr'
+ - '"[WARNING]: Skipping test plugin" in stderr'
+ - stderr|regex_findall('bad_collection_filter')|length == 3
+ - stderr|regex_findall('bad_collection_filter2')|length == 1
+ - stderr|regex_findall('bad_collection_test')|length == 2
diff --git a/test/integration/targets/jinja_plugins/test_plugins/bad_test.py b/test/integration/targets/jinja_plugins/test_plugins/bad_test.py
new file mode 100644
index 0000000..0cc7a5a
--- /dev/null
+++ b/test/integration/targets/jinja_plugins/test_plugins/bad_test.py
@@ -0,0 +1,11 @@
+# Copyright (c) 2021 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+class TestModule:
+ def tests(self):
+ raise TypeError('bad_test')
diff --git a/test/integration/targets/jinja_plugins/test_plugins/good_test.py b/test/integration/targets/jinja_plugins/test_plugins/good_test.py
new file mode 100644
index 0000000..a4ca2ff
--- /dev/null
+++ b/test/integration/targets/jinja_plugins/test_plugins/good_test.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2021 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+class TestModule:
+ def tests(self):
+ return {
+ 'world': lambda x: x.lower() == 'world',
+ }
diff --git a/test/integration/targets/json_cleanup/aliases b/test/integration/targets/json_cleanup/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/json_cleanup/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/json_cleanup/library/bad_json b/test/integration/targets/json_cleanup/library/bad_json
new file mode 100644
index 0000000..1df8c72
--- /dev/null
+++ b/test/integration/targets/json_cleanup/library/bad_json
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+set -eu
+
+echo 'this stuff should be ignored'
+
+echo '[ looks like a json list]'
+
+echo '{"changed": false, "failed": false, "msg": "good json response"}'
+
+echo 'moar garbage'
diff --git a/test/integration/targets/json_cleanup/module_output_cleaning.yml b/test/integration/targets/json_cleanup/module_output_cleaning.yml
new file mode 100644
index 0000000..165352a
--- /dev/null
+++ b/test/integration/targets/json_cleanup/module_output_cleaning.yml
@@ -0,0 +1,26 @@
+- name: ensure we clean module output well
+ hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: call module that spews extra stuff
+ bad_json:
+ register: clean_json
+ ignore_errors: true
+
+ - name: all expected is there
+ assert:
+ that:
+ - clean_json is success
+ - clean_json is not changed
+ - "clean_json['msg'] == 'good json response'"
+
+ - name: all non wanted is not there
+ assert:
+ that:
+ - item not in clean_json.values()
+ loop:
+ - this stuff should be ignored
+ - [ looks like a json list]
+ - '[ looks like a json list]'
+ - ' looks like a json list'
+ - moar garbage
diff --git a/test/integration/targets/json_cleanup/runme.sh b/test/integration/targets/json_cleanup/runme.sh
new file mode 100755
index 0000000..2de3bd0
--- /dev/null
+++ b/test/integration/targets/json_cleanup/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook module_output_cleaning.yml "$@"
diff --git a/test/integration/targets/keyword_inheritance/aliases b/test/integration/targets/keyword_inheritance/aliases
new file mode 100644
index 0000000..a6a3341
--- /dev/null
+++ b/test/integration/targets/keyword_inheritance/aliases
@@ -0,0 +1,3 @@
+shippable/posix/group4
+context/controller
+needs/target/setup_test_user
diff --git a/test/integration/targets/keyword_inheritance/roles/whoami/tasks/main.yml b/test/integration/targets/keyword_inheritance/roles/whoami/tasks/main.yml
new file mode 100644
index 0000000..80252c0
--- /dev/null
+++ b/test/integration/targets/keyword_inheritance/roles/whoami/tasks/main.yml
@@ -0,0 +1,3 @@
+- command: whoami
+ register: result
+ failed_when: result.stdout_lines|first != 'ansibletest0'
diff --git a/test/integration/targets/keyword_inheritance/runme.sh b/test/integration/targets/keyword_inheritance/runme.sh
new file mode 100755
index 0000000..6b78a06
--- /dev/null
+++ b/test/integration/targets/keyword_inheritance/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ANSIBLE_ROLES_PATH=../ ansible-playbook -i ../../inventory test.yml "$@"
diff --git a/test/integration/targets/keyword_inheritance/test.yml b/test/integration/targets/keyword_inheritance/test.yml
new file mode 100644
index 0000000..886f985
--- /dev/null
+++ b/test/integration/targets/keyword_inheritance/test.yml
@@ -0,0 +1,8 @@
+- hosts: testhost
+ gather_facts: false
+ become_user: ansibletest0
+ become: yes
+ roles:
+ - role: setup_test_user
+ become_user: root
+ - role: whoami
diff --git a/test/integration/targets/known_hosts/aliases b/test/integration/targets/known_hosts/aliases
new file mode 100644
index 0000000..765b70d
--- /dev/null
+++ b/test/integration/targets/known_hosts/aliases
@@ -0,0 +1 @@
+shippable/posix/group2
diff --git a/test/integration/targets/known_hosts/defaults/main.yml b/test/integration/targets/known_hosts/defaults/main.yml
new file mode 100644
index 0000000..b1b56ac
--- /dev/null
+++ b/test/integration/targets/known_hosts/defaults/main.yml
@@ -0,0 +1,6 @@
+---
+example_org_rsa_key: >
+ example.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAglyZmHHWskQ9wkh8LYbIqzvg99/oloneH7BaZ02ripJUy/2Zynv4tgUfm9fdXvAb1XXCEuTRnts9FBer87+voU0FPRgx3CfY9Sgr0FspUjnm4lqs53FIab1psddAaS7/F7lrnjl6VqBtPwMRQZG7qlml5uogGJwYJHxX0PGtsdoTJsM=
+
+example_org_ed25519_key: >
+ example.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIzlnSq5ESxLgW0avvPk3j7zLV59hcAPkxrMNdnZMKP2 \ No newline at end of file
diff --git a/test/integration/targets/known_hosts/files/existing_known_hosts b/test/integration/targets/known_hosts/files/existing_known_hosts
new file mode 100644
index 0000000..2564f40
--- /dev/null
+++ b/test/integration/targets/known_hosts/files/existing_known_hosts
@@ -0,0 +1,5 @@
+example.com ssh-dss AAAAB3NzaC1kc3MAAACBALT8YHxZ59d8yX4oQNPbpdK9AMPRQGKFY9X13S2fp4UMPijiB3ETxU1bAyVTjTbsoag065naFt13aIVl+u0MDPfMuYgVJFEorAZkDlBixvT25zpKyQhI4CtHhZ9Y9YWug4xLqSaFUYEPO31Bie7k8xRfDwsHtzTRPp/0zRURwARHAAAAFQDLx2DZMm3cR8cZtbq4zdSvkXLh0wAAAIAalkQYziu2b5dDRQMiFpDLpPdbymyVhDMmRKnXwAB1+dhGyJLGvfe0xO+ibqGXMp1aZ1iC3a/vHTpYKDVqKIIpFg5r0fxAcAZkJR0aRC8RDxW/IclbIliETD71osIT8I47OFc7vAVCWP8JbV3ZYzR+i98WUkmZ4/ZUzsDl2gi7WAAAAIAsdTGwAo4Fs784TdP2tIHCqxAIz2k4tWmZyeRmXkH5K/P1o9XSh3RNxvFKK7BY6dQK+h9jLunMBs0SCzhMoTcXaJq331kmLJltjq5peo0PnLGnQz5pas0PD7p7gb+soklmHoVp7J2oMC/U4N1Rxr6g9sv8Rpsf1PTPDT3sEbze6A== root@freezer
+|1|d71/U7CbOH3Su+d2zxlbmiNfXtI=|g2YSPAVoK7bmg16FCOOPKTZe2BM= ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
+|1|L0TqxOhAVh6mLZ2lbHdTv3owun0=|vn0La5pbHNxin3XzQQdvaOulvVU= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCNLCAA/SjVF3jkmlAlkgh+GtZdgxtusHaK66fcA7XSgCpQOdri1dGmND6pQDGwsxiKMy4Ou1GB2DR4N0G9T5E8=
+|1|WPo7yAOdlQKLSuRatNJCmDoga0k=|D/QybGglKokWuEQUe9Okpy5uSh0= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCNLCAA/SjVF3jkmlAlkgh+GtZdgxtusHaK66fcA7XSgCpQOdri1dGmND6pQDGwsxiKMy4Ou1GB2DR4N0G9T5E8=
+# example.net ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM6OSqweGdPdQ/metQaf738AdN3P+itYp1AypOTgXkyj root@localhost
diff --git a/test/integration/targets/known_hosts/meta/main.yml b/test/integration/targets/known_hosts/meta/main.yml
new file mode 100644
index 0000000..cb6005d
--- /dev/null
+++ b/test/integration/targets/known_hosts/meta/main.yml
@@ -0,0 +1,3 @@
+dependencies:
+ - prepare_tests
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/known_hosts/tasks/main.yml b/test/integration/targets/known_hosts/tasks/main.yml
new file mode 100644
index 0000000..dc00ded
--- /dev/null
+++ b/test/integration/targets/known_hosts/tasks/main.yml
@@ -0,0 +1,409 @@
+# test code for the known_hosts module
+# (c) 2017, Marius Gedminas <marius@gedmin.as>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: copy an existing file in place
+ copy:
+ src: existing_known_hosts
+ dest: "{{ remote_tmp_dir }}/known_hosts"
+
+# test addition
+
+- name: add a new host in check mode
+ check_mode: yes
+ known_hosts:
+ name: example.org
+ key: "{{ example_org_rsa_key }}"
+ state: present
+ path: "{{remote_tmp_dir}}/known_hosts"
+ register: diff
+
+- name: assert that the diff looks as expected (the key was added at the end)
+ assert:
+ that:
+ - 'diff is changed'
+ - 'diff.diff.before_header == diff.diff.after_header == remote_tmp_dir|expanduser + "/known_hosts"'
+ - 'diff.diff.after.splitlines()[:-1] == diff.diff.before.splitlines()'
+ - 'diff.diff.after.splitlines()[-1] == example_org_rsa_key.strip()'
+
+- name: add a new host
+ known_hosts:
+ name: example.org
+ key: "{{ example_org_rsa_key }}"
+ state: present
+ path: "{{remote_tmp_dir}}/known_hosts"
+ register: result
+
+- name: get the file content
+ command: "cat {{remote_tmp_dir}}/known_hosts"
+ register: known_hosts
+
+- name: assert that the key was added and ordering preserved
+ assert:
+ that:
+ - 'result is changed'
+ - 'known_hosts.stdout_lines[0].startswith("example.com")'
+ - 'known_hosts.stdout_lines[4].startswith("# example.net")'
+ - 'known_hosts.stdout_lines[-1].strip() == example_org_rsa_key.strip()'
+
+# test idempotence of addition
+
+- name: add the same host in check mode
+ check_mode: yes
+ known_hosts:
+ name: example.org
+ key: "{{ example_org_rsa_key }}"
+ state: present
+ path: "{{remote_tmp_dir}}/known_hosts"
+ register: check
+
+- name: assert that no changes were expected
+ assert:
+ that:
+ - 'check is not changed'
+ - 'check.diff.before == check.diff.after'
+
+- name: add the same host
+ known_hosts:
+ name: example.org
+ key: "{{ example_org_rsa_key }}"
+ state: present
+ path: "{{remote_tmp_dir}}/known_hosts"
+ register: result
+
+- name: get the file content
+ command: "cat {{remote_tmp_dir}}/known_hosts"
+ register: known_hosts_v2
+
+- name: assert that no changes happened
+ assert:
+ that:
+ - 'result is not changed'
+ - 'result.diff.before == result.diff.after'
+ - 'known_hosts.stdout == known_hosts_v2.stdout'
+
+# https://github.com/ansible/ansible/issues/78598
+# test removing nonexistent host key when the other keys exist for the host
+- name: remove different key
+ known_hosts:
+ name: example.org
+ key: "{{ example_org_ed25519_key }}"
+ state: absent
+ path: "{{remote_tmp_dir}}/known_hosts"
+ register: result
+
+- name: remove nonexistent key with check mode
+ known_hosts:
+ name: example.org
+ key: "{{ example_org_ed25519_key }}"
+ state: absent
+ path: "{{remote_tmp_dir}}/known_hosts"
+ check_mode: yes
+ register: check_mode_result
+
+- name: get the file content
+ command: "cat {{remote_tmp_dir}}/known_hosts"
+ register: known_hosts_different_key_removal
+
+- name: assert that no changes happened
+ assert:
+ that:
+ - 'result is not changed'
+ - 'check_mode_result is not changed'
+ - 'result.diff.before == result.diff.after'
+ - 'known_hosts_v2.stdout == known_hosts_different_key_removal.stdout'
+
+# test removal
+
+- name: remove the host in check mode
+ check_mode: yes
+ known_hosts:
+ name: example.org
+ key: "{{ example_org_rsa_key }}"
+ state: absent
+ path: "{{remote_tmp_dir}}/known_hosts"
+ register: diff
+
+- name: assert that the diff looks as expected (the key was removed)
+ assert:
+ that:
+ - 'diff.diff.before_header == diff.diff.after_header == remote_tmp_dir|expanduser + "/known_hosts"'
+ - 'diff.diff.before.splitlines()[-1] == example_org_rsa_key.strip()'
+ - 'diff.diff.after.splitlines() == diff.diff.before.splitlines()[:-1]'
+
+- name: remove the host
+ known_hosts:
+ name: example.org
+ key: "{{ example_org_rsa_key }}"
+ state: absent
+ path: "{{remote_tmp_dir}}/known_hosts"
+ register: result
+
+- name: get the file content
+ command: "cat {{remote_tmp_dir}}/known_hosts"
+ register: known_hosts_v3
+
+- name: assert that the key was removed and ordering preserved
+ assert:
+ that:
+ - 'diff is changed'
+ - 'result is changed'
+ - '"example.org" not in known_hosts_v3.stdout'
+ - 'known_hosts_v3.stdout_lines[0].startswith("example.com")'
+ - 'known_hosts_v3.stdout_lines[-1].startswith("# example.net")'
+
+# test idempotence of removal
+
+- name: remove the same host in check mode
+ check_mode: yes
+ known_hosts:
+ name: example.org
+ key: "{{ example_org_rsa_key }}"
+ state: absent
+ path: "{{remote_tmp_dir}}/known_hosts"
+ register: check
+
+- name: assert that no changes were expected
+ assert:
+ that:
+ - 'check is not changed'
+ - 'check.diff.before == check.diff.after'
+
+- name: remove the same host
+ known_hosts:
+ name: example.org
+ key: "{{ example_org_rsa_key }}"
+ state: absent
+ path: "{{remote_tmp_dir}}/known_hosts"
+ register: result
+
+- name: get the file content
+ command: "cat {{remote_tmp_dir}}/known_hosts"
+ register: known_hosts_v4
+
+- name: assert that no changes happened
+ assert:
+ that:
+ - 'result is not changed'
+ - 'result.diff.before == result.diff.after'
+ - 'known_hosts_v3.stdout == known_hosts_v4.stdout'
+
+# test addition as hashed_host
+
+- name: add a new hashed host
+ known_hosts:
+ name: example.org
+ key: "{{ example_org_rsa_key }}"
+ state: present
+ path: "{{remote_tmp_dir}}/known_hosts"
+ hash_host: yes
+ register: result
+
+- name: get the file content
+ command: "cat {{remote_tmp_dir}}/known_hosts"
+ register: known_hosts_v5
+
+- name: assert that the key was added and ordering preserved
+ assert:
+ that:
+ - 'result is changed'
+ - 'known_hosts_v5.stdout_lines[0].startswith("example.com")'
+ - 'known_hosts_v5.stdout_lines[4].startswith("# example.net")'
+ - 'known_hosts_v5.stdout_lines[-1].strip().startswith("|1|")'
+ - 'known_hosts_v5.stdout_lines[-1].strip().endswith(example_org_rsa_key.strip().split()[-1])'
+
+# test idempotence of hashed addition
+
+- name: add the same host hashed
+ known_hosts:
+ name: example.org
+ key: "{{ example_org_rsa_key }}"
+ state: present
+ path: "{{remote_tmp_dir}}/known_hosts"
+ hash_host: yes
+ register: result
+
+- name: get the file content
+ command: "cat {{remote_tmp_dir}}/known_hosts"
+ register: known_hosts_v6
+
+- name: assert that no changes happened
+ assert:
+ that:
+ - 'result is not changed'
+ - 'result.diff.before == result.diff.after'
+ - 'known_hosts_v5.stdout == known_hosts_v6.stdout'
+
+# test hashed removal
+
+- name: remove the hashed host
+ known_hosts:
+ name: example.org
+ key: "{{ example_org_rsa_key }}"
+ state: absent
+ path: "{{remote_tmp_dir}}/known_hosts"
+ register: result
+
+- name: get the file content
+ command: "cat {{remote_tmp_dir}}/known_hosts"
+ register: known_hosts_v7
+
+- name: assert that the key was removed and ordering preserved
+ assert:
+ that:
+ - 'result is changed'
+ - 'example_org_rsa_key.strip().split()[-1] not in known_hosts_v7.stdout'
+ - 'known_hosts_v7.stdout_lines[0].startswith("example.com")'
+ - 'known_hosts_v7.stdout_lines[-1].startswith("# example.net")'
+
+# test idempotence of removal
+
+- name: remove the same hashed host
+ known_hosts:
+ name: example.org
+ key: "{{ example_org_rsa_key }}"
+ state: absent
+ path: "{{remote_tmp_dir}}/known_hosts"
+ register: result
+
+- name: get the file content
+ command: "cat {{remote_tmp_dir}}/known_hosts"
+ register: known_hosts_v8
+
+- name: assert that no changes happened
+ assert:
+ that:
+ - 'result is not changed'
+ - 'result.diff.before == result.diff.after'
+ - 'known_hosts_v7.stdout == known_hosts_v8.stdout'
+
+# test roundtrip plaintext => hashed => plaintext
+# The assertions are rather relaxed, because most of this hash been tested previously
+
+- name: add a new host
+ known_hosts:
+ name: example.org
+ key: "{{ example_org_rsa_key }}"
+ state: present
+ path: "{{remote_tmp_dir}}/known_hosts"
+
+- name: get the file content
+ command: "cat {{remote_tmp_dir}}/known_hosts"
+ register: known_hosts_v8
+
+- name: assert the plaintext host is there
+ assert:
+ that:
+ - 'known_hosts_v8.stdout_lines[-1].strip() == example_org_rsa_key.strip()'
+
+- name: update the host to hashed mode
+ known_hosts:
+ name: example.org
+ key: "{{ example_org_rsa_key }}"
+ state: present
+ path: "{{remote_tmp_dir}}/known_hosts"
+ hash_host: true
+
+- name: get the file content
+ command: "cat {{remote_tmp_dir}}/known_hosts"
+ register: known_hosts_v9
+
+- name: assert the hashed host is there
+ assert:
+ that:
+ - 'known_hosts_v9.stdout_lines[-1].strip().startswith("|1|")'
+ - 'known_hosts_v9.stdout_lines[-1].strip().endswith(example_org_rsa_key.strip().split()[-1])'
+
+- name: downgrade the host to plaintext mode
+ known_hosts:
+ name: example.org
+ key: "{{ example_org_rsa_key }}"
+ state: present
+ path: "{{remote_tmp_dir}}/known_hosts"
+
+- name: get the file content
+ command: "cat {{remote_tmp_dir}}/known_hosts"
+ register: known_hosts_v10
+
+- name: assert the plaintext host is there
+ assert:
+ that:
+ - 'known_hosts_v10.stdout_lines[5].strip() == example_org_rsa_key.strip()'
+
+# ... and remove the host again for the next test
+
+- name: copy an existing file in place
+ copy:
+ src: existing_known_hosts
+ dest: "{{ remote_tmp_dir }}/known_hosts"
+
+# Test key changes
+
+- name: add a hashed host
+ known_hosts:
+ name: example.org
+ key: "{{ example_org_rsa_key }}"
+ state: present
+ path: "{{remote_tmp_dir}}/known_hosts"
+ hash_host: true
+
+- name: change the key of a hashed host
+ known_hosts:
+ name: example.org
+ key: "{{ example_org_rsa_key.strip()[:-7] + 'RANDOM=' }}"
+ state: present
+ path: "{{remote_tmp_dir}}/known_hosts"
+ hash_host: true
+
+- name: get the file content
+ command: "cat {{remote_tmp_dir}}/known_hosts"
+ register: known_hosts_v11
+
+- name: assert the change took place and the key got modified
+ assert:
+ that:
+ - 'known_hosts_v11.stdout_lines[-1].strip().endswith("RANDOM=")'
+
+# test errors
+
+- name: Try using a comma separated list of hosts
+ known_hosts:
+ name: example.org,acme.com
+ key: "{{ example_org_rsa_key }}"
+ path: "{{remote_tmp_dir}}/known_hosts"
+ ignore_errors: yes
+ register: result
+
+- name: Assert that error message was displayed
+ assert:
+ that:
+ - result is failed
+ - result.msg == 'Comma separated list of names is not supported. Please pass a single name to lookup in the known_hosts file.'
+
+- name: Try using a name that does not match the key
+ known_hosts:
+ name: example.com
+ key: "{{ example_org_rsa_key }}"
+ path: "{{remote_tmp_dir}}/known_hosts"
+ ignore_errors: yes
+ register: result
+
+- name: Assert that name checking failed with error message
+ assert:
+ that:
+ - result is failed
+ - result.msg == 'Host parameter does not match hashed host field in supplied key'
diff --git a/test/integration/targets/limit_inventory/aliases b/test/integration/targets/limit_inventory/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/limit_inventory/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/limit_inventory/hosts.yml b/test/integration/targets/limit_inventory/hosts.yml
new file mode 100644
index 0000000..2e1b192
--- /dev/null
+++ b/test/integration/targets/limit_inventory/hosts.yml
@@ -0,0 +1,5 @@
+all:
+ hosts:
+ host1:
+ host2:
+ host3:
diff --git a/test/integration/targets/limit_inventory/runme.sh b/test/integration/targets/limit_inventory/runme.sh
new file mode 100755
index 0000000..6a142b3
--- /dev/null
+++ b/test/integration/targets/limit_inventory/runme.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+
+set -eux
+
+trap 'echo "Host pattern limit test failed"' ERR
+
+# https://github.com/ansible/ansible/issues/61964
+
+# These tests should return all hosts
+ansible -i hosts.yml all --limit ,, --list-hosts | tee out ; grep -q 'hosts (3)' out
+ansible -i hosts.yml ,, --list-hosts | tee out ; grep -q 'hosts (3)' out
+ansible -i hosts.yml , --list-hosts | tee out ; grep -q 'hosts (3)' out
+ansible -i hosts.yml all --limit , --list-hosts | tee out ; grep -q 'hosts (3)' out
+ansible -i hosts.yml all --limit '' --list-hosts | tee out ; grep -q 'hosts (3)' out
+
+
+# Only one host
+ansible -i hosts.yml all --limit ,,host1 --list-hosts | tee out ; grep -q 'hosts (1)' out
+ansible -i hosts.yml ,,host1 --list-hosts | tee out ; grep -q 'hosts (1)' out
+
+ansible -i hosts.yml all --limit host1,, --list-hosts | tee out ; grep -q 'hosts (1)' out
+ansible -i hosts.yml host1,, --list-hosts | tee out ; grep -q 'hosts (1)' out
+
+
+# Only two hosts
+ansible -i hosts.yml all --limit host1,,host3 --list-hosts | tee out ; grep -q 'hosts (2)' out
+ansible -i hosts.yml host1,,host3 --list-hosts | tee out ; grep -q 'hosts (2)' out
+
+ansible -i hosts.yml all --limit 'host1, , ,host3' --list-hosts | tee out ; grep -q 'hosts (2)' out
+ansible -i hosts.yml 'host1, , ,host3' --list-hosts | tee out ; grep -q 'hosts (2)' out
+
diff --git a/test/integration/targets/lineinfile/aliases b/test/integration/targets/lineinfile/aliases
new file mode 100644
index 0000000..765b70d
--- /dev/null
+++ b/test/integration/targets/lineinfile/aliases
@@ -0,0 +1 @@
+shippable/posix/group2
diff --git a/test/integration/targets/lineinfile/files/firstmatch.txt b/test/integration/targets/lineinfile/files/firstmatch.txt
new file mode 100644
index 0000000..347132c
--- /dev/null
+++ b/test/integration/targets/lineinfile/files/firstmatch.txt
@@ -0,0 +1,5 @@
+line1
+line1
+line1
+line2
+line3
diff --git a/test/integration/targets/lineinfile/files/test.conf b/test/integration/targets/lineinfile/files/test.conf
new file mode 100644
index 0000000..15404cd
--- /dev/null
+++ b/test/integration/targets/lineinfile/files/test.conf
@@ -0,0 +1,5 @@
+[section_one]
+
+[section_two]
+
+[section_three]
diff --git a/test/integration/targets/lineinfile/files/test.txt b/test/integration/targets/lineinfile/files/test.txt
new file mode 100644
index 0000000..8187db9
--- /dev/null
+++ b/test/integration/targets/lineinfile/files/test.txt
@@ -0,0 +1,5 @@
+This is line 1
+This is line 2
+REF this is a line for backrefs REF
+This is line 4
+This is line 5
diff --git a/test/integration/targets/lineinfile/files/test_58923.txt b/test/integration/targets/lineinfile/files/test_58923.txt
new file mode 100644
index 0000000..34579fd
--- /dev/null
+++ b/test/integration/targets/lineinfile/files/test_58923.txt
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+case "`uname`" in
+ Darwin*) if [ -z "$JAVA_HOME" ] ; then
diff --git a/test/integration/targets/lineinfile/files/testempty.txt b/test/integration/targets/lineinfile/files/testempty.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/lineinfile/files/testempty.txt
diff --git a/test/integration/targets/lineinfile/files/testmultiple.txt b/test/integration/targets/lineinfile/files/testmultiple.txt
new file mode 100644
index 0000000..fb57082
--- /dev/null
+++ b/test/integration/targets/lineinfile/files/testmultiple.txt
@@ -0,0 +1,7 @@
+This is line 1
+
+This is line 2
+
+This is line 3
+
+This is line 4
diff --git a/test/integration/targets/lineinfile/files/testnoeof.txt b/test/integration/targets/lineinfile/files/testnoeof.txt
new file mode 100644
index 0000000..152780b
--- /dev/null
+++ b/test/integration/targets/lineinfile/files/testnoeof.txt
@@ -0,0 +1,2 @@
+This is line 1
+This is line 2 \ No newline at end of file
diff --git a/test/integration/targets/lineinfile/files/teststring.conf b/test/integration/targets/lineinfile/files/teststring.conf
new file mode 100644
index 0000000..15404cd
--- /dev/null
+++ b/test/integration/targets/lineinfile/files/teststring.conf
@@ -0,0 +1,5 @@
+[section_one]
+
+[section_two]
+
+[section_three]
diff --git a/test/integration/targets/lineinfile/files/teststring.txt b/test/integration/targets/lineinfile/files/teststring.txt
new file mode 100644
index 0000000..b204494
--- /dev/null
+++ b/test/integration/targets/lineinfile/files/teststring.txt
@@ -0,0 +1,5 @@
+This is line 1
+This is line 2
+(\\w)(\\s+)([\\.,])
+This is line 4
+<FilesMatch ".py[45]?$">
diff --git a/test/integration/targets/lineinfile/files/teststring_58923.txt b/test/integration/targets/lineinfile/files/teststring_58923.txt
new file mode 100644
index 0000000..34579fd
--- /dev/null
+++ b/test/integration/targets/lineinfile/files/teststring_58923.txt
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+case "`uname`" in
+ Darwin*) if [ -z "$JAVA_HOME" ] ; then
diff --git a/test/integration/targets/lineinfile/meta/main.yml b/test/integration/targets/lineinfile/meta/main.yml
new file mode 100644
index 0000000..a91e684
--- /dev/null
+++ b/test/integration/targets/lineinfile/meta/main.yml
@@ -0,0 +1,21 @@
+# test code for the lineinfile module
+# (c) 2014, James Cammarata <jcammarata@ansible.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+dependencies:
+ - prepare_tests
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/lineinfile/tasks/main.yml b/test/integration/targets/lineinfile/tasks/main.yml
new file mode 100644
index 0000000..3d4678c
--- /dev/null
+++ b/test/integration/targets/lineinfile/tasks/main.yml
@@ -0,0 +1,1399 @@
+# test code for the lineinfile module
+# (c) 2014, James Cammarata <jcammarata@ansible.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: deploy the test file for lineinfile
+ copy:
+ src: test.txt
+ dest: "{{ remote_tmp_dir }}/test.txt"
+ register: result
+
+- name: assert that the test file was deployed
+ assert:
+ that:
+ - result is changed
+ - "result.checksum == '5feac65e442c91f557fc90069ce6efc4d346ab51'"
+ - "result.state == 'file'"
+
+- name: "create a file that does not yet exist with `create: yes` and produce diff"
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/a/a.txt"
+ state: present
+ line: "First line"
+ create: yes
+ diff: yes
+ register: result1
+
+- name: assert that a diff was returned
+ assert:
+ that:
+ - result1.diff | length > 0
+
+- name: stat the new file
+ stat:
+ path: "{{ remote_tmp_dir }}/a/a.txt"
+ register: result
+
+- name: assert that the file exists
+ assert:
+ that:
+ - result.stat.exists
+
+- block:
+ - name: "EXPECTED FAILURE - test source file does not exist w/o `create: yes`"
+ lineinfile:
+ path: "/some/where/that/doesnotexist.txt"
+ state: present
+ line: "Doesn't matter"
+ - fail:
+ msg: "Should not get here"
+ rescue:
+ - name: Validate failure
+ assert:
+ that:
+ - "'Destination /some/where/that/doesnotexist.txt does not exist !' in ansible_failed_result.msg"
+
+- block:
+ - name: EXPECTED FAILURE - test invalid `validate` value
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/test.txt"
+ state: present
+ line: "Doesn't matter"
+ validate: '/some/path'
+ - fail:
+ msg: "Should not get here"
+ rescue:
+ - name: Validate failure
+ assert:
+ that:
+ - "'validate must contain %s: /some/path' in ansible_failed_result.msg"
+
+- name: insert a line at the beginning of the file, and back it up
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/test.txt"
+ state: present
+ line: "New line at the beginning"
+ insertbefore: "BOF"
+ backup: yes
+ register: result1
+
+- name: insert a line at the beginning of the file again
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/test.txt"
+ state: present
+ line: "New line at the beginning"
+ insertbefore: "BOF"
+ register: result2
+
+- name: assert that the line was inserted at the head of the file
+ assert:
+ that:
+ - result1 is changed
+ - result2 is not changed
+ - result1.msg == 'line added'
+ - result1.backup != ''
+
+- name: stat the backup file
+ stat:
+ path: "{{ result1.backup }}"
+ register: result
+
+- name: assert the backup file matches the previous hash
+ assert:
+ that:
+ - "result.stat.checksum == '5feac65e442c91f557fc90069ce6efc4d346ab51'"
+
+- name: stat the test after the insert at the head
+ stat:
+ path: "{{ remote_tmp_dir }}/test.txt"
+ register: result
+
+- name: assert test hash is what we expect for the file with the insert at the head
+ assert:
+ that:
+ - "result.stat.checksum == '7eade4042b23b800958fe807b5bfc29f8541ec09'"
+
+- name: insert a line at the end of the file
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/test.txt"
+ state: present
+ line: "New line at the end"
+ insertafter: "EOF"
+ register: result
+
+- name: assert that the line was inserted at the end of the file
+ assert:
+ that:
+ - result is changed
+ - "result.msg == 'line added'"
+
+- name: stat the test after the insert at the end
+ stat:
+ path: "{{ remote_tmp_dir }}/test.txt"
+ register: result
+
+- name: assert test checksum matches after the insert at the end
+ assert:
+ that:
+ - "result.stat.checksum == 'fb57af7dc10a1006061b000f1f04c38e4bef50a9'"
+
+- name: insert a line after the first line
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/test.txt"
+ state: present
+ line: "New line after line 1"
+ insertafter: "^This is line 1$"
+ register: result
+
+- name: assert that the line was inserted after the first line
+ assert:
+ that:
+ - result is changed
+ - "result.msg == 'line added'"
+
+- name: stat the test after insert after the first line
+ stat:
+ path: "{{ remote_tmp_dir }}/test.txt"
+ register: result
+
+- name: assert test checksum matches after the insert after the first line
+ assert:
+ that:
+ - "result.stat.checksum == '5348da605b1bc93dbadf3a16474cdf22ef975bec'"
+
+- name: insert a line before the last line
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/test.txt"
+ state: present
+ line: "New line before line 5"
+ insertbefore: "^This is line 5$"
+ register: result
+
+- name: assert that the line was inserted before the last line
+ assert:
+ that:
+ - result is changed
+ - "result.msg == 'line added'"
+
+- name: stat the test after the insert before the last line
+ stat:
+ path: "{{ remote_tmp_dir }}/test.txt"
+ register: result
+
+- name: assert test checksum matches after the insert before the last line
+ assert:
+ that:
+ - "result.stat.checksum == '2e9e460ff68929e4453eb765761fd99814f6e286'"
+
+- name: Replace a line with backrefs
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/test.txt"
+ state: present
+ line: "This is line 3"
+ backrefs: yes
+ regexp: "^(REF) .* \\1$"
+ register: backrefs_result1
+
+- name: Replace a line with backrefs again
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/test.txt"
+ state: present
+ line: "This is line 3"
+ backrefs: yes
+ regexp: "^(REF) .* \\1$"
+ register: backrefs_result2
+- command: cat {{ remote_tmp_dir }}/test.txt
+
+- name: assert that the line with backrefs was changed
+ assert:
+ that:
+ - backrefs_result1 is changed
+ - backrefs_result2 is not changed
+ - "backrefs_result1.msg == 'line replaced'"
+
+- name: stat the test after the backref line was replaced
+ stat:
+ path: "{{ remote_tmp_dir }}/test.txt"
+ register: result
+
+- name: assert test checksum matches after backref line was replaced
+ assert:
+ that:
+ - "result.stat.checksum == '72f60239a735ae06e769d823f5c2b4232c634d9c'"
+
+- name: remove the middle line
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/test.txt"
+ state: absent
+ regexp: "^This is line 3$"
+ register: result
+
+- name: assert that the line was removed
+ assert:
+ that:
+ - result is changed
+ - "result.msg == '1 line(s) removed'"
+
+- name: stat the test after the middle line was removed
+ stat:
+ path: "{{ remote_tmp_dir }}/test.txt"
+ register: result
+
+- name: assert test checksum matches after the middle line was removed
+ assert:
+ that:
+ - "result.stat.checksum == 'd4eeb07bdebab2d1cdb3ec4a3635afa2618ad4ea'"
+
+- name: run a validation script that succeeds
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/test.txt"
+ state: absent
+ regexp: "^This is line 5$"
+ validate: "true %s"
+ register: result
+
+- name: assert that the file validated after removing a line
+ assert:
+ that:
+ - result is changed
+ - "result.msg == '1 line(s) removed'"
+
+- name: stat the test after the validation succeeded
+ stat:
+ path: "{{ remote_tmp_dir }}/test.txt"
+ register: result
+
+- name: assert test checksum matches after the validation succeeded
+ assert:
+ that:
+ - "result.stat.checksum == 'ab56c210ea82839a54487464800fed4878cb2608'"
+
+- name: run a validation script that fails
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/test.txt"
+ state: absent
+ regexp: "^This is line 1$"
+ validate: "/bin/false %s"
+ register: result
+ ignore_errors: yes
+
+- name: assert that the validate failed
+ assert:
+ that:
+ - "result.failed == true"
+
+- name: stat the test after the validation failed
+ stat:
+ path: "{{ remote_tmp_dir }}/test.txt"
+ register: result
+
+- name: assert test checksum matches the previous after the validation failed
+ assert:
+ that:
+ - "result.stat.checksum == 'ab56c210ea82839a54487464800fed4878cb2608'"
+
+- import_tasks: test_string01.yml
+
+- name: use create=yes
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/new_test.txt"
+ create: yes
+ insertbefore: BOF
+ state: present
+ line: "This is a new file"
+ register: result
+
+- name: assert that the new file was created
+ assert:
+ that:
+ - result is changed
+ - "result.msg == 'line added'"
+
+- name: validate that the newly created file exists
+ stat:
+ path: "{{ remote_tmp_dir }}/new_test.txt"
+ register: result
+ ignore_errors: yes
+
+- name: assert the newly created test checksum matches
+ assert:
+ that:
+ - "result.stat.checksum == '038f10f9e31202451b093163e81e06fbac0c6f3a'"
+
+- name: Create a file without a path
+ lineinfile:
+ dest: file.txt
+ create: yes
+ line: Test line
+ register: create_no_path_test
+
+- name: Stat the file
+ stat:
+ path: file.txt
+ register: create_no_path_file
+
+- name: Ensure file was created
+ assert:
+ that:
+ - create_no_path_test is changed
+ - create_no_path_file.stat.exists
+
+# Test EOF in cases where file has no newline at EOF
+- name: testnoeof deploy the file for lineinfile
+ copy:
+ src: testnoeof.txt
+ dest: "{{ remote_tmp_dir }}/testnoeof.txt"
+ register: result
+
+- name: testnoeof insert a line at the end of the file
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/testnoeof.txt"
+ state: present
+ line: "New line at the end"
+ insertafter: "EOF"
+ register: result
+
+- name: testempty assert that the line was inserted at the end of the file
+ assert:
+ that:
+ - result is changed
+ - "result.msg == 'line added'"
+
+- name: insert a multiple lines at the end of the file
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/test.txt"
+ state: present
+ line: "This is a line\nwith \\n character"
+ insertafter: "EOF"
+ register: result
+
+- name: assert that the multiple lines was inserted
+ assert:
+ that:
+ - result is changed
+ - "result.msg == 'line added'"
+
+- name: testnoeof stat the no newline EOF test after the insert at the end
+ stat:
+ path: "{{ remote_tmp_dir }}/testnoeof.txt"
+ register: result
+
+- name: testnoeof assert test checksum matches after the insert at the end
+ assert:
+ that:
+ - "result.stat.checksum == 'f9af7008e3cb67575ce653d094c79cabebf6e523'"
+
+# Test EOF with empty file to make sure no unnecessary newline is added
+- name: testempty deploy the testempty file for lineinfile
+ copy:
+ src: testempty.txt
+ dest: "{{ remote_tmp_dir }}/testempty.txt"
+ register: result
+
+- name: testempty insert a line at the end of the file
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/testempty.txt"
+ state: present
+ line: "New line at the end"
+ insertafter: "EOF"
+ register: result
+
+- name: testempty assert that the line was inserted at the end of the file
+ assert:
+ that:
+ - result is changed
+ - "result.msg == 'line added'"
+
+- name: testempty stat the test after the insert at the end
+ stat:
+ path: "{{ remote_tmp_dir }}/testempty.txt"
+ register: result
+
+- name: testempty assert test checksum matches after the insert at the end
+ assert:
+ that:
+ - "result.stat.checksum == 'f440dc65ea9cec3fd496c1479ddf937e1b949412'"
+
+- stat:
+ path: "{{ remote_tmp_dir }}/test.txt"
+ register: result
+
+- name: assert test checksum matches after inserting multiple lines
+ assert:
+ that:
+ - "result.stat.checksum == 'fde683229429a4f05d670e6c10afc875e1d5c489'"
+
+- name: replace a line with backrefs included in the line
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/test.txt"
+ state: present
+ line: "New \\1 created with the backref"
+ backrefs: yes
+ regexp: "^This is (line 4)$"
+ register: result
+
+- name: assert that the line with backrefs was changed
+ assert:
+ that:
+ - result is changed
+ - "result.msg == 'line replaced'"
+
+- name: stat the test after the backref line was replaced
+ stat:
+ path: "{{ remote_tmp_dir }}/test.txt"
+ register: result
+
+- name: assert test checksum matches after backref line was replaced
+ assert:
+ that:
+ - "result.stat.checksum == '981ad35c4b30b03bc3a1beedce0d1e72c491898e'"
+
+###################################################################
+# issue 8535
+
+- name: create a new file for testing quoting issues
+ file:
+ dest: "{{ remote_tmp_dir }}/test_quoting.txt"
+ state: touch
+ register: result
+
+- name: assert the new file was created
+ assert:
+ that:
+ - result is changed
+
+- name: use with_items to add code-like strings to the quoting txt file
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/test_quoting.txt"
+ line: "{{ item }}"
+ insertbefore: BOF
+ with_items:
+ - "'foo'"
+ - "dotenv.load();"
+ - "var dotenv = require('dotenv');"
+ register: result
+
+- name: assert the quote test file was modified correctly
+ assert:
+ that:
+ - result.results|length == 3
+ - result.results[0] is changed
+ - result.results[0].item == "'foo'"
+ - result.results[1] is changed
+ - result.results[1].item == "dotenv.load();"
+ - result.results[2] is changed
+ - result.results[2].item == "var dotenv = require('dotenv');"
+
+- name: stat the quote test file
+ stat:
+ path: "{{ remote_tmp_dir }}/test_quoting.txt"
+ register: result
+
+- name: assert test checksum matches after backref line was replaced
+ assert:
+ that:
+ - "result.stat.checksum == '7dc3cb033c3971e73af0eaed6623d4e71e5743f1'"
+
+- name: insert a line into the quoted file with a single quote
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/test_quoting.txt"
+ line: "import g'"
+ register: result
+
+- name: assert that the quoted file was changed
+ assert:
+ that:
+ - result is changed
+
+- name: stat the quote test file
+ stat:
+ path: "{{ remote_tmp_dir }}/test_quoting.txt"
+ register: result
+
+- name: assert test checksum matches after backref line was replaced
+ assert:
+ that:
+ - "result.stat.checksum == '73b271c2cc1cef5663713bc0f00444b4bf9f4543'"
+
+- name: insert a line into the quoted file with many double quotation strings
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/test_quoting.txt"
+ line: "\"quote\" and \"unquote\""
+ register: result
+
+- name: assert that the quoted file was changed
+ assert:
+ that:
+ - result is changed
+
+- name: stat the quote test file
+ stat:
+ path: "{{ remote_tmp_dir }}/test_quoting.txt"
+ register: result
+
+- name: assert test checksum matches after backref line was replaced
+ assert:
+ that:
+ - "result.stat.checksum == 'b10ab2a3c3b6492680c8d0b1d6f35aa6b8f9e731'"
+
+###################################################################
+# Issue 28721
+
+- name: Deploy the testmultiple file
+ copy:
+ src: testmultiple.txt
+ dest: "{{ remote_tmp_dir }}/testmultiple.txt"
+ register: result
+
+- name: Assert that the testmultiple file was deployed
+ assert:
+ that:
+ - result is changed
+ - result.checksum == '3e0090a34fb641f3c01e9011546ff586260ea0ea'
+ - result.state == 'file'
+
+# Test insertafter
+- name: Write the same line to a file inserted after different lines
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/testmultiple.txt"
+ insertafter: "{{ item.regex }}"
+ line: "{{ item.replace }}"
+ register: _multitest_1
+ with_items: "{{ test_regexp }}"
+
+- name: Assert that the line is added once only
+ assert:
+ that:
+ - _multitest_1.results.0 is changed
+ - _multitest_1.results.1 is not changed
+ - _multitest_1.results.2 is not changed
+ - _multitest_1.results.3 is not changed
+
+- name: Do the same thing again to check for changes
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/testmultiple.txt"
+ insertafter: "{{ item.regex }}"
+ line: "{{ item.replace }}"
+ register: _multitest_2
+ with_items: "{{ test_regexp }}"
+
+- name: Assert that the line is not added anymore
+ assert:
+ that:
+ - _multitest_2.results.0 is not changed
+ - _multitest_2.results.1 is not changed
+ - _multitest_2.results.2 is not changed
+ - _multitest_2.results.3 is not changed
+
+- name: Stat the insertafter file
+ stat:
+ path: "{{ remote_tmp_dir }}/testmultiple.txt"
+ register: result
+
+- name: Assert that the insertafter file matches expected checksum
+ assert:
+ that:
+ - result.stat.checksum == 'c6733b6c53ddd0e11e6ba39daa556ef8f4840761'
+
+# Test insertbefore
+
+- name: Deploy the testmultiple file
+ copy:
+ src: testmultiple.txt
+ dest: "{{ remote_tmp_dir }}/testmultiple.txt"
+ register: result
+
+- name: Assert that the testmultiple file was deployed
+ assert:
+ that:
+ - result is changed
+ - result.checksum == '3e0090a34fb641f3c01e9011546ff586260ea0ea'
+ - result.state == 'file'
+
+- name: Write the same line to a file inserted before different lines
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/testmultiple.txt"
+ insertbefore: "{{ item.regex }}"
+ line: "{{ item.replace }}"
+ register: _multitest_3
+ with_items: "{{ test_regexp }}"
+
+- name: Assert that the line is added once only
+ assert:
+ that:
+ - _multitest_3.results.0 is changed
+ - _multitest_3.results.1 is not changed
+ - _multitest_3.results.2 is not changed
+ - _multitest_3.results.3 is not changed
+
+- name: Do the same thing again to check for changes
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/testmultiple.txt"
+ insertbefore: "{{ item.regex }}"
+ line: "{{ item.replace }}"
+ register: _multitest_4
+ with_items: "{{ test_regexp }}"
+
+- name: Assert that the line is not added anymore
+ assert:
+ that:
+ - _multitest_4.results.0 is not changed
+ - _multitest_4.results.1 is not changed
+ - _multitest_4.results.2 is not changed
+ - _multitest_4.results.3 is not changed
+
+- name: Stat the insertbefore file
+ stat:
+ path: "{{ remote_tmp_dir }}/testmultiple.txt"
+ register: result
+
+- name: Assert that the insertbefore file matches expected checksum
+ assert:
+ that:
+ - result.stat.checksum == '5d298651fbc377b45257da10308a9dc2fe1f8be5'
+
+###################################################################
+# Issue 36156
+# Test insertbefore and insertafter with regexp
+
+- name: Deploy the test.conf file
+ copy:
+ src: test.conf
+ dest: "{{ remote_tmp_dir }}/test.conf"
+ register: result
+
+- name: Assert that the test.conf file was deployed
+ assert:
+ that:
+ - result is changed
+ - result.checksum == '6037f13e419b132eb3fd20a89e60c6c87a6add38'
+ - result.state == 'file'
+
+# Test instertafter
+- name: Insert lines after with regexp
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/test.conf"
+ regexp: "{{ item.regexp }}"
+ line: "{{ item.line }}"
+ insertafter: "{{ item.after }}"
+ with_items: "{{ test_befaf_regexp }}"
+ register: _multitest_5
+
+- name: Do the same thing again and check for changes
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/test.conf"
+ regexp: "{{ item.regexp }}"
+ line: "{{ item.line }}"
+ insertafter: "{{ item.after }}"
+ with_items: "{{ test_befaf_regexp }}"
+ register: _multitest_6
+
+- name: Assert that the file was changed the first time but not the second time
+ assert:
+ that:
+ - item.0 is changed
+ - item.1 is not changed
+ with_together:
+ - "{{ _multitest_5.results }}"
+ - "{{ _multitest_6.results }}"
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir }}/test.conf"
+ register: result
+
+- name: Assert that the file contents match what is expected
+ assert:
+ that:
+ - result.stat.checksum == '06e2c456e5028dd7bcd0b117b5927a1139458c82'
+
+- name: Do the same thing a third time without regexp and check for changes
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/test.conf"
+ line: "{{ item.line }}"
+ insertafter: "{{ item.after }}"
+ with_items: "{{ test_befaf_regexp }}"
+ register: _multitest_7
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir }}/test.conf"
+ register: result
+
+- name: Assert that the file was changed when no regexp was provided
+ assert:
+ that:
+ - item is not changed
+ with_items: "{{ _multitest_7.results }}"
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir }}/test.conf"
+ register: result
+
+- name: Assert that the file contents match what is expected
+ assert:
+ that:
+ - result.stat.checksum == '06e2c456e5028dd7bcd0b117b5927a1139458c82'
+
+# Test insertbefore
+- name: Deploy the test.conf file
+ copy:
+ src: test.conf
+ dest: "{{ remote_tmp_dir }}/test.conf"
+ register: result
+
+- name: Assert that the test.conf file was deployed
+ assert:
+ that:
+ - result is changed
+ - result.checksum == '6037f13e419b132eb3fd20a89e60c6c87a6add38'
+ - result.state == 'file'
+
+- name: Insert lines before with regexp
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/test.conf"
+ regexp: "{{ item.regexp }}"
+ line: "{{ item.line }}"
+ insertbefore: "{{ item.before }}"
+ with_items: "{{ test_befaf_regexp }}"
+ register: _multitest_8
+
+- name: Do the same thing again and check for changes
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/test.conf"
+ regexp: "{{ item.regexp }}"
+ line: "{{ item.line }}"
+ insertbefore: "{{ item.before }}"
+ with_items: "{{ test_befaf_regexp }}"
+ register: _multitest_9
+
+- name: Assert that the file was changed the first time but not the second time
+ assert:
+ that:
+ - item.0 is changed
+ - item.1 is not changed
+ with_together:
+ - "{{ _multitest_8.results }}"
+ - "{{ _multitest_9.results }}"
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir }}/test.conf"
+ register: result
+
+- name: Assert that the file contents match what is expected
+ assert:
+ that:
+ - result.stat.checksum == 'c3be9438a07c44d4c256cebfcdbca15a15b1db91'
+
+- name: Do the same thing a third time without regexp and check for changes
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/test.conf"
+ line: "{{ item.line }}"
+ insertbefore: "{{ item.before }}"
+ with_items: "{{ test_befaf_regexp }}"
+ register: _multitest_10
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir }}/test.conf"
+ register: result
+
+- name: Assert that the file was changed when no regexp was provided
+ assert:
+ that:
+ - item is not changed
+ with_items: "{{ _multitest_10.results }}"
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir }}/test.conf"
+ register: result
+
+- name: Assert that the file contents match what is expected
+ assert:
+ that:
+ - result.stat.checksum == 'c3be9438a07c44d4c256cebfcdbca15a15b1db91'
+
+- name: Copy empty file to test with insertbefore
+ copy:
+ src: testempty.txt
+ dest: "{{ remote_tmp_dir }}/testempty.txt"
+
+- name: Add a line to empty file with insertbefore
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/testempty.txt"
+ line: top
+ insertbefore: '^not in the file$'
+ register: oneline_insbefore_test1
+
+- name: Add a line to file with only one line using insertbefore
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/testempty.txt"
+ line: top
+ insertbefore: '^not in the file$'
+ register: oneline_insbefore_test2
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir }}/testempty.txt"
+ register: oneline_insbefore_file
+
+- name: Assert that insertebefore worked properly with a one line file
+ assert:
+ that:
+ - oneline_insbefore_test1 is changed
+ - oneline_insbefore_test2 is not changed
+ - oneline_insbefore_file.stat.checksum == '4dca56d05a21f0d018cd311f43e134e4501cf6d9'
+
+- import_tasks: test_string02.yml
+
+# Issue 29443
+# When using an empty regexp, replace the last line (since it matches every line)
+# but also provide a warning.
+
+- name: Deploy the test file for lineinfile
+ copy:
+ src: test.txt
+ dest: "{{ remote_tmp_dir }}/test.txt"
+ register: result
+
+- name: Assert that the test file was deployed
+ assert:
+ that:
+ - result is changed
+ - result.checksum == '5feac65e442c91f557fc90069ce6efc4d346ab51'
+ - result.state == 'file'
+
+- name: Insert a line in the file using an empty string as a regular expression
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/test.txt"
+ regexp: ''
+ line: This is line 6
+ register: insert_empty_regexp
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir }}/test.txt"
+ register: result
+
+- name: Assert that the file contents match what is expected and a warning was displayed
+ assert:
+ that:
+ - insert_empty_regexp is changed
+ - warning_message in insert_empty_regexp.warnings
+ - result.stat.checksum == '23555a98ceaa88756b4c7c7bba49d9f86eed868f'
+ vars:
+ warning_message: >-
+ The regular expression is an empty string, which will match every line in the file.
+ This may have unintended consequences, such as replacing the last line in the file rather than appending.
+ If this is desired, use '^' to match every line in the file and avoid this warning.
+
+###################################################################
+# When using an empty search string, replace the last line (since it matches every line)
+# but also provide a warning.
+
+- name: Deploy the test file for lineinfile
+ copy:
+ src: teststring.txt
+ dest: "{{ remote_tmp_dir }}/teststring.txt"
+ register: result
+
+- name: Assert that the test file was deployed
+ assert:
+ that:
+ - result is changed
+ - result.checksum == '481c2b73fe062390afdd294063a4f8285d69ac85'
+ - result.state == 'file'
+
+- name: Insert a line in the file using an empty string as a search string
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/teststring.txt"
+ search_string: ''
+ line: This is line 6
+ register: insert_empty_literal
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir }}/teststring.txt"
+ register: result
+
+- name: Assert that the file contents match what is expected and a warning was displayed
+ assert:
+ that:
+ - insert_empty_literal is changed
+ - warning_message in insert_empty_literal.warnings
+ - result.stat.checksum == 'eaa79f878557d4bd8d96787a850526a0facab342'
+ vars:
+ warning_message: >-
+ The search string is an empty string, which will match every line in the file.
+ This may have unintended consequences, such as replacing the last line in the file rather than appending.
+
+- name: meta
+ meta: end_play
+
+###################################################################
+## Issue #58923
+## Using firstmatch with insertafter and ensure multiple lines are not inserted
+
+- name: Deploy the firstmatch test file
+ copy:
+ src: firstmatch.txt
+ dest: "{{ remote_tmp_dir }}/firstmatch.txt"
+ register: result
+
+- name: Assert that the test file was deployed
+ assert:
+ that:
+ - result is changed
+ - result.checksum == '1d644e5e2e51c67f1bd12d7bbe2686017f39923d'
+ - result.state == 'file'
+
+- name: Insert a line before an existing line using firstmatch
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/firstmatch.txt"
+ line: INSERT
+ insertafter: line1
+ firstmatch: yes
+ register: insertafter1
+
+- name: Insert a line before an existing line using firstmatch again
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/firstmatch.txt"
+ line: INSERT
+ insertafter: line1
+ firstmatch: yes
+ register: insertafter2
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir }}/firstmatch.txt"
+ register: result
+
+- name: Assert that the file was modified appropriately
+ assert:
+ that:
+ - insertafter1 is changed
+ - insertafter2 is not changed
+ - result.stat.checksum == '114aae024073a3ee8ec8db0ada03c5483326dd86'
+
+########################################################################################
+# Tests of fixing the same issue as above (#58923) by @Andersson007 <aaklychkov@mail.ru>
+# and @samdoran <sdoran@redhat.com>:
+
+# Test insertafter with regexp
+- name: Deploy the test file
+ copy:
+ src: test_58923.txt
+ dest: "{{ remote_tmp_dir }}/test_58923.txt"
+ register: initial_file
+
+- name: Assert that the test file was deployed
+ assert:
+ that:
+ - initial_file is changed
+ - initial_file.checksum == 'b6379ba43261c451a62102acb2c7f438a177c66e'
+ - initial_file.state == 'file'
+
+# Regarding the documentation:
+# If regular expressions are passed to both regexp and
+# insertafter, insertafter is only honored if no match for regexp is found.
+# Therefore,
+# when regular expressions are passed to both regexp and insertafter, then:
+# 1. regexp was found -> ignore insertafter, replace the founded line
+# 2. regexp was not found -> insert the line after 'insertafter' line
+
+# Regexp is not present in the file, so the line must be inserted after ^#!/bin/sh
+- name: Add the line using firstmatch, regexp, and insertafter
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/test_58923.txt"
+ insertafter: '^#!/bin/sh'
+ regexp: ^export FISHEYE_OPTS
+ firstmatch: true
+ line: export FISHEYE_OPTS="-Xmx4096m -Xms2048m"
+ register: insertafter_test1
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir }}/test_58923.txt"
+ register: insertafter_test1_file
+
+- name: Add the line using firstmatch, regexp, and insertafter again
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/test_58923.txt"
+ insertafter: '^#!/bin/sh'
+ regexp: ^export FISHEYE_OPTS
+ firstmatch: true
+ line: export FISHEYE_OPTS="-Xmx4096m -Xms2048m"
+ register: insertafter_test2
+
+# Check of the prev step.
+# We tried to add the same line with the same playbook,
+# so nothing has been added:
+- name: Stat the file again
+ stat:
+ path: "{{ remote_tmp_dir }}/test_58923.txt"
+ register: insertafter_test2_file
+
+- name: Assert insertafter tests gave the expected results
+ assert:
+ that:
+ - insertafter_test1 is changed
+ - insertafter_test1_file.stat.checksum == '9232aed6fe88714964d9e29d13e42cd782070b08'
+ - insertafter_test2 is not changed
+ - insertafter_test2_file.stat.checksum == '9232aed6fe88714964d9e29d13e42cd782070b08'
+
+# Test insertafter without regexp
+- name: Deploy the test file
+ copy:
+ src: test_58923.txt
+ dest: "{{ remote_tmp_dir }}/test_58923.txt"
+ register: initial_file
+
+- name: Assert that the test file was deployed
+ assert:
+ that:
+ - initial_file is changed
+ - initial_file.checksum == 'b6379ba43261c451a62102acb2c7f438a177c66e'
+ - initial_file.state == 'file'
+
+- name: Insert the line using firstmatch and insertafter without regexp
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/test_58923.txt"
+ insertafter: '^#!/bin/sh'
+ firstmatch: true
+ line: export FISHEYE_OPTS="-Xmx4096m -Xms2048m"
+ register: insertafter_test3
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir }}/test_58923.txt"
+ register: insertafter_test3_file
+
+- name: Insert the line using firstmatch and insertafter without regexp again
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/test_58923.txt"
+ insertafter: '^#!/bin/sh'
+ firstmatch: true
+ line: export FISHEYE_OPTS="-Xmx4096m -Xms2048m"
+ register: insertafter_test4
+
+- name: Stat the file again
+ stat:
+ path: "{{ remote_tmp_dir }}/test_58923.txt"
+ register: insertafter_test4_file
+
+- name: Assert insertafter without regexp tests gave the expected results
+ assert:
+ that:
+ - insertafter_test3 is changed
+ - insertafter_test3_file.stat.checksum == '9232aed6fe88714964d9e29d13e42cd782070b08'
+ - insertafter_test4 is not changed
+ - insertafter_test4_file.stat.checksum == '9232aed6fe88714964d9e29d13e42cd782070b08'
+
+
+# Test insertbefore with regexp
+- name: Deploy the test file
+ copy:
+ src: test_58923.txt
+ dest: "{{ remote_tmp_dir }}/test_58923.txt"
+ register: initial_file
+
+- name: Assert that the test file was deployed
+ assert:
+ that:
+ - initial_file is changed
+ - initial_file.checksum == 'b6379ba43261c451a62102acb2c7f438a177c66e'
+ - initial_file.state == 'file'
+
+- name: Add the line using regexp, firstmatch, and insertbefore
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/test_58923.txt"
+ insertbefore: '^#!/bin/sh'
+ regexp: ^export FISHEYE_OPTS
+ firstmatch: true
+ line: export FISHEYE_OPTS="-Xmx4096m -Xms2048m"
+ register: insertbefore_test1
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir }}/test_58923.txt"
+ register: insertbefore_test1_file
+
+- name: Add the line using regexp, firstmatch, and insertbefore again
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/test_58923.txt"
+ insertbefore: '^#!/bin/sh'
+ regexp: ^export FISHEYE_OPTS
+ firstmatch: true
+ line: export FISHEYE_OPTS="-Xmx4096m -Xms2048m"
+ register: insertbefore_test2
+
+- name: Stat the file again
+ stat:
+ path: "{{ remote_tmp_dir }}/test_58923.txt"
+ register: insertbefore_test2_file
+
+- name: Assert insertbefore with regexp tests gave the expected results
+ assert:
+ that:
+ - insertbefore_test1 is changed
+ - insertbefore_test1_file.stat.checksum == '3c6630b9d44f561ea9ad999be56a7504cadc12f7'
+ - insertbefore_test2 is not changed
+ - insertbefore_test2_file.stat.checksum == '3c6630b9d44f561ea9ad999be56a7504cadc12f7'
+
+
+# Test insertbefore without regexp
+- name: Deploy the test file
+ copy:
+ src: test_58923.txt
+ dest: "{{ remote_tmp_dir }}/test_58923.txt"
+ register: initial_file
+
+- name: Assert that the test file was deployed
+ assert:
+ that:
+ - initial_file is changed
+ - initial_file.checksum == 'b6379ba43261c451a62102acb2c7f438a177c66e'
+ - initial_file.state == 'file'
+
+- name: Add the line using insertbefore and firstmatch
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/test_58923.txt"
+ insertbefore: '^#!/bin/sh'
+ firstmatch: true
+ line: export FISHEYE_OPTS="-Xmx4096m -Xms2048m"
+ register: insertbefore_test3
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir }}/test_58923.txt"
+ register: insertbefore_test3_file
+
+- name: Add the line using insertbefore and firstmatch again
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/test_58923.txt"
+ insertbefore: '^#!/bin/sh'
+ firstmatch: true
+ line: export FISHEYE_OPTS="-Xmx4096m -Xms2048m"
+ register: insertbefore_test4
+
+- name: Stat the file again
+ stat:
+ path: "{{ remote_tmp_dir }}/test_58923.txt"
+ register: insertbefore_test4_file
+
+# Test when the line is presented in the file but
+# not in the before/after spot and it does match the regexp:
+- name: >
+ Add the line using insertbefore and firstmatch when the regexp line
+ is presented but not close to insertbefore spot
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/test_58923.txt"
+ insertbefore: ' Darwin\*\) if \[ -z \"\$JAVA_HOME\" \] ; then'
+ firstmatch: true
+ line: export FISHEYE_OPTS="-Xmx4096m -Xms2048m"
+ register: insertbefore_test5
+
+- name: Stat the file again
+ stat:
+ path: "{{ remote_tmp_dir }}/test_58923.txt"
+ register: insertbefore_test5_file
+
+- name: Assert insertbefore with regexp tests gave the expected results
+ assert:
+ that:
+ - insertbefore_test3 is changed
+ - insertbefore_test3_file.stat.checksum == '3c6630b9d44f561ea9ad999be56a7504cadc12f7'
+ - insertbefore_test4 is not changed
+ - insertbefore_test4_file.stat.checksum == '3c6630b9d44f561ea9ad999be56a7504cadc12f7'
+ - insertbefore_test5 is not changed
+ - insertbefore_test5_file.stat.checksum == '3c6630b9d44f561ea9ad999be56a7504cadc12f7'
+
+########################################################################################
+# Same tests for literal
+
+# Test insertafter with literal
+- name: Deploy the test file
+ copy:
+ src: teststring_58923.txt
+ dest: "{{ remote_tmp_dir }}/teststring_58923.txt"
+ register: initial_file
+
+- name: Assert that the test file was deployed
+ assert:
+ that:
+ - initial_file is changed
+ - initial_file.checksum == 'b6379ba43261c451a62102acb2c7f438a177c66e'
+ - initial_file.state == 'file'
+
+# Regarding the documentation:
+# If the search string is passed to both search_string and
+# insertafter, insertafter is only honored if no match for search_string is found.
+# Therefore,
+# when search_string expressions are passed to both search_string and insertafter, then:
+# 1. search_string was found -> ignore insertafter, replace the founded line
+# 2. search_string was not found -> insert the line after 'insertafter' line
+
+# literal is not present in the file, so the line must be inserted after ^#!/bin/sh
+- name: Add the line using firstmatch, regexp, and insertafter
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/teststring_58923.txt"
+ insertafter: '^#!/bin/sh'
+ search_string: export FISHEYE_OPTS
+ firstmatch: true
+ line: export FISHEYE_OPTS="-Xmx4096m -Xms2048m"
+ register: insertafter_test1
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir }}/teststring_58923.txt"
+ register: insertafter_test1_file
+
+- name: Add the line using firstmatch, literal, and insertafter again
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/teststring_58923.txt"
+ insertafter: '^#!/bin/sh'
+ search_string: export FISHEYE_OPTS
+ firstmatch: true
+ line: export FISHEYE_OPTS="-Xmx4096m -Xms2048m"
+ register: insertafter_test2
+
+# Check of the prev step.
+# We tried to add the same line with the same playbook,
+# so nothing has been added:
+- name: Stat the file again
+ stat:
+ path: "{{ remote_tmp_dir }}/teststring_58923.txt"
+ register: insertafter_test2_file
+
+- name: Assert insertafter tests gave the expected results
+ assert:
+ that:
+ - insertafter_test1 is changed
+ - insertafter_test1_file.stat.checksum == '9232aed6fe88714964d9e29d13e42cd782070b08'
+ - insertafter_test2 is not changed
+ - insertafter_test2_file.stat.checksum == '9232aed6fe88714964d9e29d13e42cd782070b08'
+
+# Test insertbefore with literal
+- name: Deploy the test file
+ copy:
+ src: teststring_58923.txt
+ dest: "{{ remote_tmp_dir }}/teststring_58923.txt"
+ register: initial_file
+
+- name: Assert that the test file was deployed
+ assert:
+ that:
+ - initial_file is changed
+ - initial_file.checksum == 'b6379ba43261c451a62102acb2c7f438a177c66e'
+ - initial_file.state == 'file'
+
+- name: Add the line using literal, firstmatch, and insertbefore
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/teststring_58923.txt"
+ insertbefore: '^#!/bin/sh'
+ search_string: export FISHEYE_OPTS
+ firstmatch: true
+ line: export FISHEYE_OPTS="-Xmx4096m -Xms2048m"
+ register: insertbefore_test1
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir }}/teststring_58923.txt"
+ register: insertbefore_test1_file
+
+- name: Add the line using literal, firstmatch, and insertbefore again
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/teststring_58923.txt"
+ insertbefore: '^#!/bin/sh'
+ search_string: export FISHEYE_OPTS
+ firstmatch: true
+ line: export FISHEYE_OPTS="-Xmx4096m -Xms2048m"
+ register: insertbefore_test2
+
+- name: Stat the file again
+ stat:
+ path: "{{ remote_tmp_dir }}/teststring_58923.txt"
+ register: insertbefore_test2_file
+
+- name: Assert insertbefore with literal tests gave the expected results
+ assert:
+ that:
+ - insertbefore_test1 is changed
+ - insertbefore_test1_file.stat.checksum == '3c6630b9d44f561ea9ad999be56a7504cadc12f7'
+ - insertbefore_test2 is not changed
+ - insertbefore_test2_file.stat.checksum == '3c6630b9d44f561ea9ad999be56a7504cadc12f7'
+
+# Test inserting a line at the end of the file using regexp with insertafter
+# https://github.com/ansible/ansible/issues/63684
+- name: Create a file by inserting a line
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/testend.txt"
+ create: yes
+ line: testline
+ register: testend1
+
+- name: Insert a line at the end of the file
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/testend.txt"
+ insertafter: testline
+ regexp: line at the end
+ line: line at the end
+ register: testend2
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir }}/testend.txt"
+ register: testend_file
+
+- name: Assert inserting at the end gave the expected results.
+ assert:
+ that:
+ - testend1 is changed
+ - testend2 is changed
+ - testend_file.stat.checksum == 'ef36116966836ce04f6b249fd1837706acae4e19'
+
+# Test inserting a line at the end of the file using search_string with insertafter
+
+- name: Create a file by inserting a line
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/testendliteral.txt"
+ create: yes
+ line: testline
+ register: testend1
+
+- name: Insert a line at the end of the file
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/testendliteral.txt"
+ insertafter: testline
+ search_string: line at the end
+ line: line at the end
+ register: testend2
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir }}/testendliteral.txt"
+ register: testend_file
+
+- name: Assert inserting at the end gave the expected results.
+ assert:
+ that:
+ - testend1 is changed
+ - testend2 is changed
+ - testend_file.stat.checksum == 'ef36116966836ce04f6b249fd1837706acae4e19'
diff --git a/test/integration/targets/lineinfile/tasks/test_string01.yml b/test/integration/targets/lineinfile/tasks/test_string01.yml
new file mode 100644
index 0000000..b86cd09
--- /dev/null
+++ b/test/integration/targets/lineinfile/tasks/test_string01.yml
@@ -0,0 +1,142 @@
+---
+###################################################################
+# 1st search_string tests
+
+- name: deploy the test file for lineinfile string
+ copy:
+ src: teststring.txt
+ dest: "{{ remote_tmp_dir }}/teststring.txt"
+ register: result
+
+- name: assert that the test file was deployed
+ assert:
+ that:
+ - result is changed
+ - "result.checksum == '481c2b73fe062390afdd294063a4f8285d69ac85'"
+ - "result.state == 'file'"
+
+- name: insert a line at the beginning of the file, and back it up
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/teststring.txt"
+ state: present
+ line: "New line at the beginning"
+ insertbefore: "BOF"
+ backup: yes
+ register: result1
+
+- name: insert a line at the beginning of the file again
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/teststring.txt"
+ state: present
+ line: "New line at the beginning"
+ insertbefore: "BOF"
+ register: result2
+
+- name: Replace a line using string
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/teststring.txt"
+ state: present
+ line: "Thi$ i^ [ine 3"
+ search_string: (\\w)(\\s+)([\\.,])
+ register: backrefs_result1
+
+- name: Replace a line again using string
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/teststring.txt"
+ state: present
+ line: "Thi$ i^ [ine 3"
+ search_string: (\\w)(\\s+)([\\.,])
+ register: backrefs_result2
+
+- command: cat {{ remote_tmp_dir }}/teststring.txt
+
+- name: assert that the line with backrefs was changed
+ assert:
+ that:
+ - backrefs_result1 is changed
+ - backrefs_result2 is not changed
+ - "backrefs_result1.msg == 'line replaced'"
+
+- name: stat the test after the backref line was replaced
+ stat:
+ path: "{{ remote_tmp_dir }}/teststring.txt"
+ register: result
+
+- name: assert test checksum matches after backref line was replaced
+ assert:
+ that:
+ - "result.stat.checksum == '8084519b53e268920a46592a112297715951f167'"
+
+- name: remove the middle line using string
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/teststring.txt"
+ state: absent
+ search_string: "Thi$ i^ [ine 3"
+ register: result
+
+- name: assert that the line was removed
+ assert:
+ that:
+ - result is changed
+ - "result.msg == '1 line(s) removed'"
+
+- name: stat the test after the middle line was removed
+ stat:
+ path: "{{ remote_tmp_dir }}/teststring.txt"
+ register: result
+
+- name: assert test checksum matches after the middle line was removed
+ assert:
+ that:
+ - "result.stat.checksum == '89919ef2ef91e48ad02e0ca2bcb76dfc2a86d516'"
+
+- name: run a validation script that succeeds using string
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/teststring.txt"
+ state: absent
+ search_string: <FilesMatch ".py[45]?$">
+ validate: "true %s"
+ register: result
+
+- name: assert that the file validated after removing a line
+ assert:
+ that:
+ - result is changed
+ - "result.msg == '1 line(s) removed'"
+
+- name: stat the test after the validation succeeded
+ stat:
+ path: "{{ remote_tmp_dir }}/teststring.txt"
+ register: result
+
+- name: assert test checksum matches after the validation succeeded
+ assert:
+ that:
+ - "result.stat.checksum == 'ba9600b34febbc88bfb3ca99cd6b57f1010c19a4'"
+
+- name: run a validation script that fails using string
+ lineinfile:
+ dest: "{{ remote_tmp_dir }}/teststring.txt"
+ state: absent
+ search_string: "This is line 1"
+ validate: "/bin/false %s"
+ register: result
+ ignore_errors: yes
+
+- name: assert that the validate failed
+ assert:
+ that:
+ - "result.failed == true"
+
+- name: stat the test after the validation failed
+ stat:
+ path: "{{ remote_tmp_dir }}/teststring.txt"
+ register: result
+
+- name: assert test checksum matches the previous after the validation failed
+ assert:
+ that:
+ - "result.stat.checksum == 'ba9600b34febbc88bfb3ca99cd6b57f1010c19a4'"
+
+# End of string tests
+###################################################################
diff --git a/test/integration/targets/lineinfile/tasks/test_string02.yml b/test/integration/targets/lineinfile/tasks/test_string02.yml
new file mode 100644
index 0000000..1fa48b8
--- /dev/null
+++ b/test/integration/targets/lineinfile/tasks/test_string02.yml
@@ -0,0 +1,166 @@
+---
+###################################################################
+# 2nd search_string tests
+
+- name: Deploy the teststring.conf file
+ copy:
+ src: teststring.conf
+ dest: "{{ remote_tmp_dir }}/teststring.conf"
+ register: result
+
+- name: Assert that the teststring.conf file was deployed
+ assert:
+ that:
+ - result is changed
+ - result.checksum == '6037f13e419b132eb3fd20a89e60c6c87a6add38'
+ - result.state == 'file'
+
+# Test instertafter
+- name: Insert lines after with string
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/teststring.conf"
+ search_string: "{{ item.regexp }}"
+ line: "{{ item.line }}"
+ insertafter: "{{ item.after }}"
+ with_items: "{{ test_befaf_regexp }}"
+ register: _multitest_5
+
+- name: Do the same thing again and check for changes
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/teststring.conf"
+ search_string: "{{ item.regexp }}"
+ line: "{{ item.line }}"
+ insertafter: "{{ item.after }}"
+ with_items: "{{ test_befaf_regexp }}"
+ register: _multitest_6
+
+- name: Assert that the file was changed the first time but not the second time
+ assert:
+ that:
+ - item.0 is changed
+ - item.1 is not changed
+ with_together:
+ - "{{ _multitest_5.results }}"
+ - "{{ _multitest_6.results }}"
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir }}/teststring.conf"
+ register: result
+
+- name: Assert that the file contents match what is expected
+ assert:
+ that:
+ - result.stat.checksum == '06e2c456e5028dd7bcd0b117b5927a1139458c82'
+
+- name: Do the same thing a third time without string and check for changes
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/teststring.conf"
+ line: "{{ item.line }}"
+ insertafter: "{{ item.after }}"
+ with_items: "{{ test_befaf_regexp }}"
+ register: _multitest_7
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir }}/teststring.conf"
+ register: result
+
+- name: Assert that the file was changed when no string was provided
+ assert:
+ that:
+ - item is not changed
+ with_items: "{{ _multitest_7.results }}"
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir }}/teststring.conf"
+ register: result
+
+- name: Assert that the file contents match what is expected
+ assert:
+ that:
+ - result.stat.checksum == '06e2c456e5028dd7bcd0b117b5927a1139458c82'
+
+# Test insertbefore
+- name: Deploy the test.conf file
+ copy:
+ src: teststring.conf
+ dest: "{{ remote_tmp_dir }}/teststring.conf"
+ register: result
+
+- name: Assert that the teststring.conf file was deployed
+ assert:
+ that:
+ - result is changed
+ - result.checksum == '6037f13e419b132eb3fd20a89e60c6c87a6add38'
+ - result.state == 'file'
+
+- name: Insert lines before with string
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/teststring.conf"
+ search_string: "{{ item.regexp }}"
+ line: "{{ item.line }}"
+ insertbefore: "{{ item.before }}"
+ with_items: "{{ test_befaf_regexp }}"
+ register: _multitest_8
+
+- name: Do the same thing again and check for changes
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/teststring.conf"
+ search_string: "{{ item.regexp }}"
+ line: "{{ item.line }}"
+ insertbefore: "{{ item.before }}"
+ with_items: "{{ test_befaf_regexp }}"
+ register: _multitest_9
+
+- name: Assert that the file was changed the first time but not the second time
+ assert:
+ that:
+ - item.0 is changed
+ - item.1 is not changed
+ with_together:
+ - "{{ _multitest_8.results }}"
+ - "{{ _multitest_9.results }}"
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir }}/teststring.conf"
+ register: result
+
+- name: Assert that the file contents match what is expected
+ assert:
+ that:
+ - result.stat.checksum == 'c3be9438a07c44d4c256cebfcdbca15a15b1db91'
+
+- name: Do the same thing a third time without string and check for changes
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/teststring.conf"
+ line: "{{ item.line }}"
+ insertbefore: "{{ item.before }}"
+ with_items: "{{ test_befaf_regexp }}"
+ register: _multitest_10
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir }}/teststring.conf"
+ register: result
+
+- name: Assert that the file was changed when no string was provided
+ assert:
+ that:
+ - item is not changed
+ with_items: "{{ _multitest_10.results }}"
+
+- name: Stat the file
+ stat:
+ path: "{{ remote_tmp_dir }}/teststring.conf"
+ register: result
+
+- name: Assert that the file contents match what is expected
+ assert:
+ that:
+ - result.stat.checksum == 'c3be9438a07c44d4c256cebfcdbca15a15b1db91'
+
+# End of string tests
+###################################################################
diff --git a/test/integration/targets/lineinfile/vars/main.yml b/test/integration/targets/lineinfile/vars/main.yml
new file mode 100644
index 0000000..6e99d4f
--- /dev/null
+++ b/test/integration/targets/lineinfile/vars/main.yml
@@ -0,0 +1,29 @@
+test_regexp:
+ - regex: '1'
+ replace: 'bar'
+
+ - regex: '2'
+ replace: 'bar'
+
+ - regex: '3'
+ replace: 'bar'
+
+ - regex: '4'
+ replace: 'bar'
+
+
+test_befaf_regexp:
+ - before: section_three
+ after: section_one
+ regexp: option_one=
+ line: option_one=1
+
+ - before: section_three
+ after: section_one
+ regexp: option_two=
+ line: option_two=2
+
+ - before: section_three
+ after: section_one
+ regexp: option_three=
+ line: option_three=3
diff --git a/test/integration/targets/lookup_config/aliases b/test/integration/targets/lookup_config/aliases
new file mode 100644
index 0000000..3005e4b
--- /dev/null
+++ b/test/integration/targets/lookup_config/aliases
@@ -0,0 +1 @@
+shippable/posix/group4
diff --git a/test/integration/targets/lookup_config/tasks/main.yml b/test/integration/targets/lookup_config/tasks/main.yml
new file mode 100644
index 0000000..356d2f8
--- /dev/null
+++ b/test/integration/targets/lookup_config/tasks/main.yml
@@ -0,0 +1,74 @@
+- name: Verify lookup_config errors with no on_missing (failure expected)
+ set_fact:
+ foo: '{{lookup("config", "THIS_DOES_NOT_EXIST")}}'
+ ignore_errors: yes
+ register: lookup_config_1
+
+- name: Verify lookup_config errors with on_missing=error (failure expected)
+ set_fact:
+ foo: '{{lookup("config", "THIS_DOES_NOT_EXIST", on_missing="error")}}'
+ ignore_errors: yes
+ register: lookup_config_2
+
+- name: Verify lookup_config does not error with on_missing=skip
+ set_fact:
+ lookup3: '{{lookup("config", "THIS_DOES_NOT_EXIST", on_missing="skip")}}'
+ register: lookup_config_3
+
+# TODO: Is there a decent way to check that the warning is actually triggered?
+- name: Verify lookup_config does not error with on_missing=warn (warning expected)
+ set_fact:
+ lookup4: '{{lookup("config", "THIS_DOES_NOT_EXIST", on_missing="warn")}}'
+ register: lookup_config_4
+
+- name: Verify lookup_config errors with invalid on_missing (failure expected)
+ set_fact:
+ foo: '{{lookup("config", "THIS_DOES_NOT_EXIST", on_missing="boo")}}'
+ ignore_errors: yes
+ register: lookup_config_5
+
+- name: Verify lookup_config errors with invalid param type (failure expected)
+ set_fact:
+ foo: '{{lookup("config", 1337)}}'
+ ignore_errors: yes
+ register: lookup_config_6
+
+- name: Verify lookup_config errors with callable arg (failure expected)
+ set_fact:
+ foo: '{{lookup("config", "ConfigManager")}}'
+ ignore_errors: yes
+ register: lookup_config_7
+
+- name: remote user and port for ssh connection
+ set_fact:
+ ssh_user_and_port: '{{q("config", "remote_user", "port", plugin_type="connection", plugin_name="ssh")}}'
+ vars:
+ ansible_ssh_user: lola
+ ansible_ssh_port: 2022
+
+- name: remote_tmp for sh shell plugin
+ set_fact:
+ yolo_remote: '{{q("config", "remote_tmp", plugin_type="shell", plugin_name="sh")}}'
+ vars:
+ ansible_remote_tmp: yolo
+
+- name: Verify lookup_config
+ assert:
+ that:
+ - '"meow" in lookup("config", "ANSIBLE_COW_ACCEPTLIST")'
+ - lookup_config_1 is failed
+ - '"Unable to find setting" in lookup_config_1.msg'
+ - lookup_config_2 is failed
+ - '"Unable to find setting" in lookup_config_2.msg'
+ - lookup_config_3 is success
+ - 'lookup3|length == 0'
+ - lookup_config_4 is success
+ - 'lookup4|length == 0'
+ - lookup_config_5 is failed
+ - '"valid values are" in lookup_config_5.msg'
+ - lookup_config_6 is failed
+ - '"Invalid setting identifier" in lookup_config_6.msg'
+ - lookup_config_7 is failed
+ - '"Invalid setting" in lookup_config_7.msg'
+ - ssh_user_and_port == ['lola', 2022]
+ - yolo_remote == ["yolo"]
diff --git a/test/integration/targets/lookup_csvfile/aliases b/test/integration/targets/lookup_csvfile/aliases
new file mode 100644
index 0000000..3005e4b
--- /dev/null
+++ b/test/integration/targets/lookup_csvfile/aliases
@@ -0,0 +1 @@
+shippable/posix/group4
diff --git a/test/integration/targets/lookup_csvfile/files/cool list of things.csv b/test/integration/targets/lookup_csvfile/files/cool list of things.csv
new file mode 100644
index 0000000..b1a74a0
--- /dev/null
+++ b/test/integration/targets/lookup_csvfile/files/cool list of things.csv
@@ -0,0 +1,3 @@
+woo,i,have,spaces,in,my,filename
+i,am,so,cool,haha,be,jealous
+maybe,i,will,work,like,i,should
diff --git a/test/integration/targets/lookup_csvfile/files/crlf.csv b/test/integration/targets/lookup_csvfile/files/crlf.csv
new file mode 100644
index 0000000..a17f6c4
--- /dev/null
+++ b/test/integration/targets/lookup_csvfile/files/crlf.csv
@@ -0,0 +1,2 @@
+this file,has,crlf,line,endings
+ansible,parses,them,just,fine
diff --git a/test/integration/targets/lookup_csvfile/files/people.csv b/test/integration/targets/lookup_csvfile/files/people.csv
new file mode 100644
index 0000000..f93498c
--- /dev/null
+++ b/test/integration/targets/lookup_csvfile/files/people.csv
@@ -0,0 +1,6 @@
+# Last,First,Email,Extension
+Smith,Jane,jsmith@example.com,1234
+Ipsum,Lorem,lipsum@another.example.com,9001
+"German von Lastname",Demo,hello@example.com,123123
+Example,Person,"crazy email"@example.com,9876
+"""The Rock"" Johnson",Dwayne,uhoh@example.com,1337
diff --git a/test/integration/targets/lookup_csvfile/files/tabs.csv b/test/integration/targets/lookup_csvfile/files/tabs.csv
new file mode 100644
index 0000000..69f4d87
--- /dev/null
+++ b/test/integration/targets/lookup_csvfile/files/tabs.csv
@@ -0,0 +1,4 @@
+fruit bananas 30
+fruit apples 9
+electronics tvs 8
+shoes sneakers 26
diff --git a/test/integration/targets/lookup_csvfile/files/x1a.csv b/test/integration/targets/lookup_csvfile/files/x1a.csv
new file mode 100644
index 0000000..d2d5a0d
--- /dev/null
+++ b/test/integration/targets/lookup_csvfile/files/x1a.csv
@@ -0,0 +1,3 @@
+separatedbyx1achars
+againbecause
+wecan
diff --git a/test/integration/targets/lookup_csvfile/tasks/main.yml b/test/integration/targets/lookup_csvfile/tasks/main.yml
new file mode 100644
index 0000000..758da71
--- /dev/null
+++ b/test/integration/targets/lookup_csvfile/tasks/main.yml
@@ -0,0 +1,83 @@
+- name: using deprecated syntax but missing keyword
+ set_fact:
+ this_will_error: "{{ lookup('csvfile', 'file=people.csv, delimiter=, col=1') }}"
+ ignore_errors: yes
+ register: no_keyword
+
+- name: extra arg in k=v syntax (deprecated)
+ set_fact:
+ this_will_error: "{{ lookup('csvfile', 'foo file=people.csv delimiter=, col=1 thisarg=doesnotexist') }}"
+ ignore_errors: yes
+ register: invalid_arg
+
+- name: extra arg in config syntax
+ set_fact:
+ this_will_error: "{{ lookup('csvfile', 'foo', file='people.csv', delimiter=',' col=1, thisarg='doesnotexist') }}"
+ ignore_errors: yes
+ register: invalid_arg2
+
+- set_fact:
+ this_will_error: "{{ lookup('csvfile', 'foo', file='doesnotexist', delimiter=',', col=1) }}"
+ ignore_errors: yes
+ register: missing_file
+
+- name: Make sure we failed above
+ assert:
+ that:
+ - no_keyword is failed
+ - >
+ "Search key is required but was not found" in no_keyword.msg
+ - invalid_arg is failed
+ - invalid_arg2 is failed
+ - >
+ "is not a valid option" in invalid_arg.msg
+ - missing_file is failed
+ - >
+ "need string or buffer" in missing_file.msg or
+ "expected str, bytes or os.PathLike object" in missing_file.msg or
+ "No such file or directory" in missing_file.msg
+
+- name: Check basic comma-separated file
+ assert:
+ that:
+ - lookup('csvfile', 'Smith', file='people.csv', delimiter=',', col=1) == "Jane"
+ - lookup('csvfile', 'German von Lastname file=people.csv delimiter=, col=1') == "Demo"
+
+- name: Check tab-separated file
+ assert:
+ that:
+ - lookup('csvfile', 'electronics file=tabs.csv delimiter=TAB col=1') == "tvs"
+ - "lookup('csvfile', 'fruit', file='tabs.csv', delimiter='TAB', col=1) == 'bananas'"
+ - lookup('csvfile', 'fruit file=tabs.csv delimiter="\t" col=1') == "bananas"
+ - lookup('csvfile', 'electronics', 'fruit', file='tabs.csv', delimiter='\t', col=1) == "tvs,bananas"
+ - lookup('csvfile', 'electronics', 'fruit', file='tabs.csv', delimiter='\t', col=1, wantlist=True) == ["tvs", "bananas"]
+
+- name: Check \x1a-separated file
+ assert:
+ that:
+ - lookup('csvfile', 'again file=x1a.csv delimiter=\x1a col=1') == "because"
+
+- name: Check CSV file with CRLF line endings
+ assert:
+ that:
+ - lookup('csvfile', 'this file file=crlf.csv delimiter=, col=2') == "crlf"
+ - lookup('csvfile', 'ansible file=crlf.csv delimiter=, col=1') == "parses"
+
+- name: Check file with multi word filename
+ assert:
+ that:
+ - lookup('csvfile', 'maybe file="cool list of things.csv" delimiter=, col=3') == "work"
+
+- name: Test default behavior
+ assert:
+ that:
+ - lookup('csvfile', 'notfound file=people.csv delimiter=, col=2') == []
+ - lookup('csvfile', 'notfound file=people.csv delimiter=, col=2, default=what?') == "what?"
+
+# NOTE: For historical reasons, this is correct; quotes in the search field must
+# be treated literally as if they appear (escaped as required) in the field in the
+# file. They cannot be used to surround the search text in general.
+- name: Test quotes in the search field
+ assert:
+ that:
+ - lookup('csvfile', '"The Rock" Johnson file=people.csv delimiter=, col=1') == "Dwayne"
diff --git a/test/integration/targets/lookup_dict/aliases b/test/integration/targets/lookup_dict/aliases
new file mode 100644
index 0000000..b598321
--- /dev/null
+++ b/test/integration/targets/lookup_dict/aliases
@@ -0,0 +1 @@
+shippable/posix/group3
diff --git a/test/integration/targets/lookup_dict/tasks/main.yml b/test/integration/targets/lookup_dict/tasks/main.yml
new file mode 100644
index 0000000..b132413
--- /dev/null
+++ b/test/integration/targets/lookup_dict/tasks/main.yml
@@ -0,0 +1,56 @@
+- name: Define users dict
+ set_fact:
+ users:
+ alice:
+ name: Alice
+ age: 21
+ bob:
+ name: Bob
+ age: 22
+
+- name: Convert users dict to list
+ set_fact:
+ user_list: "{{ lookup('dict', users) | sort(attribute='key') }}"
+
+- name: Verify results
+ assert:
+ that:
+ - user_list | length == 2
+ - user_list[0].key == 'alice'
+ - user_list[0].value | length == 2
+ - user_list[0].value.name == 'Alice'
+ - user_list[0].value.age == 21
+ - user_list[1].key == 'bob'
+ - user_list[1].value | length == 2
+ - user_list[1].value.name == 'Bob'
+ - user_list[1].value.age == 22
+
+- name: Convert a non-dict (failure expected)
+ set_fact:
+ bad_fact: "{{ bbbbad }}"
+ vars:
+ bbbbad: "{{ lookup('dict', 1) }}"
+ register: result
+ ignore_errors: yes
+
+- name: Verify conversion failed
+ assert:
+ that:
+ - result is failed
+
+- name: Define simple dict
+ set_fact:
+ simple:
+ hello: World
+
+- name: Convert using with_dict to cause terms to not be a list
+ set_fact:
+ hello: "{{ item }}"
+ with_dict: "{{ simple }}"
+
+- name: Verify conversion
+ assert:
+ that:
+ - hello | length == 2
+ - hello.key == 'hello'
+ - hello.value == 'World'
diff --git a/test/integration/targets/lookup_env/aliases b/test/integration/targets/lookup_env/aliases
new file mode 100644
index 0000000..b598321
--- /dev/null
+++ b/test/integration/targets/lookup_env/aliases
@@ -0,0 +1 @@
+shippable/posix/group3
diff --git a/test/integration/targets/lookup_env/runme.sh b/test/integration/targets/lookup_env/runme.sh
new file mode 100755
index 0000000..698d6bf
--- /dev/null
+++ b/test/integration/targets/lookup_env/runme.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+set -ex
+
+unset USR
+# this should succeed and return 'nobody' as var is undefined
+ansible -m debug -a msg="{{ lookup('env', 'USR', default='nobody')}}" localhost |grep nobody
+# var is defined but empty, so should return empty
+USR='' ansible -m debug -a msg="{{ lookup('env', 'USR', default='nobody')}}" localhost |grep -v nobody
+
+# this should fail with undefined
+ansible -m debug -a msg="{{ lookup('env', 'USR', default=Undefined)}}" localhost && exit 1 || exit 0
diff --git a/test/integration/targets/lookup_env/tasks/main.yml b/test/integration/targets/lookup_env/tasks/main.yml
new file mode 100644
index 0000000..daaeb35
--- /dev/null
+++ b/test/integration/targets/lookup_env/tasks/main.yml
@@ -0,0 +1,15 @@
+- name: get HOME environment var value
+ shell: "echo $HOME"
+ register: home_var_value
+
+- name: use env lookup to get HOME var
+ set_fact:
+ test_val: "{{ lookup('env', 'HOME') }}"
+
+- debug: var=home_var_value.stdout
+- debug: var=test_val
+
+- name: compare values
+ assert:
+ that:
+ - "test_val == home_var_value.stdout"
diff --git a/test/integration/targets/lookup_file/aliases b/test/integration/targets/lookup_file/aliases
new file mode 100644
index 0000000..b598321
--- /dev/null
+++ b/test/integration/targets/lookup_file/aliases
@@ -0,0 +1 @@
+shippable/posix/group3
diff --git a/test/integration/targets/lookup_file/tasks/main.yml b/test/integration/targets/lookup_file/tasks/main.yml
new file mode 100644
index 0000000..a6d636d
--- /dev/null
+++ b/test/integration/targets/lookup_file/tasks/main.yml
@@ -0,0 +1,13 @@
+- name: make a new file to read
+ copy: dest={{output_dir}}/foo.txt mode=0644 content="bar"
+
+- name: load the file as a fact
+ set_fact:
+ foo: "{{ lookup('file', output_dir + '/foo.txt' ) }}"
+
+- debug: var=foo
+
+- name: verify file lookup
+ assert:
+ that:
+ - "foo == 'bar'"
diff --git a/test/integration/targets/lookup_fileglob/aliases b/test/integration/targets/lookup_fileglob/aliases
new file mode 100644
index 0000000..b598321
--- /dev/null
+++ b/test/integration/targets/lookup_fileglob/aliases
@@ -0,0 +1 @@
+shippable/posix/group3
diff --git a/test/integration/targets/lookup_fileglob/find_levels/files/play_adj_subdir.txt b/test/integration/targets/lookup_fileglob/find_levels/files/play_adj_subdir.txt
new file mode 100644
index 0000000..5025588
--- /dev/null
+++ b/test/integration/targets/lookup_fileglob/find_levels/files/play_adj_subdir.txt
@@ -0,0 +1 @@
+in files subdir adjacent to play
diff --git a/test/integration/targets/lookup_fileglob/find_levels/files/somepath/play_adj_subsubdir.txt b/test/integration/targets/lookup_fileglob/find_levels/files/somepath/play_adj_subsubdir.txt
new file mode 100644
index 0000000..96c7a54
--- /dev/null
+++ b/test/integration/targets/lookup_fileglob/find_levels/files/somepath/play_adj_subsubdir.txt
@@ -0,0 +1 @@
+in play adjacent subdir of files/
diff --git a/test/integration/targets/lookup_fileglob/find_levels/play.yml b/test/integration/targets/lookup_fileglob/find_levels/play.yml
new file mode 100644
index 0000000..578d482
--- /dev/null
+++ b/test/integration/targets/lookup_fileglob/find_levels/play.yml
@@ -0,0 +1,13 @@
+- hosts: localhost
+ gather_facts: false
+ vars:
+ expected:
+ play_adj: adjacent to play
+ play_adj_subdir: in files subdir adjacent to play
+ somepath/play_adj_subsubdir: in play adjacent subdir of files/
+ in_role: file in role
+ otherpath/in_role_subdir: file in role subdir
+ tasks:
+ - name: Import role lookup
+ import_role:
+ name: get_file
diff --git a/test/integration/targets/lookup_fileglob/find_levels/play_adj.txt b/test/integration/targets/lookup_fileglob/find_levels/play_adj.txt
new file mode 100644
index 0000000..2cc4411
--- /dev/null
+++ b/test/integration/targets/lookup_fileglob/find_levels/play_adj.txt
@@ -0,0 +1 @@
+adjacent to play
diff --git a/test/integration/targets/lookup_fileglob/find_levels/roles/get_file/files/in_role.txt b/test/integration/targets/lookup_fileglob/find_levels/roles/get_file/files/in_role.txt
new file mode 100644
index 0000000..fdfc947
--- /dev/null
+++ b/test/integration/targets/lookup_fileglob/find_levels/roles/get_file/files/in_role.txt
@@ -0,0 +1 @@
+file in role
diff --git a/test/integration/targets/lookup_fileglob/find_levels/roles/get_file/files/otherpath/in_role_subdir.txt b/test/integration/targets/lookup_fileglob/find_levels/roles/get_file/files/otherpath/in_role_subdir.txt
new file mode 100644
index 0000000..40e75a4
--- /dev/null
+++ b/test/integration/targets/lookup_fileglob/find_levels/roles/get_file/files/otherpath/in_role_subdir.txt
@@ -0,0 +1 @@
+file in role subdir
diff --git a/test/integration/targets/lookup_fileglob/find_levels/roles/get_file/tasks/main.yml b/test/integration/targets/lookup_fileglob/find_levels/roles/get_file/tasks/main.yml
new file mode 100644
index 0000000..2fc21df
--- /dev/null
+++ b/test/integration/targets/lookup_fileglob/find_levels/roles/get_file/tasks/main.yml
@@ -0,0 +1,10 @@
+- name: show file contents
+ debug:
+ msg: '{{ q("fileglob", seed + ".*") }}'
+ register: found
+
+- name: did we get right one?
+ assert:
+ that:
+ - found['msg'][0].endswith(seed + '.txt')
+ - q('file', found['msg'][0])[0] == expected[seed]
diff --git a/test/integration/targets/lookup_fileglob/issue72873/test.yml b/test/integration/targets/lookup_fileglob/issue72873/test.yml
new file mode 100644
index 0000000..218ee58
--- /dev/null
+++ b/test/integration/targets/lookup_fileglob/issue72873/test.yml
@@ -0,0 +1,31 @@
+- hosts: localhost
+ connection: local
+ gather_facts: false
+ vars:
+ dir: files
+ tasks:
+ - file: path='{{ dir }}' state=directory
+
+ - file: path='setvars.bat' state=touch # in current directory!
+
+ - file: path='{{ dir }}/{{ item }}' state=touch
+ loop:
+ - json.c
+ - strlcpy.c
+ - base64.c
+ - json.h
+ - base64.h
+ - strlcpy.h
+ - jo.c
+
+ - name: Get working order results and sort them
+ set_fact:
+ working: '{{ query("fileglob", "setvars.bat", "{{ dir }}/*.[ch]") | sort }}'
+
+ - name: Get broken order results and sort them
+ set_fact:
+ broken: '{{ query("fileglob", "{{ dir }}/*.[ch]", "setvars.bat") | sort }}'
+
+ - assert:
+ that:
+ - working == broken
diff --git a/test/integration/targets/lookup_fileglob/non_existent/play.yml b/test/integration/targets/lookup_fileglob/non_existent/play.yml
new file mode 100644
index 0000000..e92dff5
--- /dev/null
+++ b/test/integration/targets/lookup_fileglob/non_existent/play.yml
@@ -0,0 +1,6 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: fileglob should be empty
+ assert:
+ that: q("fileglob", seed) | length == 0
diff --git a/test/integration/targets/lookup_fileglob/runme.sh b/test/integration/targets/lookup_fileglob/runme.sh
new file mode 100755
index 0000000..be04421
--- /dev/null
+++ b/test/integration/targets/lookup_fileglob/runme.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# fun multilevel finds
+for seed in play_adj play_adj_subdir somepath/play_adj_subsubdir in_role otherpath/in_role_subdir
+do
+ ansible-playbook find_levels/play.yml -e "seed='${seed}'" "$@"
+done
+
+# non-existent paths
+for seed in foo foo/bar foo/bar/baz
+do
+ ansible-playbook non_existent/play.yml -e "seed='${seed}'" "$@"
+done
+
+# test for issue 72873 fix
+ansible-playbook issue72873/test.yml "$@"
diff --git a/test/integration/targets/lookup_first_found/aliases b/test/integration/targets/lookup_first_found/aliases
new file mode 100644
index 0000000..3005e4b
--- /dev/null
+++ b/test/integration/targets/lookup_first_found/aliases
@@ -0,0 +1 @@
+shippable/posix/group4
diff --git a/test/integration/targets/lookup_first_found/files/bar1 b/test/integration/targets/lookup_first_found/files/bar1
new file mode 100644
index 0000000..5716ca5
--- /dev/null
+++ b/test/integration/targets/lookup_first_found/files/bar1
@@ -0,0 +1 @@
+bar
diff --git a/test/integration/targets/lookup_first_found/files/foo1 b/test/integration/targets/lookup_first_found/files/foo1
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/test/integration/targets/lookup_first_found/files/foo1
@@ -0,0 +1 @@
+foo
diff --git a/test/integration/targets/lookup_first_found/files/vars file spaces.yml b/test/integration/targets/lookup_first_found/files/vars file spaces.yml
new file mode 100644
index 0000000..790bc26
--- /dev/null
+++ b/test/integration/targets/lookup_first_found/files/vars file spaces.yml
@@ -0,0 +1 @@
+foo: 1
diff --git a/test/integration/targets/lookup_first_found/tasks/main.yml b/test/integration/targets/lookup_first_found/tasks/main.yml
new file mode 100644
index 0000000..9aeaf1d
--- /dev/null
+++ b/test/integration/targets/lookup_first_found/tasks/main.yml
@@ -0,0 +1,96 @@
+- name: test with_first_found
+ set_fact: "first_found={{ item }}"
+ with_first_found:
+ - "does_not_exist"
+ - "foo1"
+ - "{{ role_path + '/files/bar1' }}" # will only hit this if dwim search is broken
+
+- name: set expected
+ set_fact: first_expected="{{ role_path + '/files/foo1' }}"
+
+- name: set unexpected
+ set_fact: first_unexpected="{{ role_path + '/files/bar1' }}"
+
+- name: verify with_first_found results
+ assert:
+ that:
+ - "first_found == first_expected"
+ - "first_found != first_unexpected"
+
+- name: test q(first_found) with no files produces empty list
+ set_fact:
+ first_found_var: "{{ q('first_found', params, errors='ignore') }}"
+ vars:
+ params:
+ files: "not_a_file.yaml"
+ skip: True
+
+- name: verify q(first_found) result
+ assert:
+ that:
+ - "first_found_var == []"
+
+- name: test lookup(first_found) with no files produces empty string
+ set_fact:
+ first_found_var: "{{ lookup('first_found', params, errors='ignore') }}"
+ vars:
+ params:
+ files: "not_a_file.yaml"
+
+- name: verify lookup(first_found) result
+ assert:
+ that:
+ - "first_found_var == ''"
+
+# NOTE: skip: True deprecated e17a2b502d6601be53c60d7ba1c627df419460c9, remove 2.12
+- name: test first_found with no matches and skip=True does nothing
+ set_fact: "this_not_set={{ item }}"
+ vars:
+ params:
+ files:
+ - not/a/file.yaml
+ - another/non/file.yaml
+ skip: True
+ loop: "{{ q('first_found', params) }}"
+
+- name: verify skip
+ assert:
+ that:
+ - "this_not_set is not defined"
+
+- name: test first_found with no matches and errors='ignore' skips in a loop
+ set_fact: "this_not_set={{ item }}"
+ vars:
+ params:
+ files:
+ - not/a/file.yaml
+ - another/non/file.yaml
+ loop: "{{ query('first_found', params, errors='ignore') }}"
+
+- name: verify errors=ignore
+ assert:
+ that:
+ - "this_not_set is not defined"
+
+- name: test legacy formats
+ set_fact: hatethisformat={{item}}
+ vars:
+ params:
+ files: not/a/file.yaml;hosts
+ paths: not/a/path:/etc
+ loop: "{{ q('first_found', params) }}"
+
+- name: verify /etc/hosts was found
+ assert:
+ that:
+ - "hatethisformat == '/etc/hosts'"
+
+- name: test spaces in names
+ include_vars: "{{ item }}"
+ with_first_found:
+ - files:
+ - "{{ role_path + '/files/vars file spaces.yml' }}"
+
+- assert:
+ that:
+ - foo is defined
diff --git a/test/integration/targets/lookup_indexed_items/aliases b/test/integration/targets/lookup_indexed_items/aliases
new file mode 100644
index 0000000..3005e4b
--- /dev/null
+++ b/test/integration/targets/lookup_indexed_items/aliases
@@ -0,0 +1 @@
+shippable/posix/group4
diff --git a/test/integration/targets/lookup_indexed_items/tasks/main.yml b/test/integration/targets/lookup_indexed_items/tasks/main.yml
new file mode 100644
index 0000000..434fe0f
--- /dev/null
+++ b/test/integration/targets/lookup_indexed_items/tasks/main.yml
@@ -0,0 +1,32 @@
+- name: create unindexed list
+ shell: for i in $(seq 1 5); do echo "x" ; done;
+ register: list_data
+
+- name: create indexed list
+ set_fact: "{{ item[1] + item[0]|string }}=set"
+ with_indexed_items: "{{list_data.stdout_lines}}"
+
+- name: verify with_indexed_items result
+ assert:
+ that:
+ - "x0 == 'set'"
+ - "x1 == 'set'"
+ - "x2 == 'set'"
+ - "x3 == 'set'"
+ - "x4 == 'set'"
+
+- block:
+ - name: "EXPECTED FAILURE - test not a list"
+ debug:
+ msg: "{{ item.0 }} is {{ item.1 }}"
+ with_indexed_items:
+ "a": 1
+
+ - fail:
+ msg: "should not get here"
+
+ rescue:
+ - assert:
+ that:
+ - ansible_failed_task.name == "EXPECTED FAILURE - test not a list"
+ - ansible_failed_result.msg == "with_indexed_items expects a list"
diff --git a/test/integration/targets/lookup_ini/aliases b/test/integration/targets/lookup_ini/aliases
new file mode 100644
index 0000000..70a7b7a
--- /dev/null
+++ b/test/integration/targets/lookup_ini/aliases
@@ -0,0 +1 @@
+shippable/posix/group5
diff --git a/test/integration/targets/lookup_ini/duplicate.ini b/test/integration/targets/lookup_ini/duplicate.ini
new file mode 100644
index 0000000..db510dd
--- /dev/null
+++ b/test/integration/targets/lookup_ini/duplicate.ini
@@ -0,0 +1,3 @@
+[reggae]
+name = bob
+name = marley
diff --git a/test/integration/targets/lookup_ini/duplicate_case_check.ini b/test/integration/targets/lookup_ini/duplicate_case_check.ini
new file mode 100644
index 0000000..abb0128
--- /dev/null
+++ b/test/integration/targets/lookup_ini/duplicate_case_check.ini
@@ -0,0 +1,3 @@
+[reggae]
+name = bob
+NAME = marley
diff --git a/test/integration/targets/lookup_ini/inventory b/test/integration/targets/lookup_ini/inventory
new file mode 100644
index 0000000..ae76427
--- /dev/null
+++ b/test/integration/targets/lookup_ini/inventory
@@ -0,0 +1,2 @@
+[all]
+testhost ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
diff --git a/test/integration/targets/lookup_ini/lookup-8859-15.ini b/test/integration/targets/lookup_ini/lookup-8859-15.ini
new file mode 100644
index 0000000..33f9c29
--- /dev/null
+++ b/test/integration/targets/lookup_ini/lookup-8859-15.ini
@@ -0,0 +1,7 @@
+[global]
+# A comment
+value1=Text associated with value1 and global section
+value2=Same for value2 and global section
+value.dot=Properties with dot
+field.with.space = another space
+field_with_unicode=été indien où à château français ïîôû
diff --git a/test/integration/targets/lookup_ini/lookup.ini b/test/integration/targets/lookup_ini/lookup.ini
new file mode 100644
index 0000000..5b7cc34
--- /dev/null
+++ b/test/integration/targets/lookup_ini/lookup.ini
@@ -0,0 +1,25 @@
+[global]
+# A comment
+value1=Text associated with value1 and global section
+value2=Same for value2 and global section
+value.dot=Properties with dot
+field.with.space = another space
+unicode=été indien où à château français ïîôû
+
+[section1]
+value1=section1/value1
+value2=section1/value2
+
+[value_section]
+value1=1
+value2=2
+value3=3
+other1=4
+other2=5
+
+[other_section]
+value1=1
+value2=2
+value3=3
+other1=4
+other2=5
diff --git a/test/integration/targets/lookup_ini/lookup.properties b/test/integration/targets/lookup_ini/lookup.properties
new file mode 100644
index 0000000..d71ce12
--- /dev/null
+++ b/test/integration/targets/lookup_ini/lookup.properties
@@ -0,0 +1,6 @@
+# A comment
+value1=Text associated with value1
+value2=Same for value2
+value.dot=Properties with dot
+field.with.space = another space
+field.with.unicode = été indien où à château français ïîôû
diff --git a/test/integration/targets/lookup_ini/lookup_case_check.properties b/test/integration/targets/lookup_ini/lookup_case_check.properties
new file mode 100644
index 0000000..ed3faaf
--- /dev/null
+++ b/test/integration/targets/lookup_ini/lookup_case_check.properties
@@ -0,0 +1,2 @@
+name = captain
+NAME = fantastic
diff --git a/test/integration/targets/lookup_ini/mysql.ini b/test/integration/targets/lookup_ini/mysql.ini
new file mode 100644
index 0000000..fa62d87
--- /dev/null
+++ b/test/integration/targets/lookup_ini/mysql.ini
@@ -0,0 +1,8 @@
+[mysqld]
+user = mysql
+pid-file = /var/run/mysqld/mysqld.pid
+skip-external-locking
+old_passwords = 1
+skip-bdb
+# we don't need ACID today
+skip-innodb
diff --git a/test/integration/targets/lookup_ini/runme.sh b/test/integration/targets/lookup_ini/runme.sh
new file mode 100755
index 0000000..6f44332
--- /dev/null
+++ b/test/integration/targets/lookup_ini/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook test_ini.yml -i inventory -v "$@"
diff --git a/test/integration/targets/lookup_ini/test_allow_no_value.yml b/test/integration/targets/lookup_ini/test_allow_no_value.yml
new file mode 100644
index 0000000..bfdc376
--- /dev/null
+++ b/test/integration/targets/lookup_ini/test_allow_no_value.yml
@@ -0,0 +1,23 @@
+- name: Lookup test
+ hosts: testhost
+ tasks:
+ - name: "Read mysql.ini allow_none=False (default)"
+ set_fact:
+ test1: "{{ lookup('ini', 'user', file='mysql.ini', section='mysqld') }}"
+ register: result
+ ignore_errors: true
+
+ - name: "Read mysql.ini allow_no_value=True"
+ set_fact:
+ test2: "{{ lookup('ini', 'user', file='mysql.ini', section='mysqld', allow_no_value=True) }}"
+
+ - name: "Read mysql.ini allow_none=True"
+ set_fact:
+ test3: "{{ lookup('ini', 'skip-innodb', file='mysql.ini', section='mysqld', allow_none=True) }}"
+
+ - assert:
+ that:
+ - result is failed
+ - test2 == 'mysql'
+ - test3 == []
+ - test3|length == 0
diff --git a/test/integration/targets/lookup_ini/test_case_sensitive.yml b/test/integration/targets/lookup_ini/test_case_sensitive.yml
new file mode 100644
index 0000000..f66674c
--- /dev/null
+++ b/test/integration/targets/lookup_ini/test_case_sensitive.yml
@@ -0,0 +1,31 @@
+- name: Test case sensitive option
+ hosts: all
+
+ tasks:
+ - name: Lookup a file with keys that differ only in case with case sensitivity enabled
+ debug:
+ msg: "{{ lookup('ini', 'name', file='duplicate_case_check.ini', section='reggae', case_sensitive=True) }}"
+ register: duplicate_case_sensitive_name
+
+ - name: Lookup a file with keys that differ only in case with case sensitivity enabled
+ debug:
+ msg: "{{ lookup('ini', 'NAME', file='duplicate_case_check.ini', section='reggae', case_sensitive=True) }}"
+ register: duplicate_case_sensitive_NAME
+
+ - name: Lookup a properties file with keys that differ only in case with case sensitivity enabled
+ debug:
+ msg: "{{ lookup('ini', 'name', file='lookup_case_check.properties', type='properties', case_sensitive=True) }}"
+ register: duplicate_case_sensitive_properties_name
+
+ - name: Lookup a properties file with keys that differ only in case with case sensitivity enabled
+ debug:
+ msg: "{{ lookup('ini', 'NAME', file='lookup_case_check.properties', type='properties', case_sensitive=True) }}"
+ register: duplicate_case_sensitive_properties_NAME
+
+ - name: Ensure the correct case-sensitive values were retieved
+ assert:
+ that:
+ - duplicate_case_sensitive_name.msg == 'bob'
+ - duplicate_case_sensitive_NAME.msg == 'marley'
+ - duplicate_case_sensitive_properties_name.msg == 'captain'
+ - duplicate_case_sensitive_properties_NAME.msg == 'fantastic'
diff --git a/test/integration/targets/lookup_ini/test_errors.yml b/test/integration/targets/lookup_ini/test_errors.yml
new file mode 100644
index 0000000..c1832a3
--- /dev/null
+++ b/test/integration/targets/lookup_ini/test_errors.yml
@@ -0,0 +1,62 @@
+- name: Test INI lookup errors
+ hosts: testhost
+
+ tasks:
+ - name: Test for failure on Python 3
+ when: ansible_facts.python.version_info[0] >= 3
+ block:
+ - name: Lookup a file with duplicate keys
+ debug:
+ msg: "{{ lookup('ini', 'name', file='duplicate.ini', section='reggae') }}"
+ ignore_errors: yes
+ register: duplicate
+
+ - name: Lookup a file with keys that differ only in case
+ debug:
+ msg: "{{ lookup('ini', 'name', file='duplicate_case_check.ini', section='reggae') }}"
+ ignore_errors: yes
+ register: duplicate_case_sensitive
+
+ - name: Ensure duplicate key errors were handled properly
+ assert:
+ that:
+ - duplicate is failed
+ - "'Duplicate option in' in duplicate.msg"
+ - duplicate_case_sensitive is failed
+ - "'Duplicate option in' in duplicate_case_sensitive.msg"
+
+ - name: Lookup a file with a missing section
+ debug:
+ msg: "{{ lookup('ini', 'name', file='lookup.ini', section='missing') }}"
+ ignore_errors: yes
+ register: missing_section
+
+ - name: Ensure error was shown for a missing section
+ assert:
+ that:
+ - missing_section is failed
+ - "'No section' in missing_section.msg"
+
+ - name: Mix options type and push key out of order
+ debug:
+ msg: "{{ lookup('ini', 'file=lookup.ini', 'value1', section='value_section') }}"
+ register: bad_mojo
+ ignore_errors: yes
+
+ - name: Verify bad behavior reported an error
+ assert:
+ that:
+ - bad_mojo is failed
+ - '"No key to lookup was provided as first term with in string inline option" in bad_mojo.msg'
+
+ - name: Test invalid option
+ debug:
+ msg: "{{ lookup('ini', 'invalid=option') }}"
+ ignore_errors: yes
+ register: invalid_option
+
+ - name: Ensure invalid option failed
+ assert:
+ that:
+ - invalid_option is failed
+ - "'is not a valid option' in invalid_option.msg"
diff --git a/test/integration/targets/lookup_ini/test_ini.yml b/test/integration/targets/lookup_ini/test_ini.yml
new file mode 100644
index 0000000..11a5e57
--- /dev/null
+++ b/test/integration/targets/lookup_ini/test_ini.yml
@@ -0,0 +1,4 @@
+- import_playbook: test_lookup_properties.yml
+- import_playbook: test_errors.yml
+- import_playbook: test_case_sensitive.yml
+- import_playbook: test_allow_no_value.yml
diff --git a/test/integration/targets/lookup_ini/test_lookup_properties.yml b/test/integration/targets/lookup_ini/test_lookup_properties.yml
new file mode 100644
index 0000000..a6fc0f7
--- /dev/null
+++ b/test/integration/targets/lookup_ini/test_lookup_properties.yml
@@ -0,0 +1,88 @@
+- name: Lookup test
+ hosts: testhost
+
+ tasks:
+ - name: "read properties value"
+ set_fact:
+ test1: "{{lookup('ini', 'value1 type=properties file=lookup.properties')}}"
+ test2: "{{lookup('ini', 'value2', type='properties', file='lookup.properties')}}"
+ test_dot: "{{lookup('ini', 'value.dot', type='properties', file='lookup.properties')}}"
+ field_with_space: "{{lookup('ini', 'field.with.space type=properties file=lookup.properties')}}"
+
+ - assert:
+ that: "{{item}} is defined"
+ with_items: [ 'test1', 'test2', 'test_dot', 'field_with_space' ]
+
+ - name: "read ini value"
+ set_fact:
+ value1_global: "{{lookup('ini', 'value1', section='global', file='lookup.ini')}}"
+ value2_global: "{{lookup('ini', 'value2', section='global', file='lookup.ini')}}"
+ value1_section1: "{{lookup('ini', 'value1', section='section1', file='lookup.ini')}}"
+ field_with_unicode: "{{lookup('ini', 'unicode', section='global', file='lookup.ini')}}"
+
+ - debug: var={{item}}
+ with_items: [ 'value1_global', 'value2_global', 'value1_section1', 'field_with_unicode' ]
+
+ - assert:
+ that:
+ - "field_with_unicode == 'été indien où à château français ïîôû'"
+
+ - name: "read ini value from iso8859-15 file"
+ set_fact:
+ field_with_unicode: "{{lookup('ini', 'field_with_unicode section=global encoding=iso8859-1 file=lookup-8859-15.ini')}}"
+
+ - assert:
+ that:
+ - "field_with_unicode == 'été indien où à château français ïîôû'"
+
+ - name: "read ini value with section and regexp"
+ set_fact:
+ value_section: "{{lookup('ini', 'value[1-2] section=value_section file=lookup.ini re=true')}}"
+ other_section: "{{lookup('ini', 'other[1-2] section=other_section file=lookup.ini re=true')}}"
+
+ - debug: var={{item}}
+ with_items: [ 'value_section', 'other_section' ]
+
+ - assert:
+ that:
+ - "value_section == '1,2'"
+ - "other_section == '4,5'"
+
+ - name: "Reading unknown value"
+ set_fact:
+ unknown: "{{lookup('ini', 'unknown default=unknown section=section1 file=lookup.ini')}}"
+
+ - debug: var=unknown
+
+ - assert:
+ that:
+ - 'unknown == "unknown"'
+
+ - name: "Looping over section section1"
+ debug: msg="{{item}}"
+ with_ini: value[1-2] section=section1 file=lookup.ini re=true
+ register: _
+
+ - assert:
+ that:
+ - '_.results.0.item == "section1/value1"'
+ - '_.results.1.item == "section1/value2"'
+
+ - name: "Looping over section value_section"
+ debug: msg="{{item}}"
+ with_ini: value[1-2] section=value_section file=lookup.ini re=true
+ register: _
+
+ - assert:
+ that:
+ - '_.results.0.item == "1"'
+ - '_.results.1.item == "2"'
+
+ - debug: msg="{{item}}"
+ with_ini: value[1-2] section=section1 file=lookup.ini re=true
+ register: _
+
+ - assert:
+ that:
+ - '_.results.0.item == "section1/value1"'
+ - '_.results.1.item == "section1/value2"'
diff --git a/test/integration/targets/lookup_inventory_hostnames/aliases b/test/integration/targets/lookup_inventory_hostnames/aliases
new file mode 100644
index 0000000..3005e4b
--- /dev/null
+++ b/test/integration/targets/lookup_inventory_hostnames/aliases
@@ -0,0 +1 @@
+shippable/posix/group4
diff --git a/test/integration/targets/lookup_inventory_hostnames/inventory b/test/integration/targets/lookup_inventory_hostnames/inventory
new file mode 100644
index 0000000..ca7234f
--- /dev/null
+++ b/test/integration/targets/lookup_inventory_hostnames/inventory
@@ -0,0 +1,18 @@
+nogroup01
+nogroup02
+nogroup03
+
+[group01]
+test01
+test05
+test03
+test02
+test04
+
+[group02]
+test03
+test04
+test200
+test201
+test203
+test204
diff --git a/test/integration/targets/lookup_inventory_hostnames/main.yml b/test/integration/targets/lookup_inventory_hostnames/main.yml
new file mode 100644
index 0000000..4b822e3
--- /dev/null
+++ b/test/integration/targets/lookup_inventory_hostnames/main.yml
@@ -0,0 +1,23 @@
+---
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - set_fact:
+ hosts_a: "{{ lookup('inventory_hostnames', 'group01', wantlist=true) }}"
+ hosts_b: "{{ groups['group01'] }}"
+ exclude: "{{ lookup('inventory_hostnames', 'group01:!test03', wantlist=true) }}"
+ intersect: "{{ lookup('inventory_hostnames', 'group01:&group02', wantlist=true) }}"
+ nogroup: "{{ lookup('inventory_hostnames', 'nogroup01', wantlist=true) }}"
+ doesnotexist: "{{ lookup('inventory_hostnames', 'doesnotexist', wantlist=true) }}"
+ all: "{{ lookup('inventory_hostnames', 'all', wantlist=true) }}"
+ from_patterns: "{{ lookup('inventory_hostnames', 't?s?03', wantlist=True) }}"
+
+ - assert:
+ that:
+ - hosts_a == hosts_b
+ - "'test03' not in exclude"
+ - intersect == ['test03', 'test04']
+ - nogroup == ['nogroup01']
+ - doesnotexist == []
+ - all|length == 12
+ - from_patterns == ['test03']
diff --git a/test/integration/targets/lookup_inventory_hostnames/runme.sh b/test/integration/targets/lookup_inventory_hostnames/runme.sh
new file mode 100755
index 0000000..449c66b
--- /dev/null
+++ b/test/integration/targets/lookup_inventory_hostnames/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook main.yml -i inventory "$@"
diff --git a/test/integration/targets/lookup_items/aliases b/test/integration/targets/lookup_items/aliases
new file mode 100644
index 0000000..3005e4b
--- /dev/null
+++ b/test/integration/targets/lookup_items/aliases
@@ -0,0 +1 @@
+shippable/posix/group4
diff --git a/test/integration/targets/lookup_items/tasks/main.yml b/test/integration/targets/lookup_items/tasks/main.yml
new file mode 100644
index 0000000..15a2cf2
--- /dev/null
+++ b/test/integration/targets/lookup_items/tasks/main.yml
@@ -0,0 +1,20 @@
+- name: test with_items
+ set_fact: "{{ item }}=moo"
+ with_items:
+ - 'foo'
+ - 'bar'
+
+- name: Undefined var, skipped
+ debug:
+ with_items:
+ - '{{ baz }}'
+ when: false
+
+- debug: var=foo
+- debug: var=bar
+
+- name: verify with_items results
+ assert:
+ that:
+ - "foo == 'moo'"
+ - "bar == 'moo'"
diff --git a/test/integration/targets/lookup_lines/aliases b/test/integration/targets/lookup_lines/aliases
new file mode 100644
index 0000000..3005e4b
--- /dev/null
+++ b/test/integration/targets/lookup_lines/aliases
@@ -0,0 +1 @@
+shippable/posix/group4
diff --git a/test/integration/targets/lookup_lines/tasks/main.yml b/test/integration/targets/lookup_lines/tasks/main.yml
new file mode 100644
index 0000000..f864d72
--- /dev/null
+++ b/test/integration/targets/lookup_lines/tasks/main.yml
@@ -0,0 +1,13 @@
+- name: test with_lines
+ #shell: echo "{{ item }}"
+ set_fact: "{{ item }}=set"
+ with_lines: for i in $(seq 1 5); do echo "l$i" ; done;
+
+- name: verify with_lines results
+ assert:
+ that:
+ - "l1 == 'set'"
+ - "l2 == 'set'"
+ - "l3 == 'set'"
+ - "l4 == 'set'"
+ - "l5 == 'set'"
diff --git a/test/integration/targets/lookup_list/aliases b/test/integration/targets/lookup_list/aliases
new file mode 100644
index 0000000..3005e4b
--- /dev/null
+++ b/test/integration/targets/lookup_list/aliases
@@ -0,0 +1 @@
+shippable/posix/group4
diff --git a/test/integration/targets/lookup_list/tasks/main.yml b/test/integration/targets/lookup_list/tasks/main.yml
new file mode 100644
index 0000000..a9be8ec
--- /dev/null
+++ b/test/integration/targets/lookup_list/tasks/main.yml
@@ -0,0 +1,19 @@
+ - name: Set variables to verify lookup_list
+ set_fact: "{{ item if (item is string)|bool else item[0] }}={{ item }}"
+ with_list:
+ - a
+ - [b, c]
+ - d
+
+ - name: Verify lookup_list
+ assert:
+ that:
+ - a is defined
+ - b is defined
+ - c is not defined
+ - d is defined
+ - b is iterable and b is not string
+ - b|length == 2
+ - a == a
+ - b == ['b', 'c']
+ - d == d
diff --git a/test/integration/targets/lookup_nested/aliases b/test/integration/targets/lookup_nested/aliases
new file mode 100644
index 0000000..3005e4b
--- /dev/null
+++ b/test/integration/targets/lookup_nested/aliases
@@ -0,0 +1 @@
+shippable/posix/group4
diff --git a/test/integration/targets/lookup_nested/tasks/main.yml b/test/integration/targets/lookup_nested/tasks/main.yml
new file mode 100644
index 0000000..fec081a
--- /dev/null
+++ b/test/integration/targets/lookup_nested/tasks/main.yml
@@ -0,0 +1,18 @@
+- name: test with_nested
+ set_fact: "{{ item.0 + item.1 }}=x"
+ with_nested:
+ - [ 'a', 'b' ]
+ - [ 'c', 'd' ]
+
+- debug: var=ac
+- debug: var=ad
+- debug: var=bc
+- debug: var=bd
+
+- name: verify with_nested results
+ assert:
+ that:
+ - "ac == 'x'"
+ - "ad == 'x'"
+ - "bc == 'x'"
+ - "bd == 'x'"
diff --git a/test/integration/targets/lookup_password/aliases b/test/integration/targets/lookup_password/aliases
new file mode 100644
index 0000000..b598321
--- /dev/null
+++ b/test/integration/targets/lookup_password/aliases
@@ -0,0 +1 @@
+shippable/posix/group3
diff --git a/test/integration/targets/lookup_password/runme.sh b/test/integration/targets/lookup_password/runme.sh
new file mode 100755
index 0000000..a3637a7
--- /dev/null
+++ b/test/integration/targets/lookup_password/runme.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+set -eux
+
+source virtualenv.sh
+
+# Requirements have to be installed prior to running ansible-playbook
+# because plugins and requirements are loaded before the task runs
+pip install passlib
+
+ANSIBLE_ROLES_PATH=../ ansible-playbook runme.yml -e "output_dir=${OUTPUT_DIR}" "$@"
diff --git a/test/integration/targets/lookup_password/runme.yml b/test/integration/targets/lookup_password/runme.yml
new file mode 100644
index 0000000..4f55c1d
--- /dev/null
+++ b/test/integration/targets/lookup_password/runme.yml
@@ -0,0 +1,4 @@
+- hosts: localhost
+ gather_facts: no
+ roles:
+ - { role: lookup_password }
diff --git a/test/integration/targets/lookup_password/tasks/main.yml b/test/integration/targets/lookup_password/tasks/main.yml
new file mode 100644
index 0000000..dacf032
--- /dev/null
+++ b/test/integration/targets/lookup_password/tasks/main.yml
@@ -0,0 +1,149 @@
+- name: create a password file
+ set_fact:
+ newpass: "{{ lookup('password', output_dir + '/lookup/password length=8') }}"
+
+- name: stat the password file directory
+ stat: path="{{output_dir}}/lookup"
+ register: result
+
+- name: assert the directory's permissions
+ assert:
+ that:
+ - result.stat.mode == '0700'
+
+- name: stat the password file
+ stat: path="{{output_dir}}/lookup/password"
+ register: result
+
+- name: assert the directory's permissions
+ assert:
+ that:
+ - result.stat.mode == '0600'
+
+- name: get password length
+ shell: wc -c {{output_dir}}/lookup/password | awk '{print $1}'
+ register: wc_result
+
+- debug: var=wc_result.stdout
+
+- name: read password
+ shell: cat {{output_dir}}/lookup/password
+ register: cat_result
+
+- debug: var=cat_result.stdout
+
+- name: verify password
+ assert:
+ that:
+ - "wc_result.stdout == '9'"
+ - "cat_result.stdout == newpass"
+ - "' salt=' not in cat_result.stdout"
+
+- name: fetch password from an existing file
+ set_fact:
+ pass2: "{{ lookup('password', output_dir + '/lookup/password length=8') }}"
+
+- name: read password (again)
+ shell: cat {{output_dir}}/lookup/password
+ register: cat_result2
+
+- debug: var=cat_result2.stdout
+
+- name: verify password (again)
+ assert:
+ that:
+ - "cat_result2.stdout == newpass"
+ - "' salt=' not in cat_result2.stdout"
+
+
+
+- name: create a password (with salt) file
+ debug: msg={{ lookup('password', output_dir + '/lookup/password_with_salt encrypt=sha256_crypt') }}
+
+- name: read password and salt
+ shell: cat {{output_dir}}/lookup/password_with_salt
+ register: cat_pass_salt
+
+- debug: var=cat_pass_salt.stdout
+
+- name: fetch unencrypted password
+ set_fact:
+ newpass: "{{ lookup('password', output_dir + '/lookup/password_with_salt') }}"
+
+- debug: var=newpass
+
+- name: verify password and salt
+ assert:
+ that:
+ - "cat_pass_salt.stdout != newpass"
+ - "cat_pass_salt.stdout.startswith(newpass)"
+ - "' salt=' in cat_pass_salt.stdout"
+ - "' salt=' not in newpass"
+
+
+- name: fetch unencrypted password (using empty encrypt parameter)
+ set_fact:
+ newpass2: "{{ lookup('password', output_dir + '/lookup/password_with_salt encrypt=') }}"
+
+- name: verify lookup password behavior
+ assert:
+ that:
+ - "newpass == newpass2"
+
+- name: verify that we can generate a 1st password without writing it
+ set_fact:
+ newpass: "{{ lookup('password', '/dev/null') }}"
+
+- name: verify that we can generate a 2nd password without writing it
+ set_fact:
+ newpass2: "{{ lookup('password', '/dev/null') }}"
+
+- name: verify lookup password behavior with /dev/null
+ assert:
+ that:
+ - "newpass != newpass2"
+
+- name: test both types of args and that seed guarantees same results
+ vars:
+ pns: "{{passwords_noseed['results']}}"
+ inl: "{{passwords_inline['results']}}"
+ kv: "{{passwords['results']}}"
+ l: [1, 2, 3]
+ block:
+ - name: generate passwords w/o seed
+ debug:
+ msg: '{{ lookup("password", "/dev/null")}}'
+ loop: "{{ l }}"
+ register: passwords_noseed
+
+ - name: verify they are all different, this is not guaranteed, but statisically almost impossible
+ assert:
+ that:
+ - pns[0]['msg'] != pns[1]['msg']
+ - pns[0]['msg'] != pns[2]['msg']
+ - pns[1]['msg'] != pns[2]['msg']
+
+ - name: generate passwords, with seed inline
+ debug:
+ msg: '{{ lookup("password", "/dev/null seed=foo")}}'
+ loop: "{{ l }}"
+ register: passwords_inline
+
+ - name: verify they are all the same
+ assert:
+ that:
+ - inl[0]['msg'] == inl[1]['msg']
+ - inl[0]['msg'] == inl[2]['msg']
+
+ - name: generate passwords, with seed k=v
+ debug:
+ msg: '{{ lookup("password", "/dev/null", seed="foo")}}'
+ loop: "{{ l }}"
+ register: passwords
+
+ - name: verify they are all the same
+ assert:
+ that:
+ - kv[0]['msg'] == kv[1]['msg']
+ - kv[0]['msg'] == kv[2]['msg']
+ - kv[0]['msg'] == inl[0]['msg']
diff --git a/test/integration/targets/lookup_pipe/aliases b/test/integration/targets/lookup_pipe/aliases
new file mode 100644
index 0000000..b598321
--- /dev/null
+++ b/test/integration/targets/lookup_pipe/aliases
@@ -0,0 +1 @@
+shippable/posix/group3
diff --git a/test/integration/targets/lookup_pipe/tasks/main.yml b/test/integration/targets/lookup_pipe/tasks/main.yml
new file mode 100644
index 0000000..8aa1bc6
--- /dev/null
+++ b/test/integration/targets/lookup_pipe/tasks/main.yml
@@ -0,0 +1,9 @@
+# https://github.com/ansible/ansible/issues/6550
+- name: confirm pipe lookup works with a single positional arg
+ set_fact:
+ result: "{{ lookup('pipe', 'echo $OUTPUT_DIR') }}"
+
+- name: verify the expected output was received
+ assert:
+ that:
+ - "result == output_dir"
diff --git a/test/integration/targets/lookup_random_choice/aliases b/test/integration/targets/lookup_random_choice/aliases
new file mode 100644
index 0000000..3005e4b
--- /dev/null
+++ b/test/integration/targets/lookup_random_choice/aliases
@@ -0,0 +1 @@
+shippable/posix/group4
diff --git a/test/integration/targets/lookup_random_choice/tasks/main.yml b/test/integration/targets/lookup_random_choice/tasks/main.yml
new file mode 100644
index 0000000..e18126a
--- /dev/null
+++ b/test/integration/targets/lookup_random_choice/tasks/main.yml
@@ -0,0 +1,10 @@
+- name: test with_random_choice
+ set_fact: "random={{ item }}"
+ with_random_choice:
+ - "foo"
+ - "bar"
+
+- name: verify with_random_choice
+ assert:
+ that:
+ - "random in ['foo', 'bar']"
diff --git a/test/integration/targets/lookup_sequence/aliases b/test/integration/targets/lookup_sequence/aliases
new file mode 100644
index 0000000..3005e4b
--- /dev/null
+++ b/test/integration/targets/lookup_sequence/aliases
@@ -0,0 +1 @@
+shippable/posix/group4
diff --git a/test/integration/targets/lookup_sequence/tasks/main.yml b/test/integration/targets/lookup_sequence/tasks/main.yml
new file mode 100644
index 0000000..bd0a4d8
--- /dev/null
+++ b/test/integration/targets/lookup_sequence/tasks/main.yml
@@ -0,0 +1,198 @@
+- name: test with_sequence
+ set_fact: "{{ 'x' + item }}={{ item }}"
+ with_sequence: start=0 end=3
+
+- name: test with_sequence backwards
+ set_fact: "{{ 'y' + item }}={{ item }}"
+ with_sequence: start=3 end=0 stride=-1
+
+- name: verify with_sequence
+ assert:
+ that:
+ - "x0 == '0'"
+ - "x1 == '1'"
+ - "x2 == '2'"
+ - "x3 == '3'"
+ - "y3 == '3'"
+ - "y2 == '2'"
+ - "y1 == '1'"
+ - "y0 == '0'"
+
+- name: test with_sequence not failing on count == 0
+ debug: msg='previously failed with backward counting error'
+ with_sequence: count=0
+ register: count_of_zero
+
+- name: test with_sequence does 1 when start == end
+ debug: msg='should run once'
+ with_sequence: start=1 end=1
+ register: start_equal_end
+
+- name: test with_sequence count 1
+ set_fact: "{{ 'x' + item }}={{ item }}"
+ with_sequence: count=1
+ register: count_of_one
+
+- assert:
+ that:
+ - start_equal_end is not skipped
+ - count_of_zero is skipped
+ - count_of_one is not skipped
+
+- name: test with_sequence shortcut syntax (end)
+ set_fact: "{{ 'ws_z_' + item }}={{ item }}"
+ with_sequence: '4'
+
+- name: test with_sequence shortcut syntax (start-end/stride)
+ set_fact: "{{ 'ws_z_' + item }}=stride_{{ item }}"
+ with_sequence: '2-6/2'
+
+- name: test with_sequence shortcut syntax (start-end:format)
+ set_fact: "{{ 'ws_z_' + item }}={{ item }}"
+ with_sequence: '7-8:host%02d'
+
+- name: verify with_sequence shortcut syntax
+ assert:
+ that:
+ - "ws_z_1 == '1'"
+ - "ws_z_2 == 'stride_2'"
+ - "ws_z_3 == '3'"
+ - "ws_z_4 == 'stride_4'"
+ - "ws_z_6 == 'stride_6'"
+ - "ws_z_host07 == 'host07'"
+ - "ws_z_host08 == 'host08'"
+
+- block:
+ - name: EXPECTED FAILURE - test invalid arg
+ set_fact: "{{ 'x' + item }}={{ item }}"
+ with_sequence: start=0 junk=3
+
+ - fail:
+ msg: "should not get here"
+ rescue:
+ - assert:
+ that:
+ - ansible_failed_task.name == "EXPECTED FAILURE - test invalid arg"
+ - ansible_failed_result.msg in [expected1, expected2]
+ vars:
+ expected1: "unrecognized arguments to with_sequence: ['junk']"
+ expected2: "unrecognized arguments to with_sequence: [u'junk']"
+
+- block:
+ - name: EXPECTED FAILURE - test bad kv value
+ set_fact: "{{ 'x' + item }}={{ item }}"
+ with_sequence: start=A end=3
+
+ - fail:
+ msg: "should not get here"
+ rescue:
+ - assert:
+ that:
+ - ansible_failed_task.name == "EXPECTED FAILURE - test bad kv value"
+ - ansible_failed_result.msg == "can't parse start=A as integer"
+
+- block:
+ - name: EXPECTED FAILURE - test bad simple form start value
+ set_fact: "{{ 'x' + item }}={{ item }}"
+ with_sequence: A-4/2
+
+ - fail:
+ msg: "should not get here"
+ rescue:
+ - assert:
+ that:
+ - ansible_failed_task.name == "EXPECTED FAILURE - test bad simple form start value"
+ - ansible_failed_result.msg == "can't parse start=A as integer"
+
+- block:
+ - name: EXPECTED FAILURE - test bad simple form end value
+ set_fact: "{{ 'x' + item }}={{ item }}"
+ with_sequence: 1-B/2
+
+ - fail:
+ msg: "should not get here"
+ rescue:
+ - assert:
+ that:
+ - ansible_failed_task.name == "EXPECTED FAILURE - test bad simple form end value"
+ - ansible_failed_result.msg == "can't parse end=B as integer"
+
+- block:
+ - name: EXPECTED FAILURE - test bad simple form stride value
+ set_fact: "{{ 'x' + item }}={{ item }}"
+ with_sequence: 1-4/C
+
+ - fail:
+ msg: "should not get here"
+ rescue:
+ - assert:
+ that:
+ - ansible_failed_task.name == "EXPECTED FAILURE - test bad simple form stride value"
+ - ansible_failed_result.msg == "can't parse stride=C as integer"
+
+- block:
+ - name: EXPECTED FAILURE - test no count or end
+ set_fact: "{{ 'x' + item }}={{ item }}"
+ with_sequence: start=1
+
+ - fail:
+ msg: "should not get here"
+ rescue:
+ - assert:
+ that:
+ - ansible_failed_task.name == "EXPECTED FAILURE - test no count or end"
+ - ansible_failed_result.msg == "must specify count or end in with_sequence"
+
+- block:
+ - name: EXPECTED FAILURE - test both count and end
+ set_fact: "{{ 'x' + item }}={{ item }}"
+ with_sequence: start=1 end=4 count=2
+
+ - fail:
+ msg: "should not get here"
+ rescue:
+ - assert:
+ that:
+ - ansible_failed_task.name == "EXPECTED FAILURE - test both count and end"
+ - ansible_failed_result.msg == "can't specify both count and end in with_sequence"
+
+- block:
+ - name: EXPECTED FAILURE - test count backwards message
+ set_fact: "{{ 'x' + item }}={{ item }}"
+ with_sequence: start=4 end=1 stride=2
+
+ - fail:
+ msg: "should not get here"
+ rescue:
+ - assert:
+ that:
+ - ansible_failed_task.name == "EXPECTED FAILURE - test count backwards message"
+ - ansible_failed_result.msg == "to count backwards make stride negative"
+
+- block:
+ - name: EXPECTED FAILURE - test count forward message
+ set_fact: "{{ 'x' + item }}={{ item }}"
+ with_sequence: start=1 end=4 stride=-2
+
+ - fail:
+ msg: "should not get here"
+ rescue:
+ - assert:
+ that:
+ - ansible_failed_task.name == "EXPECTED FAILURE - test count forward message"
+ - ansible_failed_result.msg == "to count forward don't make stride negative"
+
+- block:
+ - name: EXPECTED FAILURE - test bad format string message
+ set_fact: "{{ 'x' + item }}={{ item }}"
+ with_sequence: start=1 end=4 format=d
+
+ - fail:
+ msg: "should not get here"
+ rescue:
+ - assert:
+ that:
+ - ansible_failed_task.name == "EXPECTED FAILURE - test bad format string message"
+ - ansible_failed_result.msg == expected
+ vars:
+ expected: "bad formatting string: d" \ No newline at end of file
diff --git a/test/integration/targets/lookup_subelements/aliases b/test/integration/targets/lookup_subelements/aliases
new file mode 100644
index 0000000..3005e4b
--- /dev/null
+++ b/test/integration/targets/lookup_subelements/aliases
@@ -0,0 +1 @@
+shippable/posix/group4
diff --git a/test/integration/targets/lookup_subelements/tasks/main.yml b/test/integration/targets/lookup_subelements/tasks/main.yml
new file mode 100644
index 0000000..9d93cf2
--- /dev/null
+++ b/test/integration/targets/lookup_subelements/tasks/main.yml
@@ -0,0 +1,224 @@
+- name: test with_subelements
+ set_fact: "{{ '_'+ item.0.id + item.1 }}={{ item.1 }}"
+ with_subelements:
+ - "{{element_data}}"
+ - the_list
+
+- name: verify with_subelements results
+ assert:
+ that:
+ - "_xf == 'f'"
+ - "_xd == 'd'"
+ - "_ye == 'e'"
+ - "_yf == 'f'"
+
+- name: test with_subelements in subkeys
+ set_fact: "{{ '_'+ item.0.id + item.1 }}={{ item.1 }}"
+ with_subelements:
+ - "{{element_data}}"
+ - the.sub.key.list
+
+- name: verify with_subelements in subkeys results
+ assert:
+ that:
+ - "_xq == 'q'"
+ - "_xr == 'r'"
+ - "_yi == 'i'"
+ - "_yo == 'o'"
+
+- name: test with_subelements with missing key or subkey
+ set_fact: "{{ '_'+ item.0.id + item.1 }}={{ item.1 }}"
+ with_subelements:
+ - "{{element_data_missing}}"
+ - the.sub.key.list
+ - skip_missing: yes
+ register: _subelements_missing_subkeys
+
+- debug: var=_subelements_missing_subkeys
+- debug: var=_subelements_missing_subkeys.results|length
+- name: verify with_subelements in subkeys results
+ assert:
+ that:
+ - _subelements_missing_subkeys is not skipped
+ - _subelements_missing_subkeys.results|length == 2
+ - "_xk == 'k'"
+ - "_xl == 'l'"
+
+# Example from the DOCUMENTATION block
+- set_fact:
+ users:
+ - name: alice
+ authorized:
+ - /tmp/alice/onekey.pub
+ - /tmp/alice/twokey.pub
+ mysql:
+ password: mysql-password
+ hosts:
+ - "%"
+ - "127.0.0.1"
+ - "::1"
+ - "localhost"
+ privs:
+ - "*.*:SELECT"
+ - "DB1.*:ALL"
+ groups:
+ - wheel
+ - name: bob
+ authorized:
+ - /tmp/bob/id_rsa.pub
+ mysql:
+ password: other-mysql-password
+ hosts:
+ - "db1"
+ privs:
+ - "*.*:SELECT"
+ - "DB2.*:ALL"
+ - name: carol
+ skipped: true
+ authorized:
+ - /tmp/carol/id_rsa.pub
+ mysql:
+ password: third-mysql-password
+ hosts:
+ - "db9"
+ privs:
+ - "*.*:SELECT"
+ - "DB9.*:ALL"
+
+
+- name: Ensure it errors properly with non-dict
+ set_fact:
+ err: "{{ lookup('subelements', 9001, 'groups', wantlist=true) }}"
+ ignore_errors: true
+ register: err1
+
+- assert:
+ that:
+ - err1 is failed
+ - "'expects a dictionary' in err1.msg"
+
+- name: Ensure it errors properly when pointing to non-list
+ set_fact:
+ err: "{{ lookup('subelements', users, 'mysql.password', wantlist=true) }}"
+ ignore_errors: true
+ register: err2
+
+- assert:
+ that:
+ - err2 is failed
+ - "'should point to a list' in err2.msg"
+
+- name: Ensure it properly skips missing keys
+ set_fact:
+ err: "{{ lookup('subelements', users, 'mysql.hosts.doesnotexist', wantlist=true) }}"
+ ignore_errors: true
+ register: err3
+
+- assert:
+ that:
+ - err3 is failed
+ - "'should point to a dictionary' in err3.msg"
+
+- name: Ensure it properly skips missing keys
+ set_fact:
+ err: "{{ lookup('subelements', users, 'mysql.monkey', wantlist=true) }}"
+ ignore_errors: true
+ register: err4
+
+- assert:
+ that:
+ - err4 is failed
+ - >-
+ "could not find 'monkey' key in iterated item" in err4.msg
+
+- assert:
+ that:
+ - "'{{ item.0.name }}' != 'carol'"
+ with_subelements:
+ - "{{ users }}"
+ - mysql.privs
+
+- name: Ensure it errors properly when optional arg is nonsensical
+ set_fact:
+ err: neverset
+ with_subelements:
+ - "{{ users }}"
+ - mysql.privs
+ - wolves
+ ignore_errors: true
+ register: err5
+
+- assert:
+ that:
+ - err5 is failed
+ - "'the optional third item must be a dict' in err5.msg"
+
+- name: Ensure it errors properly when given way too many args
+ set_fact:
+ err: neverset
+ with_subelements:
+ - "{{ users }}"
+ - mysql.privs
+ - wolves
+ - foo
+ - bar
+ - baz
+ - bye now
+ ignore_errors: true
+ register: err6
+
+- assert:
+ that:
+ - err6 is failed
+ - "'expects a list of two or three' in err6.msg"
+
+- name: Ensure it errors properly when second arg is invalid type
+ set_fact:
+ err: neverset
+ with_subelements:
+ - "{{ users }}"
+ - true
+ ignore_errors: true
+ register: err7
+
+- assert:
+ that:
+ - err7 is failed
+ - "'second a string' in err7.msg"
+
+- name: Ensure it errors properly when first arg is invalid type
+ set_fact:
+ err: neverset
+ with_subelements:
+ - true
+ - "{{ users }}"
+ ignore_errors: true
+ register: err8
+
+- assert:
+ that:
+ - err8 is failed
+ - "'first a dict or a list' in err8.msg"
+
+- set_fact:
+ empty_subelements: "{{ lookup('subelements', {'skipped': true}, 'mysql.hosts', wantlist=true) }}"
+
+- assert:
+ that:
+ - empty_subelements == []
+
+- set_fact:
+ some_dict:
+ key: "{{ users[0] }}"
+ another: "{{ users[1] }}"
+
+- name: Ensure it works when we give a dict instead of a list
+ set_fact: "user_{{ item.0.name }}={{ item.1 }}"
+ with_subelements:
+ - "{{ some_dict }}"
+ - mysql.hosts
+
+- assert:
+ that:
+ - "'{{ user_alice }}' == 'localhost'"
+ - "'{{ user_bob }}' == 'db1'"
diff --git a/test/integration/targets/lookup_subelements/vars/main.yml b/test/integration/targets/lookup_subelements/vars/main.yml
new file mode 100644
index 0000000..f7ef50f
--- /dev/null
+++ b/test/integration/targets/lookup_subelements/vars/main.yml
@@ -0,0 +1,43 @@
+element_data:
+ - id: x
+ the_list:
+ - "f"
+ - "d"
+ the:
+ sub:
+ key:
+ list:
+ - "q"
+ - "r"
+ - id: y
+ the_list:
+ - "e"
+ - "f"
+ the:
+ sub:
+ key:
+ list:
+ - "i"
+ - "o"
+element_data_missing:
+ - id: x
+ the_list:
+ - "f"
+ - "d"
+ the:
+ sub:
+ key:
+ list:
+ - "k"
+ - "l"
+ - id: y
+ the_list:
+ - "f"
+ - "d"
+ - id: z
+ the_list:
+ - "e"
+ - "f"
+ the:
+ sub:
+ key:
diff --git a/test/integration/targets/lookup_template/aliases b/test/integration/targets/lookup_template/aliases
new file mode 100644
index 0000000..b598321
--- /dev/null
+++ b/test/integration/targets/lookup_template/aliases
@@ -0,0 +1 @@
+shippable/posix/group3
diff --git a/test/integration/targets/lookup_template/tasks/main.yml b/test/integration/targets/lookup_template/tasks/main.yml
new file mode 100644
index 0000000..9ebdf0c
--- /dev/null
+++ b/test/integration/targets/lookup_template/tasks/main.yml
@@ -0,0 +1,34 @@
+# ref #18526
+- name: Test that we have a proper jinja search path in template lookup
+ set_fact:
+ hello_world: "{{ lookup('template', 'hello.txt') }}"
+
+- assert:
+ that:
+ - "hello_world|trim == 'Hello world!'"
+
+
+- name: Test that we have a proper jinja search path in template lookup with different variable start and end string
+ vars:
+ my_var: world
+ set_fact:
+ hello_world_string: "{{ lookup('template', 'hello_string.txt', variable_start_string='[%', variable_end_string='%]') }}"
+
+- assert:
+ that:
+ - "hello_world_string|trim == 'Hello world!'"
+
+- name: Test that we have a proper jinja search path in template lookup with different comment start and end string
+ set_fact:
+ hello_world_comment: "{{ lookup('template', 'hello_comment.txt', comment_start_string='[#', comment_end_string='#]') }}"
+
+- assert:
+ that:
+ - "hello_world_comment|trim == 'Hello world!'"
+
+# 77004
+- assert:
+ that:
+ - lookup('template', 'dict.j2') is mapping
+ - lookup('template', 'dict.j2', convert_data=True) is mapping
+ - lookup('template', 'dict.j2', convert_data=False) is not mapping
diff --git a/test/integration/targets/lookup_template/templates/dict.j2 b/test/integration/targets/lookup_template/templates/dict.j2
new file mode 100644
index 0000000..0439155
--- /dev/null
+++ b/test/integration/targets/lookup_template/templates/dict.j2
@@ -0,0 +1 @@
+{"foo": "{{ 'bar' }}"}
diff --git a/test/integration/targets/lookup_template/templates/hello.txt b/test/integration/targets/lookup_template/templates/hello.txt
new file mode 100644
index 0000000..be15a4f
--- /dev/null
+++ b/test/integration/targets/lookup_template/templates/hello.txt
@@ -0,0 +1 @@
+Hello {% include 'world.txt' %}!
diff --git a/test/integration/targets/lookup_template/templates/hello_comment.txt b/test/integration/targets/lookup_template/templates/hello_comment.txt
new file mode 100644
index 0000000..92af4b3
--- /dev/null
+++ b/test/integration/targets/lookup_template/templates/hello_comment.txt
@@ -0,0 +1,2 @@
+[# Comment #]
+Hello world!
diff --git a/test/integration/targets/lookup_template/templates/hello_string.txt b/test/integration/targets/lookup_template/templates/hello_string.txt
new file mode 100644
index 0000000..75199af
--- /dev/null
+++ b/test/integration/targets/lookup_template/templates/hello_string.txt
@@ -0,0 +1 @@
+Hello [% my_var %]!
diff --git a/test/integration/targets/lookup_template/templates/world.txt b/test/integration/targets/lookup_template/templates/world.txt
new file mode 100644
index 0000000..cc628cc
--- /dev/null
+++ b/test/integration/targets/lookup_template/templates/world.txt
@@ -0,0 +1 @@
+world
diff --git a/test/integration/targets/lookup_together/aliases b/test/integration/targets/lookup_together/aliases
new file mode 100644
index 0000000..3005e4b
--- /dev/null
+++ b/test/integration/targets/lookup_together/aliases
@@ -0,0 +1 @@
+shippable/posix/group4
diff --git a/test/integration/targets/lookup_together/tasks/main.yml b/test/integration/targets/lookup_together/tasks/main.yml
new file mode 100644
index 0000000..71365a1
--- /dev/null
+++ b/test/integration/targets/lookup_together/tasks/main.yml
@@ -0,0 +1,29 @@
+- name: test with_together
+ #shell: echo {{ item }}
+ set_fact: "{{ item.0 }}={{ item.1 }}"
+ with_together:
+ - [ 'a', 'b', 'c', 'd' ]
+ - [ '1', '2', '3', '4' ]
+
+- name: verify with_together results
+ assert:
+ that:
+ - "a == '1'"
+ - "b == '2'"
+ - "c == '3'"
+ - "d == '4'"
+
+- block:
+ - name: "EXPECTED FAILURE - test empty list"
+ debug:
+ msg: "{{ item.0 }} and {{ item.1 }}"
+ with_together: []
+
+ - fail:
+ msg: "should not get here"
+
+ rescue:
+ - assert:
+ that:
+ - ansible_failed_task.name == "EXPECTED FAILURE - test empty list"
+ - ansible_failed_result.msg == "with_together requires at least one element in each list" \ No newline at end of file
diff --git a/test/integration/targets/lookup_unvault/aliases b/test/integration/targets/lookup_unvault/aliases
new file mode 100644
index 0000000..8526691
--- /dev/null
+++ b/test/integration/targets/lookup_unvault/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+needs/root
diff --git a/test/integration/targets/lookup_unvault/files/foot.txt b/test/integration/targets/lookup_unvault/files/foot.txt
new file mode 100644
index 0000000..5716ca5
--- /dev/null
+++ b/test/integration/targets/lookup_unvault/files/foot.txt
@@ -0,0 +1 @@
+bar
diff --git a/test/integration/targets/lookup_unvault/files/foot.txt.vault b/test/integration/targets/lookup_unvault/files/foot.txt.vault
new file mode 100644
index 0000000..98ee41b
--- /dev/null
+++ b/test/integration/targets/lookup_unvault/files/foot.txt.vault
@@ -0,0 +1,6 @@
+$ANSIBLE_VAULT;1.1;AES256
+35363932323438383333343462373431376162373631636238353061616565323630656464393939
+3937313630326662336264636662313163343832643239630a646436313833633135353834343364
+63363039663765363365626531643533616232333533383239323234393934356639373136323635
+3632356163343031300a373766636130626237346630653537633764663063313439666135623032
+6139
diff --git a/test/integration/targets/lookup_unvault/runme.sh b/test/integration/targets/lookup_unvault/runme.sh
new file mode 100755
index 0000000..a7a0be5
--- /dev/null
+++ b/test/integration/targets/lookup_unvault/runme.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# run tests
+ansible-playbook unvault.yml --vault-password-file='secret' -v "$@"
diff --git a/test/integration/targets/lookup_unvault/secret b/test/integration/targets/lookup_unvault/secret
new file mode 100644
index 0000000..f925edd
--- /dev/null
+++ b/test/integration/targets/lookup_unvault/secret
@@ -0,0 +1 @@
+ssssshhhhhh
diff --git a/test/integration/targets/lookup_unvault/unvault.yml b/test/integration/targets/lookup_unvault/unvault.yml
new file mode 100644
index 0000000..f1f3b98
--- /dev/null
+++ b/test/integration/targets/lookup_unvault/unvault.yml
@@ -0,0 +1,9 @@
+- name: test vault lookup plugin
+ hosts: localhost
+ gather_facts: false
+ tasks:
+ - debug: msg={{lookup('unvault', 'foot.txt.vault')}}
+ - name: verify vault lookup works with both vaulted and unvaulted
+ assert:
+ that:
+ - lookup('unvault', 'foot.txt.vault') == lookup('unvault', 'foot.txt')
diff --git a/test/integration/targets/lookup_url/aliases b/test/integration/targets/lookup_url/aliases
new file mode 100644
index 0000000..ef37fce
--- /dev/null
+++ b/test/integration/targets/lookup_url/aliases
@@ -0,0 +1,4 @@
+destructive
+shippable/posix/group3
+needs/httptester
+skip/macos/12.0 # This test crashes Python due to https://wefearchange.org/2018/11/forkmacos.rst.html
diff --git a/test/integration/targets/lookup_url/meta/main.yml b/test/integration/targets/lookup_url/meta/main.yml
new file mode 100644
index 0000000..374b5fd
--- /dev/null
+++ b/test/integration/targets/lookup_url/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - prepare_http_tests
diff --git a/test/integration/targets/lookup_url/tasks/main.yml b/test/integration/targets/lookup_url/tasks/main.yml
new file mode 100644
index 0000000..a7de506
--- /dev/null
+++ b/test/integration/targets/lookup_url/tasks/main.yml
@@ -0,0 +1,54 @@
+- name: Test that retrieving a url works
+ set_fact:
+ web_data: "{{ lookup('url', 'https://gist.githubusercontent.com/abadger/9858c22712f62a8effff/raw/43dd47ea691c90a5fa7827892c70241913351963/test') }}"
+
+- name: Assert that the url was retrieved
+ assert:
+ that:
+ - "'one' in web_data"
+
+- name: Test that retrieving a url with invalid cert fails
+ set_fact:
+ web_data: "{{ lookup('url', 'https://{{ badssl_host }}/') }}"
+ ignore_errors: True
+ register: url_invalid_cert
+
+- assert:
+ that:
+ - "url_invalid_cert.failed"
+ - "'Error validating the server' in url_invalid_cert.msg or 'Hostname mismatch' in url_invalid_cert.msg or ( url_invalid_cert.msg is search('hostname .* doesn.t match .*'))"
+
+- name: Test that retrieving a url with invalid cert with validate_certs=False works
+ set_fact:
+ web_data: "{{ lookup('url', 'https://{{ badssl_host }}/', validate_certs=False) }}"
+ register: url_no_validate_cert
+
+- assert:
+ that:
+ - "'{{ badssl_host_substring }}' in web_data"
+
+- vars:
+ url: https://{{ httpbin_host }}/get
+ block:
+ - name: test good cipher
+ debug:
+ msg: '{{ lookup("url", url) }}'
+ vars:
+ ansible_lookup_url_ciphers: ECDHE-RSA-AES128-SHA256
+ register: good_ciphers
+
+ - name: test bad cipher
+ debug:
+ msg: '{{ lookup("url", url) }}'
+ vars:
+ ansible_lookup_url_ciphers: ECDHE-ECDSA-AES128-SHA
+ ignore_errors: true
+ register: bad_ciphers
+
+ - assert:
+ that:
+ - good_ciphers is successful
+ - bad_ciphers is failed
+
+- name: Test use_netrc=False
+ import_tasks: use_netrc.yml
diff --git a/test/integration/targets/lookup_url/tasks/use_netrc.yml b/test/integration/targets/lookup_url/tasks/use_netrc.yml
new file mode 100644
index 0000000..68dc893
--- /dev/null
+++ b/test/integration/targets/lookup_url/tasks/use_netrc.yml
@@ -0,0 +1,37 @@
+- name: Write out ~/.netrc
+ copy:
+ dest: "~/.netrc"
+ # writing directly to ~/.netrc because plug-in doesn't support NETRC environment overwrite
+ content: |
+ machine {{ httpbin_host }}
+ login foo
+ password bar
+ mode: "0600"
+
+- name: test Url lookup with ~/.netrc forced Basic auth
+ set_fact:
+ web_data: "{{ lookup('ansible.builtin.url', 'https://{{ httpbin_host }}/bearer', headers={'Authorization':'Bearer foobar'}) }}"
+ ignore_errors: yes
+
+- name: assert test Url lookup with ~/.netrc forced Basic auth
+ assert:
+ that:
+ - "web_data.token.find('v=' ~ 'Zm9vOmJhcg==') == -1"
+ fail_msg: "Was expecting 'foo:bar' in base64, but received: {{ web_data }}"
+ success_msg: "Expected Basic authentication even Bearer headers were sent"
+
+- name: test Url lookup with use_netrc=False
+ set_fact:
+ web_data: "{{ lookup('ansible.builtin.url', 'https://{{ httpbin_host }}/bearer', headers={'Authorization':'Bearer foobar'}, use_netrc='False') }}"
+
+- name: assert test Url lookup with netrc=False used Bearer authentication
+ assert:
+ that:
+ - "web_data.token.find('v=' ~ 'foobar') == -1"
+ fail_msg: "Was expecting 'foobar' Bearer token, but received: {{ web_data }}"
+ success_msg: "Expected to ignore ~/.netrc and authorize with Bearer token"
+
+- name: Clean up. Removing ~/.netrc
+ file:
+ path: ~/.netrc
+ state: absent \ No newline at end of file
diff --git a/test/integration/targets/lookup_varnames/aliases b/test/integration/targets/lookup_varnames/aliases
new file mode 100644
index 0000000..3005e4b
--- /dev/null
+++ b/test/integration/targets/lookup_varnames/aliases
@@ -0,0 +1 @@
+shippable/posix/group4
diff --git a/test/integration/targets/lookup_varnames/tasks/main.yml b/test/integration/targets/lookup_varnames/tasks/main.yml
new file mode 100644
index 0000000..fec3efd
--- /dev/null
+++ b/test/integration/targets/lookup_varnames/tasks/main.yml
@@ -0,0 +1,38 @@
+# Example copied from docs
+- name: Set some variables
+ set_fact:
+ qz_1: hello
+ qz_2: world
+ qa_1: "I won't show"
+ qz_: "I won't show either"
+
+- name: Try various regexes and make sure they work
+ assert:
+ that:
+ - lookup('varnames', '^qz_.+', wantlist=True) == ['qz_1', 'qz_2']
+ - lookup('varnames', '^qz_.+', '^qa.*', wantlist=True) == ['qz_1', 'qz_2', 'qa_1']
+ - "'ansible_python_interpreter' in lookup('varnames', '^ansible_.*', wantlist=True)"
+ - lookup('varnames', '^doesnotexist.*', wantlist=True) == []
+ - lookup('varnames', '^doesnotexist.*', '.*python_inter.*', wantlist=True) == ['ansible_python_interpreter']
+ - lookup('varnames', '^q.*_\d', wantlist=True) == ['qz_1', 'qz_2', 'qa_1']
+ - lookup('varnames', '^q.*_\d') == 'qz_1,qz_2,qa_1'
+
+- name: Make sure it fails successfully
+ set_fact:
+ fail1: "{{ lookup('varnames', True, wantlist=True) }}"
+ register: fail1res
+ ignore_errors: yes
+
+- name: Make sure it fails successfully
+ set_fact:
+ fail2: "{{ lookup('varnames', '*', wantlist=True) }}"
+ register: fail2res
+ ignore_errors: yes
+
+- assert:
+ that:
+ - fail1res is failed
+ - "'Invalid setting identifier' in fail1res.msg"
+ - fail2res is failed
+ - "'Unable to use' in fail2res.msg"
+ - "'nothing to repeat' in fail2res.msg"
diff --git a/test/integration/targets/lookup_vars/aliases b/test/integration/targets/lookup_vars/aliases
new file mode 100644
index 0000000..b598321
--- /dev/null
+++ b/test/integration/targets/lookup_vars/aliases
@@ -0,0 +1 @@
+shippable/posix/group3
diff --git a/test/integration/targets/lookup_vars/tasks/main.yml b/test/integration/targets/lookup_vars/tasks/main.yml
new file mode 100644
index 0000000..57b05b8
--- /dev/null
+++ b/test/integration/targets/lookup_vars/tasks/main.yml
@@ -0,0 +1,56 @@
+- name: Test that we can give it a single value and receive a single value
+ set_fact:
+ var_host: '{{ lookup("vars", "ansible_host") }}'
+
+- assert:
+ that:
+ - 'var_host == ansible_host'
+
+- name: Test that we can give a list of values to var and receive a list of values back
+ set_fact:
+ var_host_info: '{{ query("vars", "ansible_host", "ansible_connection") }}'
+
+- assert:
+ that:
+ - 'var_host_info[0] == ansible_host'
+ - 'var_host_info[1] == ansible_connection'
+
+- block:
+ - name: EXPECTED FAILURE - test invalid var
+ debug:
+ var: '{{ lookup("vars", "doesnotexist") }}'
+
+ - fail:
+ msg: "should not get here"
+
+ rescue:
+ - assert:
+ that:
+ - ansible_failed_task.name == "EXPECTED FAILURE - test invalid var"
+ - expected in ansible_failed_result.msg
+ vars:
+ expected: "No variable found with this name: doesnotexist"
+
+- block:
+ - name: EXPECTED FAILURE - test invalid var type
+ debug:
+ var: '{{ lookup("vars", 42) }}'
+
+ - fail:
+ msg: "should not get here"
+
+ rescue:
+ - assert:
+ that:
+ - ansible_failed_task.name == "EXPECTED FAILURE - test invalid var type"
+ - expected in ansible_failed_result.msg
+ vars:
+ expected: "Invalid setting identifier, \"42\" is not a string"
+
+- name: test default
+ set_fact:
+ expected_default_var: '{{ lookup("vars", "doesnotexist", default="some text") }}'
+
+- assert:
+ that:
+ - expected_default_var == "some text"
diff --git a/test/integration/targets/loop-connection/aliases b/test/integration/targets/loop-connection/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/loop-connection/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/loop-connection/collections/ansible_collections/ns/name/meta/runtime.yml b/test/integration/targets/loop-connection/collections/ansible_collections/ns/name/meta/runtime.yml
new file mode 100644
index 0000000..09322a9
--- /dev/null
+++ b/test/integration/targets/loop-connection/collections/ansible_collections/ns/name/meta/runtime.yml
@@ -0,0 +1,4 @@
+plugin_routing:
+ connection:
+ redirected_dummy:
+ redirect: ns.name.dummy \ No newline at end of file
diff --git a/test/integration/targets/loop-connection/collections/ansible_collections/ns/name/plugins/connection/dummy.py b/test/integration/targets/loop-connection/collections/ansible_collections/ns/name/plugins/connection/dummy.py
new file mode 100644
index 0000000..cb14991
--- /dev/null
+++ b/test/integration/targets/loop-connection/collections/ansible_collections/ns/name/plugins/connection/dummy.py
@@ -0,0 +1,50 @@
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = '''
+name: dummy
+short_description: Used for loop-connection tests
+description:
+- See above
+author: ansible (@core)
+'''
+
+from ansible.errors import AnsibleError
+from ansible.plugins.connection import ConnectionBase
+
+
+class Connection(ConnectionBase):
+
+ transport = 'ns.name.dummy'
+
+ def __init__(self, *args, **kwargs):
+ self._cmds_run = 0
+ super().__init__(*args, **kwargs)
+
+ @property
+ def connected(self):
+ return True
+
+ def _connect(self):
+ return
+
+ def exec_command(self, cmd, in_data=None, sudoable=True):
+ if 'become_test' in cmd:
+ stderr = f"become - {self.become.name if self.become else None}"
+
+ elif 'connected_test' in cmd:
+ self._cmds_run += 1
+ stderr = f"ran - {self._cmds_run}"
+
+ else:
+ raise AnsibleError(f"Unknown test cmd {cmd}")
+
+ return 0, cmd.encode(), stderr.encode()
+
+ def put_file(self, in_path, out_path):
+ return
+
+ def fetch_file(self, in_path, out_path):
+ return
+
+ def close(self):
+ return
diff --git a/test/integration/targets/loop-connection/main.yml b/test/integration/targets/loop-connection/main.yml
new file mode 100644
index 0000000..fbffe30
--- /dev/null
+++ b/test/integration/targets/loop-connection/main.yml
@@ -0,0 +1,33 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: test changing become activation on the same connection
+ raw: become_test
+ register: become_test
+ become: '{{ item }}'
+ vars:
+ ansible_connection: ns.name.dummy
+ loop:
+ - true
+ - false
+
+ - assert:
+ that:
+ - become_test.results[0].stderr == "become - sudo"
+ - become_test.results[0].stdout.startswith("sudo ")
+ - become_test.results[1].stderr == "become - None"
+ - become_test.results[1].stdout == "become_test"
+
+ - name: test loop reusing connection with redirected plugin name
+ raw: connected_test
+ register: connected_test
+ vars:
+ ansible_connection: ns.name.redirected_dummy
+ loop:
+ - 1
+ - 2
+
+ - assert:
+ that:
+ - connected_test.results[0].stderr == "ran - 1"
+ - connected_test.results[1].stderr == "ran - 2" \ No newline at end of file
diff --git a/test/integration/targets/loop-connection/runme.sh b/test/integration/targets/loop-connection/runme.sh
new file mode 100755
index 0000000..db4031d
--- /dev/null
+++ b/test/integration/targets/loop-connection/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eux -o pipefail
+
+ansible-playbook main.yml "$@"
diff --git a/test/integration/targets/loop-until/aliases b/test/integration/targets/loop-until/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/loop-until/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/loop-until/tasks/main.yml b/test/integration/targets/loop-until/tasks/main.yml
new file mode 100644
index 0000000..bb3799a
--- /dev/null
+++ b/test/integration/targets/loop-until/tasks/main.yml
@@ -0,0 +1,160 @@
+# Test code for integration of until and loop options
+# Copyright: (c) 2018, Ansible Project
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <https://www.gnu.org/licenses/>.
+- shell: '{{ ansible_python.executable }} -c "import tempfile; print(tempfile.mkstemp()[1])"'
+ register: tempfilepaths
+ # 0 to 3:
+ loop: "{{ range(0, 3 + 1) | list }}"
+
+- set_fact:
+ "until_tempfile_path_{{ idx }}": "{{ tmp_file.stdout }}"
+ until_tempfile_path_var_names: >
+ {{ [ 'until_tempfile_path_' + idx | string ] + until_tempfile_path_var_names | default([]) }}
+ loop: "{{ tempfilepaths.results }}"
+ loop_control:
+ index_var: idx
+ loop_var: tmp_file
+
+# `select` filter is only available since Jinja 2.7,
+# thus tests are failing under CentOS in CI
+#- set_fact:
+# until_tempfile_path_var_names: >
+# {{ vars | select('match', '^until_tempfile_path_') | list }}
+
+- name: loop and until with 6 retries
+ shell: echo "run" >> {{ lookup('vars', tmp_file_var) }} && wc -w < {{ lookup('vars', tmp_file_var) }} | tr -d ' '
+ register: runcount
+ until: runcount.stdout | int == idx + 3
+ retries: "{{ idx + 2 }}"
+ delay: 0.01
+ loop: "{{ until_tempfile_path_var_names }}"
+ loop_control:
+ index_var: idx
+ loop_var: tmp_file_var
+
+- debug: var=runcount
+
+- assert:
+ that: item.stdout | int == idx + 3
+ loop: "{{ runcount.results }}"
+ loop_control:
+ index_var: idx
+
+- &cleanup-tmp-files
+ name: Empty tmp files
+ copy:
+ content: ""
+ dest: "{{ lookup('vars', tmp_file_var) }}"
+ loop: "{{ until_tempfile_path_var_names }}"
+ loop_control:
+ index_var: idx
+ loop_var: tmp_file_var
+
+- name: loop with specified max retries
+ shell: echo "run" >> {{ lookup('vars', tmp_file_var) }}
+ until: 1==0
+ retries: 5
+ delay: 0.01
+ ignore_errors: true
+ loop: "{{ until_tempfile_path_var_names }}"
+ loop_control:
+ index_var: idx
+ loop_var: tmp_file_var
+
+- name: validate output
+ shell: wc -l < {{ lookup('vars', tmp_file_var) }}
+ register: runcount
+ loop: "{{ until_tempfile_path_var_names }}"
+ loop_control:
+ index_var: idx
+ loop_var: tmp_file_var
+
+- assert:
+ that: item.stdout | int == 6 # initial + 5 retries
+ loop: "{{ runcount.results }}"
+
+- *cleanup-tmp-files
+
+- name: Test failed_when impacting until
+ shell: echo "run" >> {{ lookup('vars', tmp_file_var) }}
+ register: failed_when_until
+ failed_when: True
+ until: failed_when_until is successful
+ retries: 3
+ delay: 0.5
+ ignore_errors: True
+ loop: "{{ until_tempfile_path_var_names }}"
+ loop_control:
+ index_var: idx
+ loop_var: tmp_file_var
+
+- name: Get attempts number
+ shell: wc -l < {{ lookup('vars', tmp_file_var) }}
+ register: runcount
+ loop: "{{ until_tempfile_path_var_names }}"
+ loop_control:
+ index_var: idx
+ loop_var: tmp_file_var
+
+- assert:
+ that: item.stdout | int == 3 + 1
+ loop: "{{ runcount.results }}"
+
+- *cleanup-tmp-files
+
+- name: Test changed_when impacting until
+ shell: echo "run" >> {{ lookup('vars', tmp_file_var) }}
+ register: changed_when_until
+ changed_when: False
+ until: changed_when_until is changed
+ retries: 3
+ delay: 0.5
+ ignore_errors: True
+ loop: "{{ until_tempfile_path_var_names }}"
+ loop_control:
+ index_var: idx
+ loop_var: tmp_file_var
+
+- name: Get attempts number
+ shell: wc -l < {{ lookup('vars', tmp_file_var) }}
+ register: runcount
+ loop: "{{ until_tempfile_path_var_names }}"
+ loop_control:
+ index_var: idx
+ loop_var: tmp_file_var
+
+- assert:
+ that: item.stdout | int == 3 + 1
+ loop: "{{ runcount.results }}"
+
+- *cleanup-tmp-files
+
+- name: Test access to attempts in changed_when/failed_when
+ shell: 'true'
+ register: changed_when_attempts
+ until: 1 == 0
+ retries: 5
+ delay: 0.5
+ failed_when: changed_when_attempts.attempts > 6
+ loop: "{{ runcount.results }}"
+
+- &wipe-out-tmp-files
+ file: path="{{ lookup('vars', tmp_file_var) }}" state=absent
+ loop: "{{ until_tempfile_path_var_names }}"
+ loop_control:
+ index_var: idx
+ loop_var: tmp_file_var
diff --git a/test/integration/targets/loop_control/aliases b/test/integration/targets/loop_control/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/loop_control/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/loop_control/extended.yml b/test/integration/targets/loop_control/extended.yml
new file mode 100644
index 0000000..3467d32
--- /dev/null
+++ b/test/integration/targets/loop_control/extended.yml
@@ -0,0 +1,23 @@
+- name: loop_control/extended/include https://github.com/ansible/ansible/issues/61218
+ hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: loop on an include
+ include_tasks: inner.yml
+ loop:
+ - first
+ - second
+ - third
+ loop_control:
+ extended: yes
+
+ - debug:
+ var: ansible_loop
+ loop:
+ - first
+ - second
+ - third
+ loop_control:
+ extended: yes
+ extended_allitems: no
+ failed_when: ansible_loop.allitems is defined
diff --git a/test/integration/targets/loop_control/inner.yml b/test/integration/targets/loop_control/inner.yml
new file mode 100644
index 0000000..1c286fa
--- /dev/null
+++ b/test/integration/targets/loop_control/inner.yml
@@ -0,0 +1,9 @@
+- name: assert ansible_loop variables in include_tasks
+ assert:
+ that:
+ - ansible_loop.index == ansible_loop.index0 + 1
+ - ansible_loop.revindex == ansible_loop.revindex0 + 1
+ - ansible_loop.first == {{ ansible_loop.index == 1 }}
+ - ansible_loop.last == {{ ansible_loop.index == ansible_loop.length }}
+ - ansible_loop.length == 3
+ - ansible_loop.allitems|join(',') == 'first,second,third'
diff --git a/test/integration/targets/loop_control/label.yml b/test/integration/targets/loop_control/label.yml
new file mode 100644
index 0000000..5ac85fd
--- /dev/null
+++ b/test/integration/targets/loop_control/label.yml
@@ -0,0 +1,23 @@
+- name: loop_control/label https://github.com/ansible/ansible/pull/36430
+ hosts: localhost
+ gather_facts: false
+ tasks:
+ - set_fact:
+ loopthis:
+ - name: foo
+ label: foo_label
+ - name: bar
+ label: bar_label
+
+ - name: check that item label is updated each iteration
+ debug:
+ msg: "{{ looped_var.name }}"
+ with_items: "{{ loopthis }}"
+ loop_control:
+ loop_var: looped_var
+ label: "looped_var {{ looped_var.label }}"
+#
+# - assert:
+# that:
+# - "output.results[0]['_ansible_item_label'] == 'looped_var foo_label'"
+# - "output.results[1]['_ansible_item_label'] == 'looped_var bar_label'"
diff --git a/test/integration/targets/loop_control/runme.sh b/test/integration/targets/loop_control/runme.sh
new file mode 100755
index 0000000..af065ea
--- /dev/null
+++ b/test/integration/targets/loop_control/runme.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# user output has:
+#ok: [localhost] => (item=looped_var foo_label) => {
+#ok: [localhost] => (item=looped_var bar_label) => {
+MATCH='foo_label
+bar_label'
+[ "$(ansible-playbook label.yml "$@" |grep 'item='|sed -e 's/^.*(item=looped_var \(.*\)).*$/\1/')" == "${MATCH}" ]
+
+ansible-playbook extended.yml "$@"
diff --git a/test/integration/targets/loops/aliases b/test/integration/targets/loops/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/loops/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/loops/files/data1.txt b/test/integration/targets/loops/files/data1.txt
new file mode 100644
index 0000000..b044a82
--- /dev/null
+++ b/test/integration/targets/loops/files/data1.txt
@@ -0,0 +1 @@
+ Hello World
diff --git a/test/integration/targets/loops/files/data2.txt b/test/integration/targets/loops/files/data2.txt
new file mode 100644
index 0000000..e9359ad
--- /dev/null
+++ b/test/integration/targets/loops/files/data2.txt
@@ -0,0 +1 @@
+ Olá Mundo
diff --git a/test/integration/targets/loops/tasks/index_var_tasks.yml b/test/integration/targets/loops/tasks/index_var_tasks.yml
new file mode 100644
index 0000000..fa9a5bd
--- /dev/null
+++ b/test/integration/targets/loops/tasks/index_var_tasks.yml
@@ -0,0 +1,3 @@
+- name: check that index var exists inside included tasks file
+ assert:
+ that: my_idx == item|int
diff --git a/test/integration/targets/loops/tasks/main.yml b/test/integration/targets/loops/tasks/main.yml
new file mode 100644
index 0000000..03c7c44
--- /dev/null
+++ b/test/integration/targets/loops/tasks/main.yml
@@ -0,0 +1,407 @@
+#
+# loop_control/pause
+#
+
+- name: Measure time before
+ shell: date +%s
+ register: before
+
+- debug:
+ var: i
+ with_sequence: count=3
+ loop_control:
+ loop_var: i
+ pause: 2
+
+- name: Measure time after
+ shell: date +%s
+ register: after
+
+# since there is 3 rounds, and 2 seconds between, it should last 4 seconds
+# we do not test the upper bound, since CI can lag significantly
+- assert:
+ that:
+ - '(after.stdout |int) - (before.stdout|int) >= 4'
+
+- name: test subsecond pause
+ block:
+ - name: Measure time before loop with .5s pause
+ set_fact:
+ times: "{{times|default([]) + [ now(fmt='%s.%f') ]}}"
+ with_sequence: count=3
+ loop_control:
+ pause: 0.6
+
+ - name: Debug times var
+ debug:
+ var: times
+
+ - vars:
+ tdiff: '{{ times[2]|float - times[0]|float }}'
+ block:
+ - name: Debug tdiff used in next task
+ debug:
+ msg: 'tdiff={{ tdiff }}'
+
+ - name: ensure lag, since there is 3 rounds, and 0.5 seconds between, it should last 1.2 seconds, but allowing leeway due to CI lag
+ assert:
+ that:
+ - tdiff|float >= 1.2
+ - tdiff|int < 3
+
+#
+# Tests of loop syntax with args
+#
+
+- name: Test that with_list works with a list
+ ping:
+ data: '{{ item }}'
+ with_list:
+ - 'Hello World'
+ - 'Olá Mundo'
+ register: results
+
+- name: Assert that we ran the module twice with the correct strings
+ assert:
+ that:
+ - 'results["results"][0]["ping"] == "Hello World"'
+ - 'results["results"][1]["ping"] == "Olá Mundo"'
+
+- name: Test that with_list works with a list inside a variable
+ ping:
+ data: '{{ item }}'
+ with_list: '{{ phrases }}'
+ register: results2
+
+- name: Assert that we ran the module twice with the correct strings
+ assert:
+ that:
+ - 'results2["results"][0]["ping"] == "Hello World"'
+ - 'results2["results"][1]["ping"] == "Olá Mundo"'
+
+- name: Test that loop works with a manual list
+ ping:
+ data: '{{ item }}'
+ loop:
+ - 'Hello World'
+ - 'Olá Mundo'
+ register: results3
+
+- name: Assert that we ran the module twice with the correct strings
+ assert:
+ that:
+ - 'results3["results"][0]["ping"] == "Hello World"'
+ - 'results3["results"][1]["ping"] == "Olá Mundo"'
+
+- name: Test that loop works with a list in a variable
+ ping:
+ data: '{{ item }}'
+ loop: '{{ phrases }}'
+ register: results4
+
+- name: Assert that we ran the module twice with the correct strings
+ assert:
+ that:
+ - 'results4["results"][0]["ping"] == "Hello World"'
+ - 'results4["results"][1]["ping"] == "Olá Mundo"'
+
+- name: Test that loop works with a list via the list lookup
+ ping:
+ data: '{{ item }}'
+ loop: '{{ lookup("list", "Hello World", "Olá Mundo", wantlist=True) }}'
+ register: results5
+
+- name: Assert that we ran the module twice with the correct strings
+ assert:
+ that:
+ - 'results5["results"][0]["ping"] == "Hello World"'
+ - 'results5["results"][1]["ping"] == "Olá Mundo"'
+
+- name: Test that loop works with a list in a variable via the list lookup
+ ping:
+ data: '{{ item }}'
+ loop: '{{ lookup("list", wantlist=True, *phrases) }}'
+ register: results6
+
+- name: Assert that we ran the module twice with the correct strings
+ assert:
+ that:
+ - 'results6["results"][0]["ping"] == "Hello World"'
+ - 'results6["results"][1]["ping"] == "Olá Mundo"'
+
+- name: Test that loop works with a list via the query lookup
+ ping:
+ data: '{{ item }}'
+ loop: '{{ query("list", "Hello World", "Olá Mundo") }}'
+ register: results7
+
+- name: Assert that we ran the module twice with the correct strings
+ assert:
+ that:
+ - 'results7["results"][0]["ping"] == "Hello World"'
+ - 'results7["results"][1]["ping"] == "Olá Mundo"'
+
+- name: Test that loop works with a list in a variable via the query lookup
+ ping:
+ data: '{{ item }}'
+ loop: '{{ q("list", *phrases) }}'
+ register: results8
+
+- name: Assert that we ran the module twice with the correct strings
+ assert:
+ that:
+ - 'results8["results"][0]["ping"] == "Hello World"'
+ - 'results8["results"][1]["ping"] == "Olá Mundo"'
+
+- name: Test that loop works with a list and keyword args
+ ping:
+ data: '{{ item }}'
+ loop: '{{ q("file", "data1.txt", "data2.txt", lstrip=True) }}'
+ register: results9
+
+- name: Assert that we ran the module twice with the correct strings
+ assert:
+ that:
+ - 'results9["results"][0]["ping"] == "Hello World"'
+ - 'results9["results"][1]["ping"] == "Olá Mundo"'
+
+- name: Test that loop works with a list in variable and keyword args
+ ping:
+ data: '{{ item }}'
+ loop: '{{ q("file", lstrip=True, *filenames) }}'
+ register: results10
+
+- name: Assert that we ran the module twice with the correct strings
+ assert:
+ that:
+ - 'results10["results"][0]["ping"] == "Hello World"'
+ - 'results10["results"][1]["ping"] == "Olá Mundo"'
+
+#
+# loop_control/index_var
+#
+
+- name: check that the index var is created and increments as expected
+ assert:
+ that: my_idx == item|int
+ with_sequence: start=0 count=3
+ loop_control:
+ index_var: my_idx
+
+- name: check that value of index var matches position of current item in source list
+ assert:
+ that: 'test_var.index(item) == my_idx'
+ vars:
+ test_var: ['a', 'b', 'c']
+ with_items: "{{ test_var }}"
+ loop_control:
+ index_var: my_idx
+
+- name: check index var with included tasks file
+ include_tasks: index_var_tasks.yml
+ with_sequence: start=0 count=3
+ loop_control:
+ index_var: my_idx
+
+
+# The following test cases are to ensure that we don't have a regression on
+# GitHub Issue https://github.com/ansible/ansible/issues/35481
+#
+# This should execute and not cause a RuntimeError
+- debug:
+ msg: "with_dict passed a list: {{item}}"
+ with_dict: "{{ a_list }}"
+ register: with_dict_passed_a_list
+ ignore_errors: True
+ vars:
+ a_list:
+ - 1
+ - 2
+- assert:
+ that:
+ - with_dict_passed_a_list is failed
+ - '"with_dict expects a dict" in with_dict_passed_a_list.msg'
+- debug:
+ msg: "with_list passed a dict: {{item}}"
+ with_list: "{{ a_dict }}"
+ register: with_list_passed_a_dict
+ ignore_errors: True
+ vars:
+ a_dict:
+ key: value
+- assert:
+ that:
+ - with_list_passed_a_dict is failed
+ - '"with_list expects a list" in with_list_passed_a_dict.msg'
+
+- debug:
+ var: "item"
+ loop:
+ - "{{ ansible_search_path }}"
+ register: loop_search_path
+
+- assert:
+ that:
+ - ansible_search_path == loop_search_path.results.0.item
+
+# https://github.com/ansible/ansible/issues/45189
+- name: with_X conditional delegate_to shortcircuit on templating error
+ debug:
+ msg: "loop"
+ when: false
+ delegate_to: localhost
+ with_list: "{{ fake_var }}"
+ register: result
+ failed_when: result is not skipped
+
+- name: loop conditional delegate_to shortcircuit on templating error
+ debug:
+ msg: "loop"
+ when: false
+ delegate_to: localhost
+ loop: "{{ fake_var }}"
+ register: result
+ failed_when: result is not skipped
+
+- name: Loop on literal empty list
+ debug:
+ loop: []
+ register: literal_empty_list
+ failed_when: literal_empty_list is not skipped
+
+# https://github.com/ansible/ansible/issues/47372
+- name: Loop unsafe list
+ debug:
+ var: item
+ with_items: "{{ things|list|unique }}"
+ vars:
+ things:
+ - !unsafe foo
+ - !unsafe bar
+
+- name: extended loop info
+ assert:
+ that:
+ - ansible_loop.nextitem == 'orange'
+ - ansible_loop.index == 1
+ - ansible_loop.index0 == 0
+ - ansible_loop.first
+ - not ansible_loop.last
+ - ansible_loop.previtem is undefined
+ - ansible_loop.allitems == ['apple', 'orange', 'banana']
+ - ansible_loop.revindex == 3
+ - ansible_loop.revindex0 == 2
+ - ansible_loop.length == 3
+ loop:
+ - apple
+ - orange
+ - banana
+ loop_control:
+ extended: true
+ when: item == 'apple'
+
+- name: extended loop info 2
+ assert:
+ that:
+ - ansible_loop.nextitem == 'banana'
+ - ansible_loop.index == 2
+ - ansible_loop.index0 == 1
+ - not ansible_loop.first
+ - not ansible_loop.last
+ - ansible_loop.previtem == 'apple'
+ - ansible_loop.allitems == ['apple', 'orange', 'banana']
+ - ansible_loop.revindex == 2
+ - ansible_loop.revindex0 == 1
+ - ansible_loop.length == 3
+ loop:
+ - apple
+ - orange
+ - banana
+ loop_control:
+ extended: true
+ when: item == 'orange'
+
+- name: extended loop info 3
+ assert:
+ that:
+ - ansible_loop.nextitem is undefined
+ - ansible_loop.index == 3
+ - ansible_loop.index0 == 2
+ - not ansible_loop.first
+ - ansible_loop.last
+ - ansible_loop.previtem == 'orange'
+ - ansible_loop.allitems == ['apple', 'orange', 'banana']
+ - ansible_loop.revindex == 1
+ - ansible_loop.revindex0 == 0
+ - ansible_loop.length == 3
+ loop:
+ - apple
+ - orange
+ - banana
+ loop_control:
+ extended: true
+ when: item == 'banana'
+
+- name: Validate the loop_var name
+ assert:
+ that:
+ - ansible_loop_var == 'alvin'
+ loop:
+ - 1
+ loop_control:
+ loop_var: alvin
+
+# https://github.com/ansible/ansible/issues/58820
+- name: Test using templated loop_var inside include_tasks
+ include_tasks: templated_loop_var_tasks.yml
+ loop:
+ - value
+ loop_control:
+ loop_var: "{{ loop_var_name }}"
+ vars:
+ loop_var_name: templated_loop_var_name
+
+# https://github.com/ansible/ansible/issues/59414
+- name: Test preserving original connection related vars
+ debug:
+ var: ansible_remote_tmp
+ vars:
+ ansible_remote_tmp: /tmp/test1
+ with_items:
+ - 1
+ - 2
+ register: loop_out
+
+- assert:
+ that:
+ - loop_out['results'][1]['ansible_remote_tmp'] == '/tmp/test1'
+
+# https://github.com/ansible/ansible/issues/64169
+- include_vars: 64169.yml
+
+- set_fact: "{{ item.key }}={{ hostvars[inventory_hostname][item.value] }}"
+ with_dict:
+ foo: __foo
+
+- debug:
+ var: foo
+
+- assert:
+ that:
+ - foo[0] != 'foo1.0'
+ - foo[0] == unsafe_value
+ vars:
+ unsafe_value: !unsafe 'foo{{ version_64169 }}'
+
+- set_fact: "{{ item.key }}={{ hostvars[inventory_hostname][item.value] }}"
+ loop: "{{ dicty_dict|dict2items }}"
+ vars:
+ dicty_dict:
+ foo: __foo
+
+- debug:
+ var: foo
+
+- assert:
+ that:
+ - foo[0] == 'foo1.0'
diff --git a/test/integration/targets/loops/tasks/templated_loop_var_tasks.yml b/test/integration/targets/loops/tasks/templated_loop_var_tasks.yml
new file mode 100644
index 0000000..1f8f969
--- /dev/null
+++ b/test/integration/targets/loops/tasks/templated_loop_var_tasks.yml
@@ -0,0 +1,4 @@
+- name: Validate that the correct value was used
+ assert:
+ that:
+ - templated_loop_var_name == 'value'
diff --git a/test/integration/targets/loops/vars/64169.yml b/test/integration/targets/loops/vars/64169.yml
new file mode 100644
index 0000000..f48d616
--- /dev/null
+++ b/test/integration/targets/loops/vars/64169.yml
@@ -0,0 +1,2 @@
+__foo:
+ - "foo{{ version_64169 }}"
diff --git a/test/integration/targets/loops/vars/main.yml b/test/integration/targets/loops/vars/main.yml
new file mode 100644
index 0000000..5d85370
--- /dev/null
+++ b/test/integration/targets/loops/vars/main.yml
@@ -0,0 +1,8 @@
+---
+phrases:
+ - 'Hello World'
+ - 'Olá Mundo'
+filenames:
+ - 'data1.txt'
+ - 'data2.txt'
+version_64169: '1.0'
diff --git a/test/integration/targets/meta_tasks/aliases b/test/integration/targets/meta_tasks/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/meta_tasks/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/meta_tasks/inventory.yml b/test/integration/targets/meta_tasks/inventory.yml
new file mode 100644
index 0000000..5fb39e5
--- /dev/null
+++ b/test/integration/targets/meta_tasks/inventory.yml
@@ -0,0 +1,9 @@
+local:
+ hosts:
+ testhost:
+ host_var_role_name: role3
+ testhost2:
+ host_var_role_name: role2
+ vars:
+ ansible_connection: local
+ ansible_python_interpreter: "{{ ansible_playbook_python }}"
diff --git a/test/integration/targets/meta_tasks/inventory_new.yml b/test/integration/targets/meta_tasks/inventory_new.yml
new file mode 100644
index 0000000..6d6dec7
--- /dev/null
+++ b/test/integration/targets/meta_tasks/inventory_new.yml
@@ -0,0 +1,8 @@
+all:
+ hosts:
+ two:
+ parity: even
+ three:
+ parity: odd
+ four:
+ parity: even
diff --git a/test/integration/targets/meta_tasks/inventory_old.yml b/test/integration/targets/meta_tasks/inventory_old.yml
new file mode 100644
index 0000000..42d9bdd
--- /dev/null
+++ b/test/integration/targets/meta_tasks/inventory_old.yml
@@ -0,0 +1,8 @@
+all:
+ hosts:
+ one:
+ parity: odd
+ two:
+ parity: even
+ three:
+ parity: odd
diff --git a/test/integration/targets/meta_tasks/inventory_refresh.yml b/test/integration/targets/meta_tasks/inventory_refresh.yml
new file mode 100644
index 0000000..42d9bdd
--- /dev/null
+++ b/test/integration/targets/meta_tasks/inventory_refresh.yml
@@ -0,0 +1,8 @@
+all:
+ hosts:
+ one:
+ parity: odd
+ two:
+ parity: even
+ three:
+ parity: odd
diff --git a/test/integration/targets/meta_tasks/refresh.yml b/test/integration/targets/meta_tasks/refresh.yml
new file mode 100644
index 0000000..ac24b7d
--- /dev/null
+++ b/test/integration/targets/meta_tasks/refresh.yml
@@ -0,0 +1,38 @@
+- hosts: all
+ gather_facts: false
+ tasks:
+ - block:
+ - name: check initial state
+ assert:
+ that:
+ - "'one' in ansible_play_hosts"
+ - "'two' in ansible_play_hosts"
+ - "'three' in ansible_play_hosts"
+ - "'four' not in ansible_play_hosts"
+ run_once: true
+
+ - name: change symlink
+ file: src=./inventory_new.yml dest=./inventory_refresh.yml state=link force=yes follow=false
+ delegate_to: localhost
+ run_once: true
+
+ - name: refresh the inventory to new source
+ meta: refresh_inventory
+
+ always:
+ - name: revert symlink, invenotry was already reread or failed
+ file: src=./inventory_old.yml dest=./inventory_refresh.yml state=link force=yes follow=false
+ delegate_to: localhost
+ run_once: true
+
+- hosts: all
+ gather_facts: false
+ tasks:
+ - name: check refreshed state
+ assert:
+ that:
+ - "'one' not in ansible_play_hosts"
+ - "'two' in ansible_play_hosts"
+ - "'three' in ansible_play_hosts"
+ - "'four' in ansible_play_hosts"
+ run_once: true
diff --git a/test/integration/targets/meta_tasks/refresh_preserve_dynamic.yml b/test/integration/targets/meta_tasks/refresh_preserve_dynamic.yml
new file mode 100644
index 0000000..7766a38
--- /dev/null
+++ b/test/integration/targets/meta_tasks/refresh_preserve_dynamic.yml
@@ -0,0 +1,69 @@
+- hosts: all
+ gather_facts: false
+ tasks:
+ - name: check initial state
+ assert:
+ that:
+ - "'one' in ansible_play_hosts"
+ - "'two' in ansible_play_hosts"
+ - "'three' in ansible_play_hosts"
+ - "'four' not in ansible_play_hosts"
+ run_once: true
+
+ - name: add a host
+ add_host:
+ name: yolo
+ parity: null
+
+ - name: group em
+ group_by:
+ key: '{{parity}}'
+
+- hosts: all
+ gather_facts: false
+ tasks:
+ - name: test and ensure we restore symlink
+ run_once: true
+ block:
+ - name: check added host state
+ assert:
+ that:
+ - "'yolo' in ansible_play_hosts"
+ - "'even' in groups"
+ - "'odd' in groups"
+ - "'two' in groups['even']"
+ - "'three' in groups['odd']"
+
+ - name: change symlink
+ file: src=./inventory_new.yml dest=./inventory_refresh.yml state=link force=yes follow=false
+ delegate_to: localhost
+
+ - name: refresh the inventory to new source
+ meta: refresh_inventory
+
+ always:
+ - name: revert symlink, invenotry was already reread or failed
+ file: src=./inventory_old.yml dest=./inventory_refresh.yml state=link force=yes follow=false
+ delegate_to: localhost
+
+- hosts: all
+ gather_facts: false
+ tasks:
+ - name: check refreshed state
+ assert:
+ that:
+ - "'one' not in ansible_play_hosts"
+ - "'two' in ansible_play_hosts"
+ - "'three' in ansible_play_hosts"
+ - "'four' in ansible_play_hosts"
+ run_once: true
+
+ - name: check added host state
+ assert:
+ that:
+ - "'yolo' in ansible_play_hosts"
+ - "'even' in groups"
+ - "'odd' in groups"
+ - "'two' in groups['even']"
+ - "'three' in groups['odd']"
+ run_once: true
diff --git a/test/integration/targets/meta_tasks/runme.sh b/test/integration/targets/meta_tasks/runme.sh
new file mode 100755
index 0000000..f7d8d89
--- /dev/null
+++ b/test/integration/targets/meta_tasks/runme.sh
@@ -0,0 +1,78 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# test end_host meta task, with when conditional
+for test_strategy in linear free; do
+ out="$(ansible-playbook test_end_host.yml -i inventory.yml -e test_strategy=$test_strategy -vv "$@")"
+
+ grep -q "META: end_host conditional evaluated to False, continuing execution for testhost" <<< "$out"
+ grep -q "META: ending play for testhost2" <<< "$out"
+ grep -q '"skip_reason": "end_host conditional evaluated to False, continuing execution for testhost"' <<< "$out"
+ grep -q "play not ended for testhost" <<< "$out"
+ grep -qv "play not ended for testhost2" <<< "$out"
+
+ out="$(ansible-playbook test_end_host_fqcn.yml -i inventory.yml -e test_strategy=$test_strategy -vv "$@")"
+
+ grep -q "META: end_host conditional evaluated to False, continuing execution for testhost" <<< "$out"
+ grep -q "META: ending play for testhost2" <<< "$out"
+ grep -q '"skip_reason": "end_host conditional evaluated to False, continuing execution for testhost"' <<< "$out"
+ grep -q "play not ended for testhost" <<< "$out"
+ grep -qv "play not ended for testhost2" <<< "$out"
+done
+
+# test end_host meta task, on all hosts
+for test_strategy in linear free; do
+ out="$(ansible-playbook test_end_host_all.yml -i inventory.yml -e test_strategy=$test_strategy -vv "$@")"
+
+ grep -q "META: ending play for testhost" <<< "$out"
+ grep -q "META: ending play for testhost2" <<< "$out"
+ grep -qv "play not ended for testhost" <<< "$out"
+ grep -qv "play not ended for testhost2" <<< "$out"
+
+ out="$(ansible-playbook test_end_host_all_fqcn.yml -i inventory.yml -e test_strategy=$test_strategy -vv "$@")"
+
+ grep -q "META: ending play for testhost" <<< "$out"
+ grep -q "META: ending play for testhost2" <<< "$out"
+ grep -qv "play not ended for testhost" <<< "$out"
+ grep -qv "play not ended for testhost2" <<< "$out"
+done
+
+# test end_play meta task
+for test_strategy in linear free; do
+ out="$(ansible-playbook test_end_play.yml -i inventory.yml -e test_strategy=$test_strategy -vv "$@")"
+
+ grep -q "META: ending play" <<< "$out"
+ grep -qv 'Failed to end using end_play' <<< "$out"
+
+ out="$(ansible-playbook test_end_play_fqcn.yml -i inventory.yml -e test_strategy=$test_strategy -vv "$@")"
+
+ grep -q "META: ending play" <<< "$out"
+ grep -qv 'Failed to end using end_play' <<< "$out"
+
+ out="$(ansible-playbook test_end_play_serial_one.yml -i inventory.yml -e test_strategy=$test_strategy -vv "$@")"
+
+ [ "$(grep -c "Testing end_play on host" <<< "$out" )" -eq 1 ]
+ grep -q "META: ending play" <<< "$out"
+ grep -qv 'Failed to end using end_play' <<< "$out"
+
+ out="$(ansible-playbook test_end_play_multiple_plays.yml -i inventory.yml -e test_strategy=$test_strategy -vv "$@")"
+
+ grep -q "META: ending play" <<< "$out"
+ grep -q "Play 1" <<< "$out"
+ grep -q "Play 2" <<< "$out"
+ grep -qv 'Failed to end using end_play' <<< "$out"
+done
+
+# test end_batch meta task
+for test_strategy in linear free; do
+ out="$(ansible-playbook test_end_batch.yml -i inventory.yml -e test_strategy=$test_strategy -vv "$@")"
+
+ [ "$(grep -c "Using end_batch" <<< "$out" )" -eq 2 ]
+ [ "$(grep -c "META: ending batch" <<< "$out" )" -eq 2 ]
+ grep -qv 'Failed to end_batch' <<< "$out"
+done
+
+# test refresh
+ansible-playbook -i inventory_refresh.yml refresh.yml "$@"
+ansible-playbook -i inventory_refresh.yml refresh_preserve_dynamic.yml "$@"
diff --git a/test/integration/targets/meta_tasks/test_end_batch.yml b/test/integration/targets/meta_tasks/test_end_batch.yml
new file mode 100644
index 0000000..4af020a
--- /dev/null
+++ b/test/integration/targets/meta_tasks/test_end_batch.yml
@@ -0,0 +1,13 @@
+- name: Testing end_batch with strategy {{ test_strategy | default('linear') }}
+ hosts: testhost:testhost2
+ gather_facts: no
+ serial: 1
+ strategy: "{{ test_strategy | default('linear') }}"
+ tasks:
+ - debug:
+ msg: "Using end_batch, current host: {{ inventory_hostname }}, current batch: {{ ansible_play_batch }}"
+
+ - meta: end_batch
+
+ - fail:
+ msg: "Failed to end_batch, current host: {{ inventory_hostname }}, current batch: {{ ansible_play_batch }}"
diff --git a/test/integration/targets/meta_tasks/test_end_host.yml b/test/integration/targets/meta_tasks/test_end_host.yml
new file mode 100644
index 0000000..a8bb056
--- /dev/null
+++ b/test/integration/targets/meta_tasks/test_end_host.yml
@@ -0,0 +1,14 @@
+- name: "Testing end_host with strategy={{ test_strategy | default('linear') }}"
+ hosts:
+ - testhost
+ - testhost2
+ gather_facts: no
+ strategy: "{{ test_strategy | default('linear') }}"
+ tasks:
+ - debug:
+
+ - meta: end_host
+ when: "host_var_role_name == 'role2'" # end play for testhost2, see inventory
+
+ - debug:
+ msg: "play not ended for {{ inventory_hostname }}"
diff --git a/test/integration/targets/meta_tasks/test_end_host_all.yml b/test/integration/targets/meta_tasks/test_end_host_all.yml
new file mode 100644
index 0000000..dab5e88
--- /dev/null
+++ b/test/integration/targets/meta_tasks/test_end_host_all.yml
@@ -0,0 +1,13 @@
+- name: "Testing end_host all hosts with strategy={{ test_strategy | default('linear') }}"
+ hosts:
+ - testhost
+ - testhost2
+ gather_facts: no
+ strategy: "{{ test_strategy | default('linear') }}"
+ tasks:
+ - debug:
+
+ - meta: end_host
+
+ - debug:
+ msg: "play not ended {{ inventory_hostname }}"
diff --git a/test/integration/targets/meta_tasks/test_end_host_all_fqcn.yml b/test/integration/targets/meta_tasks/test_end_host_all_fqcn.yml
new file mode 100644
index 0000000..78b5a2e
--- /dev/null
+++ b/test/integration/targets/meta_tasks/test_end_host_all_fqcn.yml
@@ -0,0 +1,13 @@
+- name: "Testing end_host all hosts with strategy={{ test_strategy | default('linear') }}"
+ hosts:
+ - testhost
+ - testhost2
+ gather_facts: no
+ strategy: "{{ test_strategy | default('linear') }}"
+ tasks:
+ - debug:
+
+ - ansible.builtin.meta: end_host
+
+ - debug:
+ msg: "play not ended {{ inventory_hostname }}"
diff --git a/test/integration/targets/meta_tasks/test_end_host_fqcn.yml b/test/integration/targets/meta_tasks/test_end_host_fqcn.yml
new file mode 100644
index 0000000..bdb38b5
--- /dev/null
+++ b/test/integration/targets/meta_tasks/test_end_host_fqcn.yml
@@ -0,0 +1,14 @@
+- name: "Testing end_host with strategy={{ test_strategy | default('linear') }}"
+ hosts:
+ - testhost
+ - testhost2
+ gather_facts: no
+ strategy: "{{ test_strategy | default('linear') }}"
+ tasks:
+ - debug:
+
+ - ansible.builtin.meta: end_host
+ when: "host_var_role_name == 'role2'" # end play for testhost2, see inventory
+
+ - debug:
+ msg: "play not ended for {{ inventory_hostname }}"
diff --git a/test/integration/targets/meta_tasks/test_end_play.yml b/test/integration/targets/meta_tasks/test_end_play.yml
new file mode 100644
index 0000000..29489dc
--- /dev/null
+++ b/test/integration/targets/meta_tasks/test_end_play.yml
@@ -0,0 +1,12 @@
+- name: Testing end_play with strategy {{ test_strategy | default('linear') }}
+ hosts: testhost:testhost2
+ gather_facts: no
+ strategy: "{{ test_strategy | default('linear') }}"
+ tasks:
+ - debug:
+ msg: "Testing end_play on host {{ inventory_hostname }}"
+
+ - meta: end_play
+
+ - fail:
+ msg: 'Failed to end using end_play'
diff --git a/test/integration/targets/meta_tasks/test_end_play_fqcn.yml b/test/integration/targets/meta_tasks/test_end_play_fqcn.yml
new file mode 100644
index 0000000..2ae67fb
--- /dev/null
+++ b/test/integration/targets/meta_tasks/test_end_play_fqcn.yml
@@ -0,0 +1,12 @@
+- name: Testing end_play with strategy {{ test_strategy | default('linear') }}
+ hosts: testhost:testhost2
+ gather_facts: no
+ strategy: "{{ test_strategy | default('linear') }}"
+ tasks:
+ - debug:
+ msg: "Testing end_play on host {{ inventory_hostname }}"
+
+ - ansible.builtin.meta: end_play
+
+ - fail:
+ msg: 'Failed to end using end_play'
diff --git a/test/integration/targets/meta_tasks/test_end_play_multiple_plays.yml b/test/integration/targets/meta_tasks/test_end_play_multiple_plays.yml
new file mode 100644
index 0000000..2cc8d1e
--- /dev/null
+++ b/test/integration/targets/meta_tasks/test_end_play_multiple_plays.yml
@@ -0,0 +1,18 @@
+- name: Testing end_play with multiple plays with strategy {{ test_strategy | default('linear') }}
+ hosts: testhost
+ gather_facts: no
+ strategy: "{{ test_strategy | default('linear') }}"
+ tasks:
+ - debug:
+ msg: "Play 1"
+ - meta: end_play
+ - fail:
+ msg: 'Failed to end using end_play'
+
+- name: Testing end_play with multiple plays with strategy {{ test_strategy | default('linear') }}
+ hosts: testhost
+ gather_facts: no
+ strategy: "{{ test_strategy | default('linear') }}"
+ tasks:
+ - debug:
+ msg: "Play 2"
diff --git a/test/integration/targets/meta_tasks/test_end_play_serial_one.yml b/test/integration/targets/meta_tasks/test_end_play_serial_one.yml
new file mode 100644
index 0000000..f838d4a
--- /dev/null
+++ b/test/integration/targets/meta_tasks/test_end_play_serial_one.yml
@@ -0,0 +1,13 @@
+- name: Testing end_play with serial 1 and strategy {{ test_strategy | default('linear') }}
+ hosts: testhost:testhost2
+ gather_facts: no
+ serial: 1
+ strategy: "{{ test_strategy | default('linear') }}"
+ tasks:
+ - debug:
+ msg: "Testing end_play on host {{ inventory_hostname }}"
+
+ - meta: end_play
+
+ - fail:
+ msg: 'Failed to end using end_play'
diff --git a/test/integration/targets/missing_required_lib/aliases b/test/integration/targets/missing_required_lib/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/missing_required_lib/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/missing_required_lib/library/missing_required_lib.py b/test/integration/targets/missing_required_lib/library/missing_required_lib.py
new file mode 100644
index 0000000..480ea00
--- /dev/null
+++ b/test/integration/targets/missing_required_lib/library/missing_required_lib.py
@@ -0,0 +1,37 @@
+#!/usr/bin/python
+# Copyright: (c) 2020, Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+
+try:
+ import ansible_missing_lib
+ HAS_LIB = True
+except ImportError as e:
+ HAS_LIB = False
+
+
+def main():
+ module = AnsibleModule({
+ 'url': {'type': 'bool'},
+ 'reason': {'type': 'bool'},
+ })
+ kwargs = {}
+ if module.params['url']:
+ kwargs['url'] = 'https://github.com/ansible/ansible'
+ if module.params['reason']:
+ kwargs['reason'] = 'for fun'
+ if not HAS_LIB:
+ module.fail_json(
+ msg=missing_required_lib(
+ 'ansible_missing_lib',
+ **kwargs
+ ),
+ )
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/missing_required_lib/runme.sh b/test/integration/targets/missing_required_lib/runme.sh
new file mode 100755
index 0000000..2e1ea8d
--- /dev/null
+++ b/test/integration/targets/missing_required_lib/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eux
+export ANSIBLE_ROLES_PATH=../
+ansible-playbook -i ../../inventory runme.yml -e "output_dir=${OUTPUT_DIR}" -v "$@"
diff --git a/test/integration/targets/missing_required_lib/runme.yml b/test/integration/targets/missing_required_lib/runme.yml
new file mode 100644
index 0000000..e1df795
--- /dev/null
+++ b/test/integration/targets/missing_required_lib/runme.yml
@@ -0,0 +1,57 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - command: ansible localhost -m import_role -a role=missing_required_lib -e url=true -e reason=true
+ register: missing_required_lib_all
+ failed_when: missing_required_lib_all.rc == 0
+
+ - command: ansible localhost -m import_role -a role=missing_required_lib
+ register: missing_required_lib_none
+ failed_when: missing_required_lib_none.rc == 0
+
+ - command: ansible localhost -m import_role -a role=missing_required_lib -e url=true
+ register: missing_required_lib_url
+ failed_when: missing_required_lib_url.rc == 0
+
+ - command: ansible localhost -m import_role -a role=missing_required_lib -e reason=true
+ register: missing_required_lib_reason
+ failed_when: missing_required_lib_reason.rc == 0
+
+ - assert:
+ that:
+ - missing_required_lib_all.stdout is search(expected_all)
+ - missing_required_lib_none.stdout is search(expected_none)
+ - missing_required_lib_url.stdout is search(expected_url)
+ - missing_required_lib_reason.stdout is search(expected_reason)
+ vars:
+ expected_all: >-
+ Failed to import the required Python library \(ansible_missing_lib\) on
+ \S+'s Python \S+\.
+ This is required for fun\. See https://github.com/ansible/ansible for
+ more info. Please read the module documentation and install it in the
+ appropriate location\. If the required library is installed, but Ansible
+ is using the wrong Python interpreter, please consult the documentation
+ on ansible_python_interpreter
+ expected_none: >-
+ Failed to import the required Python library \(ansible_missing_lib\) on
+ \S+'s Python \S+\.
+ Please read the module documentation and install it in the
+ appropriate location\. If the required library is installed, but Ansible
+ is using the wrong Python interpreter, please consult the documentation
+ on ansible_python_interpreter
+ expected_url: >-
+ Failed to import the required Python library \(ansible_missing_lib\) on
+ \S+'s Python \S+\.
+ See https://github.com/ansible/ansible for
+ more info\. Please read the module documentation and install it in the
+ appropriate location\. If the required library is installed, but Ansible
+ is using the wrong Python interpreter, please consult the documentation
+ on ansible_python_interpreter
+ expected_reason: >-
+ Failed to import the required Python library \(ansible_missing_lib\) on
+ \S+'s Python \S+\.
+ This is required for fun\.
+ Please read the module documentation and install it in the
+ appropriate location\. If the required library is installed, but Ansible
+ is using the wrong Python interpreter, please consult the documentation
+ on ansible_python_interpreter
diff --git a/test/integration/targets/missing_required_lib/tasks/main.yml b/test/integration/targets/missing_required_lib/tasks/main.yml
new file mode 100644
index 0000000..a50f5ac
--- /dev/null
+++ b/test/integration/targets/missing_required_lib/tasks/main.yml
@@ -0,0 +1,3 @@
+- missing_required_lib:
+ url: '{{ url|default(omit) }}'
+ reason: '{{ reason|default(omit) }}'
diff --git a/test/integration/targets/module_defaults/action_plugins/debug.py b/test/integration/targets/module_defaults/action_plugins/debug.py
new file mode 100644
index 0000000..2584fd3
--- /dev/null
+++ b/test/integration/targets/module_defaults/action_plugins/debug.py
@@ -0,0 +1,80 @@
+# Copyright 2012, Dag Wieers <dag@wieers.com>
+# Copyright 2016, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.errors import AnsibleUndefinedVariable
+from ansible.module_utils.six import string_types
+from ansible.module_utils._text import to_text
+from ansible.plugins.action import ActionBase
+
+
+class ActionModule(ActionBase):
+ ''' Print statements during execution '''
+
+ TRANSFERS_FILES = False
+ _VALID_ARGS = frozenset(('msg', 'var', 'verbosity'))
+
+ def run(self, tmp=None, task_vars=None):
+ if task_vars is None:
+ task_vars = dict()
+
+ if 'msg' in self._task.args and 'var' in self._task.args:
+ return {"failed": True, "msg": "'msg' and 'var' are incompatible options"}
+
+ result = super(ActionModule, self).run(tmp, task_vars)
+ del tmp # tmp no longer has any effect
+
+ # get task verbosity
+ verbosity = int(self._task.args.get('verbosity', 0))
+
+ if verbosity <= self._display.verbosity:
+ if 'msg' in self._task.args:
+ result['msg'] = self._task.args['msg']
+
+ elif 'var' in self._task.args:
+ try:
+ results = self._templar.template(self._task.args['var'], convert_bare=True, fail_on_undefined=True)
+ if results == self._task.args['var']:
+ # if results is not str/unicode type, raise an exception
+ if not isinstance(results, string_types):
+ raise AnsibleUndefinedVariable
+ # If var name is same as result, try to template it
+ results = self._templar.template("{{" + results + "}}", convert_bare=True, fail_on_undefined=True)
+ except AnsibleUndefinedVariable as e:
+ results = u"VARIABLE IS NOT DEFINED!"
+ if self._display.verbosity > 0:
+ results += u": %s" % to_text(e)
+
+ if isinstance(self._task.args['var'], (list, dict)):
+ # If var is a list or dict, use the type as key to display
+ result[to_text(type(self._task.args['var']))] = results
+ else:
+ result[self._task.args['var']] = results
+ else:
+ result['msg'] = 'Hello world!'
+
+ # force flag to make debug output module always verbose
+ result['_ansible_verbose_always'] = True
+ else:
+ result['skipped_reason'] = "Verbosity threshold not met."
+ result['skipped'] = True
+
+ result['failed'] = False
+
+ return result
diff --git a/test/integration/targets/module_defaults/aliases b/test/integration/targets/module_defaults/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/module_defaults/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/othercoll/plugins/action/other_echoaction.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/othercoll/plugins/action/other_echoaction.py
new file mode 100644
index 0000000..f7777b8
--- /dev/null
+++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/othercoll/plugins/action/other_echoaction.py
@@ -0,0 +1,8 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible_collections.testns.testcoll.plugins.action.echoaction import ActionModule as BaseAM
+
+
+class ActionModule(BaseAM):
+ pass
diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/othercoll/plugins/modules/other_echo1.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/othercoll/plugins/modules/other_echo1.py
new file mode 100644
index 0000000..771395f
--- /dev/null
+++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/othercoll/plugins/modules/other_echo1.py
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible_collections.testns.testcoll.plugins.module_utils.echo_impl import do_echo
+
+
+def main():
+ do_echo()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/meta/runtime.yml b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/meta/runtime.yml
new file mode 100644
index 0000000..a8c2c8c
--- /dev/null
+++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/meta/runtime.yml
@@ -0,0 +1,85 @@
+plugin_routing:
+ action:
+ # Backwards compat for modules-redirected-as-actions:
+ # By default, each module_defaults entry is resolved as an action plugin,
+ # and if it does not exist, it is resolved a a module.
+ # All modules that redirect to the same action will resolve to the same action.
+ module_uses_action_defaults:
+ redirect: testns.testcoll.eos
+
+ # module-redirected-as-action overridden by action_plugin
+ iosfacts:
+ redirect: testns.testcoll.nope
+ ios_facts:
+ redirect: testns.testcoll.nope
+
+ redirected_action:
+ redirect: testns.testcoll.ios
+ modules:
+ # Any module_defaults for testns.testcoll.module will not apply to a module_uses_action_defaults task:
+ #
+ # module_defaults:
+ # testns.testcoll.module:
+ # option: value
+ #
+ # But defaults for testns.testcoll.module_uses_action_defaults or testns.testcoll.eos will:
+ #
+ # module_defaults:
+ # testns.testcoll.module_uses_action_defaults:
+ # option: value
+ # testns.testcoll.eos:
+ # option: defined_last_i_win
+ module_uses_action_defaults:
+ redirect: testns.testcoll.module
+
+ # Not "eos_facts" to ensure TE is not finding handler via prefix
+ # eosfacts tasks should not get eos module_defaults (or defaults for other modules that use eos action plugin)
+ eosfacts:
+ action_plugin: testns.testcoll.eos
+
+ # Test that `action_plugin` has higher precedence than module-redirected-as-action - reverse this?
+ # Current behavior is iosfacts/ios_facts do not get ios defaults.
+ iosfacts:
+ redirect: testns.testcoll.ios_facts
+ ios_facts:
+ action_plugin: testns.testcoll.redirected_action
+
+action_groups:
+ testgroup:
+ # Test metadata 'extend_group' feature does not get stuck in a recursive loop
+ - metadata:
+ extend_group: othergroup
+ - metadata
+ - ping
+ - testns.testcoll.echo1
+ - testns.testcoll.echo2
+# note we can define defaults for an action
+ - testns.testcoll.echoaction
+# note we can define defaults in this group for actions/modules in another collection
+ - testns.othercoll.other_echoaction
+ - testns.othercoll.other_echo1
+ othergroup:
+ - metadata:
+ extend_group:
+ - testgroup
+ empty_metadata:
+ - metadata: {}
+ bad_metadata_format:
+ - unexpected_key:
+ key: value
+ metadata:
+ extend_group: testgroup
+ multiple_metadata:
+ - metadata:
+ extend_group: testgroup
+ - metadata:
+ extend_group: othergroup
+ bad_metadata_options:
+ - metadata:
+ unexpected_key: testgroup
+ bad_metadata_type:
+ - metadata: [testgroup]
+ bad_metadata_option_type:
+ - metadata:
+ extend_group:
+ name: testgroup
diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/echoaction.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/echoaction.py
new file mode 100644
index 0000000..2fa097b
--- /dev/null
+++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/echoaction.py
@@ -0,0 +1,19 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.action import ActionBase
+
+
+class ActionModule(ActionBase):
+ TRANSFERS_FILES = False
+ _VALID_ARGS = frozenset()
+
+ def run(self, tmp=None, task_vars=None):
+ if task_vars is None:
+ task_vars = dict()
+
+ result = super(ActionModule, self).run(None, task_vars)
+
+ result = dict(changed=False, args_in=self._task.args)
+
+ return result
diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/eos.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/eos.py
new file mode 100644
index 0000000..0d39f26
--- /dev/null
+++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/eos.py
@@ -0,0 +1,18 @@
+# Copyright: (c) 2022, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.action.normal import ActionModule as ActionBase
+from ansible.utils.vars import merge_hash
+
+
+class ActionModule(ActionBase):
+
+ def run(self, tmp=None, task_vars=None):
+
+ result = super(ActionModule, self).run(tmp, task_vars)
+ result['action_plugin'] = 'eos'
+
+ return result
diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/ios.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/ios.py
new file mode 100644
index 0000000..20284fd
--- /dev/null
+++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/ios.py
@@ -0,0 +1,18 @@
+# Copyright: (c) 2022, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.action.normal import ActionModule as ActionBase
+from ansible.utils.vars import merge_hash
+
+
+class ActionModule(ActionBase):
+
+ def run(self, tmp=None, task_vars=None):
+
+ result = super(ActionModule, self).run(tmp, task_vars)
+ result['action_plugin'] = 'ios'
+
+ return result
diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/vyos.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/vyos.py
new file mode 100644
index 0000000..b0e1904
--- /dev/null
+++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/vyos.py
@@ -0,0 +1,18 @@
+# Copyright: (c) 2022, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.action.normal import ActionModule as ActionBase
+from ansible.utils.vars import merge_hash
+
+
+class ActionModule(ActionBase):
+
+ def run(self, tmp=None, task_vars=None):
+
+ result = super(ActionModule, self).run(tmp, task_vars)
+ result['action_plugin'] = 'vyos'
+
+ return result
diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/module_utils/echo_impl.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/module_utils/echo_impl.py
new file mode 100644
index 0000000..f5c5d73
--- /dev/null
+++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/module_utils/echo_impl.py
@@ -0,0 +1,15 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+from ansible.module_utils import basic
+from ansible.module_utils.basic import _load_params, AnsibleModule
+
+
+def do_echo():
+ p = _load_params()
+ d = json.loads(basic._ANSIBLE_ARGS)
+ d['ANSIBLE_MODULE_ARGS'] = {}
+ basic._ANSIBLE_ARGS = json.dumps(d).encode('utf-8')
+ module = AnsibleModule(argument_spec={})
+ module.exit_json(args_in=p)
diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/echo1.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/echo1.py
new file mode 100644
index 0000000..771395f
--- /dev/null
+++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/echo1.py
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible_collections.testns.testcoll.plugins.module_utils.echo_impl import do_echo
+
+
+def main():
+ do_echo()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/echo2.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/echo2.py
new file mode 100644
index 0000000..771395f
--- /dev/null
+++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/echo2.py
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible_collections.testns.testcoll.plugins.module_utils.echo_impl import do_echo
+
+
+def main():
+ do_echo()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/eosfacts.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/eosfacts.py
new file mode 100644
index 0000000..8c73fe1
--- /dev/null
+++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/eosfacts.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2022, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: eosfacts
+short_description: module to test module_defaults
+description: module to test module_defaults
+version_added: '2.13'
+'''
+
+EXAMPLES = r'''
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ eosfacts=dict(type=bool),
+ ),
+ supports_check_mode=True
+ )
+ module.exit_json(eosfacts=module.params['eosfacts'])
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/ios_facts.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/ios_facts.py
new file mode 100644
index 0000000..e2ed598
--- /dev/null
+++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/ios_facts.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2022, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: ios_facts
+short_description: module to test module_defaults
+description: module to test module_defaults
+version_added: '2.13'
+'''
+
+EXAMPLES = r'''
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ ios_facts=dict(type=bool),
+ ),
+ supports_check_mode=True
+ )
+ module.exit_json(ios_facts=module.params['ios_facts'])
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/metadata.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/metadata.py
new file mode 100644
index 0000000..6a818fd
--- /dev/null
+++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/metadata.py
@@ -0,0 +1,45 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: metadata
+version_added: 2.12
+short_description: Test module with a specific name
+description: Test module with a specific name
+options:
+ data:
+ description: Required option to test module_defaults work
+ required: True
+ type: str
+author:
+ - Ansible Core Team
+'''
+
+EXAMPLES = '''
+'''
+
+RETURN = '''
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ data=dict(type='str', required=True),
+ ),
+ )
+
+ module.exit_json()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/module.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/module.py
new file mode 100644
index 0000000..b98a5f9
--- /dev/null
+++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/module.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2022, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: module
+short_description: module to test module_defaults
+description: module to test module_defaults
+version_added: '2.13'
+'''
+
+EXAMPLES = r'''
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ action_option=dict(type=bool),
+ ),
+ supports_check_mode=True
+ )
+ module.exit_json(action_option=module.params['action_option'])
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/ping.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/ping.py
new file mode 100644
index 0000000..2cb1fb2
--- /dev/null
+++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/ping.py
@@ -0,0 +1,83 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# (c) 2016, Toshio Kuratomi <tkuratomi@ansible.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: ping
+version_added: historical
+short_description: Try to connect to host, verify a usable python and return C(pong) on success
+description:
+ - A trivial test module, this module always returns C(pong) on successful
+ contact. It does not make sense in playbooks, but it is useful from
+ C(/usr/bin/ansible) to verify the ability to login and that a usable Python is configured.
+ - This is NOT ICMP ping, this is just a trivial test module that requires Python on the remote-node.
+ - For Windows targets, use the M(ansible.windows.win_ping) module instead.
+ - For Network targets, use the M(ansible.netcommon.net_ping) module instead.
+options:
+ data:
+ description:
+ - Data to return for the C(ping) return value.
+ - If this parameter is set to C(crash), the module will cause an exception.
+ type: str
+ default: pong
+seealso:
+ - module: ansible.netcommon.net_ping
+ - module: ansible.windows.win_ping
+author:
+ - Ansible Core Team
+ - Michael DeHaan
+notes:
+ - Supports C(check_mode).
+'''
+
+EXAMPLES = '''
+# Test we can logon to 'webservers' and execute python with json lib.
+# ansible webservers -m ping
+
+- name: Example from an Ansible Playbook
+ ansible.builtin.ping:
+
+- name: Induce an exception to see what happens
+ ansible.builtin.ping:
+ data: crash
+'''
+
+RETURN = '''
+ping:
+ description: Value provided with the data parameter.
+ returned: success
+ type: str
+ sample: pong
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ data=dict(type='str', default='pong'),
+ ),
+ supports_check_mode=True
+ )
+
+ if module.params['data'] == 'crash':
+ raise Exception("boom")
+
+ result = dict(
+ ping=module.params['data'],
+ )
+
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/vyosfacts.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/vyosfacts.py
new file mode 100644
index 0000000..3a9abbc
--- /dev/null
+++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/vyosfacts.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2022, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: vyosfacts
+short_description: module to test module_defaults
+description: module to test module_defaults
+version_added: '2.13'
+'''
+
+EXAMPLES = r'''
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ vyosfacts=dict(type=bool),
+ ),
+ supports_check_mode=True
+ )
+ module.exit_json(vyosfacts=module.params['vyosfacts'])
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_defaults/library/legacy_ping.py b/test/integration/targets/module_defaults/library/legacy_ping.py
new file mode 100644
index 0000000..2cb1fb2
--- /dev/null
+++ b/test/integration/targets/module_defaults/library/legacy_ping.py
@@ -0,0 +1,83 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# (c) 2016, Toshio Kuratomi <tkuratomi@ansible.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: ping
+version_added: historical
+short_description: Try to connect to host, verify a usable python and return C(pong) on success
+description:
+ - A trivial test module, this module always returns C(pong) on successful
+ contact. It does not make sense in playbooks, but it is useful from
+ C(/usr/bin/ansible) to verify the ability to login and that a usable Python is configured.
+ - This is NOT ICMP ping, this is just a trivial test module that requires Python on the remote-node.
+ - For Windows targets, use the M(ansible.windows.win_ping) module instead.
+ - For Network targets, use the M(ansible.netcommon.net_ping) module instead.
+options:
+ data:
+ description:
+ - Data to return for the C(ping) return value.
+ - If this parameter is set to C(crash), the module will cause an exception.
+ type: str
+ default: pong
+seealso:
+ - module: ansible.netcommon.net_ping
+ - module: ansible.windows.win_ping
+author:
+ - Ansible Core Team
+ - Michael DeHaan
+notes:
+ - Supports C(check_mode).
+'''
+
+EXAMPLES = '''
+# Test we can logon to 'webservers' and execute python with json lib.
+# ansible webservers -m ping
+
+- name: Example from an Ansible Playbook
+ ansible.builtin.ping:
+
+- name: Induce an exception to see what happens
+ ansible.builtin.ping:
+ data: crash
+'''
+
+RETURN = '''
+ping:
+ description: Value provided with the data parameter.
+ returned: success
+ type: str
+ sample: pong
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ data=dict(type='str', default='pong'),
+ ),
+ supports_check_mode=True
+ )
+
+ if module.params['data'] == 'crash':
+ raise Exception("boom")
+
+ result = dict(
+ ping=module.params['data'],
+ )
+
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_defaults/library/test_module_defaults.py b/test/integration/targets/module_defaults/library/test_module_defaults.py
new file mode 100644
index 0000000..ede8c99
--- /dev/null
+++ b/test/integration/targets/module_defaults/library/test_module_defaults.py
@@ -0,0 +1,30 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ arg1=dict(type='str', default='default1'),
+ arg2=dict(type='str', default='default2'),
+ arg3=dict(type='str', default='default3'),
+ ),
+ supports_check_mode=True
+ )
+
+ result = dict(
+ test_module_defaults=dict(
+ arg1=module.params['arg1'],
+ arg2=module.params['arg2'],
+ arg3=module.params['arg3'],
+ ),
+ )
+
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_defaults/runme.sh b/test/integration/targets/module_defaults/runme.sh
new file mode 100755
index 0000000..fe9c40c
--- /dev/null
+++ b/test/integration/targets/module_defaults/runme.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# Symlink is test for backwards-compat (only workaround for https://github.com/ansible/ansible/issues/77059)
+sudo ln -s "${PWD}/collections/ansible_collections/testns/testcoll/plugins/action/vyos.py" ./collections/ansible_collections/testns/testcoll/plugins/action/vyosfacts.py
+
+ansible-playbook test_defaults.yml "$@"
+
+sudo rm ./collections/ansible_collections/testns/testcoll/plugins/action/vyosfacts.py
+
+ansible-playbook test_action_groups.yml "$@"
+
+ansible-playbook test_action_group_metadata.yml "$@"
diff --git a/test/integration/targets/module_defaults/tasks/main.yml b/test/integration/targets/module_defaults/tasks/main.yml
new file mode 100644
index 0000000..747c2f9
--- /dev/null
+++ b/test/integration/targets/module_defaults/tasks/main.yml
@@ -0,0 +1,89 @@
+- name: main block
+ vars:
+ test_file: /tmp/ansible-test.module_defaults.foo
+ module_defaults:
+ debug:
+ msg: test default
+ file:
+ path: '{{ test_file }}'
+ block:
+ - debug:
+ register: foo
+
+ - name: test that 'debug' task used default 'msg' param
+ assert:
+ that: foo.msg == "test default"
+
+ - name: remove test file
+ file:
+ state: absent
+
+ - name: touch test file
+ file:
+ state: touch
+
+ - name: stat test file
+ stat:
+ path: '{{ test_file }}'
+ register: foo
+
+ - name: check that test file exists
+ assert:
+ that: foo.stat.exists
+
+ - name: remove test file
+ file:
+ state: absent
+
+ - name: test that module defaults from parent are inherited and merged
+ module_defaults:
+ # Meaningless values to make sure that 'module_defaults' gets
+ # evaluated for this block
+ ping:
+ bar: baz
+ block:
+ - debug:
+ register: foo
+
+ - assert:
+ that: foo.msg == "test default"
+
+ - name: test that we can override module defaults inherited from parent
+ module_defaults:
+ debug:
+ msg: "different test message"
+ block:
+ - debug:
+ register: foo
+
+ - assert:
+ that: foo.msg == "different test message"
+
+ - name: test that module defaults inherited from parent can be removed
+ module_defaults:
+ debug: {}
+ block:
+ - debug:
+ register: foo
+
+ - assert:
+ that:
+ foo.msg == "Hello world!"
+
+ - name: test that module defaults can be overridden by module params
+ block:
+ - debug:
+ msg: another test message
+ register: foo
+
+ - assert:
+ that:
+ foo.msg == "another test message"
+
+ - debug:
+ msg: '{{ omit }}'
+ register: foo
+
+ - assert:
+ that:
+ foo.msg == "Hello world!"
diff --git a/test/integration/targets/module_defaults/templates/test_metadata_warning.yml.j2 b/test/integration/targets/module_defaults/templates/test_metadata_warning.yml.j2
new file mode 100644
index 0000000..b45aaba
--- /dev/null
+++ b/test/integration/targets/module_defaults/templates/test_metadata_warning.yml.j2
@@ -0,0 +1,8 @@
+---
+- hosts: localhost
+ gather_facts: no
+ module_defaults:
+ group/{{ group_name }}:
+ data: value
+ tasks:
+ - ping:
diff --git a/test/integration/targets/module_defaults/test_action_group_metadata.yml b/test/integration/targets/module_defaults/test_action_group_metadata.yml
new file mode 100644
index 0000000..e2555b1
--- /dev/null
+++ b/test/integration/targets/module_defaults/test_action_group_metadata.yml
@@ -0,0 +1,123 @@
+---
+- hosts: localhost
+ gather_facts: no
+ environment:
+ ANSIBLE_NOCOLOR: True
+ ANSIBLE_FORCE_COLOR: False
+ tasks:
+
+ - template:
+ src: test_metadata_warning.yml.j2
+ dest: test_metadata_warning.yml
+ vars:
+ group_name: testns.testcoll.empty_metadata
+
+ - command: ansible-playbook test_metadata_warning.yml
+ register: result
+
+ - assert:
+ that: metadata_warning not in warnings
+ vars:
+ warnings: "{{ result.stderr | regex_replace('\\n', ' ') }}"
+ metadata_warning: "Invalid metadata was found"
+
+ - template:
+ src: test_metadata_warning.yml.j2
+ dest: test_metadata_warning.yml
+ vars:
+ group_name: testns.testcoll.bad_metadata_format
+
+ - command: ansible-playbook test_metadata_warning.yml
+ register: result
+
+ - assert:
+ that: metadata_warning in warnings
+ vars:
+ warnings: "{{ result.stderr | regex_replace('\\n', ' ') }}"
+ metadata_warning: >-
+ Invalid metadata was found for action_group testns.testcoll.bad_metadata_format while loading module_defaults.
+ The only expected key is metadata, but got keys: metadata, unexpected_key
+
+ - template:
+ src: test_metadata_warning.yml.j2
+ dest: test_metadata_warning.yml
+ vars:
+ group_name: testns.testcoll.multiple_metadata
+
+ - command: ansible-playbook test_metadata_warning.yml
+ register: result
+
+ - assert:
+ that: metadata_warning in warnings
+ vars:
+ warnings: "{{ result.stderr | regex_replace('\\n', ' ') }}"
+ metadata_warning: >-
+ Invalid metadata was found for action_group testns.testcoll.multiple_metadata while loading module_defaults.
+ The group contains multiple metadata entries.
+
+ - template:
+ src: test_metadata_warning.yml.j2
+ dest: test_metadata_warning.yml
+ vars:
+ group_name: testns.testcoll.bad_metadata_options
+
+ - command: 'ansible-playbook test_metadata_warning.yml'
+ register: result
+
+ - assert:
+ that: metadata_warning in warnings
+ vars:
+ warnings: "{{ result.stderr | regex_replace('\\n', ' ') }}"
+ metadata_warning: >-
+ Invalid metadata was found for action_group testns.testcoll.bad_metadata_options while loading module_defaults.
+ The metadata contains unexpected keys: unexpected_key
+
+ - template:
+ src: test_metadata_warning.yml.j2
+ dest: test_metadata_warning.yml
+ vars:
+ group_name: testns.testcoll.bad_metadata_type
+
+ - command: ansible-playbook test_metadata_warning.yml
+ register: result
+
+ - assert:
+ that: metadata_warning in warnings
+ vars:
+ warnings: "{{ result.stderr | regex_replace('\\n', ' ') }}"
+ metadata_warning: >-
+ Invalid metadata was found for action_group testns.testcoll.bad_metadata_type while loading module_defaults.
+ The metadata is not a dictionary. Got ['testgroup']
+
+ - template:
+ src: test_metadata_warning.yml.j2
+ dest: test_metadata_warning.yml
+ vars:
+ group_name: testns.testcoll.bad_metadata_option_type
+
+ - command: ansible-playbook test_metadata_warning.yml
+ register: result
+
+ - assert:
+ that: metadata_warning in warnings
+ vars:
+ warnings: "{{ result.stderr | regex_replace('\\n', ' ') }}"
+ metadata_warning: >-
+ Invalid metadata was found for action_group testns.testcoll.bad_metadata_option_type while loading module_defaults.
+ The metadata contains unexpected key types: extend_group is {'name': 'testgroup'} (expected type list)
+
+ - name: test disabling action_group metadata validation
+ command: ansible-playbook test_metadata_warning.yml
+ environment:
+ ANSIBLE_VALIDATE_ACTION_GROUP_METADATA: False
+ register: result
+
+ - assert:
+ that: metadata_warning not in warnings
+ vars:
+ warnings: "{{ result.stderr | regex_replace('\\n', ' ') }}"
+ metadata_warning: "Invalid metadata was found for action_group"
+
+ - file:
+ path: test_metadata_warning.yml
+ state: absent
diff --git a/test/integration/targets/module_defaults/test_action_groups.yml b/test/integration/targets/module_defaults/test_action_groups.yml
new file mode 100644
index 0000000..33a3c9c
--- /dev/null
+++ b/test/integration/targets/module_defaults/test_action_groups.yml
@@ -0,0 +1,132 @@
+---
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - name: test ansible.legacy short group name
+ module_defaults:
+ group/testgroup:
+ data: test
+ block:
+ - legacy_ping:
+ register: result
+ - assert:
+ that: "result.ping == 'pong'"
+
+ - ansible.legacy.legacy_ping:
+ register: result
+ - assert:
+ that: "result.ping == 'pong'"
+
+ - ping:
+ register: result
+ - assert:
+ that: "result.ping == 'test'"
+
+ - ansible.legacy.ping: # resolves to ansible.builtin.ping
+ register: result
+ - assert:
+ that: "result.ping == 'test'"
+
+ - ansible.builtin.ping:
+ register: result
+ - assert:
+ that: "result.ping == 'test'"
+
+ - formerly_core_ping:
+ register: result
+ - assert:
+ that: "result.ping == 'test'"
+
+ - ansible.builtin.formerly_core_ping:
+ register: result
+ - assert:
+ that: "result.ping == 'test'"
+
+ - name: test group that includes a legacy action
+ module_defaults:
+ # As of 2.12, legacy actions must be included in the action group definition
+ group/testlegacy:
+ data: test
+ block:
+ - legacy_ping:
+ register: result
+ - assert:
+ that: "result.ping == 'test'"
+
+ - ansible.legacy.legacy_ping:
+ register: result
+ - assert:
+ that: "result.ping == 'test'"
+
+ - name: test ansible.builtin fully qualified group name
+ module_defaults:
+ group/ansible.builtin.testgroup:
+ data: test
+ block:
+ # ansible.builtin does not contain ansible.legacy
+ - legacy_ping:
+ register: result
+ - assert:
+ that: "result.ping != 'test'"
+
+ # ansible.builtin does not contain ansible.legacy
+ - ansible.legacy.legacy_ping:
+ register: result
+ - assert:
+ that: "result.ping != 'test'"
+
+ - ping:
+ register: result
+ - assert:
+ that: "result.ping == 'test'"
+
+ # Resolves to ansible.builtin.ping
+ - ansible.legacy.ping:
+ register: result
+ - assert:
+ that: "result.ping == 'test'"
+
+ - ansible.builtin.ping:
+ register: result
+ - assert:
+ that: "result.ping == 'test'"
+
+ - formerly_core_ping:
+ register: result
+ - assert:
+ that: "result.ping == 'test'"
+
+ - ansible.builtin.formerly_core_ping:
+ register: result
+ - assert:
+ that: "result.ping == 'test'"
+
+ - name: test collection group name
+ module_defaults:
+ group/testns.testcoll.testgroup:
+ data: test
+ block:
+ # Plugin resolving to a different collection does not get the default
+ - ping:
+ register: result
+ - assert:
+ that: "result.ping != 'test'"
+
+ - formerly_core_ping:
+ register: result
+ - assert:
+ that: "result.ping == 'test'"
+
+ - ansible.builtin.formerly_core_ping:
+ register: result
+ - assert:
+ that: "result.ping == 'test'"
+
+ - testns.testcoll.ping:
+ register: result
+ - assert:
+ that: "result.ping == 'test'"
+
+ - metadata:
+ collections:
+ - testns.testcoll
diff --git a/test/integration/targets/module_defaults/test_defaults.yml b/test/integration/targets/module_defaults/test_defaults.yml
new file mode 100644
index 0000000..6206d3a
--- /dev/null
+++ b/test/integration/targets/module_defaults/test_defaults.yml
@@ -0,0 +1,249 @@
+- hosts: localhost
+ gather_facts: no
+ collections:
+ - testns.testcoll
+ - testns.othercoll
+ module_defaults:
+ testns.testcoll.echoaction:
+ explicit_module_default: from playbook
+ testns.testcoll.echo1:
+ explicit_module_default: from playbook
+ group/testgroup:
+ group_module_default: from playbook
+ tasks:
+ - testns.testcoll.echoaction:
+ task_arg: from task
+ register: echoaction_fq
+ - echoaction:
+ task_arg: from task
+ register: echoaction_unq
+ - testns.testcoll.echo1:
+ task_arg: from task
+ register: echo1_fq
+ - echo1:
+ task_arg: from task
+ register: echo1_unq
+ - testns.testcoll.echo2:
+ task_arg: from task
+ register: echo2_fq
+ - echo2:
+ task_arg: from task
+ register: echo2_unq
+ - testns.othercoll.other_echoaction:
+ task_arg: from task
+ register: other_echoaction_fq
+ - other_echoaction:
+ task_arg: from task
+ register: other_echoaction_unq
+ - testns.othercoll.other_echo1:
+ task_arg: from task
+ register: other_echo1_fq
+ - other_echo1:
+ task_arg: from task
+ register: other_echo1_unq
+
+ - debug: var=echo1_fq
+
+ - legacy_ping:
+ register: legacy_ping_1
+ module_defaults:
+ legacy_ping:
+ data: from task
+
+ - legacy_ping:
+ register: legacy_ping_2
+ module_defaults:
+ ansible.legacy.legacy_ping:
+ data: from task
+
+ - ansible.legacy.legacy_ping:
+ register: legacy_ping_3
+ module_defaults:
+ legacy_ping:
+ data: from task
+
+ - ansible.legacy.legacy_ping:
+ register: legacy_ping_4
+ module_defaults:
+ ansible.legacy.legacy_ping:
+ data: from task
+
+ - name: builtin uses legacy defaults
+ ansible.builtin.debug:
+ module_defaults:
+ debug:
+ msg: legacy default
+ register: builtin_legacy_defaults_1
+
+ - name: builtin uses legacy defaults
+ ansible.builtin.debug:
+ module_defaults:
+ ansible.legacy.debug:
+ msg: legacy default
+ register: builtin_legacy_defaults_2
+
+ - name: legacy does not use builtin defaults
+ ansible.legacy.debug:
+ register: legacy_builtin_defaults
+ module_defaults:
+ ansible.builtin.debug:
+ msg: legacy default
+
+ - assert:
+ that:
+ - "echoaction_fq.args_in == {'task_arg': 'from task', 'explicit_module_default': 'from playbook', 'group_module_default': 'from playbook' }"
+ - "echoaction_unq.args_in == {'task_arg': 'from task', 'explicit_module_default': 'from playbook', 'group_module_default': 'from playbook' }"
+ - "echo1_fq.args_in == {'task_arg': 'from task', 'explicit_module_default': 'from playbook', 'group_module_default': 'from playbook' }"
+ - "echo1_unq.args_in == {'task_arg': 'from task', 'explicit_module_default': 'from playbook', 'group_module_default': 'from playbook' }"
+ - "echo2_fq.args_in == {'task_arg': 'from task', 'group_module_default': 'from playbook' }"
+ - "echo2_unq.args_in == {'task_arg': 'from task', 'group_module_default': 'from playbook' }"
+ - "other_echoaction_fq.args_in == {'task_arg': 'from task', 'group_module_default': 'from playbook' }"
+ - "other_echoaction_unq.args_in == {'task_arg': 'from task', 'group_module_default': 'from playbook' }"
+ - "other_echo1_fq.args_in == {'task_arg': 'from task', 'group_module_default': 'from playbook' }"
+ - "other_echo1_unq.args_in == {'task_arg': 'from task', 'group_module_default': 'from playbook' }"
+ - "legacy_ping_1.ping == 'from task'"
+ - "legacy_ping_2.ping == 'from task'"
+ - "legacy_ping_3.ping == 'from task'"
+ - "legacy_ping_4.ping == 'from task'"
+ - "legacy_builtin_defaults.msg == 'Hello world!'"
+ - "builtin_legacy_defaults_1.msg == 'legacy default'"
+ - "builtin_legacy_defaults_2.msg == 'legacy default'"
+
+ - include_tasks: tasks/main.yml
+
+- name: test preferring module name defaults for platform-specific actions
+ hosts: localhost
+ gather_facts: no
+ tasks:
+ - name: ensure eosfacts does not use action plugin default
+ testns.testcoll.eosfacts:
+ module_defaults:
+ testns.testcoll.eos:
+ fail: true
+
+ - name: eosfacts does use module name defaults
+ testns.testcoll.eosfacts:
+ module_defaults:
+ testns.testcoll.eosfacts:
+ eosfacts: true
+ register: result
+
+ - assert:
+ that:
+ - result.eosfacts
+ - result.action_plugin == 'eos'
+
+ - name: ensure vyosfacts does not use action plugin default
+ testns.testcoll.vyosfacts:
+ module_defaults:
+ testns.testcoll.vyos:
+ fail: true
+
+ - name: vyosfacts does use vyosfacts defaults
+ testns.testcoll.vyosfacts:
+ module_defaults:
+ testns.testcoll.vyosfacts:
+ vyosfacts: true
+ register: result
+
+ - assert:
+ that:
+ - result.vyosfacts
+ - result.action_plugin == 'vyos'
+
+ - name: iosfacts/ios_facts does not use action plugin default (module action_plugin field has precedence over module-as-action-redirect)
+ collections:
+ - testns.testcoll
+ module_defaults:
+ testns.testcoll.ios:
+ fail: true
+ block:
+ - ios_facts:
+ register: result
+ - assert:
+ that:
+ - result.action_plugin == 'ios'
+
+ - iosfacts:
+ register: result
+ - assert:
+ that:
+ - result.action_plugin == 'ios'
+
+ - name: ensure iosfacts/ios_facts uses ios_facts defaults
+ collections:
+ - testns.testcoll
+ module_defaults:
+ testns.testcoll.ios_facts:
+ ios_facts: true
+ block:
+ - ios_facts:
+ register: result
+ - assert:
+ that:
+ - result.ios_facts
+ - result.action_plugin == 'ios'
+
+ - iosfacts:
+ register: result
+ - assert:
+ that:
+ - result.ios_facts
+ - result.action_plugin == 'ios'
+
+ - name: ensure iosfacts/ios_facts uses iosfacts defaults
+ collections:
+ - testns.testcoll
+ module_defaults:
+ testns.testcoll.iosfacts:
+ ios_facts: true
+ block:
+ - ios_facts:
+ register: result
+ - assert:
+ that:
+ - result.ios_facts
+ - result.action_plugin == 'ios'
+
+ - iosfacts:
+ register: result
+ - assert:
+ that:
+ - result.ios_facts
+ - result.action_plugin == 'ios'
+
+ - name: ensure redirected action gets redirected action defaults
+ testns.testcoll.module_uses_action_defaults:
+ module_defaults:
+ testns.testcoll.module_uses_action_defaults:
+ action_option: true
+ register: result
+
+ - assert:
+ that:
+ - result.action_option
+ - result.action_plugin == 'eos'
+
+ - name: ensure redirected action gets resolved action defaults
+ testns.testcoll.module_uses_action_defaults:
+ module_defaults:
+ testns.testcoll.eos:
+ action_option: true
+ register: result
+
+ - assert:
+ that:
+ - result.action_option
+ - result.action_plugin == 'eos'
+
+ - name: ensure redirected action does not use module-specific defaults
+ testns.testcoll.module_uses_action_defaults:
+ module_defaults:
+ testns.testcoll.module:
+ fail: true
+ register: result
+
+ - assert:
+ that:
+ - not result.action_option
+ - result.action_plugin == 'eos'
diff --git a/test/integration/targets/module_no_log/aliases b/test/integration/targets/module_no_log/aliases
new file mode 100644
index 0000000..9e84f63
--- /dev/null
+++ b/test/integration/targets/module_no_log/aliases
@@ -0,0 +1,5 @@
+shippable/posix/group3
+context/controller
+skip/freebsd # not configured to log user.info to /var/log/syslog
+skip/osx # not configured to log user.info to /var/log/syslog
+skip/macos # not configured to log user.info to /var/log/syslog
diff --git a/test/integration/targets/module_no_log/library/module_that_logs.py b/test/integration/targets/module_no_log/library/module_that_logs.py
new file mode 100644
index 0000000..44b36ee
--- /dev/null
+++ b/test/integration/targets/module_no_log/library/module_that_logs.py
@@ -0,0 +1,18 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(argument_spec=dict(
+ number=dict(type='int'),
+ ))
+
+ module.log('My number is: (%d)' % module.params['number'])
+ module.exit_json()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_no_log/tasks/main.yml b/test/integration/targets/module_no_log/tasks/main.yml
new file mode 100644
index 0000000..cf9e580
--- /dev/null
+++ b/test/integration/targets/module_no_log/tasks/main.yml
@@ -0,0 +1,61 @@
+- name: Detect syslog
+ stat:
+ path: /var/log/syslog
+ register: syslog
+
+- name: Detect journalctl
+ shell: command -V journalctl
+ ignore_errors: yes
+ changed_when: no
+ register: journalctl
+
+- block:
+ - name: Skip tests if logs were not found.
+ debug:
+ msg: Did not find /var/log/syslog or journalctl. Tests will be skipped.
+ - meta: end_play
+ when: journalctl is failed and not syslog.stat.exists
+
+- name: Generate random numbers for unique log entries
+ set_fact:
+ good_number: "{{ 999999999999 | random }}"
+ bad_number: "{{ 999999999999 | random }}"
+
+- name: Generate expected log entry messages
+ set_fact:
+ good_message: 'My number is: ({{ good_number }})'
+ bad_message: 'My number is: ({{ bad_number }})'
+
+- name: Generate log message search patterns
+ set_fact:
+ # these search patterns are designed to avoid matching themselves
+ good_search: '{{ good_message.replace(":", "[:]") }}'
+ bad_search: '{{ bad_message.replace(":", "[:]") }}'
+
+- name: Generate grep command
+ set_fact:
+ grep_command: "grep -e '{{ good_search }}' -e '{{ bad_search }}'"
+
+- name: Run a module that logs without no_log
+ module_that_logs:
+ number: "{{ good_number }}"
+
+- name: Run a module that logs with no_log
+ module_that_logs:
+ number: "{{ bad_number }}"
+ no_log: yes
+
+- name: Search for expected log messages
+ # if this fails the tests are probably running on a system which stores logs elsewhere
+ shell: "({{ grep_command }} /var/log/syslog) || (journalctl | {{ grep_command }})"
+ changed_when: no
+ register: grep
+
+- name: Verify the correct log messages were found
+ assert:
+ that:
+ # if the good message is not found then the cause is likely one of:
+ # 1) the remote system does not write user.info messages to the logs
+ # 2) the AnsibleModule.log method is not working
+ - good_message in grep.stdout
+ - bad_message not in grep.stdout
diff --git a/test/integration/targets/module_precedence/aliases b/test/integration/targets/module_precedence/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/module_precedence/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/module_precedence/lib_no_extension/ping b/test/integration/targets/module_precedence/lib_no_extension/ping
new file mode 100644
index 0000000..a28f469
--- /dev/null
+++ b/test/integration/targets/module_precedence/lib_no_extension/ping
@@ -0,0 +1,71 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# (c) 2016, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['stableinterface'],
+ 'supported_by': 'core'}
+
+
+DOCUMENTATION = '''
+---
+module: ping
+version_added: historical
+short_description: Try to connect to host, verify a usable python and return C(pong) on success.
+description:
+ - A trivial test module, this module always returns C(pong) on successful
+ contact. It does not make sense in playbooks, but it is useful from
+ C(/usr/bin/ansible) to verify the ability to login and that a usable python is configured.
+ - This is NOT ICMP ping, this is just a trivial test module.
+options: {}
+author:
+ - "Ansible Core Team"
+ - "Michael DeHaan"
+'''
+
+EXAMPLES = '''
+# Test we can logon to 'webservers' and execute python with json lib.
+ansible webservers -m ping
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ data=dict(required=False, default=None),
+ ),
+ supports_check_mode=True
+ )
+ result = dict(ping='pong')
+ if module.params['data']:
+ if module.params['data'] == 'crash':
+ raise Exception("boom")
+ result['ping'] = module.params['data']
+ result['location'] = 'library'
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_precedence/lib_with_extension/a.ini b/test/integration/targets/module_precedence/lib_with_extension/a.ini
new file mode 100644
index 0000000..80278c9
--- /dev/null
+++ b/test/integration/targets/module_precedence/lib_with_extension/a.ini
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+
+def main():
+ print(json.dumps(dict(changed=False, location='a.ini')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_precedence/lib_with_extension/a.py b/test/integration/targets/module_precedence/lib_with_extension/a.py
new file mode 100644
index 0000000..8eda141
--- /dev/null
+++ b/test/integration/targets/module_precedence/lib_with_extension/a.py
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+
+def main():
+ print(json.dumps(dict(changed=False, location='a.py')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_precedence/lib_with_extension/ping.ini b/test/integration/targets/module_precedence/lib_with_extension/ping.ini
new file mode 100644
index 0000000..6f4b6a1
--- /dev/null
+++ b/test/integration/targets/module_precedence/lib_with_extension/ping.ini
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+
+def main():
+ print(json.dumps(dict(changed=False, location='ping.ini')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_precedence/lib_with_extension/ping.py b/test/integration/targets/module_precedence/lib_with_extension/ping.py
new file mode 100644
index 0000000..a28f469
--- /dev/null
+++ b/test/integration/targets/module_precedence/lib_with_extension/ping.py
@@ -0,0 +1,71 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# (c) 2016, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['stableinterface'],
+ 'supported_by': 'core'}
+
+
+DOCUMENTATION = '''
+---
+module: ping
+version_added: historical
+short_description: Try to connect to host, verify a usable python and return C(pong) on success.
+description:
+ - A trivial test module, this module always returns C(pong) on successful
+ contact. It does not make sense in playbooks, but it is useful from
+ C(/usr/bin/ansible) to verify the ability to login and that a usable python is configured.
+ - This is NOT ICMP ping, this is just a trivial test module.
+options: {}
+author:
+ - "Ansible Core Team"
+ - "Michael DeHaan"
+'''
+
+EXAMPLES = '''
+# Test we can logon to 'webservers' and execute python with json lib.
+ansible webservers -m ping
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ data=dict(required=False, default=None),
+ ),
+ supports_check_mode=True
+ )
+ result = dict(ping='pong')
+ if module.params['data']:
+ if module.params['data'] == 'crash':
+ raise Exception("boom")
+ result['ping'] = module.params['data']
+ result['location'] = 'library'
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_precedence/modules_test.yml b/test/integration/targets/module_precedence/modules_test.yml
new file mode 100644
index 0000000..cf3e888
--- /dev/null
+++ b/test/integration/targets/module_precedence/modules_test.yml
@@ -0,0 +1,10 @@
+- hosts: testhost
+ gather_facts: no
+ tasks:
+ - name: Use standard ping module
+ ping:
+ register: result
+
+ - assert:
+ that:
+ - '"location" not in result'
diff --git a/test/integration/targets/module_precedence/modules_test_envvar.yml b/test/integration/targets/module_precedence/modules_test_envvar.yml
new file mode 100644
index 0000000..f52e2f9
--- /dev/null
+++ b/test/integration/targets/module_precedence/modules_test_envvar.yml
@@ -0,0 +1,11 @@
+- hosts: testhost
+ gather_facts: no
+ tasks:
+ - name: Use ping from library path
+ ping:
+ register: result
+
+ - assert:
+ that:
+ - '"location" in result'
+ - 'result["location"] == "library"'
diff --git a/test/integration/targets/module_precedence/modules_test_envvar_ext.yml b/test/integration/targets/module_precedence/modules_test_envvar_ext.yml
new file mode 100644
index 0000000..48f27c4
--- /dev/null
+++ b/test/integration/targets/module_precedence/modules_test_envvar_ext.yml
@@ -0,0 +1,16 @@
+- hosts: testhost
+ gather_facts: no
+ tasks:
+ - name: Use ping from library path
+ ping:
+ register: result
+
+ - name: Use a from library path
+ a:
+ register: a_res
+
+ - assert:
+ that:
+ - '"location" in result'
+ - 'result["location"] == "library"'
+ - 'a_res["location"] == "a.py"'
diff --git a/test/integration/targets/module_precedence/modules_test_multiple_roles.yml b/test/integration/targets/module_precedence/modules_test_multiple_roles.yml
new file mode 100644
index 0000000..f4bd264
--- /dev/null
+++ b/test/integration/targets/module_precedence/modules_test_multiple_roles.yml
@@ -0,0 +1,17 @@
+- hosts: testhost
+ gather_facts: no
+ vars:
+ expected_location: "role: foo"
+ roles:
+ - foo
+ - bar
+
+ tasks:
+ - name: Use ping from role
+ ping:
+ register: result
+
+ - assert:
+ that:
+ - '"location" in result'
+ - 'result["location"] == "{{ expected_location}}"'
diff --git a/test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml b/test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml
new file mode 100644
index 0000000..5403ae2
--- /dev/null
+++ b/test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml
@@ -0,0 +1,16 @@
+- hosts: testhost
+ gather_facts: no
+ vars:
+ expected_location: "role: bar"
+ roles:
+ - bar
+ - foo
+ tasks:
+ - name: Use ping from role
+ ping:
+ register: result
+
+ - assert:
+ that:
+ - '"location" in result'
+ - 'result["location"] == "{{ expected_location}}"'
diff --git a/test/integration/targets/module_precedence/modules_test_role.yml b/test/integration/targets/module_precedence/modules_test_role.yml
new file mode 100644
index 0000000..ccbe31d
--- /dev/null
+++ b/test/integration/targets/module_precedence/modules_test_role.yml
@@ -0,0 +1,13 @@
+- hosts: testhost
+ gather_facts: no
+ roles:
+ - foo
+ tasks:
+ - name: Use ping from role
+ ping:
+ register: result
+
+ - assert:
+ that:
+ - '"location" in result'
+ - 'result["location"] == "role: foo"'
diff --git a/test/integration/targets/module_precedence/modules_test_role_ext.yml b/test/integration/targets/module_precedence/modules_test_role_ext.yml
new file mode 100644
index 0000000..f8816f9
--- /dev/null
+++ b/test/integration/targets/module_precedence/modules_test_role_ext.yml
@@ -0,0 +1,18 @@
+- hosts: testhost
+ gather_facts: no
+ roles:
+ - foo
+ tasks:
+ - name: Use ping from role
+ ping:
+ register: result
+
+ - name: Use from role
+ a:
+ register: a_res
+
+ - assert:
+ that:
+ - '"location" in result'
+ - 'result["location"] == "role: foo"'
+ - 'a_res["location"] == "role: foo, a.py"'
diff --git a/test/integration/targets/module_precedence/multiple_roles/bar/library/ping.py b/test/integration/targets/module_precedence/multiple_roles/bar/library/ping.py
new file mode 100644
index 0000000..98ef7b4
--- /dev/null
+++ b/test/integration/targets/module_precedence/multiple_roles/bar/library/ping.py
@@ -0,0 +1,71 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# (c) 2016, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['stableinterface'],
+ 'supported_by': 'core'}
+
+
+DOCUMENTATION = '''
+---
+module: ping
+version_added: historical
+short_description: Try to connect to host, verify a usable python and return C(pong) on success.
+description:
+ - A trivial test module, this module always returns C(pong) on successful
+ contact. It does not make sense in playbooks, but it is useful from
+ C(/usr/bin/ansible) to verify the ability to login and that a usable python is configured.
+ - This is NOT ICMP ping, this is just a trivial test module.
+options: {}
+author:
+ - "Ansible Core Team"
+ - "Michael DeHaan"
+'''
+
+EXAMPLES = '''
+# Test we can logon to 'webservers' and execute python with json lib.
+ansible webservers -m ping
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ data=dict(required=False, default=None),
+ ),
+ supports_check_mode=True
+ )
+ result = dict(ping='pong')
+ if module.params['data']:
+ if module.params['data'] == 'crash':
+ raise Exception("boom")
+ result['ping'] = module.params['data']
+ result['location'] = 'role: bar'
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml b/test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml
new file mode 100644
index 0000000..52c3402
--- /dev/null
+++ b/test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml
@@ -0,0 +1,10 @@
+---
+- name: Use ping from inside foo role
+ ping:
+ register: result
+
+- name: Make sure that we used the ping module from the foo role
+ assert:
+ that:
+ - '"location" in result'
+ - 'result["location"] == "{{ expected_location }}"'
diff --git a/test/integration/targets/module_precedence/multiple_roles/foo/library/ping.py b/test/integration/targets/module_precedence/multiple_roles/foo/library/ping.py
new file mode 100644
index 0000000..8860b7a
--- /dev/null
+++ b/test/integration/targets/module_precedence/multiple_roles/foo/library/ping.py
@@ -0,0 +1,71 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# (c) 2016, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['stableinterface'],
+ 'supported_by': 'core'}
+
+
+DOCUMENTATION = '''
+---
+module: ping
+version_added: historical
+short_description: Try to connect to host, verify a usable python and return C(pong) on success.
+description:
+ - A trivial test module, this module always returns C(pong) on successful
+ contact. It does not make sense in playbooks, but it is useful from
+ C(/usr/bin/ansible) to verify the ability to login and that a usable python is configured.
+ - This is NOT ICMP ping, this is just a trivial test module.
+options: {}
+author:
+ - "Ansible Core Team"
+ - "Michael DeHaan"
+'''
+
+EXAMPLES = '''
+# Test we can logon to 'webservers' and execute python with json lib.
+ansible webservers -m ping
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ data=dict(required=False, default=None),
+ ),
+ supports_check_mode=True
+ )
+ result = dict(ping='pong')
+ if module.params['data']:
+ if module.params['data'] == 'crash':
+ raise Exception("boom")
+ result['ping'] = module.params['data']
+ result['location'] = 'role: foo'
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml b/test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml
new file mode 100644
index 0000000..52c3402
--- /dev/null
+++ b/test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml
@@ -0,0 +1,10 @@
+---
+- name: Use ping from inside foo role
+ ping:
+ register: result
+
+- name: Make sure that we used the ping module from the foo role
+ assert:
+ that:
+ - '"location" in result'
+ - 'result["location"] == "{{ expected_location }}"'
diff --git a/test/integration/targets/module_precedence/roles_no_extension/foo/library/ping b/test/integration/targets/module_precedence/roles_no_extension/foo/library/ping
new file mode 100644
index 0000000..8860b7a
--- /dev/null
+++ b/test/integration/targets/module_precedence/roles_no_extension/foo/library/ping
@@ -0,0 +1,71 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# (c) 2016, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['stableinterface'],
+ 'supported_by': 'core'}
+
+
+DOCUMENTATION = '''
+---
+module: ping
+version_added: historical
+short_description: Try to connect to host, verify a usable python and return C(pong) on success.
+description:
+ - A trivial test module, this module always returns C(pong) on successful
+ contact. It does not make sense in playbooks, but it is useful from
+ C(/usr/bin/ansible) to verify the ability to login and that a usable python is configured.
+ - This is NOT ICMP ping, this is just a trivial test module.
+options: {}
+author:
+ - "Ansible Core Team"
+ - "Michael DeHaan"
+'''
+
+EXAMPLES = '''
+# Test we can logon to 'webservers' and execute python with json lib.
+ansible webservers -m ping
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ data=dict(required=False, default=None),
+ ),
+ supports_check_mode=True
+ )
+ result = dict(ping='pong')
+ if module.params['data']:
+ if module.params['data'] == 'crash':
+ raise Exception("boom")
+ result['ping'] = module.params['data']
+ result['location'] = 'role: foo'
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_precedence/roles_no_extension/foo/tasks/main.yml b/test/integration/targets/module_precedence/roles_no_extension/foo/tasks/main.yml
new file mode 100644
index 0000000..985fc34
--- /dev/null
+++ b/test/integration/targets/module_precedence/roles_no_extension/foo/tasks/main.yml
@@ -0,0 +1,10 @@
+---
+- name: Use ping from inside foo role
+ ping:
+ register: result
+
+- name: Make sure that we used the ping module from the foo role
+ assert:
+ that:
+ - '"location" in result'
+ - 'result["location"] == "role: foo"'
diff --git a/test/integration/targets/module_precedence/roles_with_extension/foo/library/a.ini b/test/integration/targets/module_precedence/roles_with_extension/foo/library/a.ini
new file mode 100644
index 0000000..8b17029
--- /dev/null
+++ b/test/integration/targets/module_precedence/roles_with_extension/foo/library/a.ini
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+
+def main():
+ print(json.dumps(dict(changed=False, location='role: foo, a.ini')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_precedence/roles_with_extension/foo/library/a.py b/test/integration/targets/module_precedence/roles_with_extension/foo/library/a.py
new file mode 100644
index 0000000..4bc5906
--- /dev/null
+++ b/test/integration/targets/module_precedence/roles_with_extension/foo/library/a.py
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+
+def main():
+ print(json.dumps(dict(changed=False, location='role: foo, a.py')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_precedence/roles_with_extension/foo/library/ping.ini b/test/integration/targets/module_precedence/roles_with_extension/foo/library/ping.ini
new file mode 100644
index 0000000..f9c04f5
--- /dev/null
+++ b/test/integration/targets/module_precedence/roles_with_extension/foo/library/ping.ini
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+
+def main():
+ print(json.dumps(dict(changed=False, location='role: foo, ping.ini')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_precedence/roles_with_extension/foo/library/ping.py b/test/integration/targets/module_precedence/roles_with_extension/foo/library/ping.py
new file mode 100644
index 0000000..8860b7a
--- /dev/null
+++ b/test/integration/targets/module_precedence/roles_with_extension/foo/library/ping.py
@@ -0,0 +1,71 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# (c) 2016, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['stableinterface'],
+ 'supported_by': 'core'}
+
+
+DOCUMENTATION = '''
+---
+module: ping
+version_added: historical
+short_description: Try to connect to host, verify a usable python and return C(pong) on success.
+description:
+ - A trivial test module, this module always returns C(pong) on successful
+ contact. It does not make sense in playbooks, but it is useful from
+ C(/usr/bin/ansible) to verify the ability to login and that a usable python is configured.
+ - This is NOT ICMP ping, this is just a trivial test module.
+options: {}
+author:
+ - "Ansible Core Team"
+ - "Michael DeHaan"
+'''
+
+EXAMPLES = '''
+# Test we can logon to 'webservers' and execute python with json lib.
+ansible webservers -m ping
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ data=dict(required=False, default=None),
+ ),
+ supports_check_mode=True
+ )
+ result = dict(ping='pong')
+ if module.params['data']:
+ if module.params['data'] == 'crash':
+ raise Exception("boom")
+ result['ping'] = module.params['data']
+ result['location'] = 'role: foo'
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_precedence/roles_with_extension/foo/tasks/main.yml b/test/integration/targets/module_precedence/roles_with_extension/foo/tasks/main.yml
new file mode 100644
index 0000000..985fc34
--- /dev/null
+++ b/test/integration/targets/module_precedence/roles_with_extension/foo/tasks/main.yml
@@ -0,0 +1,10 @@
+---
+- name: Use ping from inside foo role
+ ping:
+ register: result
+
+- name: Make sure that we used the ping module from the foo role
+ assert:
+ that:
+ - '"location" in result'
+ - 'result["location"] == "role: foo"'
diff --git a/test/integration/targets/module_precedence/runme.sh b/test/integration/targets/module_precedence/runme.sh
new file mode 100755
index 0000000..0f6a98f
--- /dev/null
+++ b/test/integration/targets/module_precedence/runme.sh
@@ -0,0 +1,49 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# Standard ping module
+ansible-playbook modules_test.yml -i ../../inventory -v "$@"
+
+# Library path ping module
+ANSIBLE_LIBRARY=lib_with_extension ansible-playbook modules_test_envvar.yml -i ../../inventory -v "$@"
+ANSIBLE_LIBRARY=lib_no_extension ansible-playbook modules_test_envvar.yml -i ../../inventory -v "$@"
+
+# ping module from role
+ANSIBLE_ROLES_PATH=roles_with_extension ansible-playbook modules_test_role.yml -i ../../inventory -v "$@"
+ANSIBLE_ROLES_PATH=roles_no_extension ansible-playbook modules_test_role.yml -i ../../inventory -v "$@"
+
+# ping module from role when there's a library path module too
+ANSIBLE_LIBRARY=lib_no_extension ANSIBLE_ROLES_PATH=roles_with_extension ansible-playbook modules_test_role.yml -i ../../inventory -v "$@"
+ANSIBLE_LIBRARY=lib_with_extension ANSIBLE_ROLES_PATH=roles_with_extension ansible-playbook modules_test_role.yml -i ../../inventory -v "$@"
+ANSIBLE_LIBRARY=lib_no_extension ANSIBLE_ROLES_PATH=roles_no_extension ansible-playbook modules_test_role.yml -i ../../inventory -v "$@"
+ANSIBLE_LIBRARY=lib_with_extension ANSIBLE_ROLES_PATH=roles_no_extension ansible-playbook modules_test_role.yml -i ../../inventory -v "$@"
+
+# ping module in multiple roles: Note that this will use the first module found
+# which is the current way things work but may not be the best way
+ANSIBLE_LIBRARY=lib_no_extension ANSIBLE_ROLES_PATH=multiple_roles ansible-playbook modules_test_multiple_roles.yml -i ../../inventory -v "$@"
+ANSIBLE_LIBRARY=lib_with_extension ANSIBLE_ROLES_PATH=multiple_roles ansible-playbook modules_test_multiple_roles.yml -i ../../inventory -v "$@"
+ANSIBLE_LIBRARY=lib_no_extension ANSIBLE_ROLES_PATH=multiple_roles ansible-playbook modules_test_multiple_roles.yml -i ../../inventory -v "$@"
+ANSIBLE_LIBRARY=lib_with_extension ANSIBLE_ROLES_PATH=multiple_roles ansible-playbook modules_test_multiple_roles.yml -i ../../inventory -v "$@"
+
+# And prove that with multiple roles, it's the order the roles are listed in the play that matters
+ANSIBLE_LIBRARY=lib_with_extension ANSIBLE_ROLES_PATH=multiple_roles ansible-playbook modules_test_multiple_roles_reverse_order.yml -i ../../inventory -v "$@"
+
+# Tests for MODULE_IGNORE_EXTS.
+#
+# Very similar to two tests above, but adds a check to test extension
+# precedence. Separate from the above playbooks because we *only* care about
+# extensions here and 'a' will not exist when the above playbooks run with
+# non-extension library/role paths. There is also no way to guarantee that
+# these tests will be useful due to how the pluginloader seems to work. It uses
+# os.listdir which returns things in an arbitrary order (likely dependent on
+# filesystem). If it happens to return 'a.py' on the test node before it
+# returns 'a.ini', then this test is pointless anyway because there's no chance
+# that 'a.ini' would ever have run regardless of what MODULE_IGNORE_EXTS is set
+# to. The hope is that we test across enough systems that one would fail this
+# test if the MODULE_IGNORE_EXTS broke, but there is no guarantee. This would
+# perhaps be better as a mocked unit test because of this but would require
+# a fair bit of work to be feasible as none of that loader logic is tested at
+# all right now.
+ANSIBLE_LIBRARY=lib_with_extension ansible-playbook modules_test_envvar_ext.yml -i ../../inventory -v "$@"
+ANSIBLE_ROLES_PATH=roles_with_extension ansible-playbook modules_test_role_ext.yml -i ../../inventory -v "$@"
diff --git a/test/integration/targets/module_tracebacks/aliases b/test/integration/targets/module_tracebacks/aliases
new file mode 100644
index 0000000..f4df8a9
--- /dev/null
+++ b/test/integration/targets/module_tracebacks/aliases
@@ -0,0 +1,3 @@
+shippable/posix/group3
+needs/ssh
+context/controller
diff --git a/test/integration/targets/module_tracebacks/inventory b/test/integration/targets/module_tracebacks/inventory
new file mode 100644
index 0000000..9156526
--- /dev/null
+++ b/test/integration/targets/module_tracebacks/inventory
@@ -0,0 +1,5 @@
+testhost_local ansible_connection=local
+testhost_ssh ansible_connection=ssh ansible_host=localhost
+
+[all:vars]
+ansible_python_interpreter="{{ ansible_playbook_python }}"
diff --git a/test/integration/targets/module_tracebacks/runme.sh b/test/integration/targets/module_tracebacks/runme.sh
new file mode 100755
index 0000000..b8ac806
--- /dev/null
+++ b/test/integration/targets/module_tracebacks/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook traceback.yml -i inventory "$@"
diff --git a/test/integration/targets/module_tracebacks/traceback.yml b/test/integration/targets/module_tracebacks/traceback.yml
new file mode 100644
index 0000000..b1f0b51
--- /dev/null
+++ b/test/integration/targets/module_tracebacks/traceback.yml
@@ -0,0 +1,21 @@
+- hosts: all
+ gather_facts: no
+ tasks:
+ - name: intentionally fail module execution
+ ping:
+ data: crash
+ ignore_errors: yes
+ register: ping
+
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - name: verify exceptions were properly captured
+ assert:
+ that:
+ - hostvars.testhost_local.ping is failed
+ - "'boom' in hostvars.testhost_local.ping.exception"
+ - "'boom' in hostvars.testhost_local.ping.module_stderr"
+ - hostvars.testhost_ssh.ping is failed
+ - "'boom' in hostvars.testhost_ssh.ping.exception"
+ - "'boom' in hostvars.testhost_ssh.ping.module_stdout"
diff --git a/test/integration/targets/module_utils/aliases b/test/integration/targets/module_utils/aliases
new file mode 100644
index 0000000..a1fba96
--- /dev/null
+++ b/test/integration/targets/module_utils/aliases
@@ -0,0 +1,6 @@
+shippable/posix/group2
+needs/root
+needs/target/setup_test_user
+needs/target/setup_remote_tmp_dir
+context/target
+destructive
diff --git a/test/integration/targets/module_utils/callback/pure_json.py b/test/integration/targets/module_utils/callback/pure_json.py
new file mode 100644
index 0000000..1723d7b
--- /dev/null
+++ b/test/integration/targets/module_utils/callback/pure_json.py
@@ -0,0 +1,31 @@
+# (c) 2021 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ name: pure_json
+ type: stdout
+ short_description: only outputs the module results as json
+'''
+
+import json
+
+from ansible.plugins.callback import CallbackBase
+
+
+class CallbackModule(CallbackBase):
+
+ CALLBACK_VERSION = 2.0
+ CALLBACK_TYPE = 'stdout'
+ CALLBACK_NAME = 'pure_json'
+
+ def v2_runner_on_failed(self, result, ignore_errors=False):
+ self._display.display(json.dumps(result._result))
+
+ def v2_runner_on_ok(self, result):
+ self._display.display(json.dumps(result._result))
+
+ def v2_runner_on_skipped(self, result):
+ self._display.display(json.dumps(result._result))
diff --git a/test/integration/targets/module_utils/collections/ansible_collections/testns/testcoll/plugins/module_utils/legit.py b/test/integration/targets/module_utils/collections/ansible_collections/testns/testcoll/plugins/module_utils/legit.py
new file mode 100644
index 0000000..b9d6348
--- /dev/null
+++ b/test/integration/targets/module_utils/collections/ansible_collections/testns/testcoll/plugins/module_utils/legit.py
@@ -0,0 +1,6 @@
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+def importme():
+ return "successfully imported from testns.testcoll"
diff --git a/test/integration/targets/module_utils/library/test.py b/test/integration/targets/module_utils/library/test.py
new file mode 100644
index 0000000..fb6c8a8
--- /dev/null
+++ b/test/integration/targets/module_utils/library/test.py
@@ -0,0 +1,87 @@
+#!/usr/bin/python
+# Most of these names are only available via PluginLoader so pylint doesn't
+# know they exist
+# pylint: disable=no-name-in-module
+__metaclass__ = type
+
+results = {}
+
+# Test import with no from
+import ansible.module_utils.foo0
+results['foo0'] = ansible.module_utils.foo0.data
+
+# Test depthful import with no from
+import ansible.module_utils.bar0.foo
+results['bar0'] = ansible.module_utils.bar0.foo.data
+
+# Test import of module_utils/foo1.py
+from ansible.module_utils import foo1
+results['foo1'] = foo1.data
+
+# Test import of an identifier inside of module_utils/foo2.py
+from ansible.module_utils.foo2 import data
+results['foo2'] = data
+
+# Test import of module_utils/bar1/__init__.py
+from ansible.module_utils import bar1
+results['bar1'] = bar1.data
+
+# Test import of an identifier inside of module_utils/bar2/__init__.py
+from ansible.module_utils.bar2 import data
+results['bar2'] = data
+
+# Test import of module_utils/baz1/one.py
+from ansible.module_utils.baz1 import one
+results['baz1'] = one.data
+
+# Test import of an identifier inside of module_utils/baz2/one.py
+from ansible.module_utils.baz2.one import data
+results['baz2'] = data
+
+# Test import of module_utils/spam1/ham/eggs/__init__.py
+from ansible.module_utils.spam1.ham import eggs
+results['spam1'] = eggs.data
+
+# Test import of an identifier inside module_utils/spam2/ham/eggs/__init__.py
+from ansible.module_utils.spam2.ham.eggs import data
+results['spam2'] = data
+
+# Test import of module_utils/spam3/ham/bacon.py
+from ansible.module_utils.spam3.ham import bacon
+results['spam3'] = bacon.data
+
+# Test import of an identifier inside of module_utils/spam4/ham/bacon.py
+from ansible.module_utils.spam4.ham.bacon import data
+results['spam4'] = data
+
+# Test import of module_utils.spam5.ham bacon and eggs (modules)
+from ansible.module_utils.spam5.ham import bacon, eggs
+results['spam5'] = (bacon.data, eggs.data)
+
+# Test import of module_utils.spam6.ham bacon and eggs (identifiers)
+from ansible.module_utils.spam6.ham import bacon, eggs
+results['spam6'] = (bacon, eggs)
+
+# Test import of module_utils.spam7.ham bacon and eggs (module and identifier)
+from ansible.module_utils.spam7.ham import bacon, eggs
+results['spam7'] = (bacon.data, eggs)
+
+# Test import of module_utils/spam8/ham/bacon.py and module_utils/spam8/ham/eggs.py separately
+from ansible.module_utils.spam8.ham import bacon
+from ansible.module_utils.spam8.ham import eggs
+results['spam8'] = (bacon.data, eggs)
+
+# Test that import of module_utils/qux1/quux.py using as works
+from ansible.module_utils.qux1 import quux as one
+results['qux1'] = one.data
+
+# Test that importing qux2/quux.py and qux2/quuz.py using as works
+from ansible.module_utils.qux2 import quux as one, quuz as two
+results['qux2'] = (one.data, two.data)
+
+# Test depth
+from ansible.module_utils.a.b.c.d.e.f.g.h import data
+
+results['abcdefgh'] = data
+from ansible.module_utils.basic import AnsibleModule
+AnsibleModule(argument_spec=dict()).exit_json(**results)
diff --git a/test/integration/targets/module_utils/library/test_alias_deprecation.py b/test/integration/targets/module_utils/library/test_alias_deprecation.py
new file mode 100644
index 0000000..dc36aba
--- /dev/null
+++ b/test/integration/targets/module_utils/library/test_alias_deprecation.py
@@ -0,0 +1,16 @@
+#!/usr/bin/python
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+# overridden
+from ansible.module_utils.ansible_release import data
+
+results = {"data": data}
+
+arg_spec = dict(
+ foo=dict(type='str', aliases=['baz'], deprecated_aliases=[dict(name='baz', version='9.99')])
+)
+
+AnsibleModule(argument_spec=arg_spec).exit_json(**results)
diff --git a/test/integration/targets/module_utils/library/test_cwd_missing.py b/test/integration/targets/module_utils/library/test_cwd_missing.py
new file mode 100644
index 0000000..cd1f9c7
--- /dev/null
+++ b/test/integration/targets/module_utils/library/test_cwd_missing.py
@@ -0,0 +1,33 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import os
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ # This module verifies that AnsibleModule works when cwd does not exist.
+ # This situation can occur as a race condition when the following conditions are met:
+ #
+ # 1) Execute a module which has high startup overhead prior to instantiating AnsibleModule (0.5s is enough in many cases).
+ # 2) Run the module async as the last task in a playbook using connection=local (a fire-and-forget task).
+ # 3) Remove the directory containing the playbook immediately after playbook execution ends (playbook in a temp dir).
+ #
+ # To ease testing of this race condition the deletion of cwd is handled in this module.
+ # This avoids race conditions in the test, including timing cwd deletion between AnsiballZ wrapper execution and AnsibleModule instantiation.
+ # The timing issue with AnsiballZ is due to cwd checking in the wrapper when code coverage is enabled.
+
+ temp = os.path.abspath('temp')
+
+ os.mkdir(temp)
+ os.chdir(temp)
+ os.rmdir(temp)
+
+ module = AnsibleModule(argument_spec=dict())
+ module.exit_json(before=temp, after=os.getcwd())
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_utils/library/test_cwd_unreadable.py b/test/integration/targets/module_utils/library/test_cwd_unreadable.py
new file mode 100644
index 0000000..d65f31a
--- /dev/null
+++ b/test/integration/targets/module_utils/library/test_cwd_unreadable.py
@@ -0,0 +1,28 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import os
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ # This module verifies that AnsibleModule works when cwd exists but is unreadable.
+ # This situation can occur when running tasks as an unprivileged user.
+
+ try:
+ cwd = os.getcwd()
+ except OSError:
+ # Compensate for macOS being unable to access cwd as an unprivileged user.
+ # This test is a no-op in this case.
+ # Testing for os.getcwd() failures is handled by the test_cwd_missing module.
+ cwd = '/'
+ os.chdir(cwd)
+
+ module = AnsibleModule(argument_spec=dict())
+ module.exit_json(before=cwd, after=os.getcwd())
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_utils/library/test_datetime.py b/test/integration/targets/module_utils/library/test_datetime.py
new file mode 100644
index 0000000..493a186
--- /dev/null
+++ b/test/integration/targets/module_utils/library/test_datetime.py
@@ -0,0 +1,19 @@
+#!/usr/bin/python
+# Most of these names are only available via PluginLoader so pylint doesn't
+# know they exist
+# pylint: disable=no-name-in-module
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+import datetime
+
+module = AnsibleModule(argument_spec=dict(
+ datetime=dict(type=str, required=True),
+ date=dict(type=str, required=True),
+))
+result = {
+ 'datetime': datetime.datetime.strptime(module.params.get('datetime'), '%Y-%m-%dT%H:%M:%S'),
+ 'date': datetime.datetime.strptime(module.params.get('date'), '%Y-%m-%d').date(),
+}
+module.exit_json(**result)
diff --git a/test/integration/targets/module_utils/library/test_env_override.py b/test/integration/targets/module_utils/library/test_env_override.py
new file mode 100644
index 0000000..ebfb5dd
--- /dev/null
+++ b/test/integration/targets/module_utils/library/test_env_override.py
@@ -0,0 +1,14 @@
+#!/usr/bin/python
+# Most of these names are only available via PluginLoader so pylint doesn't
+# know they exist
+# pylint: disable=no-name-in-module
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.json_utils import data
+from ansible.module_utils.mork import data as mork_data
+
+results = {"json_utils": data, "mork": mork_data}
+
+AnsibleModule(argument_spec=dict()).exit_json(**results)
diff --git a/test/integration/targets/module_utils/library/test_failure.py b/test/integration/targets/module_utils/library/test_failure.py
new file mode 100644
index 0000000..efb3dda
--- /dev/null
+++ b/test/integration/targets/module_utils/library/test_failure.py
@@ -0,0 +1,14 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+results = {}
+# Test that we are rooted correctly
+# Following files:
+# module_utils/yak/zebra/foo.py
+from ansible.module_utils.zebra import foo
+
+results['zebra'] = foo.data
+
+from ansible.module_utils.basic import AnsibleModule
+AnsibleModule(argument_spec=dict()).exit_json(**results)
diff --git a/test/integration/targets/module_utils/library/test_network.py b/test/integration/targets/module_utils/library/test_network.py
new file mode 100644
index 0000000..c6a5390
--- /dev/null
+++ b/test/integration/targets/module_utils/library/test_network.py
@@ -0,0 +1,28 @@
+#!/usr/bin/python
+
+# Copyright: (c) 2021, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.network import to_subnet
+
+
+def main():
+ module = AnsibleModule(argument_spec=dict(
+ subnet=dict(),
+ ))
+
+ subnet = module.params['subnet']
+
+ if subnet is not None:
+ split_addr = subnet.split('/')
+ if len(split_addr) != 2:
+ module.fail_json("Invalid CIDR notation: expected a subnet mask (e.g. 10.0.0.0/32)")
+ module.exit_json(resolved=to_subnet(split_addr[0], split_addr[1]))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_utils/library/test_no_log.py b/test/integration/targets/module_utils/library/test_no_log.py
new file mode 100644
index 0000000..770e0b3
--- /dev/null
+++ b/test/integration/targets/module_utils/library/test_no_log.py
@@ -0,0 +1,35 @@
+#!/usr/bin/python
+# (c) 2021 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+from ansible.module_utils.basic import AnsibleModule, env_fallback
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ explicit_pass=dict(type='str', no_log=True),
+ fallback_pass=dict(type='str', no_log=True, fallback=(env_fallback, ['SECRET_ENV'])),
+ default_pass=dict(type='str', no_log=True, default='zyx'),
+ normal=dict(type='str', default='plaintext'),
+ suboption=dict(
+ type='dict',
+ options=dict(
+ explicit_sub_pass=dict(type='str', no_log=True),
+ fallback_sub_pass=dict(type='str', no_log=True, fallback=(env_fallback, ['SECRET_SUB_ENV'])),
+ default_sub_pass=dict(type='str', no_log=True, default='xvu'),
+ normal=dict(type='str', default='plaintext'),
+ ),
+ ),
+ ),
+ )
+
+ module.exit_json(changed=False)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_utils/library/test_optional.py b/test/integration/targets/module_utils/library/test_optional.py
new file mode 100644
index 0000000..4d0225d
--- /dev/null
+++ b/test/integration/targets/module_utils/library/test_optional.py
@@ -0,0 +1,84 @@
+#!/usr/bin/python
+# Most of these names are only available via PluginLoader so pylint doesn't
+# know they exist
+# pylint: disable=no-name-in-module
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+
+# internal constants to keep pylint from griping about constant-valued conditionals
+_private_false = False
+_private_true = True
+
+# module_utils import statements nested below any block are considered optional "best-effort" for AnsiballZ to include.
+# test a number of different import shapes and nesting types to exercise this...
+
+# first, some nested imports that should succeed...
+try:
+ from ansible.module_utils.urls import fetch_url as yep1
+except ImportError:
+ yep1 = None
+
+try:
+ import ansible.module_utils.common.text.converters as yep2
+except ImportError:
+ yep2 = None
+
+try:
+ # optional import from a legit collection
+ from ansible_collections.testns.testcoll.plugins.module_utils.legit import importme as yep3
+except ImportError:
+ yep3 = None
+
+# and a bunch that should fail to be found, but not break the module_utils payload build in the process...
+try:
+ from ansible.module_utils.bogus import fromnope1
+except ImportError:
+ fromnope1 = None
+
+if _private_false:
+ from ansible.module_utils.alsobogus import fromnope2
+else:
+ fromnope2 = None
+
+try:
+ import ansible.module_utils.verybogus
+ nope1 = ansible.module_utils.verybogus
+except ImportError:
+ nope1 = None
+
+# deepish nested with multiple block types- make sure the AST walker made it all the way down
+try:
+ if _private_true:
+ if _private_true:
+ if _private_true:
+ if _private_true:
+ try:
+ import ansible.module_utils.stillbogus as nope2
+ except ImportError:
+ raise
+except ImportError:
+ nope2 = None
+
+try:
+ # optional import from a valid collection with an invalid package
+ from ansible_collections.testns.testcoll.plugins.module_utils.bogus import collnope1
+except ImportError:
+ collnope1 = None
+
+try:
+ # optional import from a bogus collection
+ from ansible_collections.bogusns.boguscoll.plugins.module_utils.bogus import collnope2
+except ImportError:
+ collnope2 = None
+
+module = AnsibleModule(argument_spec={})
+
+if not all([yep1, yep2, yep3]):
+ module.fail_json(msg='one or more existing optional imports did not resolve')
+
+if any([fromnope1, fromnope2, nope1, nope2, collnope1, collnope2]):
+ module.fail_json(msg='one or more missing optional imports resolved unexpectedly')
+
+module.exit_json(msg='all missing optional imports behaved as expected')
diff --git a/test/integration/targets/module_utils/library/test_override.py b/test/integration/targets/module_utils/library/test_override.py
new file mode 100644
index 0000000..7f6e7a5
--- /dev/null
+++ b/test/integration/targets/module_utils/library/test_override.py
@@ -0,0 +1,11 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+# overridden
+from ansible.module_utils.ansible_release import data
+
+results = {"data": data}
+
+AnsibleModule(argument_spec=dict()).exit_json(**results)
diff --git a/test/integration/targets/module_utils/library/test_recursive_diff.py b/test/integration/targets/module_utils/library/test_recursive_diff.py
new file mode 100644
index 0000000..0cf39d9
--- /dev/null
+++ b/test/integration/targets/module_utils/library/test_recursive_diff.py
@@ -0,0 +1,29 @@
+#!/usr/bin/python
+# Copyright: (c) 2020, Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.dict_transformations import recursive_diff
+
+
+def main():
+ module = AnsibleModule(
+ {
+ 'a': {'type': 'dict'},
+ 'b': {'type': 'dict'},
+ }
+ )
+
+ module.exit_json(
+ the_diff=recursive_diff(
+ module.params['a'],
+ module.params['b'],
+ ),
+ )
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_utils/module_utils/__init__.py b/test/integration/targets/module_utils/module_utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/a/__init__.py b/test/integration/targets/module_utils/module_utils/a/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/a/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/a/b/__init__.py b/test/integration/targets/module_utils/module_utils/a/b/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/a/b/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/a/b/c/__init__.py b/test/integration/targets/module_utils/module_utils/a/b/c/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/a/b/c/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/a/b/c/d/__init__.py b/test/integration/targets/module_utils/module_utils/a/b/c/d/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/a/b/c/d/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/a/b/c/d/e/__init__.py b/test/integration/targets/module_utils/module_utils/a/b/c/d/e/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/a/b/c/d/e/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/__init__.py b/test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/g/__init__.py b/test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/g/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/g/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/g/h/__init__.py b/test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/g/h/__init__.py
new file mode 100644
index 0000000..722f4b7
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/g/h/__init__.py
@@ -0,0 +1 @@
+data = 'abcdefgh'
diff --git a/test/integration/targets/module_utils/module_utils/ansible_release.py b/test/integration/targets/module_utils/module_utils/ansible_release.py
new file mode 100644
index 0000000..7d43bf8
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/ansible_release.py
@@ -0,0 +1,4 @@
+# This file overrides the builtin ansible.module_utils.ansible_release file
+# to test that it can be overridden. Previously this was facts.py but caused issues
+# with dependencies that may need to execute a module that makes use of facts
+data = 'overridden ansible_release.py'
diff --git a/test/integration/targets/module_utils/module_utils/bar0/__init__.py b/test/integration/targets/module_utils/module_utils/bar0/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/bar0/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/bar0/foo.py b/test/integration/targets/module_utils/module_utils/bar0/foo.py
new file mode 100644
index 0000000..1072dcc
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/bar0/foo.py
@@ -0,0 +1 @@
+data = 'bar0'
diff --git a/test/integration/targets/module_utils/module_utils/bar1/__init__.py b/test/integration/targets/module_utils/module_utils/bar1/__init__.py
new file mode 100644
index 0000000..68e4350
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/bar1/__init__.py
@@ -0,0 +1 @@
+data = 'bar1'
diff --git a/test/integration/targets/module_utils/module_utils/bar2/__init__.py b/test/integration/targets/module_utils/module_utils/bar2/__init__.py
new file mode 100644
index 0000000..59e86af
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/bar2/__init__.py
@@ -0,0 +1 @@
+data = 'bar2'
diff --git a/test/integration/targets/module_utils/module_utils/baz1/__init__.py b/test/integration/targets/module_utils/module_utils/baz1/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/baz1/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/baz1/one.py b/test/integration/targets/module_utils/module_utils/baz1/one.py
new file mode 100644
index 0000000..e5d7894
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/baz1/one.py
@@ -0,0 +1 @@
+data = 'baz1'
diff --git a/test/integration/targets/module_utils/module_utils/baz2/__init__.py b/test/integration/targets/module_utils/module_utils/baz2/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/baz2/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/baz2/one.py b/test/integration/targets/module_utils/module_utils/baz2/one.py
new file mode 100644
index 0000000..1efe196
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/baz2/one.py
@@ -0,0 +1 @@
+data = 'baz2'
diff --git a/test/integration/targets/module_utils/module_utils/foo.py b/test/integration/targets/module_utils/module_utils/foo.py
new file mode 100644
index 0000000..20698f1
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/foo.py
@@ -0,0 +1,3 @@
+#!/usr/bin/env python
+
+foo = "FOO FROM foo.py"
diff --git a/test/integration/targets/module_utils/module_utils/foo0.py b/test/integration/targets/module_utils/module_utils/foo0.py
new file mode 100644
index 0000000..4b528b6
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/foo0.py
@@ -0,0 +1 @@
+data = 'foo0'
diff --git a/test/integration/targets/module_utils/module_utils/foo1.py b/test/integration/targets/module_utils/module_utils/foo1.py
new file mode 100644
index 0000000..18e0cef
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/foo1.py
@@ -0,0 +1 @@
+data = 'foo1'
diff --git a/test/integration/targets/module_utils/module_utils/foo2.py b/test/integration/targets/module_utils/module_utils/foo2.py
new file mode 100644
index 0000000..feb142d
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/foo2.py
@@ -0,0 +1 @@
+data = 'foo2'
diff --git a/test/integration/targets/module_utils/module_utils/qux1/__init__.py b/test/integration/targets/module_utils/module_utils/qux1/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/qux1/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/qux1/quux.py b/test/integration/targets/module_utils/module_utils/qux1/quux.py
new file mode 100644
index 0000000..3d288c9
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/qux1/quux.py
@@ -0,0 +1 @@
+data = 'qux1'
diff --git a/test/integration/targets/module_utils/module_utils/qux2/__init__.py b/test/integration/targets/module_utils/module_utils/qux2/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/qux2/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/qux2/quux.py b/test/integration/targets/module_utils/module_utils/qux2/quux.py
new file mode 100644
index 0000000..496d446
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/qux2/quux.py
@@ -0,0 +1 @@
+data = 'qux2:quux'
diff --git a/test/integration/targets/module_utils/module_utils/qux2/quuz.py b/test/integration/targets/module_utils/module_utils/qux2/quuz.py
new file mode 100644
index 0000000..cdc0fad
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/qux2/quuz.py
@@ -0,0 +1 @@
+data = 'qux2:quuz'
diff --git a/test/integration/targets/module_utils/module_utils/service.py b/test/integration/targets/module_utils/module_utils/service.py
new file mode 100644
index 0000000..1492f46
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/service.py
@@ -0,0 +1 @@
+sysv_is_enabled = 'sysv_is_enabled'
diff --git a/test/integration/targets/module_utils/module_utils/spam1/__init__.py b/test/integration/targets/module_utils/module_utils/spam1/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam1/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam1/ham/__init__.py b/test/integration/targets/module_utils/module_utils/spam1/ham/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam1/ham/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam1/ham/eggs/__init__.py b/test/integration/targets/module_utils/module_utils/spam1/ham/eggs/__init__.py
new file mode 100644
index 0000000..f290e15
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam1/ham/eggs/__init__.py
@@ -0,0 +1 @@
+data = 'spam1'
diff --git a/test/integration/targets/module_utils/module_utils/spam2/__init__.py b/test/integration/targets/module_utils/module_utils/spam2/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam2/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam2/ham/__init__.py b/test/integration/targets/module_utils/module_utils/spam2/ham/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam2/ham/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam2/ham/eggs/__init__.py b/test/integration/targets/module_utils/module_utils/spam2/ham/eggs/__init__.py
new file mode 100644
index 0000000..5e053d8
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam2/ham/eggs/__init__.py
@@ -0,0 +1 @@
+data = 'spam2'
diff --git a/test/integration/targets/module_utils/module_utils/spam3/__init__.py b/test/integration/targets/module_utils/module_utils/spam3/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam3/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam3/ham/__init__.py b/test/integration/targets/module_utils/module_utils/spam3/ham/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam3/ham/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam3/ham/bacon.py b/test/integration/targets/module_utils/module_utils/spam3/ham/bacon.py
new file mode 100644
index 0000000..9107508
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam3/ham/bacon.py
@@ -0,0 +1 @@
+data = 'spam3'
diff --git a/test/integration/targets/module_utils/module_utils/spam4/__init__.py b/test/integration/targets/module_utils/module_utils/spam4/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam4/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam4/ham/__init__.py b/test/integration/targets/module_utils/module_utils/spam4/ham/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam4/ham/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam4/ham/bacon.py b/test/integration/targets/module_utils/module_utils/spam4/ham/bacon.py
new file mode 100644
index 0000000..7d55288
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam4/ham/bacon.py
@@ -0,0 +1 @@
+data = 'spam4'
diff --git a/test/integration/targets/module_utils/module_utils/spam5/__init__.py b/test/integration/targets/module_utils/module_utils/spam5/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam5/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam5/ham/__init__.py b/test/integration/targets/module_utils/module_utils/spam5/ham/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam5/ham/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam5/ham/bacon.py b/test/integration/targets/module_utils/module_utils/spam5/ham/bacon.py
new file mode 100644
index 0000000..cc947b8
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam5/ham/bacon.py
@@ -0,0 +1 @@
+data = 'spam5:bacon'
diff --git a/test/integration/targets/module_utils/module_utils/spam5/ham/eggs.py b/test/integration/targets/module_utils/module_utils/spam5/ham/eggs.py
new file mode 100644
index 0000000..f0394c8
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam5/ham/eggs.py
@@ -0,0 +1 @@
+data = 'spam5:eggs'
diff --git a/test/integration/targets/module_utils/module_utils/spam6/__init__.py b/test/integration/targets/module_utils/module_utils/spam6/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam6/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam6/ham/__init__.py b/test/integration/targets/module_utils/module_utils/spam6/ham/__init__.py
new file mode 100644
index 0000000..8c1a70e
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam6/ham/__init__.py
@@ -0,0 +1,2 @@
+bacon = 'spam6:bacon'
+eggs = 'spam6:eggs'
diff --git a/test/integration/targets/module_utils/module_utils/spam7/__init__.py b/test/integration/targets/module_utils/module_utils/spam7/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam7/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam7/ham/__init__.py b/test/integration/targets/module_utils/module_utils/spam7/ham/__init__.py
new file mode 100644
index 0000000..cd9a05d
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam7/ham/__init__.py
@@ -0,0 +1 @@
+eggs = 'spam7:eggs'
diff --git a/test/integration/targets/module_utils/module_utils/spam7/ham/bacon.py b/test/integration/targets/module_utils/module_utils/spam7/ham/bacon.py
new file mode 100644
index 0000000..490121f
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam7/ham/bacon.py
@@ -0,0 +1 @@
+data = 'spam7:bacon'
diff --git a/test/integration/targets/module_utils/module_utils/spam8/__init__.py b/test/integration/targets/module_utils/module_utils/spam8/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam8/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam8/ham/__init__.py b/test/integration/targets/module_utils/module_utils/spam8/ham/__init__.py
new file mode 100644
index 0000000..c02bf5f
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam8/ham/__init__.py
@@ -0,0 +1 @@
+eggs = 'spam8:eggs'
diff --git a/test/integration/targets/module_utils/module_utils/spam8/ham/bacon.py b/test/integration/targets/module_utils/module_utils/spam8/ham/bacon.py
new file mode 100644
index 0000000..28ea285
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam8/ham/bacon.py
@@ -0,0 +1 @@
+data = 'spam8:bacon'
diff --git a/test/integration/targets/module_utils/module_utils/sub/__init__.py b/test/integration/targets/module_utils/module_utils/sub/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/sub/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/sub/bam.py b/test/integration/targets/module_utils/module_utils/sub/bam.py
new file mode 100644
index 0000000..566f8b7
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/sub/bam.py
@@ -0,0 +1,3 @@
+#!/usr/bin/env python
+
+bam = "BAM FROM sub/bam.py"
diff --git a/test/integration/targets/module_utils/module_utils/sub/bam/__init__.py b/test/integration/targets/module_utils/module_utils/sub/bam/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/sub/bam/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/sub/bam/bam.py b/test/integration/targets/module_utils/module_utils/sub/bam/bam.py
new file mode 100644
index 0000000..b7ed707
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/sub/bam/bam.py
@@ -0,0 +1,3 @@
+#!/usr/bin/env python
+
+bam = "BAM FROM sub/bam/bam.py"
diff --git a/test/integration/targets/module_utils/module_utils/sub/bar/__init__.py b/test/integration/targets/module_utils/module_utils/sub/bar/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/sub/bar/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/sub/bar/bam.py b/test/integration/targets/module_utils/module_utils/sub/bar/bam.py
new file mode 100644
index 0000000..02fafd4
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/sub/bar/bam.py
@@ -0,0 +1,3 @@
+#!/usr/bin/env python
+
+bam = "BAM FROM sub/bar/bam.py"
diff --git a/test/integration/targets/module_utils/module_utils/sub/bar/bar.py b/test/integration/targets/module_utils/module_utils/sub/bar/bar.py
new file mode 100644
index 0000000..8566901
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/sub/bar/bar.py
@@ -0,0 +1,3 @@
+#!/usr/bin/env python
+
+bar = "BAR FROM sub/bar/bar.py"
diff --git a/test/integration/targets/module_utils/module_utils/yak/__init__.py b/test/integration/targets/module_utils/module_utils/yak/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/yak/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/yak/zebra/__init__.py b/test/integration/targets/module_utils/module_utils/yak/zebra/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/yak/zebra/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/yak/zebra/foo.py b/test/integration/targets/module_utils/module_utils/yak/zebra/foo.py
new file mode 100644
index 0000000..89b2bfe
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/yak/zebra/foo.py
@@ -0,0 +1 @@
+data = 'yak'
diff --git a/test/integration/targets/module_utils/module_utils_basic_setcwd.yml b/test/integration/targets/module_utils/module_utils_basic_setcwd.yml
new file mode 100644
index 0000000..71317f9
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils_basic_setcwd.yml
@@ -0,0 +1,53 @@
+- hosts: testhost
+ gather_facts: no
+ tasks:
+ - name: make sure the test user is available
+ include_role:
+ name: setup_test_user
+
+ - name: verify AnsibleModule works when cwd is missing
+ test_cwd_missing:
+ register: missing
+
+ - name: record the mode of the connection user's home directory
+ stat:
+ path: "~"
+ vars:
+ ansible_become: no
+ register: connection_user_home
+
+ - name: limit access to the connection user's home directory
+ file:
+ state: directory
+ path: "{{ connection_user_home.stat.path }}"
+ mode: "0700"
+ vars:
+ ansible_become: no
+
+ - block:
+ - name: verify AnsibleModule works when cwd is unreadable
+ test_cwd_unreadable:
+ register: unreadable
+ vars: &test_user_become
+ ansible_become: yes
+ ansible_become_user: "{{ test_user_name }}" # root can read cwd regardless of permissions, so a non-root user is required here
+ ansible_become_password: "{{ test_user_plaintext_password }}"
+ always:
+ - name: restore access to the connection user's home directory
+ file:
+ state: directory
+ path: "{{ connection_user_home.stat.path }}"
+ mode: "{{ connection_user_home.stat.mode }}"
+ vars:
+ ansible_become: no
+
+ - name: get real path of home directory of the unprivileged user
+ raw: "{{ ansible_python_interpreter }} -c 'import os.path; print(os.path.realpath(os.path.expanduser(\"~\")))'"
+ register: home
+ vars: *test_user_become
+
+ - name: verify AnsibleModule was able to adjust cwd as expected
+ assert:
+ that:
+ - missing.before != missing.after
+ - unreadable.before != unreadable.after or unreadable.before == '/' or unreadable.before == home.stdout.strip() # allow / and $HOME fallback on macOS when using an unprivileged user
diff --git a/test/integration/targets/module_utils/module_utils_common_dict_transformation.yml b/test/integration/targets/module_utils/module_utils_common_dict_transformation.yml
new file mode 100644
index 0000000..7d961c4
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils_common_dict_transformation.yml
@@ -0,0 +1,34 @@
+- hosts: testhost
+ gather_facts: no
+ tasks:
+ - test_recursive_diff:
+ a:
+ foo:
+ bar:
+ - baz:
+ qux: ham_sandwich
+ b:
+ foo:
+ bar:
+ - baz:
+ qux: turkey_sandwich
+ register: recursive_diff_diff
+
+ - test_recursive_diff:
+ a:
+ foo:
+ bar:
+ - baz:
+ qux: ham_sandwich
+ b:
+ foo:
+ bar:
+ - baz:
+ qux: ham_sandwich
+ register: recursive_diff_same
+
+ - assert:
+ that:
+ - recursive_diff_diff.the_diff is not none
+ - recursive_diff_diff.the_diff|length == 2
+ - recursive_diff_same.the_diff is none
diff --git a/test/integration/targets/module_utils/module_utils_common_network.yml b/test/integration/targets/module_utils/module_utils_common_network.yml
new file mode 100644
index 0000000..e1b953f
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils_common_network.yml
@@ -0,0 +1,10 @@
+- hosts: testhost
+ gather_facts: no
+ tasks:
+ - test_network:
+ subnet: "10.0.0.2/24"
+ register: subnet
+
+ - assert:
+ that:
+ - subnet.resolved == "10.0.0.0/24"
diff --git a/test/integration/targets/module_utils/module_utils_envvar.yml b/test/integration/targets/module_utils/module_utils_envvar.yml
new file mode 100644
index 0000000..8c37940
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils_envvar.yml
@@ -0,0 +1,51 @@
+- hosts: testhost
+ gather_facts: no
+ tasks:
+ - name: Use a specially crafted module to see if things were imported correctly
+ test:
+ register: result
+
+ - name: Check that these are all loaded from playbook dir's module_utils
+ assert:
+ that:
+ - 'result["abcdefgh"] == "abcdefgh"'
+ - 'result["bar0"] == "bar0"'
+ - 'result["bar1"] == "bar1"'
+ - 'result["bar2"] == "bar2"'
+ - 'result["baz1"] == "baz1"'
+ - 'result["baz2"] == "baz2"'
+ - 'result["foo0"] == "foo0"'
+ - 'result["foo1"] == "foo1"'
+ - 'result["foo2"] == "foo2"'
+ - 'result["qux1"] == "qux1"'
+ - 'result["qux2"] == ["qux2:quux", "qux2:quuz"]'
+ - 'result["spam1"] == "spam1"'
+ - 'result["spam2"] == "spam2"'
+ - 'result["spam3"] == "spam3"'
+ - 'result["spam4"] == "spam4"'
+ - 'result["spam5"] == ["spam5:bacon", "spam5:eggs"]'
+ - 'result["spam6"] == ["spam6:bacon", "spam6:eggs"]'
+ - 'result["spam7"] == ["spam7:bacon", "spam7:eggs"]'
+ - 'result["spam8"] == ["spam8:bacon", "spam8:eggs"]'
+
+ # Test that overriding something in module_utils with something in the local library works
+ - name: Test that playbook dir's module_utils overrides facts.py
+ test_override:
+ register: result
+
+ - name: Make sure the we used the local ansible_release.py, not the one shipped with ansible
+ assert:
+ that:
+ - 'result["data"] == "overridden ansible_release.py"'
+
+ - name: Test that importing something from the module_utils in the env_vars works
+ test_env_override:
+ register: result
+
+ - name: Make sure we used the module_utils from the env_var for these
+ assert:
+ that:
+ # Override of shipped module_utils
+ - 'result["json_utils"] == "overridden json_utils"'
+ # Only i nthe env vars directory
+ - 'result["mork"] == "mork"'
diff --git a/test/integration/targets/module_utils/module_utils_test.yml b/test/integration/targets/module_utils/module_utils_test.yml
new file mode 100644
index 0000000..4e948bd
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils_test.yml
@@ -0,0 +1,121 @@
+- hosts: testhost
+ gather_facts: no
+ tasks:
+ - name: Use a specially crafted module to see if things were imported correctly
+ test:
+ register: result
+
+ - name: Check that the module imported the correct version of each module_util
+ assert:
+ that:
+ - 'result["abcdefgh"] == "abcdefgh"'
+ - 'result["bar0"] == "bar0"'
+ - 'result["bar1"] == "bar1"'
+ - 'result["bar2"] == "bar2"'
+ - 'result["baz1"] == "baz1"'
+ - 'result["baz2"] == "baz2"'
+ - 'result["foo0"] == "foo0"'
+ - 'result["foo1"] == "foo1"'
+ - 'result["foo2"] == "foo2"'
+ - 'result["qux1"] == "qux1"'
+ - 'result["qux2"] == ["qux2:quux", "qux2:quuz"]'
+ - 'result["spam1"] == "spam1"'
+ - 'result["spam2"] == "spam2"'
+ - 'result["spam3"] == "spam3"'
+ - 'result["spam4"] == "spam4"'
+ - 'result["spam5"] == ["spam5:bacon", "spam5:eggs"]'
+ - 'result["spam6"] == ["spam6:bacon", "spam6:eggs"]'
+ - 'result["spam7"] == ["spam7:bacon", "spam7:eggs"]'
+ - 'result["spam8"] == ["spam8:bacon", "spam8:eggs"]'
+
+ # Test that overriding something in module_utils with something in the local library works
+ - name: Test that local module_utils overrides facts.py
+ test_override:
+ register: result
+
+ - name: Make sure the we used the local ansible_release.py, not the one shipped with ansible
+ assert:
+ that:
+ - result["data"] == "overridden ansible_release.py"
+
+ - name: Test that importing a module that only exists inside of a submodule does not work
+ test_failure:
+ ignore_errors: True
+ register: result
+
+ - name: Make sure we failed in AnsiBallZ
+ assert:
+ that:
+ - result is failed
+ - result['msg'] == "Could not find imported module support code for ansible.modules.test_failure. Looked for (['ansible.module_utils.zebra.foo', 'ansible.module_utils.zebra'])"
+
+ - name: Test that alias deprecation works
+ test_alias_deprecation:
+ baz: 'bar'
+ register: result
+
+ - name: Assert that the deprecation message is given correctly
+ assert:
+ that:
+ - result.deprecations[-1].msg == "Alias 'baz' is deprecated. See the module docs for more information"
+ - result.deprecations[-1].version == '9.99'
+
+ - block:
+ - import_role:
+ name: setup_remote_tmp_dir
+
+ - name: Get a string with a \0 in it
+ command: echo -e 'hi\0foo'
+ register: string_with_null
+
+ - name: Use the null string as a module parameter
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/nulltest"
+ line: "{{ string_with_null.stdout }}"
+ create: yes
+ ignore_errors: yes
+ register: nulltest
+
+ - name: See if the file exists
+ stat:
+ path: "{{ remote_tmp_dir }}/nulltest"
+ register: nullstat
+
+ - assert:
+ that:
+ - nulltest is failed
+ - nulltest.msg_to_log.startswith('Invoked ')
+ - nulltest.msg.startswith('Failed to log to syslog')
+ # Conditionalize this, because when we log with something other than
+ # syslog, it's probably successful and these assertions will fail.
+ when: nulltest is failed
+
+ # Ensure we fail out early and don't actually run the module if logging
+ # failed.
+ - assert:
+ that:
+ - nullstat.stat.exists == nulltest is successful
+ always:
+ - file:
+ path: "{{ remote_tmp_dir }}/nulltest"
+ state: absent
+
+ - name: Test that date and datetime in module output works
+ test_datetime:
+ date: "2020-10-05"
+ datetime: "2020-10-05T10:05:05"
+ register: datetimetest
+
+ - assert:
+ that:
+ - datetimetest.date == '2020-10-05'
+ - datetimetest.datetime == '2020-10-05T10:05:05'
+
+ - name: Test that optional imports behave properly
+ test_optional:
+ register: optionaltest
+
+ - assert:
+ that:
+ - optionaltest is success
+ - optionaltest.msg == 'all missing optional imports behaved as expected'
diff --git a/test/integration/targets/module_utils/module_utils_test_no_log.yml b/test/integration/targets/module_utils/module_utils_test_no_log.yml
new file mode 100644
index 0000000..2fa3e10
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils_test_no_log.yml
@@ -0,0 +1,12 @@
+# This is called by module_utils_vvvvv.yml with a custom callback
+- hosts: testhost
+ gather_facts: no
+ tasks:
+ - name: Check no_log invocation results
+ test_no_log:
+ explicit_pass: abc
+ suboption:
+ explicit_sub_pass: def
+ environment:
+ SECRET_ENV: ghi
+ SECRET_SUB_ENV: jkl
diff --git a/test/integration/targets/module_utils/module_utils_vvvvv.yml b/test/integration/targets/module_utils/module_utils_vvvvv.yml
new file mode 100644
index 0000000..fc2b0c1
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils_vvvvv.yml
@@ -0,0 +1,29 @@
+- hosts: testhost
+ gather_facts: no
+ tasks:
+ - name: Use a specially crafted module to see if things were imported correctly
+ test:
+
+ # Invocation usually is output with 3vs or more, our callback plugin displays it anyway
+ - name: Check no_log invocation results
+ command: ansible-playbook -i {{ inventory_file }} module_utils_test_no_log.yml
+ delegate_to: localhost
+ environment:
+ ANSIBLE_CALLBACK_PLUGINS: callback
+ ANSIBLE_STDOUT_CALLBACK: pure_json
+ register: no_log_invocation
+
+ - set_fact:
+ no_log_invocation: '{{ no_log_invocation.stdout | trim | from_json }}'
+
+ - name: check no log values from fallback or default are masked
+ assert:
+ that:
+ - no_log_invocation.invocation.module_args.default_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
+ - no_log_invocation.invocation.module_args.explicit_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
+ - no_log_invocation.invocation.module_args.fallback_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
+ - no_log_invocation.invocation.module_args.normal == 'plaintext'
+ - no_log_invocation.invocation.module_args.suboption.default_sub_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
+ - no_log_invocation.invocation.module_args.suboption.explicit_sub_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
+ - no_log_invocation.invocation.module_args.suboption.fallback_sub_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
+ - no_log_invocation.invocation.module_args.suboption.normal == 'plaintext'
diff --git a/test/integration/targets/module_utils/other_mu_dir/__init__.py b/test/integration/targets/module_utils/other_mu_dir/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/other_mu_dir/__init__.py
diff --git a/test/integration/targets/module_utils/other_mu_dir/a/__init__.py b/test/integration/targets/module_utils/other_mu_dir/a/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/other_mu_dir/a/__init__.py
diff --git a/test/integration/targets/module_utils/other_mu_dir/a/b/__init__.py b/test/integration/targets/module_utils/other_mu_dir/a/b/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/other_mu_dir/a/b/__init__.py
diff --git a/test/integration/targets/module_utils/other_mu_dir/a/b/c/__init__.py b/test/integration/targets/module_utils/other_mu_dir/a/b/c/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/other_mu_dir/a/b/c/__init__.py
diff --git a/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/__init__.py b/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/__init__.py
diff --git a/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/__init__.py b/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/__init__.py
diff --git a/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/__init__.py b/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/__init__.py
diff --git a/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/g/__init__.py b/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/g/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/g/__init__.py
diff --git a/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/g/h/__init__.py b/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/g/h/__init__.py
new file mode 100644
index 0000000..796fed3
--- /dev/null
+++ b/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/g/h/__init__.py
@@ -0,0 +1 @@
+data = 'should not be visible abcdefgh'
diff --git a/test/integration/targets/module_utils/other_mu_dir/facts.py b/test/integration/targets/module_utils/other_mu_dir/facts.py
new file mode 100644
index 0000000..dbfab27
--- /dev/null
+++ b/test/integration/targets/module_utils/other_mu_dir/facts.py
@@ -0,0 +1 @@
+data = 'should not be visible facts.py'
diff --git a/test/integration/targets/module_utils/other_mu_dir/json_utils.py b/test/integration/targets/module_utils/other_mu_dir/json_utils.py
new file mode 100644
index 0000000..59757e4
--- /dev/null
+++ b/test/integration/targets/module_utils/other_mu_dir/json_utils.py
@@ -0,0 +1 @@
+data = 'overridden json_utils'
diff --git a/test/integration/targets/module_utils/other_mu_dir/mork.py b/test/integration/targets/module_utils/other_mu_dir/mork.py
new file mode 100644
index 0000000..3b700fc
--- /dev/null
+++ b/test/integration/targets/module_utils/other_mu_dir/mork.py
@@ -0,0 +1 @@
+data = 'mork'
diff --git a/test/integration/targets/module_utils/runme.sh b/test/integration/targets/module_utils/runme.sh
new file mode 100755
index 0000000..15f022b
--- /dev/null
+++ b/test/integration/targets/module_utils/runme.sh
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+
+set -eux
+
+export ANSIBLE_ROLES_PATH=../
+
+ansible-playbook module_utils_basic_setcwd.yml -i ../../inventory "$@"
+
+# Keep the -vvvvv here. This acts as a test for testing that higher verbosity
+# doesn't traceback with unicode in the custom module_utils directory path.
+ansible-playbook module_utils_vvvvv.yml -i ../../inventory -vvvvv "$@"
+
+ansible-playbook module_utils_test.yml -i ../../inventory -v "$@"
+
+ANSIBLE_MODULE_UTILS=other_mu_dir ansible-playbook module_utils_envvar.yml -i ../../inventory -v "$@"
+
+ansible-playbook module_utils_common_dict_transformation.yml -i ../../inventory "$@"
+
+ansible-playbook module_utils_common_network.yml -i ../../inventory "$@"
diff --git a/test/integration/targets/module_utils_Ansible.AccessToken/aliases b/test/integration/targets/module_utils_Ansible.AccessToken/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.AccessToken/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.AccessToken/library/ansible_access_token_tests.ps1 b/test/integration/targets/module_utils_Ansible.AccessToken/library/ansible_access_token_tests.ps1
new file mode 100644
index 0000000..a1de2b4
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.AccessToken/library/ansible_access_token_tests.ps1
@@ -0,0 +1,407 @@
+# End of the setup code and start of the module code
+#!powershell
+
+#AnsibleRequires -CSharpUtil Ansible.AccessToken
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$spec = @{
+ options = @{
+ test_username = @{ type = "str"; required = $true }
+ test_password = @{ type = "str"; required = $true; no_log = $true }
+ }
+}
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$test_username = $module.Params.test_username
+$test_password = $module.Params.test_password
+
+Function Assert-Equal {
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)][AllowNull()]$Actual,
+ [Parameter(Mandatory = $true, Position = 0)][AllowNull()]$Expected
+ )
+
+ process {
+ $matched = $false
+ if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array]) {
+ $Actual.Count | Assert-Equal -Expected $Expected.Count
+ for ($i = 0; $i -lt $Actual.Count; $i++) {
+ $actual_value = $Actual[$i]
+ $expected_value = $Expected[$i]
+ Assert-Equal -Actual $actual_value -Expected $expected_value
+ }
+ $matched = $true
+ }
+ else {
+ $matched = $Actual -ceq $Expected
+ }
+
+ if (-not $matched) {
+ if ($Actual -is [PSObject]) {
+ $Actual = $Actual.ToString()
+ }
+
+ $call_stack = (Get-PSCallStack)[1]
+ $module.Result.test = $test
+ $module.Result.actual = $Actual
+ $module.Result.expected = $Expected
+ $module.Result.line = $call_stack.ScriptLineNumber
+ $module.Result.method = $call_stack.Position.Text
+
+ $module.FailJson("AssertionError: actual != expected")
+ }
+ }
+}
+
+$current_user = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
+
+$tests = [Ordered]@{
+ "Open process token" = {
+ $h_process = [Ansible.AccessToken.TokenUtil]::OpenProcess()
+
+ $h_token = [Ansible.AccessToken.TokenUtil]::OpenProcessToken($h_process, "Query")
+ try {
+ $h_token.IsClosed | Assert-Equal -Expected $false
+ $h_token.IsInvalid | Assert-Equal -Expected $false
+
+ $actual_user = [Ansible.AccessToken.TokenUtil]::GetTokenUser($h_token)
+ $actual_user | Assert-Equal -Expected $current_user
+ }
+ finally {
+ $h_token.Dispose()
+ }
+ $h_token.IsClosed | Assert-Equal -Expected $true
+ }
+
+ "Open process token of another process" = {
+ $proc_info = Start-Process -FilePath "powershell.exe" -ArgumentList "-Command Start-Sleep -Seconds 60" -WindowStyle Hidden -PassThru
+ try {
+ $h_process = [Ansible.AccessToken.TokenUtil]::OpenProcess($proc_info.Id, "QueryInformation", $false)
+ try {
+ $h_process.IsClosed | Assert-Equal -Expected $false
+ $h_process.IsInvalid | Assert-Equal -Expected $false
+
+ $h_token = [Ansible.AccessToken.TokenUtil]::OpenProcessToken($h_process, "Query")
+ try {
+ $actual_user = [Ansible.AccessToken.TokenUtil]::GetTokenUser($h_token)
+ $actual_user | Assert-Equal -Expected $current_user
+ }
+ finally {
+ $h_token.Dispose()
+ }
+ }
+ finally {
+ $h_process.Dispose()
+ }
+ $h_process.IsClosed | Assert-Equal -Expected $true
+ }
+ finally {
+ $proc_info | Stop-Process
+ }
+ }
+
+ "Failed to open process token" = {
+ $failed = $false
+ try {
+ $h_process = [Ansible.AccessToken.TokenUtil]::OpenProcess(4, "QueryInformation", $false)
+ $h_process.Dispose() # Incase this doesn't fail, make sure we still dispose of it
+ }
+ catch [Ansible.AccessToken.Win32Exception] {
+ $failed = $true
+ $msg = "Failed to open process 4 with access QueryInformation (Access is denied, Win32ErrorCode 5 - 0x00000005)"
+ $_.Exception.Message | Assert-Equal -Expected $msg
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "Duplicate access token primary" = {
+ $h_process = [Ansible.AccessToken.TokenUtil]::OpenProcess()
+ $h_token = [Ansible.AccessToken.TokenUtil]::OpenProcessToken($h_process, "Duplicate")
+ try {
+ $dup_token = [Ansible.AccessToken.TokenUtil]::DuplicateToken($h_token, "Query", "Anonymous", "Primary")
+ try {
+ $dup_token.IsClosed | Assert-Equal -Expected $false
+ $dup_token.IsInvalid | Assert-Equal -Expected $false
+
+ $actual_user = [Ansible.AccessToken.TokenUtil]::GetTokenUser($dup_token)
+
+ $actual_user | Assert-Equal -Expected $current_user
+ $actual_stat = [Ansible.AccessToken.TokenUtil]::GetTokenStatistics($dup_token)
+
+ $actual_stat.TokenType | Assert-Equal -Expected ([Ansible.AccessToken.TokenType]::Primary)
+ $actual_stat.ImpersonationLevel | Assert-Equal -Expected ([Ansible.AccessToken.SecurityImpersonationLevel]::Anonymous)
+ }
+ finally {
+ $dup_token.Dispose()
+ }
+
+ $dup_token.IsClosed | Assert-Equal -Expected $true
+ }
+ finally {
+ $h_token.Dispose()
+ }
+ }
+
+ "Duplicate access token impersonation" = {
+ $h_process = [Ansible.AccessToken.TokenUtil]::OpenProcess()
+ $h_token = [Ansible.AccessToken.TokenUtil]::OpenProcessToken($h_process, "Duplicate")
+ try {
+ "Anonymous", "Identification", "Impersonation", "Delegation" | ForEach-Object -Process {
+ $dup_token = [Ansible.AccessToken.TokenUtil]::DuplicateToken($h_token, "Query", $_, "Impersonation")
+ try {
+ $actual_user = [Ansible.AccessToken.TokenUtil]::GetTokenUser($dup_token)
+
+ $actual_user | Assert-Equal -Expected $current_user
+ $actual_stat = [Ansible.AccessToken.TokenUtil]::GetTokenStatistics($dup_token)
+
+ $actual_stat.TokenType | Assert-Equal -Expected ([Ansible.AccessToken.TokenType]::Impersonation)
+ $actual_stat.ImpersonationLevel | Assert-Equal -Expected ([Ansible.AccessToken.SecurityImpersonationLevel]"$_")
+ }
+ finally {
+ $dup_token.Dispose()
+ }
+ }
+ }
+ finally {
+ $h_token.Dispose()
+ }
+ }
+
+ "Impersonate SYSTEM token" = {
+ $system_sid = New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList @(
+ [System.Security.Principal.WellKnownSidType]::LocalSystemSid,
+ $null
+ )
+ $tested = $false
+ foreach ($h_token in [Ansible.AccessToken.TokenUtil]::EnumerateUserTokens($system_sid, "Duplicate, Impersonate, Query")) {
+ $actual_user = [Ansible.AccessToken.TokenUtil]::GetTokenUser($h_token)
+ $actual_user | Assert-Equal -Expected $system_sid
+
+ [Ansible.AccessToken.TokenUtil]::ImpersonateToken($h_token)
+ try {
+ $current_sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
+ $current_sid | Assert-Equal -Expected $system_sid
+ }
+ finally {
+ [Ansible.AccessToken.TokenUtil]::RevertToSelf()
+ }
+
+ $current_sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
+ $current_sid | Assert-Equal -Expected $current_user
+
+ # Will keep on looping for each SYSTEM token it can retrieve, we only want to test 1
+ $tested = $true
+ break
+ }
+
+ $tested | Assert-Equal -Expected $true
+ }
+
+ "Get token privileges" = {
+ $h_process = [Ansible.AccessToken.TokenUtil]::OpenProcess()
+ $h_token = [Ansible.AccessToken.TokenUtil]::OpenProcessToken($h_process, "Query")
+ try {
+ $priv_info = &whoami.exe /priv | Where-Object { $_.StartsWith("Se") }
+ $actual_privs = [Ansible.AccessToken.Tokenutil]::GetTokenPrivileges($h_token)
+ $actual_stat = [Ansible.AccessToken.TokenUtil]::GetTokenStatistics($h_token)
+
+ $actual_privs.Count | Assert-Equal -Expected $priv_info.Count
+ $actual_privs.Count | Assert-Equal -Expected $actual_stat.PrivilegeCount
+
+ foreach ($info in $priv_info) {
+ $info_split = $info.Split(" ", [System.StringSplitOptions]::RemoveEmptyEntries)
+ $priv_name = $info_split[0]
+ $priv_enabled = $info_split[-1] -eq "Enabled"
+ $actual_priv = $actual_privs | Where-Object { $_.Name -eq $priv_name }
+
+ $actual_priv -eq $null | Assert-Equal -Expected $false
+ if ($priv_enabled) {
+ $actual_priv.Attributes.HasFlag([Ansible.AccessToken.PrivilegeAttributes]::Enabled) | Assert-Equal -Expected $true
+ }
+ else {
+ $actual_priv.Attributes.HasFlag([Ansible.AccessToken.PrivilegeAttributes]::Disabled) | Assert-Equal -Expected $true
+ }
+ }
+ }
+ finally {
+ $h_token.Dispose()
+ }
+ }
+
+ "Get token statistics" = {
+ $h_process = [Ansible.AccessToken.TokenUtil]::OpenProcess()
+ $h_token = [Ansible.AccessToken.TokenUtil]::OpenProcessToken($h_process, "Query")
+ try {
+ $actual_priv = [Ansible.AccessToken.Tokenutil]::GetTokenPrivileges($h_token)
+ $actual_stat = [Ansible.AccessToken.TokenUtil]::GetTokenStatistics($h_token)
+
+ $actual_stat.TokenId.GetType().FullName | Assert-Equal -Expected "Ansible.AccessToken.Luid"
+ $actual_stat.AuthenticationId.GetType().FullName | Assert-Equal -Expected "Ansible.AccessToken.Luid"
+ $actual_stat.ExpirationTime.GetType().FullName | Assert-Equal -Expected "System.Int64"
+
+ $actual_stat.TokenType | Assert-Equal -Expected ([Ansible.AccessToken.TokenType]::Primary)
+
+ $os_version = [Version](Get-Item -LiteralPath $env:SystemRoot\System32\kernel32.dll).VersionInfo.ProductVersion
+ if ($os_version -lt [Version]"6.1") {
+ # While the token is a primary token, Server 2008 reports the SecurityImpersonationLevel for a primary token as Impersonation
+ $actual_stat.ImpersonationLevel | Assert-Equal -Expected ([Ansible.AccessToken.SecurityImpersonationLevel]::Impersonation)
+ }
+ else {
+ $actual_stat.ImpersonationLevel | Assert-Equal -Expected ([Ansible.AccessToken.SecurityImpersonationLevel]::Anonymous)
+ }
+ $actual_stat.DynamicCharged.GetType().FullName | Assert-Equal -Expected "System.UInt32"
+ $actual_stat.DynamicAvailable.GetType().FullName | Assert-Equal -Expected "System.UInt32"
+ $actual_stat.GroupCount.GetType().FullName | Assert-Equal -Expected "System.UInt32"
+ $actual_stat.PrivilegeCount | Assert-Equal -Expected $actual_priv.Count
+ $actual_stat.ModifiedId.GetType().FullName | Assert-Equal -Expected "Ansible.AccessToken.Luid"
+ }
+ finally {
+ $h_token.Dispose()
+ }
+ }
+
+ "Get token linked token impersonation" = {
+ $h_token = [Ansible.AccessToken.TokenUtil]::LogonUser($test_username, $null, $test_password, "Interactive", "Default")
+ try {
+ $actual_elevation_type = [Ansible.AccessToken.TokenUtil]::GetTokenElevationType($h_token)
+ $actual_elevation_type | Assert-Equal -Expected ([Ansible.AccessToken.TokenElevationType]::Limited)
+
+ $actual_linked = [Ansible.AccessToken.TokenUtil]::GetTokenLinkedToken($h_token)
+ try {
+ $actual_linked.IsClosed | Assert-Equal -Expected $false
+ $actual_linked.IsInvalid | Assert-Equal -Expected $false
+
+ $actual_elevation_type = [Ansible.AccessToken.TokenUtil]::GetTokenElevationType($actual_linked)
+ $actual_elevation_type | Assert-Equal -Expected ([Ansible.AccessToken.TokenElevationType]::Full)
+
+ $actual_stat = [Ansible.AccessToken.TokenUtil]::GetTokenStatistics($actual_linked)
+ $actual_stat.TokenType | Assert-Equal -Expected ([Ansible.AccessToken.TokenType]::Impersonation)
+ }
+ finally {
+ $actual_linked.Dispose()
+ }
+ $actual_linked.IsClosed | Assert-Equal -Expected $true
+ }
+ finally {
+ $h_token.Dispose()
+ }
+ }
+
+ "Get token linked token primary" = {
+ # We need a token with the SeTcbPrivilege for this to work.
+ $system_sid = New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList @(
+ [System.Security.Principal.WellKnownSidType]::LocalSystemSid,
+ $null
+ )
+ $tested = $false
+ foreach ($system_token in [Ansible.AccessToken.TokenUtil]::EnumerateUserTokens($system_sid, "Duplicate, Impersonate, Query")) {
+ $privileges = [Ansible.AccessToken.TokenUtil]::GetTokenPrivileges($system_token)
+ if ($null -eq ($privileges | Where-Object { $_.Name -eq "SeTcbPrivilege" })) {
+ continue
+ }
+
+ $h_token = [Ansible.AccessToken.TokenUtil]::LogonUser($test_username, $null, $test_password, "Interactive", "Default")
+ try {
+ [Ansible.AccessToken.TokenUtil]::ImpersonateToken($system_token)
+ try {
+ $actual_linked = [Ansible.AccessToken.TokenUtil]::GetTokenLinkedToken($h_token)
+ try {
+ $actual_linked.IsClosed | Assert-Equal -Expected $false
+ $actual_linked.IsInvalid | Assert-Equal -Expected $false
+
+ $actual_elevation_type = [Ansible.AccessToken.TokenUtil]::GetTokenElevationType($actual_linked)
+ $actual_elevation_type | Assert-Equal -Expected ([Ansible.AccessToken.TokenElevationType]::Full)
+
+ $actual_stat = [Ansible.AccessToken.TokenUtil]::GetTokenStatistics($actual_linked)
+ $actual_stat.TokenType | Assert-Equal -Expected ([Ansible.AccessToken.TokenType]::Primary)
+ }
+ finally {
+ $actual_linked.Dispose()
+ }
+ $actual_linked.IsClosed | Assert-Equal -Expected $true
+ }
+ finally {
+ [Ansible.AccessToken.TokenUtil]::RevertToSelf()
+ }
+ }
+ finally {
+ $h_token.Dispose()
+ }
+
+ $tested = $true
+ break
+ }
+ $tested | Assert-Equal -Expected $true
+ }
+
+ "Failed to get token information" = {
+ $h_process = [Ansible.AccessToken.TokenUtil]::OpenProcess()
+ $h_token = [Ansible.AccessToken.TokenUtil]::OpenProcessToken($h_process, 'Duplicate') # Without Query the below will fail
+
+ $failed = $false
+ try {
+ [Ansible.AccessToken.TokenUtil]::GetTokenUser($h_token)
+ }
+ catch [Ansible.AccessToken.Win32Exception] {
+ $failed = $true
+ $msg = "GetTokenInformation(TokenUser) failed to get buffer length (Access is denied, Win32ErrorCode 5 - 0x00000005)"
+ $_.Exception.Message | Assert-Equal -Expected $msg
+ }
+ finally {
+ $h_token.Dispose()
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "Logon with valid credentials" = {
+ $expected_user = New-Object -TypeName System.Security.Principal.NTAccount -ArgumentList $test_username
+ $expected_sid = $expected_user.Translate([System.Security.Principal.SecurityIdentifier])
+
+ $h_token = [Ansible.AccessToken.TokenUtil]::LogonUser($test_username, $null, $test_password, "Network", "Default")
+ try {
+ $h_token.IsClosed | Assert-Equal -Expected $false
+ $h_token.IsInvalid | Assert-Equal -Expected $false
+
+ $actual_user = [Ansible.AccessToken.TokenUtil]::GetTokenUser($h_token)
+ $actual_user | Assert-Equal -Expected $expected_sid
+ }
+ finally {
+ $h_token.Dispose()
+ }
+ $h_token.IsClosed | Assert-Equal -Expected $true
+ }
+
+ "Logon with invalid credentials" = {
+ $failed = $false
+ try {
+ [Ansible.AccessToken.TokenUtil]::LogonUser("fake-user", $null, "fake-pass", "Network", "Default")
+ }
+ catch [Ansible.AccessToken.Win32Exception] {
+ $failed = $true
+ $_.Exception.Message.Contains("Failed to logon fake-user") | Assert-Equal -Expected $true
+ $_.Exception.Message.Contains("Win32ErrorCode 1326 - 0x0000052E)") | Assert-Equal -Expected $true
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "Logon with invalid credential with domain account" = {
+ $failed = $false
+ try {
+ [Ansible.AccessToken.TokenUtil]::LogonUser("fake-user", "fake-domain", "fake-pass", "Network", "Default")
+ }
+ catch [Ansible.AccessToken.Win32Exception] {
+ $failed = $true
+ $_.Exception.Message.Contains("Failed to logon fake-domain\fake-user") | Assert-Equal -Expected $true
+ $_.Exception.Message.Contains("Win32ErrorCode 1326 - 0x0000052E)") | Assert-Equal -Expected $true
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+}
+
+foreach ($test_impl in $tests.GetEnumerator()) {
+ $test = $test_impl.Key
+ &$test_impl.Value
+}
+
+$module.Result.data = "success"
+$module.ExitJson()
diff --git a/test/integration/targets/module_utils_Ansible.AccessToken/tasks/main.yml b/test/integration/targets/module_utils_Ansible.AccessToken/tasks/main.yml
new file mode 100644
index 0000000..dbd64b0
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.AccessToken/tasks/main.yml
@@ -0,0 +1,29 @@
+---
+- set_fact:
+ test_username: ansible-test
+ test_password: Password123{{ lookup('password', '/dev/null chars=ascii_letters,digits length=8') }}
+
+- name: create test Admin user
+ win_user:
+ name: '{{ test_username }}'
+ password: '{{ test_password }}'
+ state: present
+ groups:
+ - Administrators
+
+- block:
+ - name: test Ansible.AccessToken.cs
+ ansible_access_token_tests:
+ test_username: '{{ test_username }}'
+ test_password: '{{ test_password }}'
+ register: ansible_access_token_test
+
+ - name: assert test Ansible.AccessToken.cs
+ assert:
+ that:
+ - ansible_access_token_test.data == "success"
+ always:
+ - name: remove test Admin user
+ win_user:
+ name: '{{ test_username }}'
+ state: absent
diff --git a/test/integration/targets/module_utils_Ansible.Basic/aliases b/test/integration/targets/module_utils_Ansible.Basic/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Basic/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.Basic/library/ansible_basic_tests.ps1 b/test/integration/targets/module_utils_Ansible.Basic/library/ansible_basic_tests.ps1
new file mode 100644
index 0000000..cfa73c6
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Basic/library/ansible_basic_tests.ps1
@@ -0,0 +1,3206 @@
+#!powershell
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
+
+Function Assert-Equal {
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)][AllowNull()]$Actual,
+ [Parameter(Mandatory = $true, Position = 0)][AllowNull()]$Expected
+ )
+
+ process {
+ $matched = $false
+ if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array]) {
+ $Actual.Count | Assert-Equal -Expected $Expected.Count
+ for ($i = 0; $i -lt $Actual.Count; $i++) {
+ $actual_value = $Actual[$i]
+ $expected_value = $Expected[$i]
+ Assert-Equal -Actual $actual_value -Expected $expected_value
+ }
+ $matched = $true
+ }
+ else {
+ $matched = $Actual -ceq $Expected
+ }
+
+ if (-not $matched) {
+ if ($Actual -is [PSObject]) {
+ $Actual = $Actual.ToString()
+ }
+
+ $call_stack = (Get-PSCallStack)[1]
+ $module.Result.failed = $true
+ $module.Result.test = $test
+ $module.Result.actual = $Actual
+ $module.Result.expected = $Expected
+ $module.Result.line = $call_stack.ScriptLineNumber
+ $module.Result.method = $call_stack.Position.Text
+ $module.Result.msg = "AssertionError: actual != expected"
+
+ Exit-Module
+ }
+ }
+}
+
+Function Assert-DictionaryEqual {
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)][AllowNull()]$Actual,
+ [Parameter(Mandatory = $true, Position = 0)][AllowNull()]$Expected
+ )
+
+ process {
+ $actual_keys = $Actual.Keys
+ $expected_keys = $Expected.Keys
+
+ $actual_keys.Count | Assert-Equal -Expected $expected_keys.Count
+ foreach ($actual_entry in $Actual.GetEnumerator()) {
+ $actual_key = $actual_entry.Key
+ ($actual_key -cin $expected_keys) | Assert-Equal -Expected $true
+ $actual_value = $actual_entry.Value
+ $expected_value = $Expected.$actual_key
+
+ if ($actual_value -is [System.Collections.IDictionary]) {
+ $actual_value | Assert-DictionaryEqual -Expected $expected_value
+ }
+ elseif ($actual_value -is [System.Collections.ArrayList] -or $actual_value -is [Array]) {
+ for ($i = 0; $i -lt $actual_value.Count; $i++) {
+ $actual_entry = $actual_value[$i]
+ $expected_entry = $expected_value[$i]
+ if ($actual_entry -is [System.Collections.IDictionary]) {
+ $actual_entry | Assert-DictionaryEqual -Expected $expected_entry
+ }
+ else {
+ Assert-Equal -Actual $actual_entry -Expected $expected_entry
+ }
+ }
+ }
+ else {
+ Assert-Equal -Actual $actual_value -Expected $expected_value
+ }
+ }
+ foreach ($expected_key in $expected_keys) {
+ ($expected_key -cin $actual_keys) | Assert-Equal -Expected $true
+ }
+ }
+}
+
+Function Exit-Module {
+ # Make sure Exit actually calls exit and not our overriden test behaviour
+ [Ansible.Basic.AnsibleModule]::Exit = { param([Int32]$rc) exit $rc }
+ Write-Output -InputObject (ConvertTo-Json -InputObject $module.Result -Compress -Depth 99)
+ $module.ExitJson()
+}
+
+$tmpdir = $module.Tmpdir
+
+# Override the Exit and WriteLine behaviour to throw an exception instead of exiting the module
+[Ansible.Basic.AnsibleModule]::Exit = {
+ param([Int32]$rc)
+ $exp = New-Object -TypeName System.Exception -ArgumentList "exit: $rc"
+ $exp | Add-Member -Type NoteProperty -Name Output -Value $_test_out
+ throw $exp
+}
+[Ansible.Basic.AnsibleModule]::WriteLine = {
+ param([String]$line)
+ Set-Variable -Name _test_out -Scope Global -Value $line
+}
+
+$tests = @{
+ "Empty spec and no options - args file" = {
+ $args_file = Join-Path -Path $tmpdir -ChildPath "args-$(Get-Random).json"
+ [System.IO.File]::WriteAllText($args_file, '{ "ANSIBLE_MODULE_ARGS": {} }')
+ $m = [Ansible.Basic.AnsibleModule]::Create(@($args_file), @{})
+
+ $m.CheckMode | Assert-Equal -Expected $false
+ $m.DebugMode | Assert-Equal -Expected $false
+ $m.DiffMode | Assert-Equal -Expected $false
+ $m.KeepRemoteFiles | Assert-Equal -Expected $false
+ $m.ModuleName | Assert-Equal -Expected "undefined win module"
+ $m.NoLog | Assert-Equal -Expected $false
+ $m.Verbosity | Assert-Equal -Expected 0
+ $m.AnsibleVersion | Assert-Equal -Expected $null
+ }
+
+ "Empty spec and no options - complex_args" = {
+ Set-Variable -Name complex_args -Scope Global -Value @{}
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+
+ $m.CheckMode | Assert-Equal -Expected $false
+ $m.DebugMode | Assert-Equal -Expected $false
+ $m.DiffMode | Assert-Equal -Expected $false
+ $m.KeepRemoteFiles | Assert-Equal -Expected $false
+ $m.ModuleName | Assert-Equal -Expected "undefined win module"
+ $m.NoLog | Assert-Equal -Expected $false
+ $m.Verbosity | Assert-Equal -Expected 0
+ $m.AnsibleVersion | Assert-Equal -Expected $null
+ }
+
+ "Internal param changes - args file" = {
+ $m_tmpdir = Join-Path -Path $tmpdir -ChildPath "moduletmpdir-$(Get-Random)"
+ New-Item -Path $m_tmpdir -ItemType Directory > $null
+ $args_file = Join-Path -Path $tmpdir -ChildPath "args-$(Get-Random).json"
+ [System.IO.File]::WriteAllText($args_file, @"
+{
+ "ANSIBLE_MODULE_ARGS": {
+ "_ansible_check_mode": true,
+ "_ansible_debug": true,
+ "_ansible_diff": true,
+ "_ansible_keep_remote_files": true,
+ "_ansible_module_name": "ansible_basic_tests",
+ "_ansible_no_log": true,
+ "_ansible_remote_tmp": "%TEMP%",
+ "_ansible_selinux_special_fs": "ignored",
+ "_ansible_shell_executable": "ignored",
+ "_ansible_socket": "ignored",
+ "_ansible_syslog_facility": "ignored",
+ "_ansible_tmpdir": "$($m_tmpdir -replace "\\", "\\")",
+ "_ansible_verbosity": 3,
+ "_ansible_version": "2.8.0"
+ }
+}
+"@)
+ $m = [Ansible.Basic.AnsibleModule]::Create(@($args_file), @{supports_check_mode = $true })
+ $m.CheckMode | Assert-Equal -Expected $true
+ $m.DebugMode | Assert-Equal -Expected $true
+ $m.DiffMode | Assert-Equal -Expected $true
+ $m.KeepRemoteFiles | Assert-Equal -Expected $true
+ $m.ModuleName | Assert-Equal -Expected "ansible_basic_tests"
+ $m.NoLog | Assert-Equal -Expected $true
+ $m.Verbosity | Assert-Equal -Expected 3
+ $m.AnsibleVersion | Assert-Equal -Expected "2.8.0"
+ $m.Tmpdir | Assert-Equal -Expected $m_tmpdir
+ }
+
+ "Internal param changes - complex_args" = {
+ $m_tmpdir = Join-Path -Path $tmpdir -ChildPath "moduletmpdir-$(Get-Random)"
+ New-Item -Path $m_tmpdir -ItemType Directory > $null
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_check_mode = $true
+ _ansible_debug = $true
+ _ansible_diff = $true
+ _ansible_keep_remote_files = $true
+ _ansible_module_name = "ansible_basic_tests"
+ _ansible_no_log = $true
+ _ansible_remote_tmp = "%TEMP%"
+ _ansible_selinux_special_fs = "ignored"
+ _ansible_shell_executable = "ignored"
+ _ansible_socket = "ignored"
+ _ansible_syslog_facility = "ignored"
+ _ansible_tmpdir = $m_tmpdir.ToString()
+ _ansible_verbosity = 3
+ _ansible_version = "2.8.0"
+ }
+ $spec = @{
+ supports_check_mode = $true
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ $m.CheckMode | Assert-Equal -Expected $true
+ $m.DebugMode | Assert-Equal -Expected $true
+ $m.DiffMode | Assert-Equal -Expected $true
+ $m.KeepRemoteFiles | Assert-Equal -Expected $true
+ $m.ModuleName | Assert-Equal -Expected "ansible_basic_tests"
+ $m.NoLog | Assert-Equal -Expected $true
+ $m.Verbosity | Assert-Equal -Expected 3
+ $m.AnsibleVersion | Assert-Equal -Expected "2.8.0"
+ $m.Tmpdir | Assert-Equal -Expected $m_tmpdir
+ }
+
+ "Parse complex module options" = {
+ $spec = @{
+ options = @{
+ option_default = @{}
+ missing_option_default = @{}
+ string_option = @{type = "str" }
+ required_option = @{required = $true }
+ missing_choices = @{choices = "a", "b" }
+ choices = @{choices = "a", "b" }
+ one_choice = @{choices = , "b" }
+ choice_with_default = @{choices = "a", "b"; default = "b" }
+ alias_direct = @{aliases = , "alias_direct1" }
+ alias_as_alias = @{aliases = "alias_as_alias1", "alias_as_alias2" }
+ bool_type = @{type = "bool" }
+ bool_from_str = @{type = "bool" }
+ dict_type = @{
+ type = "dict"
+ options = @{
+ int_type = @{type = "int" }
+ str_type = @{type = "str"; default = "str_sub_type" }
+ }
+ }
+ dict_type_missing = @{
+ type = "dict"
+ options = @{
+ int_type = @{type = "int" }
+ str_type = @{type = "str"; default = "str_sub_type" }
+ }
+ }
+ dict_type_defaults = @{
+ type = "dict"
+ apply_defaults = $true
+ options = @{
+ int_type = @{type = "int" }
+ str_type = @{type = "str"; default = "str_sub_type" }
+ }
+ }
+ dict_type_json = @{type = "dict" }
+ dict_type_str = @{type = "dict" }
+ float_type = @{type = "float" }
+ int_type = @{type = "int" }
+ json_type = @{type = "json" }
+ json_type_dict = @{type = "json" }
+ list_type = @{type = "list" }
+ list_type_str = @{type = "list" }
+ list_with_int = @{type = "list"; elements = "int" }
+ list_type_single = @{type = "list" }
+ list_with_dict = @{
+ type = "list"
+ elements = "dict"
+ options = @{
+ int_type = @{type = "int" }
+ str_type = @{type = "str"; default = "str_sub_type" }
+ }
+ }
+ path_type = @{type = "path" }
+ path_type_nt = @{type = "path" }
+ path_type_missing = @{type = "path" }
+ raw_type_str = @{type = "raw" }
+ raw_type_int = @{type = "raw" }
+ sid_type = @{type = "sid" }
+ sid_from_name = @{type = "sid" }
+ str_type = @{type = "str" }
+ delegate_type = @{type = [Func[[Object], [UInt64]]] { [System.UInt64]::Parse($args[0]) } }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_default = 1
+ string_option = 1
+ required_option = "required"
+ choices = "a"
+ one_choice = "b"
+ alias_direct = "a"
+ alias_as_alias2 = "a"
+ bool_type = $true
+ bool_from_str = "false"
+ dict_type = @{
+ int_type = "10"
+ }
+ dict_type_json = '{"a":"a","b":1,"c":["a","b"]}'
+ dict_type_str = 'a=a b="b 2" c=c'
+ float_type = "3.14159"
+ int_type = 0
+ json_type = '{"a":"a","b":1,"c":["a","b"]}'
+ json_type_dict = @{
+ a = "a"
+ b = 1
+ c = @("a", "b")
+ }
+ list_type = @("a", "b", 1, 2)
+ list_type_str = "a, b,1,2 "
+ list_with_int = @("1", 2)
+ list_type_single = "single"
+ list_with_dict = @(
+ @{
+ int_type = 2
+ str_type = "dict entry"
+ },
+ @{ int_type = 1 },
+ @{}
+ )
+ path_type = "%SystemRoot%\System32"
+ path_type_nt = "\\?\%SystemRoot%\System32"
+ path_type_missing = "T:\missing\path"
+ raw_type_str = "str"
+ raw_type_int = 1
+ sid_type = "S-1-5-18"
+ sid_from_name = "SYSTEM"
+ str_type = "str"
+ delegate_type = "1234"
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+
+ $m.Params.option_default | Assert-Equal -Expected "1"
+ $m.Params.option_default.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.missing_option_default | Assert-Equal -Expected $null
+ $m.Params.string_option | Assert-Equal -Expected "1"
+ $m.Params.string_option.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.required_option | Assert-Equal -Expected "required"
+ $m.Params.required_option.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.missing_choices | Assert-Equal -Expected $null
+ $m.Params.choices | Assert-Equal -Expected "a"
+ $m.Params.choices.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.one_choice | Assert-Equal -Expected "b"
+ $m.Params.one_choice.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.choice_with_default | Assert-Equal -Expected "b"
+ $m.Params.choice_with_default.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.alias_direct | Assert-Equal -Expected "a"
+ $m.Params.alias_direct.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.alias_as_alias | Assert-Equal -Expected "a"
+ $m.Params.alias_as_alias.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.bool_type | Assert-Equal -Expected $true
+ $m.Params.bool_type.GetType().ToString() | Assert-Equal -Expected "System.Boolean"
+ $m.Params.bool_from_str | Assert-Equal -Expected $false
+ $m.Params.bool_from_str.GetType().ToString() | Assert-Equal -Expected "System.Boolean"
+ $m.Params.dict_type | Assert-DictionaryEqual -Expected @{int_type = 10; str_type = "str_sub_type" }
+ $m.Params.dict_type.GetType().ToString() | Assert-Equal -Expected "System.Collections.Generic.Dictionary``2[System.String,System.Object]"
+ $m.Params.dict_type.int_type.GetType().ToString() | Assert-Equal -Expected "System.Int32"
+ $m.Params.dict_type.str_type.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.dict_type_missing | Assert-Equal -Expected $null
+ $m.Params.dict_type_defaults | Assert-DictionaryEqual -Expected @{int_type = $null; str_type = "str_sub_type" }
+ $m.Params.dict_type_defaults.GetType().ToString() | Assert-Equal -Expected "System.Collections.Generic.Dictionary``2[System.String,System.Object]"
+ $m.Params.dict_type_defaults.str_type.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.dict_type_json | Assert-DictionaryEqual -Expected @{
+ a = "a"
+ b = 1
+ c = @("a", "b")
+ }
+ $m.Params.dict_type_json.GetType().ToString() | Assert-Equal -Expected "System.Collections.Generic.Dictionary``2[System.String,System.Object]"
+ $m.Params.dict_type_json.a.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.dict_type_json.b.GetType().ToString() | Assert-Equal -Expected "System.Int32"
+ $m.Params.dict_type_json.c.GetType().ToString() | Assert-Equal -Expected "System.Collections.ArrayList"
+ $m.Params.dict_type_str | Assert-DictionaryEqual -Expected @{a = "a"; b = "b 2"; c = "c" }
+ $m.Params.dict_type_str.GetType().ToString() | Assert-Equal -Expected "System.Collections.Generic.Dictionary``2[System.String,System.Object]"
+ $m.Params.dict_type_str.a.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.dict_type_str.b.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.dict_type_str.c.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.float_type | Assert-Equal -Expected ([System.Single]3.14159)
+ $m.Params.float_type.GetType().ToString() | Assert-Equal -Expected "System.Single"
+ $m.Params.int_type | Assert-Equal -Expected 0
+ $m.Params.int_type.GetType().ToString() | Assert-Equal -Expected "System.Int32"
+ $m.Params.json_type | Assert-Equal -Expected '{"a":"a","b":1,"c":["a","b"]}'
+ $m.Params.json_type.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $jsonValue = ([Ansible.Basic.AnsibleModule]::FromJson('{"a":"a","b":1,"c":["a","b"]}'))
+ [Ansible.Basic.AnsibleModule]::FromJson($m.Params.json_type_dict) | Assert-DictionaryEqual -Expected $jsonValue
+ $m.Params.json_type_dict.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.list_type.GetType().ToString() | Assert-Equal -Expected "System.Collections.Generic.List``1[System.Object]"
+ $m.Params.list_type.Count | Assert-Equal -Expected 4
+ $m.Params.list_type[0] | Assert-Equal -Expected "a"
+ $m.Params.list_type[0].GetType().FullName | Assert-Equal -Expected "System.String"
+ $m.Params.list_type[1] | Assert-Equal -Expected "b"
+ $m.Params.list_type[1].GetType().FullName | Assert-Equal -Expected "System.String"
+ $m.Params.list_type[2] | Assert-Equal -Expected 1
+ $m.Params.list_type[2].GetType().FullName | Assert-Equal -Expected "System.Int32"
+ $m.Params.list_type[3] | Assert-Equal -Expected 2
+ $m.Params.list_type[3].GetType().FullName | Assert-Equal -Expected "System.Int32"
+ $m.Params.list_type_str.GetType().ToString() | Assert-Equal -Expected "System.Collections.Generic.List``1[System.Object]"
+ $m.Params.list_type_str.Count | Assert-Equal -Expected 4
+ $m.Params.list_type_str[0] | Assert-Equal -Expected "a"
+ $m.Params.list_type_str[0].GetType().FullName | Assert-Equal -Expected "System.String"
+ $m.Params.list_type_str[1] | Assert-Equal -Expected "b"
+ $m.Params.list_type_str[1].GetType().FullName | Assert-Equal -Expected "System.String"
+ $m.Params.list_type_str[2] | Assert-Equal -Expected "1"
+ $m.Params.list_type_str[2].GetType().FullName | Assert-Equal -Expected "System.String"
+ $m.Params.list_type_str[3] | Assert-Equal -Expected "2"
+ $m.Params.list_type_str[3].GetType().FullName | Assert-Equal -Expected "System.String"
+ $m.Params.list_with_int.GetType().ToString() | Assert-Equal -Expected "System.Collections.Generic.List``1[System.Object]"
+ $m.Params.list_with_int.Count | Assert-Equal -Expected 2
+ $m.Params.list_with_int[0] | Assert-Equal -Expected 1
+ $m.Params.list_with_int[0].GetType().FullName | Assert-Equal -Expected "System.Int32"
+ $m.Params.list_with_int[1] | Assert-Equal -Expected 2
+ $m.Params.list_with_int[1].GetType().FullName | Assert-Equal -Expected "System.Int32"
+ $m.Params.list_type_single.GetType().ToString() | Assert-Equal -Expected "System.Collections.Generic.List``1[System.Object]"
+ $m.Params.list_type_single.Count | Assert-Equal -Expected 1
+ $m.Params.list_type_single[0] | Assert-Equal -Expected "single"
+ $m.Params.list_type_single[0].GetType().FullName | Assert-Equal -Expected "System.String"
+ $m.Params.list_with_dict.GetType().FullName.StartsWith("System.Collections.Generic.List``1[[System.Object") | Assert-Equal -Expected $true
+ $m.Params.list_with_dict.Count | Assert-Equal -Expected 3
+ $m.Params.list_with_dict[0].GetType().FullName.StartsWith("System.Collections.Generic.Dictionary``2[[System.String") | Assert-Equal -Expected $true
+ $m.Params.list_with_dict[0] | Assert-DictionaryEqual -Expected @{int_type = 2; str_type = "dict entry" }
+ $m.Params.list_with_dict[0].int_type.GetType().FullName.ToString() | Assert-Equal -Expected "System.Int32"
+ $m.Params.list_with_dict[0].str_type.GetType().FullName.ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.list_with_dict[1].GetType().FullName.StartsWith("System.Collections.Generic.Dictionary``2[[System.String") | Assert-Equal -Expected $true
+ $m.Params.list_with_dict[1] | Assert-DictionaryEqual -Expected @{int_type = 1; str_type = "str_sub_type" }
+ $m.Params.list_with_dict[1].int_type.GetType().FullName.ToString() | Assert-Equal -Expected "System.Int32"
+ $m.Params.list_with_dict[1].str_type.GetType().FullName.ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.list_with_dict[2].GetType().FullName.StartsWith("System.Collections.Generic.Dictionary``2[[System.String") | Assert-Equal -Expected $true
+ $m.Params.list_with_dict[2] | Assert-DictionaryEqual -Expected @{int_type = $null; str_type = "str_sub_type" }
+ $m.Params.list_with_dict[2].str_type.GetType().FullName.ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.path_type | Assert-Equal -Expected "$($env:SystemRoot)\System32"
+ $m.Params.path_type.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.path_type_nt | Assert-Equal -Expected "\\?\%SystemRoot%\System32"
+ $m.Params.path_type_nt.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.path_type_missing | Assert-Equal -Expected "T:\missing\path"
+ $m.Params.path_type_missing.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.raw_type_str | Assert-Equal -Expected "str"
+ $m.Params.raw_type_str.GetType().FullName | Assert-Equal -Expected "System.String"
+ $m.Params.raw_type_int | Assert-Equal -Expected 1
+ $m.Params.raw_type_int.GetType().FullName | Assert-Equal -Expected "System.Int32"
+ $m.Params.sid_type | Assert-Equal -Expected (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList "S-1-5-18")
+ $m.Params.sid_type.GetType().ToString() | Assert-Equal -Expected "System.Security.Principal.SecurityIdentifier"
+ $m.Params.sid_from_name | Assert-Equal -Expected (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList "S-1-5-18")
+ $m.Params.sid_from_name.GetType().ToString() | Assert-Equal -Expected "System.Security.Principal.SecurityIdentifier"
+ $m.Params.str_type | Assert-Equal -Expected "str"
+ $m.Params.str_type.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.delegate_type | Assert-Equal -Expected 1234
+ $m.Params.delegate_type.GetType().ToString() | Assert-Equal -Expected "System.UInt64"
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_module_args = @{
+ option_default = "1"
+ missing_option_default = $null
+ string_option = "1"
+ required_option = "required"
+ missing_choices = $null
+ choices = "a"
+ one_choice = "b"
+ choice_with_default = "b"
+ alias_direct = "a"
+ alias_as_alias = "a"
+ alias_as_alias2 = "a"
+ bool_type = $true
+ bool_from_str = $false
+ dict_type = @{
+ int_type = 10
+ str_type = "str_sub_type"
+ }
+ dict_type_missing = $null
+ dict_type_defaults = @{
+ int_type = $null
+ str_type = "str_sub_type"
+ }
+ dict_type_json = @{
+ a = "a"
+ b = 1
+ c = @("a", "b")
+ }
+ dict_type_str = @{
+ a = "a"
+ b = "b 2"
+ c = "c"
+ }
+ float_type = 3.14159
+ int_type = 0
+ json_type = $m.Params.json_type.ToString()
+ json_type_dict = $m.Params.json_type_dict.ToString()
+ list_type = @("a", "b", 1, 2)
+ list_type_str = @("a", "b", "1", "2")
+ list_with_int = @(1, 2)
+ list_type_single = @("single")
+ list_with_dict = @(
+ @{
+ int_type = 2
+ str_type = "dict entry"
+ },
+ @{
+ int_type = 1
+ str_type = "str_sub_type"
+ },
+ @{
+ int_type = $null
+ str_type = "str_sub_type"
+ }
+ )
+ path_type = "$($env:SystemRoot)\System32"
+ path_type_nt = "\\?\%SystemRoot%\System32"
+ path_type_missing = "T:\missing\path"
+ raw_type_str = "str"
+ raw_type_int = 1
+ sid_type = "S-1-5-18"
+ sid_from_name = "S-1-5-18"
+ str_type = "str"
+ delegate_type = 1234
+ }
+ $actual.Keys.Count | Assert-Equal -Expected 2
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $expected_module_args }
+ }
+
+ "Parse module args with list elements and delegate type" = {
+ $spec = @{
+ options = @{
+ list_delegate_type = @{
+ type = "list"
+ elements = [Func[[Object], [UInt16]]] { [System.UInt16]::Parse($args[0]) }
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ list_delegate_type = @(
+ "1234",
+ 4321
+ )
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ $m.Params.list_delegate_type.GetType().Name | Assert-Equal -Expected 'List`1'
+ $m.Params.list_delegate_type[0].GetType().FullName | Assert-Equal -Expected "System.UInt16"
+ $m.Params.list_delegate_Type[1].GetType().FullName | Assert-Equal -Expected "System.UInt16"
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_module_args = @{
+ list_delegate_type = @(
+ 1234,
+ 4321
+ )
+ }
+ $actual.Keys.Count | Assert-Equal -Expected 2
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $expected_module_args }
+ }
+
+ "Parse module args with case insensitive input" = {
+ $spec = @{
+ options = @{
+ option1 = @{ type = "int"; required = $true }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_module_name = "win_test"
+ Option1 = "1"
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ # Verifies the case of the params key is set to the module spec not actual input
+ $m.Params.Keys | Assert-Equal -Expected @("option1")
+ $m.Params.option1 | Assert-Equal -Expected 1
+
+ # Verifies the type conversion happens even on a case insensitive match
+ $m.Params.option1.GetType().FullName | Assert-Equal -Expected "System.Int32"
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_warnings = "Parameters for (win_test) was a case insensitive match: Option1. "
+ $expected_warnings += "Module options will become case sensitive in a future Ansible release. "
+ $expected_warnings += "Supported parameters include: option1"
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{
+ option1 = 1
+ }
+ }
+ # We have disabled the warning for now
+ #warnings = @($expected_warnings)
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "No log values" = {
+ $spec = @{
+ options = @{
+ username = @{type = "str" }
+ password = @{type = "str"; no_log = $true }
+ password2 = @{type = "int"; no_log = $true }
+ dict = @{type = "dict" }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_module_name = "test_no_log"
+ username = "user - pass - name"
+ password = "pass"
+ password2 = 1234
+ dict = @{
+ data = "Oops this is secret: pass"
+ dict = @{
+ pass = "plain"
+ hide = "pass"
+ sub_hide = "password"
+ int_hide = 123456
+ }
+ list = @(
+ "pass",
+ "password",
+ 1234567,
+ "pa ss",
+ @{
+ pass = "plain"
+ hide = "pass"
+ sub_hide = "password"
+ int_hide = 123456
+ }
+ )
+ custom = "pass"
+ }
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ $m.Result.data = $complex_args.dict
+
+ # verify params internally aren't masked
+ $m.Params.username | Assert-Equal -Expected "user - pass - name"
+ $m.Params.password | Assert-Equal -Expected "pass"
+ $m.Params.password2 | Assert-Equal -Expected 1234
+ $m.Params.dict.custom | Assert-Equal -Expected "pass"
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ # verify no_log params are masked in invocation
+ $expected = @{
+ invocation = @{
+ module_args = @{
+ password2 = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
+ dict = @{
+ dict = @{
+ pass = "plain"
+ hide = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
+ sub_hide = "********word"
+ int_hide = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
+ }
+ custom = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
+ list = @(
+ "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
+ "********word",
+ "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
+ "pa ss",
+ @{
+ pass = "plain"
+ hide = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
+ sub_hide = "********word"
+ int_hide = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
+ }
+ )
+ data = "Oops this is secret: ********"
+ }
+ username = "user - ******** - name"
+ password = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
+ }
+ }
+ changed = $false
+ data = $complex_args.dict
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+
+ $expected_event = @'
+test_no_log - Invoked with:
+ username: user - ******** - name
+ dict: dict: sub_hide: ****word
+ pass: plain
+ int_hide: ********56
+ hide: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
+ data: Oops this is secret: ********
+ custom: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
+ list:
+ - VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
+ - ********word
+ - ********567
+ - pa ss
+ - sub_hide: ********word
+ pass: plain
+ int_hide: ********56
+ hide: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
+ password2: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
+ password: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
+'@
+ $actual_event = (Get-EventLog -LogName Application -Source Ansible -Newest 1).Message
+ $actual_event | Assert-DictionaryEqual -Expected $expected_event
+ }
+
+ "No log value with an empty string" = {
+ $spec = @{
+ options = @{
+ password1 = @{type = "str"; no_log = $true }
+ password2 = @{type = "str"; no_log = $true }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_module_name = "test_no_log"
+ password1 = ""
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ $m.Result.data = $complex_args.dict
+
+ # verify params internally aren't masked
+ $m.Params.password1 | Assert-Equal -Expected ""
+ $m.Params.password2 | Assert-Equal -Expected $null
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected = @{
+ invocation = @{
+ module_args = @{
+ password1 = ""
+ password2 = $null
+ }
+ }
+ changed = $false
+ data = $complex_args.dict
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "Removed in version" = {
+ $spec = @{
+ options = @{
+ removed1 = @{removed_in_version = "2.1" }
+ removed2 = @{removed_in_version = "2.2" }
+ removed3 = @{removed_in_version = "2.3"; removed_from_collection = "ansible.builtin" }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ removed1 = "value"
+ removed3 = "value"
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{
+ removed1 = "value"
+ removed2 = $null
+ removed3 = "value"
+ }
+ }
+ deprecations = @(
+ @{
+ msg = "Param 'removed3' is deprecated. See the module docs for more information"
+ version = "2.3"
+ collection_name = "ansible.builtin"
+ },
+ @{
+ msg = "Param 'removed1' is deprecated. See the module docs for more information"
+ version = "2.1"
+ collection_name = $null
+ }
+ )
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "Removed at date" = {
+ $spec = @{
+ options = @{
+ removed1 = @{removed_at_date = [DateTime]"2020-03-10" }
+ removed2 = @{removed_at_date = [DateTime]"2020-03-11" }
+ removed3 = @{removed_at_date = [DateTime]"2020-06-07"; removed_from_collection = "ansible.builtin" }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ removed1 = "value"
+ removed3 = "value"
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{
+ removed1 = "value"
+ removed2 = $null
+ removed3 = "value"
+ }
+ }
+ deprecations = @(
+ @{
+ msg = "Param 'removed3' is deprecated. See the module docs for more information"
+ date = "2020-06-07"
+ collection_name = "ansible.builtin"
+ },
+ @{
+ msg = "Param 'removed1' is deprecated. See the module docs for more information"
+ date = "2020-03-10"
+ collection_name = $null
+ }
+ )
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "Deprecated aliases" = {
+ $spec = @{
+ options = @{
+ option1 = @{ type = "str"; aliases = "alias1"; deprecated_aliases = @(@{name = "alias1"; version = "2.10" }) }
+ option2 = @{ type = "str"; aliases = "alias2"; deprecated_aliases = @(@{name = "alias2"; version = "2.11" }) }
+ option3 = @{
+ type = "dict"
+ options = @{
+ option1 = @{ type = "str"; aliases = "alias1"; deprecated_aliases = @(@{name = "alias1"; version = "2.10" }) }
+ option2 = @{ type = "str"; aliases = "alias2"; deprecated_aliases = @(@{name = "alias2"; version = "2.11" }) }
+ option3 = @{
+ type = "str"
+ aliases = "alias3"
+ deprecated_aliases = @(
+ @{name = "alias3"; version = "2.12"; collection_name = "ansible.builtin" }
+ )
+ }
+ option4 = @{ type = "str"; aliases = "alias4"; deprecated_aliases = @(@{name = "alias4"; date = [DateTime]"2020-03-11" }) }
+ option5 = @{ type = "str"; aliases = "alias5"; deprecated_aliases = @(@{name = "alias5"; date = [DateTime]"2020-03-09" }) }
+ option6 = @{
+ type = "str"
+ aliases = "alias6"
+ deprecated_aliases = @(
+ @{name = "alias6"; date = [DateTime]"2020-06-01"; collection_name = "ansible.builtin" }
+ )
+ }
+ }
+ }
+ option4 = @{ type = "str"; aliases = "alias4"; deprecated_aliases = @(@{name = "alias4"; date = [DateTime]"2020-03-10" }) }
+ option5 = @{ type = "str"; aliases = "alias5"; deprecated_aliases = @(@{name = "alias5"; date = [DateTime]"2020-03-12" }) }
+ option6 = @{
+ type = "str"
+ aliases = "alias6"
+ deprecated_aliases = @(
+ @{name = "alias6"; version = "2.12"; collection_name = "ansible.builtin" }
+ )
+ }
+ option7 = @{
+ type = "str"
+ aliases = "alias7"
+ deprecated_aliases = @(
+ @{name = "alias7"; date = [DateTime]"2020-06-07"; collection_name = "ansible.builtin" }
+ )
+ }
+ }
+ }
+
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ alias1 = "alias1"
+ option2 = "option2"
+ option3 = @{
+ option1 = "option1"
+ alias2 = "alias2"
+ alias3 = "alias3"
+ option4 = "option4"
+ alias5 = "alias5"
+ alias6 = "alias6"
+ }
+ option4 = "option4"
+ alias5 = "alias5"
+ alias6 = "alias6"
+ alias7 = "alias7"
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{
+ alias1 = "alias1"
+ option1 = "alias1"
+ option2 = "option2"
+ option3 = @{
+ option1 = "option1"
+ option2 = "alias2"
+ alias2 = "alias2"
+ option3 = "alias3"
+ alias3 = "alias3"
+ option4 = "option4"
+ option5 = "alias5"
+ alias5 = "alias5"
+ option6 = "alias6"
+ alias6 = "alias6"
+ }
+ option4 = "option4"
+ option5 = "alias5"
+ alias5 = "alias5"
+ option6 = "alias6"
+ alias6 = "alias6"
+ option7 = "alias7"
+ alias7 = "alias7"
+ }
+ }
+ deprecations = @(
+ @{
+ msg = "Alias 'alias7' is deprecated. See the module docs for more information"
+ date = "2020-06-07"
+ collection_name = "ansible.builtin"
+ },
+ @{
+ msg = "Alias 'alias1' is deprecated. See the module docs for more information"
+ version = "2.10"
+ collection_name = $null
+ },
+ @{
+ msg = "Alias 'alias5' is deprecated. See the module docs for more information"
+ date = "2020-03-12"
+ collection_name = $null
+ },
+ @{
+ msg = "Alias 'alias6' is deprecated. See the module docs for more information"
+ version = "2.12"
+ collection_name = "ansible.builtin"
+ },
+ @{
+ msg = "Alias 'alias2' is deprecated. See the module docs for more information - found in option3"
+ version = "2.11"
+ collection_name = $null
+ },
+ @{
+ msg = "Alias 'alias5' is deprecated. See the module docs for more information - found in option3"
+ date = "2020-03-09"
+ collection_name = $null
+ },
+ @{
+ msg = "Alias 'alias3' is deprecated. See the module docs for more information - found in option3"
+ version = "2.12"
+ collection_name = "ansible.builtin"
+ },
+ @{
+ msg = "Alias 'alias6' is deprecated. See the module docs for more information - found in option3"
+ date = "2020-06-01"
+ collection_name = "ansible.builtin"
+ }
+ )
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "Required by - single value" = {
+ $spec = @{
+ options = @{
+ option1 = @{type = "str" }
+ option2 = @{type = "str" }
+ option3 = @{type = "str" }
+ }
+ required_by = @{
+ option1 = "option2"
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option1 = "option1"
+ option2 = "option2"
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{
+ option1 = "option1"
+ option2 = "option2"
+ option3 = $null
+ }
+ }
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "Required by - multiple values" = {
+ $spec = @{
+ options = @{
+ option1 = @{type = "str" }
+ option2 = @{type = "str" }
+ option3 = @{type = "str" }
+ }
+ required_by = @{
+ option1 = "option2", "option3"
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option1 = "option1"
+ option2 = "option2"
+ option3 = "option3"
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{
+ option1 = "option1"
+ option2 = "option2"
+ option3 = "option3"
+ }
+ }
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "Required by explicit null" = {
+ $spec = @{
+ options = @{
+ option1 = @{type = "str" }
+ option2 = @{type = "str" }
+ option3 = @{type = "str" }
+ }
+ required_by = @{
+ option1 = "option2"
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option1 = "option1"
+ option2 = $null
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{
+ option1 = "option1"
+ option2 = $null
+ option3 = $null
+ }
+ }
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "Required by failed - single value" = {
+ $spec = @{
+ options = @{
+ option1 = @{type = "str" }
+ option2 = @{type = "str" }
+ option3 = @{type = "str" }
+ }
+ required_by = @{
+ option1 = "option2"
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option1 = "option1"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected = @{
+ changed = $false
+ failed = $true
+ invocation = @{
+ module_args = @{
+ option1 = "option1"
+ }
+ }
+ msg = "missing parameter(s) required by 'option1': option2"
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "Required by failed - multiple values" = {
+ $spec = @{
+ options = @{
+ option1 = @{type = "str" }
+ option2 = @{type = "str" }
+ option3 = @{type = "str" }
+ }
+ required_by = @{
+ option1 = "option2", "option3"
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option1 = "option1"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected = @{
+ changed = $false
+ failed = $true
+ invocation = @{
+ module_args = @{
+ option1 = "option1"
+ }
+ }
+ msg = "missing parameter(s) required by 'option1': option2, option3"
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "Debug without debug set" = {
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_debug = $false
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+ $m.Debug("debug message")
+ $actual_event = (Get-EventLog -LogName Application -Source Ansible -Newest 1).Message
+ $actual_event | Assert-Equal -Expected "undefined win module - Invoked with:`r`n "
+ }
+
+ "Debug with debug set" = {
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_debug = $true
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+ $m.Debug("debug message")
+ $actual_event = (Get-EventLog -LogName Application -Source Ansible -Newest 1).Message
+ $actual_event | Assert-Equal -Expected "undefined win module - [DEBUG] debug message"
+ }
+
+ "Deprecate and warn with version" = {
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+ $m.Deprecate("message", "2.7")
+ $actual_deprecate_event_1 = Get-EventLog -LogName Application -Source Ansible -Newest 1
+ $m.Deprecate("message w collection", "2.8", "ansible.builtin")
+ $actual_deprecate_event_2 = Get-EventLog -LogName Application -Source Ansible -Newest 1
+ $m.Warn("warning")
+ $actual_warn_event = Get-EventLog -LogName Application -Source Ansible -Newest 1
+
+ $actual_deprecate_event_1.Message | Assert-Equal -Expected "undefined win module - [DEPRECATION WARNING] message 2.7"
+ $actual_deprecate_event_2.Message | Assert-Equal -Expected "undefined win module - [DEPRECATION WARNING] message w collection 2.8"
+ $actual_warn_event.EntryType | Assert-Equal -Expected "Warning"
+ $actual_warn_event.Message | Assert-Equal -Expected "undefined win module - [WARNING] warning"
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{}
+ }
+ warnings = @("warning")
+ deprecations = @(
+ @{msg = "message"; version = "2.7"; collection_name = $null },
+ @{msg = "message w collection"; version = "2.8"; collection_name = "ansible.builtin" }
+ )
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "Deprecate and warn with date" = {
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+ $m.Deprecate("message", [DateTime]"2020-01-01")
+ $actual_deprecate_event_1 = Get-EventLog -LogName Application -Source Ansible -Newest 1
+ $m.Deprecate("message w collection", [DateTime]"2020-01-02", "ansible.builtin")
+ $actual_deprecate_event_2 = Get-EventLog -LogName Application -Source Ansible -Newest 1
+ $m.Warn("warning")
+ $actual_warn_event = Get-EventLog -LogName Application -Source Ansible -Newest 1
+
+ $actual_deprecate_event_1.Message | Assert-Equal -Expected "undefined win module - [DEPRECATION WARNING] message 2020-01-01"
+ $actual_deprecate_event_2.Message | Assert-Equal -Expected "undefined win module - [DEPRECATION WARNING] message w collection 2020-01-02"
+ $actual_warn_event.EntryType | Assert-Equal -Expected "Warning"
+ $actual_warn_event.Message | Assert-Equal -Expected "undefined win module - [WARNING] warning"
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{}
+ }
+ warnings = @("warning")
+ deprecations = @(
+ @{msg = "message"; date = "2020-01-01"; collection_name = $null },
+ @{msg = "message w collection"; date = "2020-01-02"; collection_name = "ansible.builtin" }
+ )
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "FailJson with message" = {
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+
+ $failed = $false
+ try {
+ $m.FailJson("fail message")
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $failed
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{}
+ }
+ failed = $true
+ msg = "fail message"
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "FailJson with Exception" = {
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+
+ try {
+ [System.IO.Path]::GetFullPath($null)
+ }
+ catch {
+ $excp = $_.Exception
+ }
+
+ $failed = $false
+ try {
+ $m.FailJson("fail message", $excp)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $failed
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{}
+ }
+ failed = $true
+ msg = "fail message"
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "FailJson with ErrorRecord" = {
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+
+ try {
+ Get-Item -LiteralPath $null
+ }
+ catch {
+ $error_record = $_
+ }
+
+ $failed = $false
+ try {
+ $m.FailJson("fail message", $error_record)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $failed
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{}
+ }
+ failed = $true
+ msg = "fail message"
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "FailJson with Exception and verbosity 3" = {
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_verbosity = 3
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+
+ try {
+ [System.IO.Path]::GetFullPath($null)
+ }
+ catch {
+ $excp = $_.Exception
+ }
+
+ $failed = $false
+ try {
+ $m.FailJson("fail message", $excp)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $failed
+
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = @{} }
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected "fail message"
+ $expected = 'System.Management.Automation.MethodInvocationException: Exception calling "GetFullPath" with "1" argument(s)'
+ $actual.exception.Contains($expected) | Assert-Equal -Expected $true
+ }
+
+ "FailJson with ErrorRecord and verbosity 3" = {
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_verbosity = 3
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+
+ try {
+ Get-Item -LiteralPath $null
+ }
+ catch {
+ $error_record = $_
+ }
+
+ $failed = $false
+ try {
+ $m.FailJson("fail message", $error_record)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $failed
+
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = @{} }
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected "fail message"
+ $actual.exception.Contains("Cannot bind argument to parameter 'LiteralPath' because it is null") | Assert-Equal -Expected $true
+ $actual.exception.Contains("+ Get-Item -LiteralPath `$null") | Assert-Equal -Expected $true
+ $actual.exception.Contains("ScriptStackTrace:") | Assert-Equal -Expected $true
+ }
+
+ "Diff entry without diff set" = {
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+ $m.Diff.before = @{a = "a" }
+ $m.Diff.after = @{b = "b" }
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $failed
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{}
+ }
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "Diff entry with diff set" = {
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_diff = $true
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+ $m.Diff.before = @{a = "a" }
+ $m.Diff.after = @{b = "b" }
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $failed
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{}
+ }
+ diff = @{
+ before = @{a = "a" }
+ after = @{b = "b" }
+ }
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "ParseBool tests" = {
+ $mapping = New-Object -TypeName 'System.Collections.Generic.Dictionary`2[[Object], [Bool]]'
+ $mapping.Add("y", $true)
+ $mapping.Add("Y", $true)
+ $mapping.Add("yes", $true)
+ $mapping.Add("Yes", $true)
+ $mapping.Add("on", $true)
+ $mapping.Add("On", $true)
+ $mapping.Add("1", $true)
+ $mapping.Add(1, $true)
+ $mapping.Add("true", $true)
+ $mapping.Add("True", $true)
+ $mapping.Add("t", $true)
+ $mapping.Add("T", $true)
+ $mapping.Add("1.0", $true)
+ $mapping.Add(1.0, $true)
+ $mapping.Add($true, $true)
+ $mapping.Add("n", $false)
+ $mapping.Add("N", $false)
+ $mapping.Add("no", $false)
+ $mapping.Add("No", $false)
+ $mapping.Add("off", $false)
+ $mapping.Add("Off", $false)
+ $mapping.Add("0", $false)
+ $mapping.Add(0, $false)
+ $mapping.Add("false", $false)
+ $mapping.Add("False", $false)
+ $mapping.Add("f", $false)
+ $mapping.Add("F", $false)
+ $mapping.Add("0.0", $false)
+ $mapping.Add(0.0, $false)
+ $mapping.Add($false, $false)
+
+ foreach ($map in $mapping.GetEnumerator()) {
+ $expected = $map.Value
+ $actual = [Ansible.Basic.AnsibleModule]::ParseBool($map.Key)
+ $actual | Assert-Equal -Expected $expected
+ $actual.GetType().FullName | Assert-Equal -Expected "System.Boolean"
+ }
+
+ $fail_bools = @(
+ "falsey",
+ "abc",
+ 2,
+ "2",
+ -1
+ )
+ foreach ($fail_bool in $fail_bools) {
+ $failed = $false
+ try {
+ [Ansible.Basic.AnsibleModule]::ParseBool($fail_bool)
+ }
+ catch {
+ $failed = $true
+ $_.Exception.Message.Contains("The value '$fail_bool' is not a valid boolean") | Assert-Equal -Expected $true
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+ }
+
+ "Unknown internal key" = {
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_invalid = "invalid"
+ }
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+
+ $expected = @{
+ invocation = @{
+ module_args = @{
+ _ansible_invalid = "invalid"
+ }
+ }
+ changed = $false
+ failed = $true
+ msg = "Unsupported parameters for (undefined win module) module: _ansible_invalid. Supported parameters include: "
+ }
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "Module tmpdir with present remote tmp" = {
+ $current_user = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
+ $dir_security = New-Object -TypeName System.Security.AccessControl.DirectorySecurity
+ $dir_security.SetOwner($current_user)
+ $dir_security.SetAccessRuleProtection($true, $false)
+ $ace = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList @(
+ $current_user, [System.Security.AccessControl.FileSystemRights]::FullControl,
+ [System.Security.AccessControl.InheritanceFlags]"ContainerInherit, ObjectInherit",
+ [System.Security.AccessControl.PropagationFlags]::None, [System.Security.AccessControl.AccessControlType]::Allow
+ )
+ $dir_security.AddAccessRule($ace)
+ $expected_sd = $dir_security.GetSecurityDescriptorSddlForm("Access, Owner")
+
+ $remote_tmp = Join-Path -Path $tmpdir -ChildPath "moduletmpdir-$(Get-Random)"
+ New-Item -Path $remote_tmp -ItemType Directory > $null
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_remote_tmp = $remote_tmp.ToString()
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+ (Test-Path -LiteralPath $remote_tmp -PathType Container) | Assert-Equal -Expected $true
+
+ $actual_tmpdir = $m.Tmpdir
+ $parent_tmpdir = Split-Path -Path $actual_tmpdir -Parent
+ $tmpdir_name = Split-Path -Path $actual_tmpdir -Leaf
+
+ $parent_tmpdir | Assert-Equal -Expected $remote_tmp
+ $tmpdir_name.StartSwith("ansible-moduletmp-") | Assert-Equal -Expected $true
+ (Test-Path -LiteralPath $actual_tmpdir -PathType Container) | Assert-Equal -Expected $true
+ (Test-Path -LiteralPath $remote_tmp -PathType Container) | Assert-Equal -Expected $true
+ $children = [System.IO.Directory]::EnumerateDirectories($remote_tmp)
+ $children.Count | Assert-Equal -Expected 1
+ $actual_tmpdir_sd = (Get-Acl -Path $actual_tmpdir).GetSecurityDescriptorSddlForm("Access, Owner")
+ $actual_tmpdir_sd | Assert-Equal -Expected $expected_sd
+
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $output = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ (Test-Path -LiteralPath $actual_tmpdir -PathType Container) | Assert-Equal -Expected $false
+ (Test-Path -LiteralPath $remote_tmp -PathType Container) | Assert-Equal -Expected $true
+ $output.warnings.Count | Assert-Equal -Expected 0
+ }
+
+ "Module tmpdir with missing remote_tmp" = {
+ $current_user = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
+ $dir_security = New-Object -TypeName System.Security.AccessControl.DirectorySecurity
+ $dir_security.SetOwner($current_user)
+ $dir_security.SetAccessRuleProtection($true, $false)
+ $ace = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList @(
+ $current_user, [System.Security.AccessControl.FileSystemRights]::FullControl,
+ [System.Security.AccessControl.InheritanceFlags]"ContainerInherit, ObjectInherit",
+ [System.Security.AccessControl.PropagationFlags]::None, [System.Security.AccessControl.AccessControlType]::Allow
+ )
+ $dir_security.AddAccessRule($ace)
+ $expected_sd = $dir_security.GetSecurityDescriptorSddlForm("Access, Owner")
+
+ $remote_tmp = Join-Path -Path $tmpdir -ChildPath "moduletmpdir-$(Get-Random)"
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_remote_tmp = $remote_tmp.ToString()
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+ (Test-Path -LiteralPath $remote_tmp -PathType Container) | Assert-Equal -Expected $false
+
+ $actual_tmpdir = $m.Tmpdir
+ $parent_tmpdir = Split-Path -Path $actual_tmpdir -Parent
+ $tmpdir_name = Split-Path -Path $actual_tmpdir -Leaf
+
+ $parent_tmpdir | Assert-Equal -Expected $remote_tmp
+ $tmpdir_name.StartSwith("ansible-moduletmp-") | Assert-Equal -Expected $true
+ (Test-Path -LiteralPath $actual_tmpdir -PathType Container) | Assert-Equal -Expected $true
+ (Test-Path -LiteralPath $remote_tmp -PathType Container) | Assert-Equal -Expected $true
+ $children = [System.IO.Directory]::EnumerateDirectories($remote_tmp)
+ $children.Count | Assert-Equal -Expected 1
+ $actual_remote_sd = (Get-Acl -Path $remote_tmp).GetSecurityDescriptorSddlForm("Access, Owner")
+ $actual_tmpdir_sd = (Get-Acl -Path $actual_tmpdir).GetSecurityDescriptorSddlForm("Access, Owner")
+ $actual_remote_sd | Assert-Equal -Expected $expected_sd
+ $actual_tmpdir_sd | Assert-Equal -Expected $expected_sd
+
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $output = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ (Test-Path -LiteralPath $actual_tmpdir -PathType Container) | Assert-Equal -Expected $false
+ (Test-Path -LiteralPath $remote_tmp -PathType Container) | Assert-Equal -Expected $true
+ $output.warnings.Count | Assert-Equal -Expected 1
+ $nt_account = $current_user.Translate([System.Security.Principal.NTAccount])
+ $actual_warning = "Module remote_tmp $remote_tmp did not exist and was created with FullControl to $nt_account, "
+ $actual_warning += "this may cause issues when running as another user. To avoid this, "
+ $actual_warning += "create the remote_tmp dir with the correct permissions manually"
+ $actual_warning | Assert-Equal -Expected $output.warnings[0]
+ }
+
+ "Module tmp, keep remote files" = {
+ $remote_tmp = Join-Path -Path $tmpdir -ChildPath "moduletmpdir-$(Get-Random)"
+ New-Item -Path $remote_tmp -ItemType Directory > $null
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_remote_tmp = $remote_tmp.ToString()
+ _ansible_keep_remote_files = $true
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+
+ $actual_tmpdir = $m.Tmpdir
+ $parent_tmpdir = Split-Path -Path $actual_tmpdir -Parent
+ $tmpdir_name = Split-Path -Path $actual_tmpdir -Leaf
+
+ $parent_tmpdir | Assert-Equal -Expected $remote_tmp
+ $tmpdir_name.StartSwith("ansible-moduletmp-") | Assert-Equal -Expected $true
+ (Test-Path -LiteralPath $actual_tmpdir -PathType Container) | Assert-Equal -Expected $true
+ (Test-Path -LiteralPath $remote_tmp -PathType Container) | Assert-Equal -Expected $true
+
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $output = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ (Test-Path -LiteralPath $actual_tmpdir -PathType Container) | Assert-Equal -Expected $true
+ (Test-Path -LiteralPath $remote_tmp -PathType Container) | Assert-Equal -Expected $true
+ $output.warnings.Count | Assert-Equal -Expected 0
+ Remove-Item -LiteralPath $actual_tmpdir -Force -Recurse
+ }
+
+ "Invalid argument spec key" = {
+ $spec = @{
+ invalid = $true
+ }
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "internal error: argument spec entry contains an invalid key 'invalid', valid keys: apply_defaults, "
+ $expected_msg += "aliases, choices, default, deprecated_aliases, elements, mutually_exclusive, no_log, options, "
+ $expected_msg += "removed_in_version, removed_at_date, removed_from_collection, required, required_by, required_if, "
+ $expected_msg += "required_one_of, required_together, supports_check_mode, type"
+
+ $actual.Keys.Count | Assert-Equal -Expected 3
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ ("exception" -cin $actual.Keys) | Assert-Equal -Expected $true
+ }
+
+ "Invalid argument spec key - nested" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ options = @{
+ sub_option_key = @{
+ invalid = $true
+ }
+ }
+ }
+ }
+ }
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "internal error: argument spec entry contains an invalid key 'invalid', valid keys: apply_defaults, "
+ $expected_msg += "aliases, choices, default, deprecated_aliases, elements, mutually_exclusive, no_log, options, "
+ $expected_msg += "removed_in_version, removed_at_date, removed_from_collection, required, required_by, required_if, "
+ $expected_msg += "required_one_of, required_together, supports_check_mode, type - found in option_key -> sub_option_key"
+
+ $actual.Keys.Count | Assert-Equal -Expected 3
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ ("exception" -cin $actual.Keys) | Assert-Equal -Expected $true
+ }
+
+ "Invalid argument spec value type" = {
+ $spec = @{
+ apply_defaults = "abc"
+ }
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "internal error: argument spec for 'apply_defaults' did not match expected "
+ $expected_msg += "type System.Boolean: actual type System.String"
+
+ $actual.Keys.Count | Assert-Equal -Expected 3
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ ("exception" -cin $actual.Keys) | Assert-Equal -Expected $true
+ }
+
+ "Invalid argument spec option type" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "invalid type"
+ }
+ }
+ }
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "internal error: type 'invalid type' is unsupported - found in option_key. "
+ $expected_msg += "Valid types are: bool, dict, float, int, json, list, path, raw, sid, str"
+
+ $actual.Keys.Count | Assert-Equal -Expected 3
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ ("exception" -cin $actual.Keys) | Assert-Equal -Expected $true
+ }
+
+ "Invalid argument spec option element type" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "list"
+ elements = "invalid type"
+ }
+ }
+ }
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "internal error: elements 'invalid type' is unsupported - found in option_key. "
+ $expected_msg += "Valid types are: bool, dict, float, int, json, list, path, raw, sid, str"
+
+ $actual.Keys.Count | Assert-Equal -Expected 3
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ ("exception" -cin $actual.Keys) | Assert-Equal -Expected $true
+ }
+
+ "Invalid deprecated aliases entry - no version and date" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "str"
+ aliases = , "alias_name"
+ deprecated_aliases = @(
+ @{name = "alias_name" }
+ )
+ }
+ }
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "internal error: One of version or date is required in a deprecated_aliases entry"
+
+ $actual.Keys.Count | Assert-Equal -Expected 3
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ ("exception" -cin $actual.Keys) | Assert-Equal -Expected $true
+ }
+
+ "Invalid deprecated aliases entry - no name (nested)" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "dict"
+ options = @{
+ sub_option_key = @{
+ type = "str"
+ aliases = , "alias_name"
+ deprecated_aliases = @(
+ @{version = "2.10" }
+ )
+ }
+ }
+ }
+ }
+ }
+
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = @{
+ sub_option_key = "a"
+ }
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.ArgumentException] {
+ $failed = $true
+ $expected_msg = "name is required in a deprecated_aliases entry - found in option_key"
+ $_.Exception.Message | Assert-Equal -Expected $expected_msg
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "Invalid deprecated aliases entry - both version and date" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "str"
+ aliases = , "alias_name"
+ deprecated_aliases = @(
+ @{
+ name = "alias_name"
+ date = [DateTime]"2020-03-10"
+ version = "2.11"
+ }
+ )
+ }
+ }
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "internal error: Only one of version or date is allowed in a deprecated_aliases entry"
+
+ $actual.Keys.Count | Assert-Equal -Expected 3
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ ("exception" -cin $actual.Keys) | Assert-Equal -Expected $true
+ }
+
+ "Invalid deprecated aliases entry - wrong date type" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "str"
+ aliases = , "alias_name"
+ deprecated_aliases = @(
+ @{
+ name = "alias_name"
+ date = "2020-03-10"
+ }
+ )
+ }
+ }
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "internal error: A deprecated_aliases date must be a DateTime object"
+
+ $actual.Keys.Count | Assert-Equal -Expected 3
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ ("exception" -cin $actual.Keys) | Assert-Equal -Expected $true
+ }
+
+ "Spec required and default set at the same time" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ required = $true
+ default = "default value"
+ }
+ }
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "internal error: required and default are mutually exclusive for option_key"
+
+ $actual.Keys.Count | Assert-Equal -Expected 3
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ ("exception" -cin $actual.Keys) | Assert-Equal -Expected $true
+ }
+
+ "Unsupported options" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "str"
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = "abc"
+ invalid_key = "def"
+ another_key = "ghi"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "Unsupported parameters for (undefined win module) module: another_key, invalid_key. "
+ $expected_msg += "Supported parameters include: option_key"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Check mode and module doesn't support check mode" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "str"
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_check_mode = $true
+ option_key = "abc"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "remote module (undefined win module) does not support check mode"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.skipped | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = @{option_key = "abc" } }
+ }
+
+ "Check mode with suboption without supports_check_mode" = {
+ $spec = @{
+ options = @{
+ sub_options = @{
+ # This tests the situation where a sub key doesn't set supports_check_mode, the logic in
+ # Ansible.Basic automatically sets that to $false and we want it to ignore it for a nested check
+ type = "dict"
+ options = @{
+ sub_option = @{ type = "str"; default = "value" }
+ }
+ }
+ }
+ supports_check_mode = $true
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_check_mode = $true
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ $m.CheckMode | Assert-Equal -Expected $true
+ }
+
+ "Type conversion error" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "int"
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = "a"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "argument for option_key is of type System.String and we were unable to convert to int: "
+ $expected_msg += "Input string was not in a correct format."
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Type conversion error - delegate" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "dict"
+ options = @{
+ sub_option_key = @{
+ type = [Func[[Object], [UInt64]]] { [System.UInt64]::Parse($args[0]) }
+ }
+ }
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = @{
+ sub_option_key = "a"
+ }
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "argument for sub_option_key is of type System.String and we were unable to convert to delegate: "
+ $expected_msg += "Exception calling `"Parse`" with `"1`" argument(s): `"Input string was not in a correct format.`" "
+ $expected_msg += "found in option_key"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Numeric choices" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ choices = 1, 2, 3
+ type = "int"
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = "2"
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $output = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $output.Keys.Count | Assert-Equal -Expected 2
+ $output.changed | Assert-Equal -Expected $false
+ $output.invocation | Assert-DictionaryEqual -Expected @{module_args = @{option_key = 2 } }
+ }
+
+ "Case insensitive choice" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ choices = "abc", "def"
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = "ABC"
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $output = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $expected_warning = "value of option_key was a case insensitive match of one of: abc, def. "
+ $expected_warning += "Checking of choices will be case sensitive in a future Ansible release. "
+ $expected_warning += "Case insensitive matches were: ABC"
+
+ $output.invocation | Assert-DictionaryEqual -Expected @{module_args = @{option_key = "ABC" } }
+ # We have disabled the warnings for now
+ #$output.warnings.Count | Assert-Equal -Expected 1
+ #$output.warnings[0] | Assert-Equal -Expected $expected_warning
+ }
+
+ "Case insensitive choice no_log" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ choices = "abc", "def"
+ no_log = $true
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = "ABC"
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $output = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $expected_warning = "value of option_key was a case insensitive match of one of: abc, def. "
+ $expected_warning += "Checking of choices will be case sensitive in a future Ansible release. "
+ $expected_warning += "Case insensitive matches were: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
+
+ $output.invocation | Assert-DictionaryEqual -Expected @{module_args = @{option_key = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" } }
+ # We have disabled the warnings for now
+ #$output.warnings.Count | Assert-Equal -Expected 1
+ #$output.warnings[0] | Assert-Equal -Expected $expected_warning
+ }
+
+ "Case insentitive choice as list" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ choices = "abc", "def", "ghi", "JKL"
+ type = "list"
+ elements = "str"
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = "AbC", "ghi", "jkl"
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $output = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $expected_warning = "value of option_key was a case insensitive match of one or more of: abc, def, ghi, JKL. "
+ $expected_warning += "Checking of choices will be case sensitive in a future Ansible release. "
+ $expected_warning += "Case insensitive matches were: AbC, jkl"
+
+ $output.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ # We have disabled the warnings for now
+ #$output.warnings.Count | Assert-Equal -Expected 1
+ #$output.warnings[0] | Assert-Equal -Expected $expected_warning
+ }
+
+ "Invalid choice" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ choices = "a", "b"
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = "c"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "value of option_key must be one of: a, b. Got no match for: c"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Invalid choice with no_log" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ choices = "a", "b"
+ no_log = $true
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = "abc"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "value of option_key must be one of: a, b. Got no match for: ********"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = @{option_key = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" } }
+ }
+
+ "Invalid choice in list" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ choices = "a", "b"
+ type = "list"
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = "a", "c"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "value of option_key must be one or more of: a, b. Got no match for: c"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Mutually exclusive options" = {
+ $spec = @{
+ options = @{
+ option1 = @{}
+ option2 = @{}
+ }
+ mutually_exclusive = @(, @("option1", "option2"))
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option1 = "a"
+ option2 = "b"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "parameters are mutually exclusive: option1, option2"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Missing required argument" = {
+ $spec = @{
+ options = @{
+ option1 = @{}
+ option2 = @{required = $true }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option1 = "a"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "missing required arguments: option2"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Missing required argument subspec - no value defined" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "dict"
+ options = @{
+ sub_option_key = @{
+ required = $true
+ }
+ }
+ }
+ }
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $actual.Keys.Count | Assert-Equal -Expected 2
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Missing required argument subspec" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "dict"
+ options = @{
+ sub_option_key = @{
+ required = $true
+ }
+ another_key = @{}
+ }
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = @{
+ another_key = "abc"
+ }
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "missing required arguments: sub_option_key found in option_key"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Required together not set" = {
+ $spec = @{
+ options = @{
+ option1 = @{}
+ option2 = @{}
+ }
+ required_together = @(, @("option1", "option2"))
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option1 = "abc"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "parameters are required together: option1, option2"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Required together not set - subspec" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "dict"
+ options = @{
+ option1 = @{}
+ option2 = @{}
+ }
+ required_together = @(, @("option1", "option2"))
+ }
+ another_option = @{}
+ }
+ required_together = @(, @("option_key", "another_option"))
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = @{
+ option1 = "abc"
+ }
+ another_option = "def"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "parameters are required together: option1, option2 found in option_key"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Required one of not set" = {
+ $spec = @{
+ options = @{
+ option1 = @{}
+ option2 = @{}
+ option3 = @{}
+ }
+ required_one_of = @(@("option1", "option2"), @("option2", "option3"))
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option1 = "abc"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "one of the following is required: option2, option3"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Required if invalid entries" = {
+ $spec = @{
+ options = @{
+ state = @{choices = "absent", "present"; default = "present" }
+ path = @{type = "path" }
+ }
+ required_if = @(, @("state", "absent"))
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "internal error: invalid required_if value count of 2, expecting 3 or 4 entries"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Required if no missing option" = {
+ $spec = @{
+ options = @{
+ state = @{choices = "absent", "present"; default = "present" }
+ name = @{}
+ path = @{type = "path" }
+ }
+ required_if = @(, @("state", "absent", @("name", "path")))
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ name = "abc"
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $actual.Keys.Count | Assert-Equal -Expected 2
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Required if missing option" = {
+ $spec = @{
+ options = @{
+ state = @{choices = "absent", "present"; default = "present" }
+ name = @{}
+ path = @{type = "path" }
+ }
+ required_if = @(, @("state", "absent", @("name", "path")))
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ state = "absent"
+ name = "abc"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "state is absent but all of the following are missing: path"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Required if missing option and required one is set" = {
+ $spec = @{
+ options = @{
+ state = @{choices = "absent", "present"; default = "present" }
+ name = @{}
+ path = @{type = "path" }
+ }
+ required_if = @(, @("state", "absent", @("name", "path"), $true))
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ state = "absent"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "state is absent but any of the following are missing: name, path"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Required if missing option but one required set" = {
+ $spec = @{
+ options = @{
+ state = @{choices = "absent", "present"; default = "present" }
+ name = @{}
+ path = @{type = "path" }
+ }
+ required_if = @(, @("state", "absent", @("name", "path"), $true))
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ state = "absent"
+ name = "abc"
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $actual.Keys.Count | Assert-Equal -Expected 2
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "PS Object in return result" = {
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+
+ # JavaScriptSerializer struggles with PS Object like PSCustomObject due to circular references, this test makes
+ # sure we can handle these types of objects without bombing
+ $m.Result.output = [PSCustomObject]@{a = "a"; b = "b" }
+ $failed = $true
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $actual.Keys.Count | Assert-Equal -Expected 3
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = @{} }
+ $actual.output | Assert-DictionaryEqual -Expected @{a = "a"; b = "b" }
+ }
+
+ "String json array to object" = {
+ $input_json = '["abc", "def"]'
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($input_json)
+ $actual -is [Array] | Assert-Equal -Expected $true
+ $actual.Length | Assert-Equal -Expected 2
+ $actual[0] | Assert-Equal -Expected "abc"
+ $actual[1] | Assert-Equal -Expected "def"
+ }
+
+ "String json array of dictionaries to object" = {
+ $input_json = '[{"abc":"def"}]'
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($input_json)
+ $actual -is [Array] | Assert-Equal -Expected $true
+ $actual.Length | Assert-Equal -Expected 1
+ $actual[0] | Assert-DictionaryEqual -Expected @{"abc" = "def" }
+ }
+
+ "Spec with fragments" = {
+ $spec = @{
+ options = @{
+ option1 = @{ type = "str" }
+ }
+ }
+ $fragment1 = @{
+ options = @{
+ option2 = @{ type = "str" }
+ }
+ }
+
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option1 = "option1"
+ option2 = "option2"
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec, @($fragment1))
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Fragment spec that with a deprecated alias" = {
+ $spec = @{
+ options = @{
+ option1 = @{
+ aliases = @("alias1_spec")
+ type = "str"
+ deprecated_aliases = @(
+ @{name = "alias1_spec"; version = "2.0" }
+ )
+ }
+ option2 = @{
+ aliases = @("alias2_spec")
+ deprecated_aliases = @(
+ @{name = "alias2_spec"; version = "2.0"; collection_name = "ansible.builtin" }
+ )
+ }
+ }
+ }
+ $fragment1 = @{
+ options = @{
+ option1 = @{
+ aliases = @("alias1")
+ deprecated_aliases = @() # Makes sure it doesn't overwrite the spec, just adds to it.
+ }
+ option2 = @{
+ aliases = @("alias2")
+ deprecated_aliases = @(
+ @{name = "alias2"; version = "2.0"; collection_name = "foo.bar" }
+ )
+ type = "str"
+ }
+ }
+ }
+
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ alias1_spec = "option1"
+ alias2 = "option2"
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec, @($fragment1))
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $actual.deprecations.Count | Assert-Equal -Expected 2
+ $actual.deprecations[0] | Assert-DictionaryEqual -Expected @{
+ msg = "Alias 'alias1_spec' is deprecated. See the module docs for more information"; version = "2.0"; collection_name = $null
+ }
+ $actual.deprecations[1] | Assert-DictionaryEqual -Expected @{
+ msg = "Alias 'alias2' is deprecated. See the module docs for more information"; version = "2.0"; collection_name = "foo.bar"
+ }
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.invocation | Assert-DictionaryEqual -Expected @{
+ module_args = @{
+ option1 = "option1"
+ alias1_spec = "option1"
+ option2 = "option2"
+ alias2 = "option2"
+ }
+ }
+ }
+
+ "Fragment spec with mutual args" = {
+ $spec = @{
+ options = @{
+ option1 = @{ type = "str" }
+ option2 = @{ type = "str" }
+ }
+ mutually_exclusive = @(
+ , @('option1', 'option2')
+ )
+ }
+ $fragment1 = @{
+ options = @{
+ fragment1_1 = @{ type = "str" }
+ fragment1_2 = @{ type = "str" }
+ }
+ mutually_exclusive = @(
+ , @('fragment1_1', 'fragment1_2')
+ )
+ }
+ $fragment2 = @{
+ options = @{
+ fragment2 = @{ type = "str" }
+ }
+ }
+
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option1 = "option1"
+ fragment1_1 = "fragment1_1"
+ fragment1_2 = "fragment1_2"
+ fragment2 = "fragment2"
+ }
+
+ $failed = $false
+ try {
+ [Ansible.Basic.AnsibleModule]::Create(@(), $spec, @($fragment1, $fragment2))
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected "parameters are mutually exclusive: fragment1_1, fragment1_2"
+ $actual.invocation | Assert-DictionaryEqual -Expected @{ module_args = $complex_args }
+ }
+
+ "Fragment spec with no_log" = {
+ $spec = @{
+ options = @{
+ option1 = @{
+ aliases = @("alias")
+ }
+ }
+ }
+ $fragment1 = @{
+ options = @{
+ option1 = @{
+ no_log = $true # Makes sure that a value set in the fragment but not in the spec is respected.
+ type = "str"
+ }
+ }
+ }
+
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ alias = "option1"
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec, @($fragment1))
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.invocation | Assert-DictionaryEqual -Expected @{
+ module_args = @{
+ option1 = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
+ alias = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
+ }
+ }
+ }
+
+ "Catch invalid fragment spec format" = {
+ $spec = @{
+ options = @{
+ option1 = @{ type = "str" }
+ }
+ }
+ $fragment = @{
+ options = @{}
+ invalid = "will fail"
+ }
+
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option1 = "option1"
+ }
+
+ $failed = $false
+ try {
+ [Ansible.Basic.AnsibleModule]::Create(@(), $spec, @($fragment))
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg.StartsWith("internal error: argument spec entry contains an invalid key 'invalid', valid keys: ") | Assert-Equal -Expected $true
+ }
+
+ "Spec with different list types" = {
+ $spec = @{
+ options = @{
+ # Single element of the same list type not in a list
+ option1 = @{
+ aliases = "alias1"
+ deprecated_aliases = @{name = "alias1"; version = "2.0"; collection_name = "foo.bar" }
+ }
+
+ # Arrays
+ option2 = @{
+ aliases = , "alias2"
+ deprecated_aliases = , @{name = "alias2"; version = "2.0"; collection_name = "foo.bar" }
+ }
+
+ # ArrayList
+ option3 = @{
+ aliases = [System.Collections.ArrayList]@("alias3")
+ deprecated_aliases = [System.Collections.ArrayList]@(@{name = "alias3"; version = "2.0"; collection_name = "foo.bar" })
+ }
+
+ # Generic.List[Object]
+ option4 = @{
+ aliases = [System.Collections.Generic.List[Object]]@("alias4")
+ deprecated_aliases = [System.Collections.Generic.List[Object]]@(@{name = "alias4"; version = "2.0"; collection_name = "foo.bar" })
+ }
+
+ # Generic.List[T]
+ option5 = @{
+ aliases = [System.Collections.Generic.List[String]]@("alias5")
+ deprecated_aliases = [System.Collections.Generic.List[Hashtable]]@()
+ }
+ }
+ }
+ $spec.options.option5.deprecated_aliases.Add(@{name = "alias5"; version = "2.0"; collection_name = "foo.bar" })
+
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ alias1 = "option1"
+ alias2 = "option2"
+ alias3 = "option3"
+ alias4 = "option4"
+ alias5 = "option5"
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.deprecations.Count | Assert-Equal -Expected 5
+ foreach ($dep in $actual.deprecations) {
+ $dep.msg -like "Alias 'alias?' is deprecated. See the module docs for more information" | Assert-Equal -Expected $true
+ $dep.version | Assert-Equal -Expected '2.0'
+ $dep.collection_name | Assert-Equal -Expected 'foo.bar'
+ }
+ $actual.invocation | Assert-DictionaryEqual -Expected @{
+ module_args = @{
+ alias1 = "option1"
+ option1 = "option1"
+ alias2 = "option2"
+ option2 = "option2"
+ alias3 = "option3"
+ option3 = "option3"
+ alias4 = "option4"
+ option4 = "option4"
+ alias5 = "option5"
+ option5 = "option5"
+ }
+ }
+ }
+}
+
+try {
+ foreach ($test_impl in $tests.GetEnumerator()) {
+ # Reset the variables before each test
+ Set-Variable -Name complex_args -Value @{} -Scope Global
+
+ $test = $test_impl.Key
+ &$test_impl.Value
+ }
+ $module.Result.data = "success"
+}
+catch [System.Management.Automation.RuntimeException] {
+ $module.Result.failed = $true
+ $module.Result.test = $test
+ $module.Result.line = $_.InvocationInfo.ScriptLineNumber
+ $module.Result.method = $_.InvocationInfo.Line.Trim()
+
+ if ($_.Exception.Message.StartSwith("exit: ")) {
+ # The exception was caused by an unexpected Exit call, log that on the output
+ $module.Result.output = (ConvertFrom-Json -InputObject $_.Exception.InnerException.Output)
+ $module.Result.msg = "Uncaught AnsibleModule exit in tests, see output"
+ }
+ else {
+ # Unrelated exception
+ $module.Result.exception = $_.Exception.ToString()
+ $module.Result.msg = "Uncaught exception: $(($_ | Out-String).ToString())"
+ }
+}
+
+Exit-Module
diff --git a/test/integration/targets/module_utils_Ansible.Basic/tasks/main.yml b/test/integration/targets/module_utils_Ansible.Basic/tasks/main.yml
new file mode 100644
index 0000000..010c2d5
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Basic/tasks/main.yml
@@ -0,0 +1,9 @@
+---
+- name: test Ansible.Basic.cs
+ ansible_basic_tests:
+ register: ansible_basic_test
+
+- name: assert test Ansible.Basic.cs
+ assert:
+ that:
+ - ansible_basic_test.data == "success"
diff --git a/test/integration/targets/module_utils_Ansible.Become/aliases b/test/integration/targets/module_utils_Ansible.Become/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Become/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.Become/library/ansible_become_tests.ps1 b/test/integration/targets/module_utils_Ansible.Become/library/ansible_become_tests.ps1
new file mode 100644
index 0000000..6e36321
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Become/library/ansible_become_tests.ps1
@@ -0,0 +1,1022 @@
+#!powershell
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#AnsibleRequires -CSharpUtil Ansible.Become
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
+
+Function Assert-Equal {
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)][AllowNull()]$Actual,
+ [Parameter(Mandatory = $true, Position = 0)][AllowNull()]$Expected
+ )
+
+ process {
+ $matched = $false
+ if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array]) {
+ $Actual.Count | Assert-Equal -Expected $Expected.Count
+ for ($i = 0; $i -lt $Actual.Count; $i++) {
+ $actual_value = $Actual[$i]
+ $expected_value = $Expected[$i]
+ Assert-Equal -Actual $actual_value -Expected $expected_value
+ }
+ $matched = $true
+ }
+ else {
+ $matched = $Actual -ceq $Expected
+ }
+
+ if (-not $matched) {
+ if ($Actual -is [PSObject]) {
+ $Actual = $Actual.ToString()
+ }
+
+ $call_stack = (Get-PSCallStack)[1]
+ $module.Result.test = $test
+ $module.Result.actual = $Actual
+ $module.Result.expected = $Expected
+ $module.Result.line = $call_stack.ScriptLineNumber
+ $module.Result.method = $call_stack.Position.Text
+ $module.FailJson("AssertionError: actual != expected")
+ }
+ }
+}
+
+# Would be great to move win_whomai out into it's own module util and share the
+# code here, for now just rely on a cut down version
+$test_whoami = {
+ Add-Type -TypeDefinition @'
+using Microsoft.Win32.SafeHandles;
+using System;
+using System.Runtime.ConstrainedExecution;
+using System.Runtime.InteropServices;
+using System.Security.Principal;
+using System.Text;
+
+namespace Ansible
+{
+ internal class NativeHelpers
+ {
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public struct LSA_UNICODE_STRING
+ {
+ public UInt16 Length;
+ public UInt16 MaximumLength;
+ public IntPtr Buffer;
+
+ public override string ToString()
+ {
+ return Marshal.PtrToStringUni(Buffer, Length / sizeof(char));
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct LUID
+ {
+ public UInt32 LowPart;
+ public Int32 HighPart;
+
+ public static explicit operator UInt64(LUID l)
+ {
+ return (UInt64)((UInt64)l.HighPart << 32) | (UInt64)l.LowPart;
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SECURITY_LOGON_SESSION_DATA
+ {
+ public UInt32 Size;
+ public LUID LogonId;
+ public LSA_UNICODE_STRING UserName;
+ public LSA_UNICODE_STRING LogonDomain;
+ public LSA_UNICODE_STRING AuthenticationPackage;
+ public SECURITY_LOGON_TYPE LogonType;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SID_AND_ATTRIBUTES
+ {
+ public IntPtr Sid;
+ public int Attributes;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct TOKEN_MANDATORY_LABEL
+ {
+ public SID_AND_ATTRIBUTES Label;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct TOKEN_SOURCE
+ {
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] public char[] SourceName;
+ public LUID SourceIdentifier;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct TOKEN_STATISTICS
+ {
+ public LUID TokenId;
+ public LUID AuthenticationId;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct TOKEN_USER
+ {
+ public SID_AND_ATTRIBUTES User;
+ }
+
+ public enum SECURITY_LOGON_TYPE
+ {
+ System = 0, // Used only by the Sytem account
+ Interactive = 2,
+ Network,
+ Batch,
+ Service,
+ Proxy,
+ Unlock,
+ NetworkCleartext,
+ NewCredentials,
+ RemoteInteractive,
+ CachedInteractive,
+ CachedRemoteInteractive,
+ CachedUnlock
+ }
+
+ public enum TokenInformationClass
+ {
+ TokenUser = 1,
+ TokenSource = 7,
+ TokenStatistics = 10,
+ TokenIntegrityLevel = 25,
+ }
+ }
+
+ internal class NativeMethods
+ {
+ [DllImport("kernel32.dll", SetLastError = true)]
+ public static extern bool CloseHandle(
+ IntPtr hObject);
+
+ [DllImport("kernel32.dll")]
+ public static extern SafeNativeHandle GetCurrentProcess();
+
+ [DllImport("userenv.dll", SetLastError = true)]
+ public static extern bool GetProfileType(
+ out UInt32 dwFlags);
+
+ [DllImport("advapi32.dll", SetLastError = true)]
+ public static extern bool GetTokenInformation(
+ SafeNativeHandle TokenHandle,
+ NativeHelpers.TokenInformationClass TokenInformationClass,
+ SafeMemoryBuffer TokenInformation,
+ UInt32 TokenInformationLength,
+ out UInt32 ReturnLength);
+
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern bool LookupAccountSid(
+ string lpSystemName,
+ IntPtr Sid,
+ StringBuilder lpName,
+ ref UInt32 cchName,
+ StringBuilder ReferencedDomainName,
+ ref UInt32 cchReferencedDomainName,
+ out UInt32 peUse);
+
+ [DllImport("secur32.dll", SetLastError = true)]
+ public static extern UInt32 LsaEnumerateLogonSessions(
+ out UInt32 LogonSessionCount,
+ out SafeLsaMemoryBuffer LogonSessionList);
+
+ [DllImport("secur32.dll", SetLastError = true)]
+ public static extern UInt32 LsaFreeReturnBuffer(
+ IntPtr Buffer);
+
+ [DllImport("secur32.dll", SetLastError = true)]
+ public static extern UInt32 LsaGetLogonSessionData(
+ IntPtr LogonId,
+ out SafeLsaMemoryBuffer ppLogonSessionData);
+
+ [DllImport("advapi32.dll")]
+ public static extern UInt32 LsaNtStatusToWinError(
+ UInt32 Status);
+
+ [DllImport("advapi32.dll", SetLastError = true)]
+ public static extern bool OpenProcessToken(
+ SafeNativeHandle ProcessHandle,
+ TokenAccessLevels DesiredAccess,
+ out SafeNativeHandle TokenHandle);
+ }
+
+ internal class SafeLsaMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ public SafeLsaMemoryBuffer() : base(true) { }
+
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
+ protected override bool ReleaseHandle()
+ {
+ UInt32 res = NativeMethods.LsaFreeReturnBuffer(handle);
+ return res == 0;
+ }
+ }
+
+ internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ public SafeMemoryBuffer() : base(true) { }
+ public SafeMemoryBuffer(int cb) : base(true)
+ {
+ base.SetHandle(Marshal.AllocHGlobal(cb));
+ }
+ public SafeMemoryBuffer(IntPtr handle) : base(true)
+ {
+ base.SetHandle(handle);
+ }
+
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
+ protected override bool ReleaseHandle()
+ {
+ Marshal.FreeHGlobal(handle);
+ return true;
+ }
+ }
+
+ internal class SafeNativeHandle : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ public SafeNativeHandle() : base(true) { }
+ public SafeNativeHandle(IntPtr handle) : base(true) { this.handle = handle; }
+
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
+ protected override bool ReleaseHandle()
+ {
+ return NativeMethods.CloseHandle(handle);
+ }
+ }
+
+ public class Win32Exception : System.ComponentModel.Win32Exception
+ {
+ private string _msg;
+
+ public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
+ public Win32Exception(int errorCode, string message) : base(errorCode)
+ {
+ _msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode);
+ }
+
+ public override string Message { get { return _msg; } }
+ public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
+ }
+
+ public class Logon
+ {
+ public string AuthenticationPackage { get; internal set; }
+ public string LogonType { get; internal set; }
+ public string MandatoryLabelName { get; internal set; }
+ public SecurityIdentifier MandatoryLabelSid { get; internal set; }
+ public bool ProfileLoaded { get; internal set; }
+ public string SourceName { get; internal set; }
+ public string UserName { get; internal set; }
+ public SecurityIdentifier UserSid { get; internal set; }
+
+ public Logon()
+ {
+ using (SafeNativeHandle process = NativeMethods.GetCurrentProcess())
+ {
+ TokenAccessLevels dwAccess = TokenAccessLevels.Query | TokenAccessLevels.QuerySource;
+
+ SafeNativeHandle hToken;
+ NativeMethods.OpenProcessToken(process, dwAccess, out hToken);
+ using (hToken)
+ {
+ SetLogonSessionData(hToken);
+ SetTokenMandatoryLabel(hToken);
+ SetTokenSource(hToken);
+ SetTokenUser(hToken);
+ }
+ }
+ SetProfileLoaded();
+ }
+
+ private void SetLogonSessionData(SafeNativeHandle hToken)
+ {
+ NativeHelpers.TokenInformationClass tokenClass = NativeHelpers.TokenInformationClass.TokenStatistics;
+ UInt32 returnLength;
+ NativeMethods.GetTokenInformation(hToken, tokenClass, new SafeMemoryBuffer(IntPtr.Zero), 0, out returnLength);
+
+ UInt64 tokenLuidId;
+ using (SafeMemoryBuffer infoPtr = new SafeMemoryBuffer((int)returnLength))
+ {
+ if (!NativeMethods.GetTokenInformation(hToken, tokenClass, infoPtr, returnLength, out returnLength))
+ throw new Win32Exception("GetTokenInformation(TokenStatistics) failed");
+
+ NativeHelpers.TOKEN_STATISTICS stats = (NativeHelpers.TOKEN_STATISTICS)Marshal.PtrToStructure(
+ infoPtr.DangerousGetHandle(), typeof(NativeHelpers.TOKEN_STATISTICS));
+ tokenLuidId = (UInt64)stats.AuthenticationId;
+ }
+
+ UInt32 sessionCount;
+ SafeLsaMemoryBuffer sessionPtr;
+ UInt32 res = NativeMethods.LsaEnumerateLogonSessions(out sessionCount, out sessionPtr);
+ if (res != 0)
+ throw new Win32Exception((int)NativeMethods.LsaNtStatusToWinError(res), "LsaEnumerateLogonSession() failed");
+ using (sessionPtr)
+ {
+ IntPtr currentSession = sessionPtr.DangerousGetHandle();
+ for (UInt32 i = 0; i < sessionCount; i++)
+ {
+ SafeLsaMemoryBuffer sessionDataPtr;
+ res = NativeMethods.LsaGetLogonSessionData(currentSession, out sessionDataPtr);
+ if (res != 0)
+ {
+ currentSession = IntPtr.Add(currentSession, Marshal.SizeOf(typeof(NativeHelpers.LUID)));
+ continue;
+ }
+ using (sessionDataPtr)
+ {
+ NativeHelpers.SECURITY_LOGON_SESSION_DATA sessionData = (NativeHelpers.SECURITY_LOGON_SESSION_DATA)Marshal.PtrToStructure(
+ sessionDataPtr.DangerousGetHandle(), typeof(NativeHelpers.SECURITY_LOGON_SESSION_DATA));
+ UInt64 sessionId = (UInt64)sessionData.LogonId;
+ if (sessionId == tokenLuidId)
+ {
+ AuthenticationPackage = sessionData.AuthenticationPackage.ToString();
+ LogonType = sessionData.LogonType.ToString();
+ break;
+ }
+ }
+
+ currentSession = IntPtr.Add(currentSession, Marshal.SizeOf(typeof(NativeHelpers.LUID)));
+ }
+ }
+ }
+
+ private void SetTokenMandatoryLabel(SafeNativeHandle hToken)
+ {
+ NativeHelpers.TokenInformationClass tokenClass = NativeHelpers.TokenInformationClass.TokenIntegrityLevel;
+ UInt32 returnLength;
+ NativeMethods.GetTokenInformation(hToken, tokenClass, new SafeMemoryBuffer(IntPtr.Zero), 0, out returnLength);
+ using (SafeMemoryBuffer infoPtr = new SafeMemoryBuffer((int)returnLength))
+ {
+ if (!NativeMethods.GetTokenInformation(hToken, tokenClass, infoPtr, returnLength, out returnLength))
+ throw new Win32Exception("GetTokenInformation(TokenIntegrityLevel) failed");
+ NativeHelpers.TOKEN_MANDATORY_LABEL label = (NativeHelpers.TOKEN_MANDATORY_LABEL)Marshal.PtrToStructure(
+ infoPtr.DangerousGetHandle(), typeof(NativeHelpers.TOKEN_MANDATORY_LABEL));
+ MandatoryLabelName = LookupSidName(label.Label.Sid);
+ MandatoryLabelSid = new SecurityIdentifier(label.Label.Sid);
+ }
+ }
+
+ private void SetTokenSource(SafeNativeHandle hToken)
+ {
+ NativeHelpers.TokenInformationClass tokenClass = NativeHelpers.TokenInformationClass.TokenSource;
+ UInt32 returnLength;
+ NativeMethods.GetTokenInformation(hToken, tokenClass, new SafeMemoryBuffer(IntPtr.Zero), 0, out returnLength);
+ using (SafeMemoryBuffer infoPtr = new SafeMemoryBuffer((int)returnLength))
+ {
+ if (!NativeMethods.GetTokenInformation(hToken, tokenClass, infoPtr, returnLength, out returnLength))
+ throw new Win32Exception("GetTokenInformation(TokenSource) failed");
+ NativeHelpers.TOKEN_SOURCE source = (NativeHelpers.TOKEN_SOURCE)Marshal.PtrToStructure(
+ infoPtr.DangerousGetHandle(), typeof(NativeHelpers.TOKEN_SOURCE));
+ SourceName = new string(source.SourceName).Replace('\0', ' ').TrimEnd();
+ }
+ }
+
+ private void SetTokenUser(SafeNativeHandle hToken)
+ {
+ NativeHelpers.TokenInformationClass tokenClass = NativeHelpers.TokenInformationClass.TokenUser;
+ UInt32 returnLength;
+ NativeMethods.GetTokenInformation(hToken, tokenClass, new SafeMemoryBuffer(IntPtr.Zero), 0, out returnLength);
+ using (SafeMemoryBuffer infoPtr = new SafeMemoryBuffer((int)returnLength))
+ {
+ if (!NativeMethods.GetTokenInformation(hToken, tokenClass, infoPtr, returnLength, out returnLength))
+ throw new Win32Exception("GetTokenInformation(TokenSource) failed");
+ NativeHelpers.TOKEN_USER user = (NativeHelpers.TOKEN_USER)Marshal.PtrToStructure(
+ infoPtr.DangerousGetHandle(), typeof(NativeHelpers.TOKEN_USER));
+ UserName = LookupSidName(user.User.Sid);
+ UserSid = new SecurityIdentifier(user.User.Sid);
+ }
+ }
+
+ private void SetProfileLoaded()
+ {
+ UInt32 flags;
+ ProfileLoaded = NativeMethods.GetProfileType(out flags);
+ }
+
+ private static string LookupSidName(IntPtr pSid)
+ {
+ StringBuilder name = new StringBuilder(0);
+ StringBuilder domain = new StringBuilder(0);
+ UInt32 nameLength = 0;
+ UInt32 domainLength = 0;
+ UInt32 peUse;
+ NativeMethods.LookupAccountSid(null, pSid, name, ref nameLength, domain, ref domainLength, out peUse);
+ name.EnsureCapacity((int)nameLength);
+ domain.EnsureCapacity((int)domainLength);
+
+ if (!NativeMethods.LookupAccountSid(null, pSid, name, ref nameLength, domain, ref domainLength, out peUse))
+ throw new Win32Exception("LookupAccountSid() failed");
+
+ return String.Format("{0}\\{1}", domain.ToString(), name.ToString());
+ }
+ }
+}
+'@
+ $logon = New-Object -TypeName Ansible.Logon
+ ConvertTo-Json -InputObject $logon
+}.ToString()
+
+$current_user_raw = [Ansible.Process.ProcessUtil]::CreateProcess($null, "powershell.exe -NoProfile -", $null, $null, $test_whoami + "`r`n")
+$current_user = ConvertFrom-Json -InputObject $current_user_raw.StandardOut
+
+$adsi = [ADSI]"WinNT://$env:COMPUTERNAME"
+
+$standard_user = "become_standard"
+$admin_user = "become_admin"
+$become_pass = "password123!$([System.IO.Path]::GetRandomFileName())"
+$medium_integrity_sid = "S-1-16-8192"
+$high_integrity_sid = "S-1-16-12288"
+$system_integrity_sid = "S-1-16-16384"
+
+$tests = @{
+ "Runas standard user" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, $become_pass,
+ "powershell.exe -NoProfile -ExecutionPolicy ByPass -File $tmp_script")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Interactive"
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.UserSid.Value | Assert-Equal -Expected $standard_user_sid
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $medium_integrity_sid
+ }
+
+ "Runas admin user" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass,
+ "powershell.exe -NoProfile -ExecutionPolicy ByPass -File $tmp_script")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Interactive"
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.UserSid.Value | Assert-Equal -Expected $admin_user_sid
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $high_integrity_sid
+ }
+
+ "Runas SYSTEM" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("SYSTEM", $null,
+ "powershell.exe -NoProfile -ExecutionPolicy ByPass -File $tmp_script")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "System"
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.UserSid.Value | Assert-Equal -Expected "S-1-5-18"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $system_integrity_sid
+
+ $with_domain = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("NT AUTHORITY\System", $null, "whoami.exe")
+ $with_domain.StandardOut | Assert-Equal -Expected "nt authority\system`r`n"
+ }
+
+ "Runas LocalService" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("LocalService", $null,
+ "powershell.exe -NoProfile -ExecutionPolicy ByPass -File $tmp_script")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Service"
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.UserSid.Value | Assert-Equal -Expected "S-1-5-19"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $system_integrity_sid
+
+ $with_domain = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("NT AUTHORITY\LocalService", $null, "whoami.exe")
+ $with_domain.StandardOut | Assert-Equal -Expected "nt authority\local service`r`n"
+ }
+
+ "Runas NetworkService" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("NetworkService", $null,
+ "powershell.exe -NoProfile -ExecutionPolicy ByPass -File $tmp_script")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Service"
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.UserSid.Value | Assert-Equal -Expected "S-1-5-20"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $system_integrity_sid
+
+ $with_domain = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("NT AUTHORITY\NetworkService", $null, "whoami.exe")
+ $with_domain.StandardOut | Assert-Equal -Expected "nt authority\network service`r`n"
+ }
+
+ "Runas without working dir set" = {
+ $expected = "$env:SystemRoot\system32`r`n"
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, $become_pass, 0, "Interactive", $null,
+ 'powershell.exe $pwd.Path', $null, $null, "")
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "Runas with working dir set" = {
+ $expected = "$env:SystemRoot`r`n"
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, $become_pass, 0, "Interactive", $null,
+ 'powershell.exe $pwd.Path', $env:SystemRoot, $null, "")
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "Runas without environment set" = {
+ $expected = "Windows_NT`r`n"
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, $become_pass, 0, "Interactive", $null,
+ 'powershell.exe $env:TEST; $env:OS', $null, $null, "")
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "Runas with environment set" = {
+ $env_vars = @{
+ TEST = "tesTing"
+ TEST2 = "Testing 2"
+ }
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, 0, "Interactive", $null,
+ 'cmd.exe /c set', $null, $env_vars, "")
+ ("TEST=tesTing" -cin $actual.StandardOut.Split("`r`n")) | Assert-Equal -Expected $true
+ ("TEST2=Testing 2" -cin $actual.StandardOut.Split("`r`n")) | Assert-Equal -Expected $true
+ ("OS=Windows_NT" -cnotin $actual.StandardOut.Split("`r`n")) | Assert-Equal -Expected $true
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "Runas with string stdin" = {
+ $expected = "input value`r`n`r`n"
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, 0, "Interactive", $null,
+ 'powershell.exe [System.Console]::In.ReadToEnd()', $null, $null, "input value")
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "Runas with string stdin and newline" = {
+ $expected = "input value`r`n`r`n"
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, 0, "Interactive", $null,
+ 'powershell.exe [System.Console]::In.ReadToEnd()', $null, $null, "input value`r`n")
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "Runas with byte stdin" = {
+ $expected = "input value`r`n"
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, 0, "Interactive", $null,
+ 'powershell.exe [System.Console]::In.ReadToEnd()', $null, $null, [System.Text.Encoding]::UTF8.GetBytes("input value"))
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "Missing executable" = {
+ $failed = $false
+ try {
+ [Ansible.Become.BecomeUtil]::CreateProcessAsUser("SYSTEM", $null, "fake.exe")
+ }
+ catch {
+ $failed = $true
+ $_.Exception.InnerException.GetType().FullName | Assert-Equal -Expected "Ansible.Process.Win32Exception"
+ $expected = 'Exception calling "CreateProcessAsUser" with "3" argument(s): "CreateProcessWithTokenW() failed '
+ $expected += '(The system cannot find the file specified, Win32ErrorCode 2)"'
+ $_.Exception.Message | Assert-Equal -Expected $expected
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "CreateProcessAsUser with lpApplicationName" = {
+ $expected = "abc`r`n"
+ $full_path = "$($env:SystemRoot)\System32\WindowsPowerShell\v1.0\powershell.exe"
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("SYSTEM", $null, 0, "Interactive", $full_path,
+ "Write-Output 'abc'", $null, $null, "")
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("SYSTEM", $null, 0, "Interactive", $full_path,
+ "powershell.exe Write-Output 'abc'", $null, $null, "")
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcessAsUser with stderr" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("SYSTEM", $null, 0, "Interactive", $null,
+ "powershell.exe [System.Console]::Error.WriteLine('hi')", $null, $null, "")
+ $actual.StandardOut | Assert-Equal -Expected ""
+ $actual.StandardError | Assert-Equal -Expected "hi`r`n"
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcessAsUser with exit code" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("SYSTEM", $null, 0, "Interactive", $null,
+ "powershell.exe exit 10", $null, $null, "")
+ $actual.StandardOut | Assert-Equal -Expected ""
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 10
+ }
+
+ "Local account with computer name" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("$env:COMPUTERNAME\$standard_user", $become_pass,
+ "powershell.exe -NoProfile -ExecutionPolicy ByPass -File $tmp_script")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Interactive"
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.UserSid.Value | Assert-Equal -Expected $standard_user_sid
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $medium_integrity_sid
+ }
+
+ "Local account with computer as period" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser(".\$standard_user", $become_pass,
+ "powershell.exe -NoProfile -ExecutionPolicy ByPass -File $tmp_script")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Interactive"
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.UserSid.Value | Assert-Equal -Expected $standard_user_sid
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $medium_integrity_sid
+ }
+
+ "Local account with invalid password" = {
+ $failed = $false
+ try {
+ [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, "incorrect", "powershell.exe Write-Output abc")
+ }
+ catch {
+ $failed = $true
+ $_.Exception.InnerException.GetType().FullName | Assert-Equal -Expected "Ansible.AccessToken.Win32Exception"
+ # Server 2008 has a slightly different error msg, just assert we get the error 1326
+ ($_.Exception.Message.Contains("Win32ErrorCode 1326")) | Assert-Equal -Expected $true
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "Invalid account" = {
+ $failed = $false
+ try {
+ [Ansible.Become.BecomeUtil]::CreateProcessAsUser("incorrect", "incorrect", "powershell.exe Write-Output abc")
+ }
+ catch {
+ $failed = $true
+ $_.Exception.InnerException.GetType().FullName | Assert-Equal -Expected "System.Security.Principal.IdentityNotMappedException"
+ $expected = 'Exception calling "CreateProcessAsUser" with "3" argument(s): "Some or all '
+ $expected += 'identity references could not be translated."'
+ $_.Exception.Message | Assert-Equal -Expected $expected
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "Interactive logon with standard" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, $become_pass, "WithProfile",
+ "Interactive", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Interactive"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $medium_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.SourceName | Assert-Equal -Expected "Advapi"
+ $stdout.UserSid.Value | Assert-Equal -Expected $standard_user_sid
+ }
+
+ "Batch logon with standard" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, $become_pass, "WithProfile",
+ "Batch", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Batch"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $medium_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.SourceName | Assert-Equal -Expected "Advapi"
+ $stdout.UserSid.Value | Assert-Equal -Expected $standard_user_sid
+ }
+
+ "Network logon with standard" = {
+ # Server 2008 will not work with become to Network or Network Credentials
+ if ([System.Environment]::OSVersion.Version -lt [Version]"6.1") {
+ continue
+ }
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, $become_pass, "WithProfile",
+ "Network", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Network"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $medium_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.SourceName | Assert-Equal -Expected "Advapi"
+ $stdout.UserSid.Value | Assert-Equal -Expected $standard_user_sid
+ }
+
+ "Network with cleartext logon with standard" = {
+ # Server 2008 will not work with become to Network or Network Cleartext
+ if ([System.Environment]::OSVersion.Version -lt [Version]"6.1") {
+ continue
+ }
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, $become_pass, "WithProfile",
+ "NetworkCleartext", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "NetworkCleartext"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $medium_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.SourceName | Assert-Equal -Expected "Advapi"
+ $stdout.UserSid.Value | Assert-Equal -Expected $standard_user_sid
+ }
+
+ "Logon without password with standard" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, [NullString]::Value, "WithProfile",
+ "Interactive", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ # Too unstable, there might be another process still lingering which causes become to steal instead of using
+ # S4U. Just don't check the type and source to verify we can become without a password
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ # $stdout.LogonType | Assert-Equal -Expected "Batch"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $medium_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ # $stdout.SourceName | Assert-Equal -Expected "ansible"
+ $stdout.UserSid.Value | Assert-Equal -Expected $standard_user_sid
+ }
+
+ "Logon without password and network type with standard" = {
+ # Server 2008 will not work with become to Network or Network Cleartext
+ if ([System.Environment]::OSVersion.Version -lt [Version]"6.1") {
+ continue
+ }
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, [NullString]::Value, "WithProfile",
+ "Network", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ # Too unstable, there might be another process still lingering which causes become to steal instead of using
+ # S4U. Just don't check the type and source to verify we can become without a password
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ # $stdout.LogonType | Assert-Equal -Expected "Network"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $medium_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ # $stdout.SourceName | Assert-Equal -Expected "ansible"
+ $stdout.UserSid.Value | Assert-Equal -Expected $standard_user_sid
+ }
+
+ "Interactive logon with admin" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, "WithProfile",
+ "Interactive", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Interactive"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $high_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.SourceName | Assert-Equal -Expected "Advapi"
+ $stdout.UserSid.Value | Assert-Equal -Expected $admin_user_sid
+ }
+
+ "Batch logon with admin" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, "WithProfile",
+ "Batch", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Batch"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $high_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.SourceName | Assert-Equal -Expected "Advapi"
+ $stdout.UserSid.Value | Assert-Equal -Expected $admin_user_sid
+ }
+
+ "Network logon with admin" = {
+ # Server 2008 will not work with become to Network or Network Credentials
+ if ([System.Environment]::OSVersion.Version -lt [Version]"6.1") {
+ continue
+ }
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, "WithProfile",
+ "Network", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Network"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $high_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.SourceName | Assert-Equal -Expected "Advapi"
+ $stdout.UserSid.Value | Assert-Equal -Expected $admin_user_sid
+ }
+
+ "Network with cleartext logon with admin" = {
+ # Server 2008 will not work with become to Network or Network Credentials
+ if ([System.Environment]::OSVersion.Version -lt [Version]"6.1") {
+ continue
+ }
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, "WithProfile",
+ "NetworkCleartext", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "NetworkCleartext"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $high_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.SourceName | Assert-Equal -Expected "Advapi"
+ $stdout.UserSid.Value | Assert-Equal -Expected $admin_user_sid
+ }
+
+ "Fail to logon with null or empty password" = {
+ $failed = $false
+ try {
+ # Having $null or an empty string means we are trying to become a user with a blank password and not
+ # become without setting the password. This is confusing as $null gets converted to "" and we need to
+ # use [NullString]::Value instead if we want that behaviour. This just tests to see that an empty
+ # string won't go the S4U route.
+ [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $null, "WithProfile",
+ "Interactive", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ }
+ catch {
+ $failed = $true
+ $_.Exception.InnerException.GetType().FullName | Assert-Equal -Expected "Ansible.AccessToken.Win32Exception"
+ # Server 2008 has a slightly different error msg, just assert we get the error 1326
+ ($_.Exception.Message.Contains("Win32ErrorCode 1326")) | Assert-Equal -Expected $true
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "Logon without password with admin" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, [NullString]::Value, "WithProfile",
+ "Interactive", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ # Too unstable, there might be another process still lingering which causes become to steal instead of using
+ # S4U. Just don't check the type and source to verify we can become without a password
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ # $stdout.LogonType | Assert-Equal -Expected "Batch"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $high_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ # $stdout.SourceName | Assert-Equal -Expected "ansible"
+ $stdout.UserSid.Value | Assert-Equal -Expected $admin_user_sid
+ }
+
+ "Logon without password and network type with admin" = {
+ # become network doesn't work on Server 2008
+ if ([System.Environment]::OSVersion.Version -lt [Version]"6.1") {
+ continue
+ }
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, [NullString]::Value, "WithProfile",
+ "Network", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ # Too unstable, there might be another process still lingering which causes become to steal instead of using
+ # S4U. Just don't check the type and source to verify we can become without a password
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ # $stdout.LogonType | Assert-Equal -Expected "Network"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $high_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ # $stdout.SourceName | Assert-Equal -Expected "ansible"
+ $stdout.UserSid.Value | Assert-Equal -Expected $admin_user_sid
+ }
+
+ "Logon without profile with admin" = {
+ # Server 2008 and 2008 R2 does not support running without the profile being set
+ if ([System.Environment]::OSVersion.Version -lt [Version]"6.2") {
+ continue
+ }
+
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, 0,
+ "Interactive", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Interactive"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $high_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $false
+ $stdout.SourceName | Assert-Equal -Expected "Advapi"
+ $stdout.UserSid.Value | Assert-Equal -Expected $admin_user_sid
+ }
+
+ "Logon with network credentials and no profile" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("fakeuser", "fakepassword", "NetcredentialsOnly",
+ "NewCredentials", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "NewCredentials"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $current_user.MandatoryLabelSid.Value
+
+ # while we didn't set WithProfile, the new process is based on the current process
+ $stdout.ProfileLoaded | Assert-Equal -Expected $current_user.ProfileLoaded
+ $stdout.SourceName | Assert-Equal -Expected "Advapi"
+ $stdout.UserSid.Value | Assert-Equal -Expected $current_user.UserSid.Value
+ }
+
+ "Logon with network credentials and with profile" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("fakeuser", "fakepassword", "NetcredentialsOnly, WithProfile",
+ "NewCredentials", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "NewCredentials"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $current_user.MandatoryLabelSid.Value
+ $stdout.ProfileLoaded | Assert-Equal -Expected $current_user.ProfileLoaded
+ $stdout.SourceName | Assert-Equal -Expected "Advapi"
+ $stdout.UserSid.Value | Assert-Equal -Expected $current_user.UserSid.Value
+ }
+}
+
+try {
+ $tmp_dir = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ([System.IO.Path]::GetRandomFileName())
+ New-Item -Path $tmp_dir -ItemType Directory > $null
+ $acl = Get-Acl -Path $tmp_dir
+ $ace = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList @(
+ New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList ([System.Security.Principal.WellKnownSidType]::WorldSid, $null)
+ [System.Security.AccessControl.FileSystemRights]::FullControl,
+ [System.Security.AccessControl.InheritanceFlags]"ContainerInherit, ObjectInherit",
+ [System.Security.AccessControl.PropagationFlags]::None,
+ [System.Security.AccessControl.AccessControlType]::Allow
+ )
+ $acl.AddAccessRule($ace)
+ Set-Acl -Path $tmp_dir -AclObject $acl
+
+ $tmp_script = Join-Path -Path $tmp_dir -ChildPath "whoami.ps1"
+ Set-Content -LiteralPath $tmp_script -Value $test_whoami
+
+ foreach ($user in $standard_user, $admin_user) {
+ $user_obj = $adsi.Children | Where-Object { $_.SchemaClassName -eq "User" -and $_.Name -eq $user }
+ if ($null -eq $user_obj) {
+ $user_obj = $adsi.Create("User", $user)
+ $user_obj.SetPassword($become_pass)
+ $user_obj.SetInfo()
+ }
+ else {
+ $user_obj.SetPassword($become_pass)
+ }
+ $user_obj.RefreshCache()
+
+ if ($user -eq $standard_user) {
+ $standard_user_sid = (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList @($user_obj.ObjectSid.Value, 0)).Value
+ $group = [System.Security.Principal.WellKnownSidType]::BuiltinUsersSid
+ }
+ else {
+ $admin_user_sid = (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList @($user_obj.ObjectSid.Value, 0)).Value
+ $group = [System.Security.Principal.WellKnownSidType]::BuiltinAdministratorsSid
+ }
+ $group = (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList $group, $null).Value
+ [string[]]$current_groups = $user_obj.Groups() | ForEach-Object {
+ New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList @(
+ $_.GetType().InvokeMember("objectSID", "GetProperty", $null, $_, $null),
+ 0
+ )
+ }
+ if ($current_groups -notcontains $group) {
+ $group_obj = $adsi.Children | Where-Object {
+ if ($_.SchemaClassName -eq "Group") {
+ $group_sid = New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList @($_.objectSID.Value, 0)
+ $group_sid -eq $group
+ }
+ }
+ $group_obj.Add($user_obj.Path)
+ }
+ }
+ foreach ($test_impl in $tests.GetEnumerator()) {
+ $test = $test_impl.Key
+ &$test_impl.Value
+ }
+}
+finally {
+ Remove-Item -LiteralPath $tmp_dir -Force -Recurse
+ foreach ($user in $standard_user, $admin_user) {
+ $user_obj = $adsi.Children | Where-Object { $_.SchemaClassName -eq "User" -and $_.Name -eq $user }
+ $adsi.Delete("User", $user_obj.Name.Value)
+ }
+}
+
+
+$module.Result.data = "success"
+$module.ExitJson()
+
diff --git a/test/integration/targets/module_utils_Ansible.Become/tasks/main.yml b/test/integration/targets/module_utils_Ansible.Become/tasks/main.yml
new file mode 100644
index 0000000..deb228b
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Become/tasks/main.yml
@@ -0,0 +1,28 @@
+---
+# Users by default don't have this right, temporarily enable it
+- name: ensure the Users group have the SeBatchLogonRight
+ win_user_right:
+ name: SeBatchLogonRight
+ users:
+ - Users
+ action: add
+ register: batch_user_add
+
+- block:
+ - name: test Ansible.Become.cs
+ ansible_become_tests:
+ register: ansible_become_tests
+
+ always:
+ - name: remove SeBatchLogonRight from users if added in test
+ win_user_right:
+ name: SeBatchLogonRight
+ users:
+ - Users
+ action: remove
+ when: batch_user_add is changed
+
+- name: assert test Ansible.Become.cs
+ assert:
+ that:
+ - ansible_become_tests.data == "success"
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/aliases b/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/library/add_type_test.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/library/add_type_test.ps1
new file mode 100644
index 0000000..d18c42d
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/library/add_type_test.ps1
@@ -0,0 +1,332 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.AddType
+
+$ErrorActionPreference = "Stop"
+
+$result = @{
+ changed = $false
+}
+
+Function Assert-Equal($actual, $expected) {
+ if ($actual -cne $expected) {
+ $call_stack = (Get-PSCallStack)[1]
+ $error_msg = -join @(
+ "AssertionError:`r`nActual: `"$actual`" != Expected: `"$expected`"`r`nLine: "
+ "$($call_stack.ScriptLineNumber), Method: $($call_stack.Position.Text)"
+ )
+ Fail-Json -obj $result -message $error_msg
+ }
+}
+
+$code = @'
+using System;
+
+namespace Namespace1
+{
+ public class Class1
+ {
+ public static string GetString(bool error)
+ {
+ if (error)
+ throw new Exception("error");
+ return "Hello World";
+ }
+ }
+}
+'@
+$res = Add-CSharpType -References $code
+Assert-Equal -actual $res -expected $null
+
+$actual = [Namespace1.Class1]::GetString($false)
+Assert-Equal $actual -expected "Hello World"
+
+try {
+ [Namespace1.Class1]::GetString($true)
+}
+catch {
+ Assert-Equal ($_.Exception.ToString().Contains("at Namespace1.Class1.GetString(Boolean error)`r`n")) -expected $true
+}
+
+$code_debug = @'
+using System;
+
+namespace Namespace2
+{
+ public class Class2
+ {
+ public static string GetString(bool error)
+ {
+ if (error)
+ throw new Exception("error");
+ return "Hello World";
+ }
+ }
+}
+'@
+$res = Add-CSharpType -References $code_debug -IncludeDebugInfo
+Assert-Equal -actual $res -expected $null
+
+$actual = [Namespace2.Class2]::GetString($false)
+Assert-Equal $actual -expected "Hello World"
+
+try {
+ [Namespace2.Class2]::GetString($true)
+}
+catch {
+ $tmp_path = [System.IO.Path]::GetFullPath($env:TMP).ToLower()
+ Assert-Equal ($_.Exception.ToString().ToLower().Contains("at namespace2.class2.getstring(boolean error) in $tmp_path")) -expected $true
+ Assert-Equal ($_.Exception.ToString().Contains(".cs:line 10")) -expected $true
+}
+
+$code_tmp = @'
+using System;
+
+namespace Namespace3
+{
+ public class Class3
+ {
+ public static string GetString(bool error)
+ {
+ if (error)
+ throw new Exception("error");
+ return "Hello World";
+ }
+ }
+}
+'@
+$tmp_path = $env:USERPROFILE
+$res = Add-CSharpType -References $code_tmp -IncludeDebugInfo -TempPath $tmp_path -PassThru
+Assert-Equal -actual $res.GetType().Name -expected "RuntimeAssembly"
+Assert-Equal -actual $res.Location -expected ""
+Assert-Equal -actual $res.GetTypes().Length -expected 1
+Assert-Equal -actual $res.GetTypes()[0].Name -expected "Class3"
+
+$actual = [Namespace3.Class3]::GetString($false)
+Assert-Equal $actual -expected "Hello World"
+
+try {
+ [Namespace3.Class3]::GetString($true)
+}
+catch {
+ $actual = $_.Exception.ToString().ToLower().Contains("at namespace3.class3.getstring(boolean error) in $($tmp_path.ToLower())")
+ Assert-Equal $actual -expected $true
+ Assert-Equal ($_.Exception.ToString().Contains(".cs:line 10")) -expected $true
+}
+
+$warning_code = @'
+using System;
+
+namespace Namespace4
+{
+ public class Class4
+ {
+ public static string GetString(bool test)
+ {
+ if (test)
+ {
+ string a = "";
+ }
+
+ return "Hello World";
+ }
+ }
+}
+'@
+$failed = $false
+try {
+ Add-CSharpType -References $warning_code
+}
+catch {
+ $failed = $true
+ $actual = $_.Exception.Message.Contains("error CS0219: Warning as Error: The variable 'a' is assigned but its value is never used")
+ Assert-Equal -actual $actual -expected $true
+}
+Assert-Equal -actual $failed -expected $true
+
+Add-CSharpType -References $warning_code -IgnoreWarnings
+$actual = [Namespace4.Class4]::GetString($true)
+Assert-Equal -actual $actual -expected "Hello World"
+
+$reference_1 = @'
+using System;
+using System.Web.Script.Serialization;
+
+//AssemblyReference -Name System.Web.Extensions.dll
+
+namespace Namespace5
+{
+ public class Class5
+ {
+ public static string GetString()
+ {
+ return "Hello World";
+ }
+ }
+}
+'@
+
+$reference_2 = @'
+using System;
+using Namespace5;
+using System.Management.Automation;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Namespace6
+{
+ public class Class6
+ {
+ public static string GetString()
+ {
+ Hashtable hash = new Hashtable();
+ hash["test"] = "abc";
+ return Class5.GetString();
+ }
+ }
+}
+'@
+
+Add-CSharpType -References $reference_1, $reference_2
+$actual = [Namespace6.Class6]::GetString()
+Assert-Equal -actual $actual -expected "Hello World"
+
+$ignored_warning = @'
+using System;
+
+//NoWarn -Name CS0219
+
+namespace Namespace7
+{
+ public class Class7
+ {
+ public static string GetString()
+ {
+ string a = "";
+ return "abc";
+ }
+ }
+}
+'@
+Add-CSharpType -References $ignored_warning
+$actual = [Namespace7.Class7]::GetString()
+Assert-Equal -actual $actual -expected "abc"
+
+$defined_symbol = @'
+using System;
+
+namespace Namespace8
+{
+ public class Class8
+ {
+ public static string GetString()
+ {
+#if SYMBOL1
+ string a = "symbol";
+#else
+ string a = "no symbol";
+#endif
+ return a;
+ }
+ }
+}
+'@
+Add-CSharpType -References $defined_symbol -CompileSymbols "SYMBOL1"
+$actual = [Namespace8.Class8]::GetString()
+Assert-Equal -actual $actual -expected "symbol"
+
+$type_accelerator = @'
+using System;
+
+//TypeAccelerator -Name AnsibleType -TypeName Class9
+
+namespace Namespace9
+{
+ public class Class9
+ {
+ public static string GetString()
+ {
+ return "a";
+ }
+ }
+}
+'@
+Add-CSharpType -Reference $type_accelerator
+$actual = [AnsibleType]::GetString()
+Assert-Equal -actual $actual -expected "a"
+
+$missing_type_class = @'
+using System;
+
+//TypeAccelerator -Name AnsibleTypeMissing -TypeName MissingClass
+
+namespace Namespace10
+{
+ public class Class10
+ {
+ public static string GetString()
+ {
+ return "b";
+ }
+ }
+}
+'@
+$failed = $false
+try {
+ Add-CSharpType -Reference $missing_type_class
+}
+catch {
+ $failed = $true
+ Assert-Equal -actual $_.Exception.Message -expected "Failed to find compiled class 'MissingClass' for custom TypeAccelerator."
+}
+Assert-Equal -actual $failed -expected $true
+
+$arch_class = @'
+using System;
+
+namespace Namespace11
+{
+ public class Class11
+ {
+ public static int GetIntPtrSize()
+ {
+#if X86
+ return 4;
+#elif AMD64
+ return 8;
+#else
+ return 0;
+#endif
+ }
+ }
+}
+'@
+Add-CSharpType -Reference $arch_class
+Assert-Equal -actual ([Namespace11.Class11]::GetIntPtrSize()) -expected ([System.IntPtr]::Size)
+
+$lib_set = @'
+using System;
+
+namespace Namespace12
+{
+ public class Class12
+ {
+ public static string GetString()
+ {
+ return "b";
+ }
+ }
+}
+'@
+$env:LIB = "C:\fake\folder\path"
+try {
+ Add-CSharpType -Reference $lib_set
+}
+finally {
+ Remove-Item -LiteralPath env:\LIB
+}
+Assert-Equal -actual ([Namespace12.Class12]::GetString()) -expected "b"
+
+$result.res = "success"
+Exit-Json -obj $result
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/tasks/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/tasks/main.yml
new file mode 100644
index 0000000..4c4810b
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/tasks/main.yml
@@ -0,0 +1,10 @@
+---
+- name: call module with AddType tests
+ add_type_test:
+ register: add_type_test
+
+- name: assert call module with AddType tests
+ assert:
+ that:
+ - not add_type_test is failed
+ - add_type_test.res == 'success'
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/aliases b/test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/library/argv_parser_test.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/library/argv_parser_test.ps1
new file mode 100644
index 0000000..d7bd4bb
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/library/argv_parser_test.ps1
@@ -0,0 +1,93 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.ArgvParser
+
+$ErrorActionPreference = 'Continue'
+
+$params = Parse-Args $args
+$exe = Get-AnsibleParam -obj $params -name "exe" -type "path" -failifempty $true
+
+Add-Type -TypeDefinition @'
+using System.IO;
+using System.Threading;
+
+namespace Ansible.Command
+{
+ public static class NativeUtil
+ {
+ public static void GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, out string stdout, out string stderr)
+ {
+ var sowait = new EventWaitHandle(false, EventResetMode.ManualReset);
+ var sewait = new EventWaitHandle(false, EventResetMode.ManualReset);
+ string so = null, se = null;
+ ThreadPool.QueueUserWorkItem((s)=>
+ {
+ so = stdoutStream.ReadToEnd();
+ sowait.Set();
+ });
+ ThreadPool.QueueUserWorkItem((s) =>
+ {
+ se = stderrStream.ReadToEnd();
+ sewait.Set();
+ });
+ foreach(var wh in new WaitHandle[] { sowait, sewait })
+ wh.WaitOne();
+ stdout = so;
+ stderr = se;
+ }
+ }
+}
+'@
+
+Function Invoke-Process($executable, $arguments) {
+ $proc = New-Object System.Diagnostics.Process
+ $psi = $proc.StartInfo
+ $psi.FileName = $executable
+ $psi.Arguments = $arguments
+ $psi.RedirectStandardOutput = $true
+ $psi.RedirectStandardError = $true
+ $psi.UseShellExecute = $false
+
+ $proc.Start() > $null # will always return $true for non shell-exec cases
+ $stdout = $stderr = [string] $null
+
+ [Ansible.Command.NativeUtil]::GetProcessOutput($proc.StandardOutput, $proc.StandardError, [ref] $stdout, [ref] $stderr) > $null
+ $proc.WaitForExit() > $null
+ $actual_args = $stdout.Substring(0, $stdout.Length - 2) -split "`r`n"
+
+ return $actual_args
+}
+
+$tests = @(
+ @('abc', 'd', 'e'),
+ @('a\\b', 'de fg', 'h'),
+ @('a\"b', 'c', 'd'),
+ @('a\\b c', 'd', 'e'),
+ @('C:\Program Files\file\', 'arg with " quote'),
+ @('ADDLOCAL="a,b,c"', '/s', 'C:\\Double\\Backslash')
+)
+
+foreach ($expected in $tests) {
+ $joined_string = Argv-ToString -arguments $expected
+ # We can't used CommandLineToArgvW to test this out as it seems to mangle
+ # \, might be something to do with unicode but not sure...
+ $actual = Invoke-Process -executable $exe -arguments $joined_string
+
+ if ($expected.Count -ne $actual.Count) {
+ $result.actual = $actual -join "`n"
+ $result.expected = $expected -join "`n"
+ Fail-Json -obj $result -message "Actual arg count: $($actual.Count) != Expected arg count: $($expected.Count)"
+ }
+ for ($i = 0; $i -lt $expected.Count; $i++) {
+ $expected_arg = $expected[$i]
+ $actual_arg = $actual[$i]
+ if ($expected_arg -cne $actual_arg) {
+ $result.actual = $actual -join "`n"
+ $result.expected = $expected -join "`n"
+ Fail-Json -obj $result -message "Actual arg: '$actual_arg' != Expected arg: '$expected_arg'"
+ }
+ }
+}
+
+Exit-Json @{ data = 'success' }
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/meta/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/meta/main.yml
new file mode 100644
index 0000000..fd0dc54
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/meta/main.yml
@@ -0,0 +1,3 @@
+---
+dependencies:
+- setup_win_printargv
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/tasks/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/tasks/main.yml
new file mode 100644
index 0000000..b39155e
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/tasks/main.yml
@@ -0,0 +1,9 @@
+---
+- name: call module with ArgvParser tests
+ argv_parser_test:
+ exe: '{{ win_printargv_path }}'
+ register: argv_test
+
+- assert:
+ that:
+ - argv_test.data == 'success'
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/aliases b/test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/library/backup_file_test.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/library/backup_file_test.ps1
new file mode 100644
index 0000000..39beab7
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/library/backup_file_test.ps1
@@ -0,0 +1,92 @@
+#!powershell
+
+# Copyright: (c) 2019, Dag Wieers (@dagwieers) <dag@wieers.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#Requires -Module Ansible.ModuleUtils.Backup
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
+
+Function Assert-Equal {
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)][AllowNull()]$Actual,
+ [Parameter(Mandatory = $true, Position = 0)][AllowNull()]$Expected
+ )
+
+ process {
+ $matched = $false
+ if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array]) {
+ $Actual.Count | Assert-Equal -Expected $Expected.Count
+ for ($i = 0; $i -lt $Actual.Count; $i++) {
+ $actual_value = $Actual[$i]
+ $expected_value = $Expected[$i]
+ Assert-Equal -Actual $actual_value -Expected $expected_value
+ }
+ $matched = $true
+ }
+ else {
+ $matched = $Actual -ceq $Expected
+ }
+
+ if (-not $matched) {
+ if ($Actual -is [PSObject]) {
+ $Actual = $Actual.ToString()
+ }
+
+ $call_stack = (Get-PSCallStack)[1]
+ $module.Result.test = $test
+ $module.Result.actual = $Actual
+ $module.Result.expected = $Expected
+ $module.Result.line = $call_stack.ScriptLineNumber
+ $module.Result.method = $call_stack.Position.Text
+ $module.FailJson("AssertionError: actual != expected")
+ }
+ }
+}
+
+$tmp_dir = $module.Tmpdir
+
+$tests = @{
+ "Test backup file with missing file" = {
+ $actual = Backup-File -path (Join-Path -Path $tmp_dir -ChildPath "missing")
+ $actual | Assert-Equal -Expected $null
+ }
+
+ "Test backup file in check mode" = {
+ $orig_file = Join-Path -Path $tmp_dir -ChildPath "file-check.txt"
+ Set-Content -LiteralPath $orig_file -Value "abc"
+ $actual = Backup-File -path $orig_file -WhatIf
+
+ (Test-Path -LiteralPath $actual) | Assert-Equal -Expected $false
+
+ $parent_dir = Split-Path -LiteralPath $actual
+ $backup_file = Split-Path -Path $actual -Leaf
+ $parent_dir | Assert-Equal -Expected $tmp_dir
+ ($backup_file -match "^file-check\.txt\.$pid\.\d{8}-\d{6}\.bak$") | Assert-Equal -Expected $true
+ }
+
+ "Test backup file" = {
+ $content = "abc"
+ $orig_file = Join-Path -Path $tmp_dir -ChildPath "file.txt"
+ Set-Content -LiteralPath $orig_file -Value $content
+ $actual = Backup-File -path $orig_file
+
+ (Test-Path -LiteralPath $actual) | Assert-Equal -Expected $true
+
+ $parent_dir = Split-Path -LiteralPath $actual
+ $backup_file = Split-Path -Path $actual -Leaf
+ $parent_dir | Assert-Equal -Expected $tmp_dir
+ ($backup_file -match "^file\.txt\.$pid\.\d{8}-\d{6}\.bak$") | Assert-Equal -Expected $true
+ (Get-Content -LiteralPath $actual -Raw) | Assert-Equal -Expected "$content`r`n"
+ }
+}
+
+foreach ($test_impl in $tests.GetEnumerator()) {
+ $test = $test_impl.Key
+ &$test_impl.Value
+}
+
+$module.Result.res = 'success'
+
+$module.ExitJson()
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/tasks/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/tasks/main.yml
new file mode 100644
index 0000000..cb979eb
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/tasks/main.yml
@@ -0,0 +1,10 @@
+---
+- name: call module with BackupFile tests
+ backup_file_test:
+ register: backup_file_test
+
+- name: assert call module with BackupFile tests
+ assert:
+ that:
+ - not backup_file_test is failed
+ - backup_file_test.res == 'success'
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/aliases b/test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/library/camel_conversion_test.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/library/camel_conversion_test.ps1
new file mode 100644
index 0000000..bcb9558
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/library/camel_conversion_test.ps1
@@ -0,0 +1,81 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.CamelConversion
+
+$ErrorActionPreference = 'Stop'
+
+Function Assert-Equal($actual, $expected) {
+ if ($actual -cne $expected) {
+ Fail-Json @{} "actual != expected`nActual: $actual`nExpected: $expected"
+ }
+}
+
+$input_dict = @{
+ alllower = 'alllower'
+ ALLUPPER = 'allupper'
+ camelCase = 'camel_case'
+ mixedCase_withCamel = 'mixed_case_with_camel'
+ TwoWords = 'two_words'
+ AllUpperAtEND = 'all_upper_at_end'
+ AllUpperButPLURALs = 'all_upper_but_plurals'
+ TargetGroupARNs = 'target_group_arns'
+ HTTPEndpoints = 'http_endpoints'
+ PLURALs = 'plurals'
+ listDict = @(
+ @{ entry1 = 'entry1'; entryTwo = 'entry_two' },
+ 'stringTwo',
+ 0
+ )
+ INNERHashTable = @{
+ ID = 'id'
+ IEnumerable = 'i_enumerable'
+ }
+ emptyList = @()
+ singleList = @("a")
+}
+
+$output_dict = Convert-DictToSnakeCase -dict $input_dict
+foreach ($entry in $output_dict.GetEnumerator()) {
+ $key = $entry.Name
+ $value = $entry.Value
+
+ if ($value -is [Hashtable]) {
+ Assert-Equal -actual $key -expected "inner_hash_table"
+ foreach ($inner_hash in $value.GetEnumerator()) {
+ Assert-Equal -actual $inner_hash.Name -expected $inner_hash.Value
+ }
+ }
+ elseif ($value -is [Array] -or $value -is [System.Collections.ArrayList]) {
+ if ($key -eq "list_dict") {
+ foreach ($inner_list in $value) {
+ if ($inner_list -is [Hashtable]) {
+ foreach ($inner_list_hash in $inner_list.GetEnumerator()) {
+ Assert-Equal -actual $inner_list_hash.Name -expected $inner_list_hash.Value
+ }
+ }
+ elseif ($inner_list -is [String]) {
+ # this is not a string key so we need to keep it the same
+ Assert-Equal -actual $inner_list -expected "stringTwo"
+ }
+ else {
+ Assert-Equal -actual $inner_list -expected 0
+ }
+ }
+ }
+ elseif ($key -eq "empty_list") {
+ Assert-Equal -actual $value.Count -expected 0
+ }
+ elseif ($key -eq "single_list") {
+ Assert-Equal -actual $value.Count -expected 1
+ }
+ else {
+ Fail-Json -obj $result -message "invalid key found for list $key"
+ }
+ }
+ else {
+ Assert-Equal -actual $key -expected $value
+ }
+}
+
+Exit-Json @{ data = 'success' }
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/tasks/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/tasks/main.yml
new file mode 100644
index 0000000..f28ea30
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/tasks/main.yml
@@ -0,0 +1,8 @@
+---
+- name: call module with camel conversion tests
+ camel_conversion_test:
+ register: camel_conversion
+
+- assert:
+ that:
+ - camel_conversion.data == 'success'
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/aliases b/test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/library/command_util_test.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/library/command_util_test.ps1
new file mode 100644
index 0000000..ebffae7
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/library/command_util_test.ps1
@@ -0,0 +1,139 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.CommandUtil
+
+$ErrorActionPreference = 'Stop'
+
+$params = Parse-Args $args
+$exe = Get-AnsibleParam -obj $params -name "exe" -type "path" -failifempty $true
+
+$result = @{
+ changed = $false
+}
+
+$exe_directory = Split-Path -Path $exe -Parent
+$exe_filename = Split-Path -Path $exe -Leaf
+$test_name = $null
+
+Function Assert-Equal($actual, $expected) {
+ if ($actual -cne $expected) {
+ Fail-Json -obj $result -message "Test $test_name failed`nActual: '$actual' != Expected: '$expected'"
+ }
+}
+
+$test_name = "full exe path"
+$actual = Run-Command -command "`"$exe`" arg1 arg2 `"arg 3`""
+Assert-Equal -actual $actual.rc -expected 0
+Assert-Equal -actual $actual.stdout -expected "arg1`r`narg2`r`narg 3`r`n"
+Assert-Equal -actual $actual.stderr -expected ""
+Assert-Equal -actual $actual.executable -expected $exe
+
+$test_name = "exe in special char dir"
+$tmp_dir = Join-Path -Path $env:TEMP -ChildPath "ansible .ÅÑŚÌβÅÈ [$!@^&test(;)]"
+try {
+ New-Item -Path $tmp_dir -ItemType Directory > $null
+ $exe_special = Join-Path $tmp_dir -ChildPath "PrintArgv.exe"
+ Copy-Item -LiteralPath $exe -Destination $exe_special
+ $actual = Run-Command -command "`"$exe_special`" arg1 arg2 `"arg 3`""
+}
+finally {
+ Remove-Item -LiteralPath $tmp_dir -Force -Recurse
+}
+Assert-Equal -actual $actual.rc -expected 0
+Assert-Equal -actual $actual.stdout -expected "arg1`r`narg2`r`narg 3`r`n"
+Assert-Equal -actual $actual.stderr -expected ""
+Assert-Equal -actual $actual.executable -expected $exe_special
+
+$test_name = "invalid exe path"
+try {
+ $actual = Run-Command -command "C:\fakepath\$exe_filename arg1"
+ Fail-Json -obj $result -message "Test $test_name failed`nCommand should have thrown an exception"
+}
+catch {
+ $expected = "Exception calling `"SearchPath`" with `"1`" argument(s): `"Could not find file 'C:\fakepath\$exe_filename'.`""
+ Assert-Equal -actual $_.Exception.Message -expected $expected
+}
+
+$test_name = "exe in current folder"
+$actual = Run-Command -command "$exe_filename arg1" -working_directory $exe_directory
+Assert-Equal -actual $actual.rc -expected 0
+Assert-Equal -actual $actual.stdout -expected "arg1`r`n"
+Assert-Equal -actual $actual.stderr -expected ""
+Assert-Equal -actual $actual.executable -expected $exe
+
+$test_name = "no working directory set"
+$actual = Run-Command -command "cmd.exe /c cd"
+Assert-Equal -actual $actual.rc -expected 0
+Assert-Equal -actual $actual.stdout -expected "$($pwd.Path)`r`n"
+Assert-Equal -actual $actual.stderr -expected ""
+Assert-Equal -actual $actual.executable.ToUpper() -expected "$env:SystemRoot\System32\cmd.exe".ToUpper()
+
+$test_name = "working directory override"
+$actual = Run-Command -command "cmd.exe /c cd" -working_directory $env:SystemRoot
+Assert-Equal -actual $actual.rc -expected 0
+Assert-Equal -actual $actual.stdout -expected "$env:SystemRoot`r`n"
+Assert-Equal -actual $actual.stderr -expected ""
+Assert-Equal -actual $actual.executable.ToUpper() -expected "$env:SystemRoot\System32\cmd.exe".ToUpper()
+
+$test_name = "working directory invalid path"
+try {
+ $actual = Run-Command -command "doesn't matter" -working_directory "invalid path here"
+ Fail-Json -obj $result -message "Test $test_name failed`nCommand should have thrown an exception"
+}
+catch {
+ Assert-Equal -actual $_.Exception.Message -expected "invalid working directory path 'invalid path here'"
+}
+
+$test_name = "invalid arguments"
+$actual = Run-Command -command "ipconfig.exe /asdf"
+Assert-Equal -actual $actual.rc -expected 1
+
+$test_name = "test stdout and stderr streams"
+$actual = Run-Command -command "cmd.exe /c echo stdout && echo stderr 1>&2"
+Assert-Equal -actual $actual.rc -expected 0
+Assert-Equal -actual $actual.stdout -expected "stdout `r`n"
+Assert-Equal -actual $actual.stderr -expected "stderr `r`n"
+
+$test_name = "Test UTF8 output from stdout stream"
+$actual = Run-Command -command "powershell.exe -ExecutionPolicy ByPass -Command `"Write-Host '💩'`""
+Assert-Equal -actual $actual.rc -expected 0
+Assert-Equal -actual $actual.stdout -expected "💩`n"
+Assert-Equal -actual $actual.stderr -expected ""
+
+$test_name = "test default environment variable"
+Set-Item -LiteralPath env:TESTENV -Value "test"
+$actual = Run-Command -command "cmd.exe /c set"
+$env_present = $actual.stdout -split "`r`n" | Where-Object { $_ -eq "TESTENV=test" }
+if ($null -eq $env_present) {
+ Fail-Json -obj $result -message "Test $test_name failed`nenvironment variable TESTENV not found in stdout`n$($actual.stdout)"
+}
+
+$test_name = "test custom environment variable1"
+$actual = Run-Command -command "cmd.exe /c set" -environment @{ TESTENV2 = "testing" }
+$env_not_present = $actual.stdout -split "`r`n" | Where-Object { $_ -eq "TESTENV=test" }
+$env_present = $actual.stdout -split "`r`n" | Where-Object { $_ -eq "TESTENV2=testing" }
+if ($null -ne $env_not_present) {
+ Fail-Json -obj $result -message "Test $test_name failed`nenvironment variabel TESTENV found in stdout when it should be`n$($actual.stdout)"
+}
+if ($null -eq $env_present) {
+ Fail-json -obj $result -message "Test $test_name failed`nenvironment variable TESTENV2 not found in stdout`n$($actual.stdout)"
+}
+
+$test_name = "input test"
+$wrapper = @"
+begin {
+ `$string = ""
+} process {
+ `$current_input = [string]`$input
+ `$string += `$current_input
+} end {
+ Write-Host `$string
+}
+"@
+$encoded_wrapper = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($wrapper))
+$actual = Run-Command -command "powershell.exe -ExecutionPolicy ByPass -EncodedCommand $encoded_wrapper" -stdin "Ansible"
+Assert-Equal -actual $actual.stdout -expected "Ansible`n"
+
+$result.data = "success"
+Exit-Json -obj $result
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/meta/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/meta/main.yml
new file mode 100644
index 0000000..fd0dc54
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/meta/main.yml
@@ -0,0 +1,3 @@
+---
+dependencies:
+- setup_win_printargv
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/tasks/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/tasks/main.yml
new file mode 100644
index 0000000..3001518
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/tasks/main.yml
@@ -0,0 +1,9 @@
+---
+- name: call module with CommandUtil tests
+ command_util_test:
+ exe: '{{ win_printargv_path }}'
+ register: command_util
+
+- assert:
+ that:
+ - command_util.data == 'success'
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/aliases b/test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/library/file_util_test.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/library/file_util_test.ps1
new file mode 100644
index 0000000..c38f4e6
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/library/file_util_test.ps1
@@ -0,0 +1,114 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.FileUtil
+
+$ErrorActionPreference = "Stop"
+
+$result = @{
+ changed = $false
+}
+
+Function Assert-Equal($actual, $expected) {
+ if ($actual -cne $expected) {
+ $call_stack = (Get-PSCallStack)[1]
+ $error_msg = -join @(
+ "AssertionError:`r`nActual: `"$actual`" != Expected: `"$expected`"`r`nLine: "
+ "$($call_stack.ScriptLineNumber), Method: $($call_stack.Position.Text)"
+ )
+ Fail-Json -obj $result -message $error_msg
+ }
+}
+
+Function Get-PagefilePath() {
+ $pagefile = $null
+ $cs = Get-CimInstance -ClassName Win32_ComputerSystem
+ if ($cs.AutomaticManagedPagefile) {
+ $pagefile = "$($env:SystemRoot.Substring(0, 1)):\pagefile.sys"
+ }
+ else {
+ $pf = Get-CimInstance -ClassName Win32_PageFileSetting
+ if ($null -ne $pf) {
+ $pagefile = $pf[0].Name
+ }
+ }
+ return $pagefile
+}
+
+$pagefile = Get-PagefilePath
+if ($pagefile) {
+ # Test-AnsiblePath Hidden system file
+ $actual = Test-AnsiblePath -Path $pagefile
+ Assert-Equal -actual $actual -expected $true
+
+ # Get-AnsibleItem file
+ $actual = Get-AnsibleItem -Path $pagefile
+ Assert-Equal -actual $actual.FullName -expected $pagefile
+ Assert-Equal -actual $actual.Attributes.HasFlag([System.IO.FileAttributes]::Directory) -expected $false
+ Assert-Equal -actual $actual.Exists -expected $true
+}
+
+# Test-AnsiblePath File that doesn't exist
+$actual = Test-AnsiblePath -Path C:\fakefile
+Assert-Equal -actual $actual -expected $false
+
+# Test-AnsiblePath Directory that doesn't exist
+$actual = Test-AnsiblePath -Path C:\fakedirectory
+Assert-Equal -actual $actual -expected $false
+
+# Test-AnsiblePath file in non-existant directory
+$actual = Test-AnsiblePath -Path C:\fakedirectory\fakefile.txt
+Assert-Equal -actual $actual -expected $false
+
+# Test-AnsiblePath Normal directory
+$actual = Test-AnsiblePath -Path C:\Windows
+Assert-Equal -actual $actual -expected $true
+
+# Test-AnsiblePath Normal file
+$actual = Test-AnsiblePath -Path C:\Windows\System32\kernel32.dll
+Assert-Equal -actual $actual -expected $true
+
+# Test-AnsiblePath fails with wildcard
+$failed = $false
+try {
+ Test-AnsiblePath -Path C:\Windows\*.exe
+}
+catch {
+ $failed = $true
+ Assert-Equal -actual $_.Exception.Message -expected "Exception calling `"GetAttributes`" with `"1`" argument(s): `"Illegal characters in path.`""
+}
+Assert-Equal -actual $failed -expected $true
+
+# Test-AnsiblePath on non file PS Provider object
+$actual = Test-AnsiblePath -Path Cert:\LocalMachine\My
+Assert-Equal -actual $actual -expected $true
+
+# Test-AnsiblePath on environment variable
+$actual = Test-AnsiblePath -Path env:SystemDrive
+Assert-Equal -actual $actual -expected $true
+
+# Test-AnsiblePath on environment variable that does not exist
+$actual = Test-AnsiblePath -Path env:FakeEnvValue
+Assert-Equal -actual $actual -expected $false
+
+# Get-AnsibleItem doesn't exist with -ErrorAction SilentlyContinue param
+$actual = Get-AnsibleItem -Path C:\fakefile -ErrorAction SilentlyContinue
+Assert-Equal -actual $actual -expected $null
+
+# Get-AnsibleItem directory
+$actual = Get-AnsibleItem -Path C:\Windows
+Assert-Equal -actual $actual.FullName -expected C:\Windows
+Assert-Equal -actual $actual.Attributes.HasFlag([System.IO.FileAttributes]::Directory) -expected $true
+Assert-Equal -actual $actual.Exists -expected $true
+
+# ensure Get-AnsibleItem doesn't fail in a try/catch and -ErrorAction SilentlyContinue - stop's a trap from trapping it
+try {
+ $actual = Get-AnsibleItem -Path C:\fakepath -ErrorAction SilentlyContinue
+}
+catch {
+ Fail-Json -obj $result -message "this should not fire"
+}
+Assert-Equal -actual $actual -expected $null
+
+$result.data = "success"
+Exit-Json -obj $result
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/tasks/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/tasks/main.yml
new file mode 100644
index 0000000..a636d32
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/tasks/main.yml
@@ -0,0 +1,8 @@
+---
+- name: call module with FileUtil tests
+ file_util_test:
+ register: file_util_test
+
+- assert:
+ that:
+ - file_util_test.data == 'success'
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/aliases b/test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/library/testlist.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/library/testlist.ps1
new file mode 100644
index 0000000..06ef17b
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/library/testlist.ps1
@@ -0,0 +1,12 @@
+#powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+$params = Parse-Args $args
+$value = Get-AnsibleParam -Obj $params -Name value -Type list
+
+if ($value -isnot [array]) {
+ Fail-Json -obj @{} -message "value was not a list but was $($value.GetType().FullName)"
+}
+
+Exit-Json @{ count = $value.Count }
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/library/testpath.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/library/testpath.ps1
new file mode 100644
index 0000000..7a6ba0b
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/library/testpath.ps1
@@ -0,0 +1,9 @@
+#powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+$params = Parse-Args $args
+
+$path = Get-AnsibleParam -Obj $params -Name path -Type path
+
+Exit-Json @{ path = $path }
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/tasks/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/tasks/main.yml
new file mode 100644
index 0000000..0bd1055
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/tasks/main.yml
@@ -0,0 +1,41 @@
+# NB: these tests are just a placeholder until we have pester unit tests.
+# They are being run as part of the Windows smoke tests. Please do not significantly
+# increase the size of these tests, as the smoke tests need to remain fast.
+# Any significant additions should be made to the (as yet nonexistent) PS module_utils unit tests.
+---
+- name: find a nonexistent drive letter
+ raw: foreach($c in [char[]]([char]'D'..[char]'Z')) { If (-not $(Get-PSDrive $c -ErrorAction SilentlyContinue)) { return $c } }
+ register: bogus_driveletter
+
+- assert:
+ that: bogus_driveletter.stdout_lines[0] | length == 1
+
+- name: test path shape validation
+ testpath:
+ path: "{{ item.path }}"
+ failed_when: path_shapes is failed != (item.should_fail | default(false))
+ register: path_shapes
+ with_items:
+ - path: C:\Windows
+ - path: HKLM:\Software
+ - path: '{{ bogus_driveletter.stdout_lines[0] }}:\goodpath'
+ - path: '{{ bogus_driveletter.stdout_lines[0] }}:\badpath*%@:\blar'
+ should_fail: true
+
+- name: test list parameters
+ testlist:
+ value: '{{item.value}}'
+ register: list_tests
+ failed_when: list_tests is failed or list_tests.count != item.count
+ with_items:
+ - value: []
+ count: 0
+ - value:
+ - 1
+ - 2
+ count: 2
+ - value:
+ - 1
+ count: 1
+ - value: "1, 2"
+ count: 2
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/aliases b/test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/library/symbolic_link_test.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/library/symbolic_link_test.ps1
new file mode 100644
index 0000000..de0bb8b
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/library/symbolic_link_test.ps1
@@ -0,0 +1,174 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.LinkUtil
+#Requires -Module Ansible.ModuleUtils.CommandUtil
+
+$ErrorActionPreference = 'Stop'
+
+$path = Join-Path -Path ([System.IO.Path]::GetFullPath($env:TEMP)) -ChildPath '.ansible .ÅÑŚÌβÅÈ [$!@^&test(;)]'
+
+$folder_target = "$path\folder"
+$file_target = "$path\file"
+$symlink_file_path = "$path\file-symlink"
+$symlink_folder_path = "$path\folder-symlink"
+$hardlink_path = "$path\hardlink"
+$hardlink_path_2 = "$path\hardlink2"
+$junction_point_path = "$path\junction"
+
+if (Test-Path -LiteralPath $path) {
+ # Remove-Item struggles with broken symlinks, rely on trusty rmdir instead
+ Run-Command -command "cmd.exe /c rmdir /S /Q `"$path`"" > $null
+}
+New-Item -Path $path -ItemType Directory | Out-Null
+New-Item -Path $folder_target -ItemType Directory | Out-Null
+New-Item -Path $file_target -ItemType File | Out-Null
+Set-Content -LiteralPath $file_target -Value "a"
+
+Function Assert-Equal($actual, $expected) {
+ if ($actual -ne $expected) {
+ Fail-Json @{} "actual != expected`nActual: $actual`nExpected: $expected"
+ }
+}
+
+Function Assert-True($expression, $message) {
+ if ($expression -ne $true) {
+ Fail-Json @{} $message
+ }
+}
+
+# need to manually set this
+Load-LinkUtils
+
+# path is not a link
+$no_link_result = Get-Link -link_path $path
+Assert-True -expression ($null -eq $no_link_result) -message "did not return null result for a non link"
+
+# fail to create hard link pointed to a directory
+try {
+ New-Link -link_path "$path\folder-hard" -link_target $folder_target -link_type "hard"
+ Assert-True -expression $false -message "creation of hard link should have failed if target was a directory"
+}
+catch {
+ Assert-Equal -actual $_.Exception.Message -expected "cannot set the target for a hard link to a directory"
+}
+
+# fail to create a junction point pointed to a file
+try {
+ New-Link -link_path "$path\junction-fail" -link_target $file_target -link_type "junction"
+ Assert-True -expression $false -message "creation of junction point should have failed if target was a file"
+}
+catch {
+ Assert-Equal -actual $_.Exception.Message -expected "cannot set the target for a junction point to a file"
+}
+
+# fail to create a symbolic link with non-existent target
+try {
+ New-Link -link_path "$path\symlink-fail" -link_target "$path\fake-folder" -link_type "link"
+ Assert-True -expression $false -message "creation of symbolic link should have failed if target did not exist"
+}
+catch {
+ Assert-Equal -actual $_.Exception.Message -expected "link_target '$path\fake-folder' does not exist, cannot create link"
+}
+
+# create recursive symlink
+Run-Command -command "cmd.exe /c mklink /D symlink-rel folder" -working_directory $path | Out-Null
+$rel_link_result = Get-Link -link_path "$path\symlink-rel"
+Assert-Equal -actual $rel_link_result.Type -expected "SymbolicLink"
+Assert-Equal -actual $rel_link_result.SubstituteName -expected "folder"
+Assert-Equal -actual $rel_link_result.PrintName -expected "folder"
+Assert-Equal -actual $rel_link_result.TargetPath -expected "folder"
+Assert-Equal -actual $rel_link_result.AbsolutePath -expected $folder_target
+Assert-Equal -actual $rel_link_result.HardTargets -expected $null
+
+# create a symbolic file test
+New-Link -link_path $symlink_file_path -link_target $file_target -link_type "link"
+$file_link_result = Get-Link -link_path $symlink_file_path
+Assert-Equal -actual $file_link_result.Type -expected "SymbolicLink"
+Assert-Equal -actual $file_link_result.SubstituteName -expected "\??\$file_target"
+Assert-Equal -actual $file_link_result.PrintName -expected $file_target
+Assert-Equal -actual $file_link_result.TargetPath -expected $file_target
+Assert-Equal -actual $file_link_result.AbsolutePath -expected $file_target
+Assert-Equal -actual $file_link_result.HardTargets -expected $null
+
+# create a symbolic link folder test
+New-Link -link_path $symlink_folder_path -link_target $folder_target -link_type "link"
+$folder_link_result = Get-Link -link_path $symlink_folder_path
+Assert-Equal -actual $folder_link_result.Type -expected "SymbolicLink"
+Assert-Equal -actual $folder_link_result.SubstituteName -expected "\??\$folder_target"
+Assert-Equal -actual $folder_link_result.PrintName -expected $folder_target
+Assert-Equal -actual $folder_link_result.TargetPath -expected $folder_target
+Assert-Equal -actual $folder_link_result.AbsolutePath -expected $folder_target
+Assert-Equal -actual $folder_link_result.HardTargets -expected $null
+
+# create a junction point test
+New-Link -link_path $junction_point_path -link_target $folder_target -link_type "junction"
+$junction_point_result = Get-Link -link_path $junction_point_path
+Assert-Equal -actual $junction_point_result.Type -expected "JunctionPoint"
+Assert-Equal -actual $junction_point_result.SubstituteName -expected "\??\$folder_target"
+Assert-Equal -actual $junction_point_result.PrintName -expected $folder_target
+Assert-Equal -actual $junction_point_result.TargetPath -expected $folder_target
+Assert-Equal -actual $junction_point_result.AbsolutePath -expected $folder_target
+Assert-Equal -actual $junction_point_result.HardTargets -expected $null
+
+# create a hard link test
+New-Link -link_path $hardlink_path -link_target $file_target -link_type "hard"
+$hardlink_result = Get-Link -link_path $hardlink_path
+Assert-Equal -actual $hardlink_result.Type -expected "HardLink"
+Assert-Equal -actual $hardlink_result.SubstituteName -expected $null
+Assert-Equal -actual $hardlink_result.PrintName -expected $null
+Assert-Equal -actual $hardlink_result.TargetPath -expected $null
+Assert-Equal -actual $hardlink_result.AbsolutePath -expected $null
+if ($hardlink_result.HardTargets[0] -ne $hardlink_path -and $hardlink_result.HardTargets[1] -ne $hardlink_path) {
+ Assert-True -expression $false -message "file $hardlink_path is not a target of the hard link"
+}
+if ($hardlink_result.HardTargets[0] -ne $file_target -and $hardlink_result.HardTargets[1] -ne $file_target) {
+ Assert-True -expression $false -message "file $file_target is not a target of the hard link"
+}
+Assert-Equal -actual (Get-Content -LiteralPath $hardlink_path -Raw) -expected (Get-Content -LiteralPath $file_target -Raw)
+
+# create a new hard link and verify targets go to 3
+New-Link -link_path $hardlink_path_2 -link_target $file_target -link_type "hard"
+$hardlink_result_2 = Get-Link -link_path $hardlink_path
+$expected = "did not return 3 targets for the hard link, actual $($hardlink_result_2.Targets.Count)"
+Assert-True -expression ($hardlink_result_2.HardTargets.Count -eq 3) -message $expected
+
+# check if broken symbolic link still works
+Remove-Item -LiteralPath $folder_target -Force | Out-Null
+$broken_link_result = Get-Link -link_path $symlink_folder_path
+Assert-Equal -actual $broken_link_result.Type -expected "SymbolicLink"
+Assert-Equal -actual $broken_link_result.SubstituteName -expected "\??\$folder_target"
+Assert-Equal -actual $broken_link_result.PrintName -expected $folder_target
+Assert-Equal -actual $broken_link_result.TargetPath -expected $folder_target
+Assert-Equal -actual $broken_link_result.AbsolutePath -expected $folder_target
+Assert-Equal -actual $broken_link_result.HardTargets -expected $null
+
+# check if broken junction point still works
+$broken_junction_result = Get-Link -link_path $junction_point_path
+Assert-Equal -actual $broken_junction_result.Type -expected "JunctionPoint"
+Assert-Equal -actual $broken_junction_result.SubstituteName -expected "\??\$folder_target"
+Assert-Equal -actual $broken_junction_result.PrintName -expected $folder_target
+Assert-Equal -actual $broken_junction_result.TargetPath -expected $folder_target
+Assert-Equal -actual $broken_junction_result.AbsolutePath -expected $folder_target
+Assert-Equal -actual $broken_junction_result.HardTargets -expected $null
+
+# delete file symbolic link
+Remove-Link -link_path $symlink_file_path
+Assert-True -expression (-not (Test-Path -LiteralPath $symlink_file_path)) -message "failed to delete file symbolic link"
+
+# delete folder symbolic link
+Remove-Link -link_path $symlink_folder_path
+Assert-True -expression (-not (Test-Path -LiteralPath $symlink_folder_path)) -message "failed to delete folder symbolic link"
+
+# delete junction point
+Remove-Link -link_path $junction_point_path
+Assert-True -expression (-not (Test-Path -LiteralPath $junction_point_path)) -message "failed to delete junction point"
+
+# delete hard link
+Remove-Link -link_path $hardlink_path
+Assert-True -expression (-not (Test-Path -LiteralPath $hardlink_path)) -message "failed to delete hard link"
+
+# cleanup after tests
+Run-Command -command "cmd.exe /c rmdir /S /Q `"$path`"" > $null
+
+Exit-Json @{ data = "success" }
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/tasks/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/tasks/main.yml
new file mode 100644
index 0000000..f121ad4
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/tasks/main.yml
@@ -0,0 +1,8 @@
+---
+- name: call module with symbolic link tests
+ symbolic_link_test:
+ register: symbolic_link
+
+- assert:
+ that:
+ - symbolic_link.data == 'success'
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/aliases b/test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/library/privilege_util_test.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/library/privilege_util_test.ps1
new file mode 100644
index 0000000..414b80a
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/library/privilege_util_test.ps1
@@ -0,0 +1,113 @@
+#!powershell
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#Requires -Module Ansible.ModuleUtils.PrivilegeUtil
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
+
+Function Assert-Equal($actual, $expected) {
+ if ($actual -cne $expected) {
+ $call_stack = (Get-PSCallStack)[1]
+ $module.Result.actual = $actual
+ $module.Result.expected = $expected
+ $module.Result.line = $call_stack.ScriptLineNumber
+ $module.Result.method = $call_stack.Position.Text
+ $module.FailJson("AssertionError: actual != expected")
+ }
+}
+
+# taken from https://docs.microsoft.com/en-us/windows/desktop/SecAuthZ/privilege-constants
+$total_privileges = @(
+ "SeAssignPrimaryTokenPrivilege",
+ "SeAuditPrivilege",
+ "SeBackupPrivilege",
+ "SeChangeNotifyPrivilege",
+ "SeCreateGlobalPrivilege",
+ "SeCreatePagefilePrivilege",
+ "SeCreatePermanentPrivilege",
+ "SeCreateSymbolicLinkPrivilege",
+ "SeCreateTokenPrivilege",
+ "SeDebugPrivilege",
+ "SeEnableDelegationPrivilege",
+ "SeImpersonatePrivilege",
+ "SeIncreaseBasePriorityPrivilege",
+ "SeIncreaseQuotaPrivilege",
+ "SeIncreaseWorkingSetPrivilege",
+ "SeLoadDriverPrivilege",
+ "SeLockMemoryPrivilege",
+ "SeMachineAccountPrivilege",
+ "SeManageVolumePrivilege",
+ "SeProfileSingleProcessPrivilege",
+ "SeRelabelPrivilege",
+ "SeRemoteShutdownPrivilege",
+ "SeRestorePrivilege",
+ "SeSecurityPrivilege",
+ "SeShutdownPrivilege",
+ "SeSyncAgentPrivilege",
+ "SeSystemEnvironmentPrivilege",
+ "SeSystemProfilePrivilege",
+ "SeSystemtimePrivilege",
+ "SeTakeOwnershipPrivilege",
+ "SeTcbPrivilege",
+ "SeTimeZonePrivilege",
+ "SeTrustedCredManAccessPrivilege",
+ "SeUndockPrivilege"
+)
+
+$raw_privilege_output = &whoami /priv | Where-Object { $_.StartsWith("Se") }
+$actual_privileges = @{}
+foreach ($raw_privilege in $raw_privilege_output) {
+ $split = $raw_privilege.TrimEnd() -split " "
+ $actual_privileges."$($split[0])" = ($split[-1] -eq "Enabled")
+}
+$process = [Ansible.Privilege.PrivilegeUtil]::GetCurrentProcess()
+
+### Test PS cmdlets ###
+# test ps Get-AnsiblePrivilege
+foreach ($privilege in $total_privileges) {
+ $expected = $null
+ if ($actual_privileges.ContainsKey($privilege)) {
+ $expected = $actual_privileges.$privilege
+ }
+ $actual = Get-AnsiblePrivilege -Name $privilege
+ Assert-Equal -actual $actual -expected $expected
+}
+
+# test c# GetAllPrivilegeInfo
+$actual = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+Assert-Equal -actual $actual.GetType().Name -expected 'Dictionary`2'
+Assert-Equal -actual $actual.Count -expected $actual_privileges.Count
+foreach ($privilege in $total_privileges) {
+ if ($actual_privileges.ContainsKey($privilege)) {
+ $actual_value = $actual.$privilege
+ if ($actual_privileges.$privilege) {
+ Assert-Equal -actual $actual_value.HasFlag([Ansible.Privilege.PrivilegeAttributes]::Enabled) -expected $true
+ }
+ else {
+ Assert-Equal -actual $actual_value.HasFlag([Ansible.Privilege.PrivilegeAttributes]::Enabled) -expected $false
+ }
+ }
+}
+
+# test Set-AnsiblePrivilege
+Set-AnsiblePrivilege -Name SeUndockPrivilege -Value $false # ensure we start with a disabled privilege
+
+Set-AnsiblePrivilege -Name SeUndockPrivilege -Value $true -WhatIf
+$actual = Get-AnsiblePrivilege -Name SeUndockPrivilege
+Assert-Equal -actual $actual -expected $false
+
+Set-AnsiblePrivilege -Name SeUndockPrivilege -Value $true
+$actual = Get-AnsiblePrivilege -Name SeUndockPrivilege
+Assert-Equal -actual $actual -expected $true
+
+Set-AnsiblePrivilege -Name SeUndockPrivilege -Value $false -WhatIf
+$actual = Get-AnsiblePrivilege -Name SeUndockPrivilege
+Assert-Equal -actual $actual -expected $true
+
+Set-AnsiblePrivilege -Name SeUndockPrivilege -Value $false
+$actual = Get-AnsiblePrivilege -Name SeUndockPrivilege
+Assert-Equal -actual $actual -expected $false
+
+$module.Result.data = "success"
+$module.ExitJson()
+
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/tasks/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/tasks/main.yml
new file mode 100644
index 0000000..5f54480
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/tasks/main.yml
@@ -0,0 +1,8 @@
+---
+- name: call module with PrivilegeUtil tests
+ privilege_util_test:
+ register: privilege_util_test
+
+- assert:
+ that:
+ - privilege_util_test.data == 'success'
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.SID/aliases b/test/integration/targets/module_utils_Ansible.ModuleUtils.SID/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.SID/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.SID/library/sid_utils_test.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.SID/library/sid_utils_test.ps1
new file mode 100644
index 0000000..85bfbe1
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.SID/library/sid_utils_test.ps1
@@ -0,0 +1,101 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.SID
+
+$params = Parse-Args $args
+$sid_account = Get-AnsibleParam -obj $params -name "sid_account" -type "str" -failifempty $true
+
+Function Assert-Equal($actual, $expected) {
+ if ($actual -ne $expected) {
+ Fail-Json @{} "actual != expected`nActual: $actual`nExpected: $expected"
+ }
+}
+
+Function Get-ComputerSID() {
+ # find any local user and trim off the final UID
+ $luser_sid = (Get-CimInstance Win32_UserAccount -Filter "Domain='$env:COMPUTERNAME'")[0].SID
+
+ return $luser_sid -replace '(S-1-5-21-\d+-\d+-\d+)-\d+', '$1'
+}
+
+$local_sid = Get-ComputerSID
+
+# most machines should have a -500 Administrator account, but it may have been renamed. Look it up by SID
+$default_admin = Get-CimInstance Win32_UserAccount -Filter "SID='$local_sid-500'"
+
+# this group is called Administrators by default on English Windows, but could named something else. Look it up by SID
+$default_admin_group = Get-CimInstance Win32_Group -Filter "SID='S-1-5-32-544'"
+
+if (@($default_admin).Length -ne 1) {
+ Fail-Json @{} "could not find a local admin account with SID ending in -500"
+}
+
+### Set this to the NETBIOS name of the domain you wish to test, not set for shippable ###
+$test_domain = $null
+
+$tests = @(
+ # Local Users
+ @{ sid = "S-1-1-0"; full_name = "Everyone"; names = @("Everyone") },
+ @{ sid = "S-1-5-18"; full_name = "NT AUTHORITY\SYSTEM"; names = @("NT AUTHORITY\SYSTEM", "SYSTEM") },
+ @{ sid = "S-1-5-20"; full_name = "NT AUTHORITY\NETWORK SERVICE"; names = @("NT AUTHORITY\NETWORK SERVICE", "NETWORK SERVICE") },
+ @{
+ sid = "$($default_admin.SID)"
+ full_name = "$($default_admin.FullName)"
+ names = @("$env:COMPUTERNAME\$($default_admin.Name)", "$($default_admin.Name)", ".\$($default_admin.Name)")
+ },
+
+ # Local Groups
+ @{
+ sid = "$($default_admin_group.SID)"
+ full_name = "BUILTIN\$($default_admin_group.Name)"
+ names = @("BUILTIN\$($default_admin_group.Name)", "$($default_admin_group.Name)", ".\$($default_admin_group.Name)")
+ }
+)
+
+# Add domain tests if the domain name has been set
+if ($null -ne $test_domain) {
+ Import-Module ActiveDirectory
+ $domain_info = Get-ADDomain -Identity $test_domain
+ $domain_sid = $domain_info.DomainSID
+ $domain_netbios = $domain_info.NetBIOSName
+ $domain_upn = $domain_info.Forest
+
+ $tests += @{
+ sid = "$domain_sid-512"
+ full_name = "$domain_netbios\Domain Admins"
+ names = @("$domain_netbios\Domain Admins", "Domain Admins@$domain_upn", "Domain Admins")
+ }
+
+ $tests += @{
+ sid = "$domain_sid-500"
+ full_name = "$domain_netbios\Administrator"
+ names = @("$domain_netbios\Administrator", "Administrator@$domain_upn")
+ }
+}
+
+foreach ($test in $tests) {
+ $actual_account_name = Convert-FromSID -sid $test.sid
+ # renamed admins may have an empty FullName; skip comparison in that case
+ if ($test.full_name) {
+ Assert-Equal -actual $actual_account_name -expected $test.full_name
+ }
+
+ foreach ($test_name in $test.names) {
+ $actual_sid = Convert-ToSID -account_name $test_name
+ Assert-Equal -actual $actual_sid -expected $test.sid
+ }
+}
+
+# the account to SID test is run outside of the normal run as we can't test it
+# in the normal test suite
+# Calling Convert-ToSID with a string like a SID should return that SID back
+$actual = Convert-ToSID -account_name $sid_account
+Assert-Equal -actual $actual -expected $sid_account
+
+# Calling COnvert-ToSID with a string prefixed with .\ should return the SID
+# for a user that is called that SID and not the SID passed in
+$actual = Convert-ToSID -account_name ".\$sid_account"
+Assert-Equal -actual ($actual -ne $sid_account) -expected $true
+
+Exit-Json @{ data = "success" }
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.SID/tasks/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.SID/tasks/main.yml
new file mode 100644
index 0000000..acbae50
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.SID/tasks/main.yml
@@ -0,0 +1,22 @@
+---
+- block:
+ - name: create test user with well know SID as the name
+ win_user:
+ name: S-1-0-0
+ password: AbcDef123!@#
+ state: present
+
+ - name: call module with SID tests
+ sid_utils_test:
+ sid_account: S-1-0-0
+ register: sid_test
+
+ always:
+ - name: remove test SID user
+ win_user:
+ name: S-1-0-0
+ state: absent
+
+- assert:
+ that:
+ - sid_test.data == 'success'
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/aliases b/test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/aliases
new file mode 100644
index 0000000..b5ad7ca
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/aliases
@@ -0,0 +1,4 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
+needs/httptester
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/library/web_request_test.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/library/web_request_test.ps1
new file mode 100644
index 0000000..c168b92
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/library/web_request_test.ps1
@@ -0,0 +1,473 @@
+#!powershell
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#Requires -Module Ansible.ModuleUtils.WebRequest
+
+$spec = @{
+ options = @{
+ httpbin_host = @{ type = 'str'; required = $true }
+ }
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$httpbin_host = $module.Params.httpbin_host
+
+Function Assert-Equal {
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)][AllowNull()]$Actual,
+ [Parameter(Mandatory = $true, Position = 0)][AllowNull()]$Expected
+ )
+
+ process {
+ $matched = $false
+ if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array] -or $Actual -is [System.Collections.IList]) {
+ $Actual.Count | Assert-Equal -Expected $Expected.Count
+ for ($i = 0; $i -lt $Actual.Count; $i++) {
+ $actualValue = $Actual[$i]
+ $expectedValue = $Expected[$i]
+ Assert-Equal -Actual $actualValue -Expected $expectedValue
+ }
+ $matched = $true
+ }
+ else {
+ $matched = $Actual -ceq $Expected
+ }
+
+ if (-not $matched) {
+ if ($Actual -is [PSObject]) {
+ $Actual = $Actual.ToString()
+ }
+
+ $call_stack = (Get-PSCallStack)[1]
+ $module.Result.test = $test
+ $module.Result.actual = $Actual
+ $module.Result.expected = $Expected
+ $module.Result.line = $call_stack.ScriptLineNumber
+ $module.Result.method = $call_stack.Position.Text
+
+ $module.FailJson("AssertionError: actual != expected")
+ }
+ }
+}
+
+Function Convert-StreamToString {
+ [CmdletBinding()]
+ param (
+ [Parameter(Mandatory = $true)]
+ [System.IO.Stream]
+ $Stream
+ )
+
+ $ms = New-Object -TypeName System.IO.MemoryStream
+ try {
+ $Stream.CopyTo($ms)
+ [System.Text.Encoding]::UTF8.GetString($ms.ToArray())
+ }
+ finally {
+ $ms.Dispose()
+ }
+}
+
+$tests = [Ordered]@{
+ 'GET request over http' = {
+ $r = Get-AnsibleWebRequest -Uri "http://$httpbin_host/get"
+
+ $r.Method | Assert-Equal -Expected 'GET'
+ $r.Timeout | Assert-Equal -Expected 30000
+ $r.UseDefaultCredentials | Assert-Equal -Expected $false
+ $r.Credentials | Assert-Equal -Expected $null
+ $r.ClientCertificates.Count | Assert-Equal -Expected 0
+ $r.Proxy.Credentials | Assert-Equal -Expected $null
+ $r.UserAgent | Assert-Equal -Expected 'ansible-httpget'
+
+ $actual = Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.StatusCode | Assert-Equal -Expected 200
+ Convert-StreamToString -Stream $Stream
+ } | ConvertFrom-Json
+
+ $actual.headers.'User-Agent' | Assert-Equal -Expected 'ansible-httpget'
+ $actual.headers.'Host' | Assert-Equal -Expected $httpbin_host
+
+ $module.Result.msg | Assert-Equal -Expected 'OK'
+ $module.Result.status_code | Assert-Equal -Expected 200
+ $module.Result.ContainsKey('elapsed') | Assert-Equal -Expected $true
+ }
+
+ 'GET request over https' = {
+ # url is an alias for the -Uri parameter.
+ $r = Get-AnsibleWebRequest -url "https://$httpbin_host/get"
+
+ $r.Method | Assert-Equal -Expected 'GET'
+ $r.Timeout | Assert-Equal -Expected 30000
+ $r.UseDefaultCredentials | Assert-Equal -Expected $false
+ $r.Credentials | Assert-Equal -Expected $null
+ $r.ClientCertificates.Count | Assert-Equal -Expected 0
+ $r.Proxy.Credentials | Assert-Equal -Expected $null
+ $r.UserAgent | Assert-Equal -Expected 'ansible-httpget'
+
+ $actual = Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.StatusCode | Assert-Equal -Expected 200
+ Convert-StreamToString -Stream $Stream
+ } | ConvertFrom-Json
+
+ $actual.headers.'User-Agent' | Assert-Equal -Expected 'ansible-httpget'
+ $actual.headers.'Host' | Assert-Equal -Expected $httpbin_host
+ }
+
+ 'POST request' = {
+ $getParams = @{
+ Headers = @{
+ 'Content-Type' = 'application/json'
+ }
+ Method = 'POST'
+ Uri = "https://$httpbin_host/post"
+ }
+ $r = Get-AnsibleWebRequest @getParams
+
+ $r.Method | Assert-Equal -Expected 'POST'
+ $r.Timeout | Assert-Equal -Expected 30000
+ $r.UseDefaultCredentials | Assert-Equal -Expected $false
+ $r.Credentials | Assert-Equal -Expected $null
+ $r.ClientCertificates.Count | Assert-Equal -Expected 0
+ $r.Proxy.Credentials | Assert-Equal -Expected $null
+ $r.ContentType | Assert-Equal -Expected 'application/json'
+ $r.UserAgent | Assert-Equal -Expected 'ansible-httpget'
+
+ $body = New-Object -TypeName System.IO.MemoryStream -ArgumentList @(,
+ ([System.Text.Encoding]::UTF8.GetBytes('{"foo":"bar"}'))
+ )
+ $actual = Invoke-WithWebRequest -Module $module -Request $r -Body $body -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.StatusCode | Assert-Equal -Expected 200
+ Convert-StreamToString -Stream $Stream
+ } | ConvertFrom-Json
+
+ $actual.headers.'User-Agent' | Assert-Equal -Expected 'ansible-httpget'
+ $actual.headers.'Host' | Assert-Equal -Expected $httpbin_host
+ $actual.data | Assert-Equal -Expected '{"foo":"bar"}'
+ }
+
+ 'Safe redirection of GET' = {
+ $r = Get-AnsibleWebRequest -Uri "http://$httpbin_host/redirect/2"
+
+ Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.ResponseUri | Assert-Equal -Expected "http://$httpbin_host/get"
+ $Response.StatusCode | Assert-Equal -Expected 200
+ }
+ }
+
+ 'Safe redirection of HEAD' = {
+ $r = Get-AnsibleWebRequest -Uri "http://$httpbin_host/redirect/2" -Method HEAD
+
+ Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.ResponseUri | Assert-Equal -Expected "http://$httpbin_host/get"
+ $Response.StatusCode | Assert-Equal -Expected 200
+ }
+ }
+
+ 'Safe redirection of PUT' = {
+ $params = @{
+ Method = 'PUT'
+ Uri = "http://$httpbin_host/redirect-to?url=https://$httpbin_host/put"
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.ResponseUri | Assert-Equal -Expected $r.RequestUri
+ $Response.StatusCode | Assert-Equal -Expected 302
+ }
+ }
+
+ 'None redirection of GET' = {
+ $params = @{
+ FollowRedirects = 'None'
+ Uri = "http://$httpbin_host/redirect/2"
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.ResponseUri | Assert-Equal -Expected $r.RequestUri
+ $Response.StatusCode | Assert-Equal -Expected 302
+ }
+ }
+
+ 'None redirection of HEAD' = {
+ $params = @{
+ follow_redirects = 'None'
+ method = 'HEAD'
+ Uri = "http://$httpbin_host/redirect/2"
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.ResponseUri | Assert-Equal -Expected $r.RequestUri
+ $Response.StatusCode | Assert-Equal -Expected 302
+ }
+ }
+
+ 'None redirection of PUT' = {
+ $params = @{
+ FollowRedirects = 'None'
+ Method = 'PUT'
+ Uri = "http://$httpbin_host/redirect-to?url=https://$httpbin_host/put"
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.ResponseUri | Assert-Equal -Expected $r.RequestUri
+ $Response.StatusCode | Assert-Equal -Expected 302
+ }
+ }
+
+ 'All redirection of GET' = {
+ $params = @{
+ FollowRedirects = 'All'
+ Uri = "http://$httpbin_host/redirect/2"
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.ResponseUri | Assert-Equal -Expected "http://$httpbin_host/get"
+ $Response.StatusCode | Assert-Equal -Expected 200
+ }
+ }
+
+ 'All redirection of HEAD' = {
+ $params = @{
+ follow_redirects = 'All'
+ method = 'HEAD'
+ Uri = "http://$httpbin_host/redirect/2"
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.ResponseUri | Assert-Equal -Expected "http://$httpbin_host/get"
+ $Response.StatusCode | Assert-Equal -Expected 200
+ }
+ }
+
+ 'All redirection of PUT' = {
+ $params = @{
+ FollowRedirects = 'All'
+ Method = 'PUT'
+ Uri = "http://$httpbin_host/redirect-to?url=https://$httpbin_host/put"
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.ResponseUri | Assert-Equal -Expected "https://$httpbin_host/put"
+ $Response.StatusCode | Assert-Equal -Expected 200
+ }
+ }
+
+ 'Exceeds maximum redirection - ignored' = {
+ $params = @{
+ MaximumRedirection = 4
+ Uri = "https://$httpbin_host/redirect/5"
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ Invoke-WithWebRequest -Module $module -Request $r -IgnoreBadResponse -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.ResponseUri | Assert-Equal -Expected "https://$httpbin_host/relative-redirect/1"
+ $Response.StatusCode | Assert-Equal -Expected 302
+ }
+ }
+
+ 'Exceeds maximum redirection - exception' = {
+ $params = @{
+ MaximumRedirection = 1
+ Uri = "https://$httpbin_host/redirect/2"
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ $failed = $false
+ try {
+ $null = Invoke-WithWebRequest -Module $module -Request $r -Script {}
+ }
+ catch {
+ $_.Exception.GetType().Name | Assert-Equal -Expected 'WebException'
+ $_.Exception.Message | Assert-Equal -Expected 'Too many automatic redirections were attempted.'
+ $failed = $true
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ 'Basic auth as Credential' = {
+ $params = @{
+ Url = "http://$httpbin_host/basic-auth/username/password"
+ UrlUsername = 'username'
+ UrlPassword = 'password'
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ Invoke-WithWebRequest -Module $module -Request $r -IgnoreBadResponse -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.StatusCode | Assert-Equal -Expected 200
+ }
+ }
+
+ 'Basic auth as Header' = {
+ $params = @{
+ Url = "http://$httpbin_host/basic-auth/username/password"
+ url_username = 'username'
+ url_password = 'password'
+ ForceBasicAuth = $true
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ Invoke-WithWebRequest -Module $module -Request $r -IgnoreBadResponse -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.StatusCode | Assert-Equal -Expected 200
+ }
+ }
+
+ 'Send request with headers' = {
+ $params = @{
+ Headers = @{
+ 'Content-Length' = 0
+ testingheader = 'testing_header'
+ TestHeader = 'test-header'
+ 'User-Agent' = 'test-agent'
+ }
+ Url = "https://$httpbin_host/get"
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ $actual = Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.StatusCode | Assert-Equal -Expected 200
+ Convert-StreamToString -Stream $Stream
+ } | ConvertFrom-Json
+
+ $actual.headers.'Testheader' | Assert-Equal -Expected 'test-header'
+ $actual.headers.'testingheader' | Assert-Equal -Expected 'testing_header'
+ $actual.Headers.'User-Agent' | Assert-Equal -Expected 'test-agent'
+ }
+
+ 'Request with timeout' = {
+ $params = @{
+ Uri = "https://$httpbin_host/delay/5"
+ Timeout = 1
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ $failed = $false
+ try {
+ $null = Invoke-WithWebRequest -Module $module -Request $r -Script {}
+ }
+ catch {
+ $failed = $true
+ $_.Exception.GetType().Name | Assert-Equal -Expected WebException
+ $_.Exception.Message | Assert-Equal -Expected 'The operation has timed out'
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ 'Request with file URI' = {
+ $filePath = Join-Path $module.Tmpdir -ChildPath 'test.txt'
+ Set-Content -LiteralPath $filePath -Value 'test'
+
+ $r = Get-AnsibleWebRequest -Uri $filePath
+
+ $actual = Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.ContentLength | Assert-Equal -Expected 6
+ Convert-StreamToString -Stream $Stream
+ }
+ $actual | Assert-Equal -Expected "test`r`n"
+ $module.Result.msg | Assert-Equal -Expected "OK"
+ $module.Result.status_code | Assert-Equal -Expected 200
+ }
+
+ 'Web request based on module options' = {
+ Set-Variable complex_args -Scope Global -Value @{
+ url = "https://$httpbin_host/redirect/2"
+ method = 'GET'
+ follow_redirects = 'safe'
+ headers = @{
+ 'User-Agent' = 'other-agent'
+ }
+ http_agent = 'actual-agent'
+ maximum_redirection = 2
+ timeout = 10
+ validate_certs = $false
+ }
+ $spec = @{
+ options = @{
+ url = @{ type = 'str'; required = $true }
+ test = @{ type = 'str'; choices = 'abc', 'def' }
+ }
+ mutually_exclusive = @(, @('url', 'test'))
+ }
+
+ $testModule = [Ansible.Basic.AnsibleModule]::Create(@(), $spec, @(Get-AnsibleWebRequestSpec))
+ $r = Get-AnsibleWebRequest -Url $testModule.Params.url -Module $testModule
+
+ $actual = Invoke-WithWebRequest -Module $testModule -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.ResponseUri | Assert-Equal -Expected "https://$httpbin_host/get"
+ Convert-StreamToString -Stream $Stream
+ } | ConvertFrom-Json
+ $actual.headers.'User-Agent' | Assert-Equal -Expected 'actual-agent'
+ }
+
+ 'Web request with default proxy' = {
+ $params = @{
+ Uri = "https://$httpbin_host/get"
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ $null -ne $r.Proxy | Assert-Equal -Expected $true
+ }
+
+ 'Web request with no proxy' = {
+ $params = @{
+ Uri = "https://$httpbin_host/get"
+ UseProxy = $false
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ $null -eq $r.Proxy | Assert-Equal -Expected $true
+ }
+}
+
+# setup and teardown should favour native tools to create and delete the service and not the util we are testing.
+foreach ($testImpl in $tests.GetEnumerator()) {
+ Set-Variable -Name complex_args -Scope Global -Value @{}
+ $test = $testImpl.Key
+ &$testImpl.Value
+}
+
+$module.Result.data = "success"
+$module.ExitJson()
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/meta/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/meta/main.yml
new file mode 100644
index 0000000..829d0a7
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/meta/main.yml
@@ -0,0 +1,3 @@
+---
+dependencies:
+- prepare_http_tests
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/tasks/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/tasks/main.yml
new file mode 100644
index 0000000..57d8138
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/tasks/main.yml
@@ -0,0 +1,10 @@
+---
+- name: test Ansible.ModuleUtils.WebRequest
+ web_request_test:
+ httpbin_host: '{{ httpbin_host }}'
+ register: web_request
+
+- name: assert test Ansible.ModuleUtils.WebRequest succeeded
+ assert:
+ that:
+ - web_request.data == 'success'
diff --git a/test/integration/targets/module_utils_Ansible.Privilege/aliases b/test/integration/targets/module_utils_Ansible.Privilege/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Privilege/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.Privilege/library/ansible_privilege_tests.ps1 b/test/integration/targets/module_utils_Ansible.Privilege/library/ansible_privilege_tests.ps1
new file mode 100644
index 0000000..58ee9c1
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Privilege/library/ansible_privilege_tests.ps1
@@ -0,0 +1,278 @@
+#!powershell
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#Ansiblerequires -CSharpUtil Ansible.Privilege
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
+
+Function Assert-Equal {
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)][AllowNull()]$Actual,
+ [Parameter(Mandatory = $true, Position = 0)][AllowNull()]$Expected
+ )
+
+ process {
+ $matched = $false
+ if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array]) {
+ $Actual.Count | Assert-Equal -Expected $Expected.Count
+ for ($i = 0; $i -lt $Actual.Count; $i++) {
+ $actual_value = $Actual[$i]
+ $expected_value = $Expected[$i]
+ Assert-Equal -Actual $actual_value -Expected $expected_value
+ }
+ $matched = $true
+ }
+ else {
+ $matched = $Actual -ceq $Expected
+ }
+
+ if (-not $matched) {
+ if ($Actual -is [PSObject]) {
+ $Actual = $Actual.ToString()
+ }
+
+ $call_stack = (Get-PSCallStack)[1]
+ $module.Result.test = $test
+ $module.Result.actual = $Actual
+ $module.Result.expected = $Expected
+ $module.Result.line = $call_stack.ScriptLineNumber
+ $module.Result.method = $call_stack.Position.Text
+ $module.FailJson("AssertionError: actual != expected")
+ }
+ }
+}
+
+Function Assert-DictionaryEqual {
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)][AllowNull()]$Actual,
+ [Parameter(Mandatory = $true, Position = 0)][AllowNull()]$Expected
+ )
+
+ process {
+ $actual_keys = $Actual.Keys
+ $expected_keys = $Expected.Keys
+
+ $actual_keys.Count | Assert-Equal -Expected $expected_keys.Count
+ foreach ($actual_entry in $Actual.GetEnumerator()) {
+ $actual_key = $actual_entry.Key
+ ($actual_key -cin $expected_keys) | Assert-Equal -Expected $true
+ $actual_value = $actual_entry.Value
+ $expected_value = $Expected.$actual_key
+
+ if ($actual_value -is [System.Collections.IDictionary]) {
+ $actual_value | Assert-DictionaryEqual -Expected $expected_value
+ }
+ elseif ($actual_value -is [System.Collections.ArrayList]) {
+ for ($i = 0; $i -lt $actual_value.Count; $i++) {
+ $actual_entry = $actual_value[$i]
+ $expected_entry = $expected_value[$i]
+ if ($actual_entry -is [System.Collections.IDictionary]) {
+ $actual_entry | Assert-DictionaryEqual -Expected $expected_entry
+ }
+ else {
+ Assert-Equal -Actual $actual_entry -Expected $expected_entry
+ }
+ }
+ }
+ else {
+ Assert-Equal -Actual $actual_value -Expected $expected_value
+ }
+ }
+ foreach ($expected_key in $expected_keys) {
+ ($expected_key -cin $actual_keys) | Assert-Equal -Expected $true
+ }
+ }
+}
+
+$process = [Ansible.Privilege.PrivilegeUtil]::GetCurrentProcess()
+
+$tests = @{
+ "Check valid privilege name" = {
+ $actual = [Ansible.Privilege.PrivilegeUtil]::CheckPrivilegeName("SeTcbPrivilege")
+ $actual | Assert-Equal -Expected $true
+ }
+
+ "Check invalid privilege name" = {
+ $actual = [Ansible.Privilege.PrivilegeUtil]::CheckPrivilegeName("SeFake")
+ $actual | Assert-Equal -Expected $false
+ }
+
+ "Disable a privilege" = {
+ # Ensure the privilege is enabled at the start
+ [Ansible.Privilege.PrivilegeUtil]::EnablePrivilege($process, "SeTimeZonePrivilege") > $null
+
+ $actual = [Ansible.Privilege.PrivilegeUtil]::DisablePrivilege($process, "SeTimeZonePrivilege")
+ $actual.GetType().Name | Assert-Equal -Expected 'Dictionary`2'
+ $actual.Count | Assert-Equal -Expected 1
+ $actual.SeTimeZonePrivilege | Assert-Equal -Expected $true
+
+ # Disable again
+ $actual = [Ansible.Privilege.PrivilegeUtil]::DisablePrivilege($process, "SeTimeZonePrivilege")
+ $actual.GetType().Name | Assert-Equal -Expected 'Dictionary`2'
+ $actual.Count | Assert-Equal -Expected 0
+ }
+
+ "Enable a privilege" = {
+ # Ensure the privilege is disabled at the start
+ [Ansible.Privilege.PrivilegeUtil]::DisablePrivilege($process, "SeTimeZonePrivilege") > $null
+
+ $actual = [Ansible.Privilege.PrivilegeUtil]::EnablePrivilege($process, "SeTimeZonePrivilege")
+ $actual.GetType().Name | Assert-Equal -Expected 'Dictionary`2'
+ $actual.Count | Assert-Equal -Expected 1
+ $actual.SeTimeZonePrivilege | Assert-Equal -Expected $false
+
+ # Disable again
+ $actual = [Ansible.Privilege.PrivilegeUtil]::EnablePrivilege($process, "SeTimeZonePrivilege")
+ $actual.GetType().Name | Assert-Equal -Expected 'Dictionary`2'
+ $actual.Count | Assert-Equal -Expected 0
+ }
+
+ "Disable and revert privileges" = {
+ $current_state = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+
+ $previous_state = [Ansible.Privilege.PrivilegeUtil]::DisableAllPrivileges($process)
+ $previous_state.GetType().Name | Assert-Equal -Expected 'Dictionary`2'
+ foreach ($previous_state_entry in $previous_state.GetEnumerator()) {
+ $previous_state_entry.Value | Assert-Equal -Expected $true
+ }
+
+ # Disable again
+ $previous_state2 = [Ansible.Privilege.PrivilegeUtil]::DisableAllPrivileges($process)
+ $previous_state2.Count | Assert-Equal -Expected 0
+
+ $actual = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+ foreach ($actual_entry in $actual.GetEnumerator()) {
+ $actual_entry.Value -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+ }
+
+ [Ansible.Privilege.PrivilegeUtil]::SetTokenPrivileges($process, $previous_state) > $null
+ $actual = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+ $actual | Assert-DictionaryEqual -Expected $current_state
+ }
+
+ "Remove a privilege" = {
+ [Ansible.Privilege.PrivilegeUtil]::RemovePrivilege($process, "SeUndockPrivilege") > $null
+ $actual = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+ $actual.ContainsKey("SeUndockPrivilege") | Assert-Equal -Expected $false
+ }
+
+ "Test Enabler" = {
+ # Disable privilege at the start
+ $new_state = @{
+ SeTimeZonePrivilege = $false
+ SeShutdownPrivilege = $false
+ SeIncreaseWorkingSetPrivilege = $false
+ }
+ [Ansible.Privilege.PrivilegeUtil]::SetTokenPrivileges($process, $new_state) > $null
+ $check_state = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+ $check_state.SeTimeZonePrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+ $check_state.SeShutdownPrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+ $check_state.SeIncreaseWorkingSetPrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+
+ # Check that strict = false won't validate privileges not held but activates the ones we want
+ $enabler = New-Object -TypeName Ansible.Privilege.PrivilegeEnabler -ArgumentList $false, "SeTimeZonePrivilege", "SeShutdownPrivilege", "SeTcbPrivilege"
+ $actual = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+ $actual.SeTimeZonePrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled |
+ Assert-Equal -Expected ([Ansible.Privilege.PrivilegeAttributes]::Enabled)
+ $actual.SeShutdownPrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled |
+ Assert-Equal -Expected ([Ansible.Privilege.PrivilegeAttributes]::Enabled)
+ $actual.SeIncreaseWorkingSetPrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+ $actual.ContainsKey("SeTcbPrivilege") | Assert-Equal -Expected $false
+
+ # Now verify a no-op enabler will not rever back to disabled
+ $enabler2 = New-Object -TypeName Ansible.Privilege.PrivilegeEnabler -ArgumentList $false, "SeTimeZonePrivilege", "SeShutdownPrivilege", "SeTcbPrivilege"
+ $enabler2.Dispose()
+ $actual = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+ $actual.SeTimeZonePrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled |
+ Assert-Equal -Expected ([Ansible.Privilege.PrivilegeAttributes]::Enabled)
+ $actual.SeShutdownPrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled |
+ Assert-Equal -Expected ([Ansible.Privilege.PrivilegeAttributes]::Enabled)
+
+ # Verify that when disposing the object the privileges are reverted
+ $enabler.Dispose()
+ $actual = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+ $actual.SeTimeZonePrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+ $actual.SeShutdownPrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+ }
+
+ "Test Enabler strict" = {
+ # Disable privilege at the start
+ $new_state = @{
+ SeTimeZonePrivilege = $false
+ SeShutdownPrivilege = $false
+ SeIncreaseWorkingSetPrivilege = $false
+ }
+ [Ansible.Privilege.PrivilegeUtil]::SetTokenPrivileges($process, $new_state) > $null
+ $check_state = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+ $check_state.SeTimeZonePrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+ $check_state.SeShutdownPrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+ $check_state.SeIncreaseWorkingSetPrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+
+ # Check that strict = false won't validate privileges not held but activates the ones we want
+ $enabler = New-Object -TypeName Ansible.Privilege.PrivilegeEnabler -ArgumentList $true, "SeTimeZonePrivilege", "SeShutdownPrivilege"
+ $actual = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+ $actual.SeTimeZonePrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled |
+ Assert-Equal -Expected ([Ansible.Privilege.PrivilegeAttributes]::Enabled)
+ $actual.SeShutdownPrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled |
+ Assert-Equal -Expected ([Ansible.Privilege.PrivilegeAttributes]::Enabled)
+ $actual.SeIncreaseWorkingSetPrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+
+ # Now verify a no-op enabler will not rever back to disabled
+ $enabler2 = New-Object -TypeName Ansible.Privilege.PrivilegeEnabler -ArgumentList $true, "SeTimeZonePrivilege", "SeShutdownPrivilege"
+ $enabler2.Dispose()
+ $actual = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+ $actual.SeTimeZonePrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled |
+ Assert-Equal -Expected ([Ansible.Privilege.PrivilegeAttributes]::Enabled)
+ $actual.SeShutdownPrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled |
+ Assert-Equal -Expected ([Ansible.Privilege.PrivilegeAttributes]::Enabled)
+
+ # Verify that when disposing the object the privileges are reverted
+ $enabler.Dispose()
+ $actual = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+ $actual.SeTimeZonePrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+ $actual.SeShutdownPrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+ }
+
+ "Test Enabler invalid privilege" = {
+ $failed = $false
+ try {
+ New-Object -TypeName Ansible.Privilege.PrivilegeEnabler -ArgumentList $false, "SeTimeZonePrivilege", "SeFake"
+ }
+ catch {
+ $failed = $true
+ $expected = "Failed to enable privilege(s) SeTimeZonePrivilege, SeFake (A specified privilege does not exist, Win32ErrorCode 1313)"
+ $_.Exception.InnerException.Message | Assert-Equal -Expected $expected
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "Test Enabler strict failure" = {
+ # Start disabled
+ [Ansible.Privilege.PrivilegeUtil]::DisablePrivilege($process, "SeTimeZonePrivilege") > $null
+ $check_state = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+ $check_state.SeTimeZonePrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+
+ $failed = $false
+ try {
+ New-Object -TypeName Ansible.Privilege.PrivilegeEnabler -ArgumentList $true, "SeTimeZonePrivilege", "SeTcbPrivilege"
+ }
+ catch {
+ $failed = $true
+ $expected = -join @(
+ "Failed to enable privilege(s) SeTimeZonePrivilege, SeTcbPrivilege "
+ "(Not all privileges or groups referenced are assigned to the caller, Win32ErrorCode 1300)"
+ )
+ $_.Exception.InnerException.Message | Assert-Equal -Expected $expected
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+}
+
+foreach ($test_impl in $tests.GetEnumerator()) {
+ $test = $test_impl.Key
+ &$test_impl.Value
+}
+
+$module.Result.data = "success"
+$module.ExitJson()
+
diff --git a/test/integration/targets/module_utils_Ansible.Privilege/tasks/main.yml b/test/integration/targets/module_utils_Ansible.Privilege/tasks/main.yml
new file mode 100644
index 0000000..888394d
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Privilege/tasks/main.yml
@@ -0,0 +1,9 @@
+---
+- name: test Ansible.Privilege.cs
+ ansible_privilege_tests:
+ register: ansible_privilege_test
+
+- name: assert test Ansible.Privilege.cs
+ assert:
+ that:
+ - ansible_privilege_test.data == "success"
diff --git a/test/integration/targets/module_utils_Ansible.Process/aliases b/test/integration/targets/module_utils_Ansible.Process/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Process/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.Process/library/ansible_process_tests.ps1 b/test/integration/targets/module_utils_Ansible.Process/library/ansible_process_tests.ps1
new file mode 100644
index 0000000..bca7eb1
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Process/library/ansible_process_tests.ps1
@@ -0,0 +1,242 @@
+#!powershell
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#AnsibleRequires -CSharpUtil Ansible.Process
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
+
+Function Assert-Equal {
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)][AllowNull()]$Actual,
+ [Parameter(Mandatory = $true, Position = 0)][AllowNull()]$Expected
+ )
+
+ process {
+ $matched = $false
+ if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array]) {
+ $Actual.Count | Assert-Equal -Expected $Expected.Count
+ for ($i = 0; $i -lt $Actual.Count; $i++) {
+ $actual_value = $Actual[$i]
+ $expected_value = $Expected[$i]
+ Assert-Equal -Actual $actual_value -Expected $expected_value
+ }
+ $matched = $true
+ }
+ else {
+ $matched = $Actual -ceq $Expected
+ }
+
+ if (-not $matched) {
+ if ($Actual -is [PSObject]) {
+ $Actual = $Actual.ToString()
+ }
+
+ $call_stack = (Get-PSCallStack)[1]
+ $module.Result.test = $test
+ $module.Result.actual = $Actual
+ $module.Result.expected = $Expected
+ $module.Result.line = $call_stack.ScriptLineNumber
+ $module.Result.method = $call_stack.Position.Text
+ $module.FailJson("AssertionError: actual != expected")
+ }
+ }
+}
+
+$tests = @{
+ "ParseCommandLine empty string" = {
+ $expected = @((Get-Process -Id $pid).Path)
+ $actual = [Ansible.Process.ProcessUtil]::ParseCommandLine("")
+ Assert-Equal -Actual $actual -Expected $expected
+ }
+
+ "ParseCommandLine single argument" = {
+ $expected = @("powershell.exe")
+ $actual = [Ansible.Process.ProcessUtil]::ParseCommandLine("powershell.exe")
+ Assert-Equal -Actual $actual -Expected $expected
+ }
+
+ "ParseCommandLine multiple arguments" = {
+ $expected = @("powershell.exe", "-File", "C:\temp\script.ps1")
+ $actual = [Ansible.Process.ProcessUtil]::ParseCommandLine("powershell.exe -File C:\temp\script.ps1")
+ Assert-Equal -Actual $actual -Expected $expected
+ }
+
+ "ParseCommandLine comples arguments" = {
+ $expected = @('abc', 'd', 'ef gh', 'i\j', 'k"l', 'm\n op', 'ADDLOCAL=qr, s', 'tuv\', 'w''x', 'yz')
+ $actual = [Ansible.Process.ProcessUtil]::ParseCommandLine('abc d "ef gh" i\j k\"l m\\"n op" ADDLOCAL="qr, s" tuv\ w''x yz')
+ Assert-Equal -Actual $actual -Expected $expected
+ }
+
+ "SearchPath normal" = {
+ $expected = "$($env:SystemRoot)\System32\WindowsPowerShell\v1.0\powershell.exe"
+ $actual = [Ansible.Process.ProcessUtil]::SearchPath("powershell.exe")
+ $actual | Assert-Equal -Expected $expected
+ }
+
+ "SearchPath missing" = {
+ $failed = $false
+ try {
+ [Ansible.Process.ProcessUtil]::SearchPath("fake.exe")
+ }
+ catch {
+ $failed = $true
+ $_.Exception.InnerException.GetType().FullName | Assert-Equal -Expected "System.IO.FileNotFoundException"
+ $expected = 'Exception calling "SearchPath" with "1" argument(s): "Could not find file ''fake.exe''."'
+ $_.Exception.Message | Assert-Equal -Expected $expected
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "CreateProcess basic" = {
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess("whoami.exe")
+ $actual.GetType().FullName | Assert-Equal -Expected "Ansible.Process.Result"
+ $actual.StandardOut | Assert-Equal -Expected "$(&whoami.exe)`r`n"
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcess stderr" = {
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess("powershell.exe [System.Console]::Error.WriteLine('hi')")
+ $actual.StandardOut | Assert-Equal -Expected ""
+ $actual.StandardError | Assert-Equal -Expected "hi`r`n"
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcess exit code" = {
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess("powershell.exe exit 10")
+ $actual.StandardOut | Assert-Equal -Expected ""
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 10
+ }
+
+ "CreateProcess bad executable" = {
+ $failed = $false
+ try {
+ [Ansible.Process.ProcessUtil]::CreateProcess("fake.exe")
+ }
+ catch {
+ $failed = $true
+ $_.Exception.InnerException.GetType().FullName | Assert-Equal -Expected "Ansible.Process.Win32Exception"
+ $expected = 'Exception calling "CreateProcess" with "1" argument(s): "CreateProcessW() failed '
+ $expected += '(The system cannot find the file specified, Win32ErrorCode 2)"'
+ $_.Exception.Message | Assert-Equal -Expected $expected
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "CreateProcess with unicode" = {
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess("cmd.exe /c echo 💩 café")
+ $actual.StandardOut | Assert-Equal -Expected "💩 café`r`n"
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, "cmd.exe /c echo 💩 café", $null, $null)
+ $actual.StandardOut | Assert-Equal -Expected "💩 café`r`n"
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcess without working dir" = {
+ $expected = $pwd.Path + "`r`n"
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe $pwd.Path', $null, $null)
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcess with working dir" = {
+ $expected = "C:\Windows`r`n"
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe $pwd.Path', "C:\Windows", $null)
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcess without environment" = {
+ $expected = "$($env:USERNAME)`r`n"
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe $env:TEST; $env:USERNAME', $null, $null)
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcess with environment" = {
+ $env_vars = @{
+ TEST = "tesTing"
+ TEST2 = "Testing 2"
+ }
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'cmd.exe /c set', $null, $env_vars)
+ ("TEST=tesTing" -cin $actual.StandardOut.Split("`r`n")) | Assert-Equal -Expected $true
+ ("TEST2=Testing 2" -cin $actual.StandardOut.Split("`r`n")) | Assert-Equal -Expected $true
+ ("USERNAME=$($env:USERNAME)" -cnotin $actual.StandardOut.Split("`r`n")) | Assert-Equal -Expected $true
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcess with string stdin" = {
+ $expected = "input value`r`n`r`n"
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe [System.Console]::In.ReadToEnd()',
+ $null, $null, "input value")
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcess with string stdin and newline" = {
+ $expected = "input value`r`n`r`n"
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe [System.Console]::In.ReadToEnd()',
+ $null, $null, "input value`r`n")
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcess with byte stdin" = {
+ $expected = "input value`r`n"
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe [System.Console]::In.ReadToEnd()',
+ $null, $null, [System.Text.Encoding]::UTF8.GetBytes("input value"))
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcess with byte stdin and newline" = {
+ $expected = "input value`r`n`r`n"
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe [System.Console]::In.ReadToEnd()',
+ $null, $null, [System.Text.Encoding]::UTF8.GetBytes("input value`r`n"))
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcess with lpApplicationName" = {
+ $expected = "abc`r`n"
+ $full_path = "$($env:SystemRoot)\System32\WindowsPowerShell\v1.0\powershell.exe"
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess($full_path, "Write-Output 'abc'", $null, $null)
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess($full_path, "powershell.exe Write-Output 'abc'", $null, $null)
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcess with unicode and us-ascii encoding" = {
+ # Coverage breaks due to script parsing encoding issues with unicode chars, just use the code point instead
+ $poop = [System.Char]::ConvertFromUtf32(0xE05A)
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, "cmd.exe /c echo $poop café", $null, $null, '', 'us-ascii')
+ $actual.StandardOut | Assert-Equal -Expected "??? caf??`r`n"
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+}
+
+foreach ($test_impl in $tests.GetEnumerator()) {
+ $test = $test_impl.Key
+ &$test_impl.Value
+}
+
+$module.Result.data = "success"
+$module.ExitJson()
diff --git a/test/integration/targets/module_utils_Ansible.Process/tasks/main.yml b/test/integration/targets/module_utils_Ansible.Process/tasks/main.yml
new file mode 100644
index 0000000..13a5c16
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Process/tasks/main.yml
@@ -0,0 +1,9 @@
+---
+- name: test Ansible.Process.cs
+ ansible_process_tests:
+ register: ansible_process_tests
+
+- name: assert test Ansible.Process.cs
+ assert:
+ that:
+ - ansible_process_tests.data == "success"
diff --git a/test/integration/targets/module_utils_Ansible.Service/aliases b/test/integration/targets/module_utils_Ansible.Service/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Service/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.Service/library/ansible_service_tests.ps1 b/test/integration/targets/module_utils_Ansible.Service/library/ansible_service_tests.ps1
new file mode 100644
index 0000000..dab42d4
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Service/library/ansible_service_tests.ps1
@@ -0,0 +1,953 @@
+#!powershell
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#AnsibleRequires -CSharpUtil Ansible.Service
+#Requires -Module Ansible.ModuleUtils.ArgvParser
+#Requires -Module Ansible.ModuleUtils.CommandUtil
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
+
+$path = "$env:SystemRoot\System32\svchost.exe"
+
+Function Assert-Equal {
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)][AllowNull()]$Actual,
+ [Parameter(Mandatory = $true, Position = 0)][AllowNull()]$Expected
+ )
+
+ process {
+ $matched = $false
+ if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array] -or $Actual -is [System.Collections.IList]) {
+ $Actual.Count | Assert-Equal -Expected $Expected.Count
+ for ($i = 0; $i -lt $Actual.Count; $i++) {
+ $actualValue = $Actual[$i]
+ $expectedValue = $Expected[$i]
+ Assert-Equal -Actual $actualValue -Expected $expectedValue
+ }
+ $matched = $true
+ }
+ else {
+ $matched = $Actual -ceq $Expected
+ }
+
+ if (-not $matched) {
+ if ($Actual -is [PSObject]) {
+ $Actual = $Actual.ToString()
+ }
+
+ $call_stack = (Get-PSCallStack)[1]
+ $module.Result.test = $test
+ $module.Result.actual = $Actual
+ $module.Result.expected = $Expected
+ $module.Result.line = $call_stack.ScriptLineNumber
+ $module.Result.method = $call_stack.Position.Text
+
+ $module.FailJson("AssertionError: actual != expected")
+ }
+ }
+}
+
+Function Invoke-Sc {
+ [CmdletBinding()]
+ param (
+ [Parameter(Mandatory = $true)]
+ [String]
+ $Action,
+
+ [Parameter(Mandatory = $true)]
+ [String]
+ $Name,
+
+ [Object]
+ $Arguments
+ )
+
+ $commandArgs = [System.Collections.Generic.List[String]]@("sc.exe", $Action, $Name)
+ if ($null -ne $Arguments) {
+ if ($Arguments -is [System.Collections.IDictionary]) {
+ foreach ($arg in $Arguments.GetEnumerator()) {
+ $commandArgs.Add("$($arg.Key)=")
+ $commandArgs.Add($arg.Value)
+ }
+ }
+ else {
+ foreach ($arg in $Arguments) {
+ $commandArgs.Add($arg)
+ }
+ }
+ }
+
+ $command = Argv-ToString -arguments $commandArgs
+
+ $res = Run-Command -command $command
+ if ($res.rc -ne 0) {
+ $module.Result.rc = $res.rc
+ $module.Result.stdout = $res.stdout
+ $module.Result.stderr = $res.stderr
+ $module.FailJson("Failed to invoke sc with: $command")
+ }
+
+ $info = @{ Name = $Name }
+
+ if ($Action -eq 'qtriggerinfo') {
+ # qtriggerinfo is in a different format which requires some manual parsing from the norm.
+ $info.Triggers = [System.Collections.Generic.List[PSObject]]@()
+ }
+
+ $currentKey = $null
+ $qtriggerSection = @{}
+ $res.stdout -split "`r`n" | Foreach-Object -Process {
+ $line = $_.Trim()
+
+ if ($Action -eq 'qtriggerinfo' -and $line -in @('START SERVICE', 'STOP SERVICE')) {
+ if ($qtriggerSection.Count -gt 0) {
+ $info.Triggers.Add([PSCustomObject]$qtriggerSection)
+ $qtriggerSection = @{}
+ }
+
+ $qtriggerSection = @{
+ Action = $line
+ }
+ }
+
+ if (-not $line -or (-not $line.Contains(':') -and $null -eq $currentKey)) {
+ return
+ }
+
+ $lineSplit = $line.Split(':', 2)
+ if ($lineSplit.Length -eq 2) {
+ $k = $lineSplit[0].Trim()
+ if (-not $k) {
+ $k = $currentKey
+ }
+
+ $v = $lineSplit[1].Trim()
+ }
+ else {
+ $k = $currentKey
+ $v = $line
+ }
+
+ if ($qtriggerSection.Count -gt 0) {
+ if ($k -eq 'DATA') {
+ $qtriggerSection.Data.Add($v)
+ }
+ else {
+ $qtriggerSection.Type = $k
+ $qtriggerSection.SubType = $v
+ $qtriggerSection.Data = [System.Collections.Generic.List[String]]@()
+ }
+ }
+ else {
+ if ($info.ContainsKey($k)) {
+ if ($info[$k] -isnot [System.Collections.Generic.List[String]]) {
+ $info[$k] = [System.Collections.Generic.List[String]]@($info[$k])
+ }
+ $info[$k].Add($v)
+ }
+ else {
+ $currentKey = $k
+ $info[$k] = $v
+ }
+ }
+ }
+
+ if ($qtriggerSection.Count -gt 0) {
+ $info.Triggers.Add([PSCustomObject]$qtriggerSection)
+ }
+
+ [PSCustomObject]$info
+}
+
+$tests = [Ordered]@{
+ "Props on service created by New-Service" = {
+ $actual = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+
+ $actual.ServiceName | Assert-Equal -Expected $serviceName
+ $actual.ServiceType | Assert-Equal -Expected ([Ansible.Service.ServiceType]::Win32OwnProcess)
+ $actual.StartType | Assert-Equal -Expected ([Ansible.Service.ServiceStartType]::DemandStart)
+ $actual.ErrorControl | Assert-Equal -Expected ([Ansible.Service.ErrorControl]::Normal)
+ $actual.Path | Assert-Equal -Expected ('"{0}"' -f $path)
+ $actual.LoadOrderGroup | Assert-Equal -Expected ""
+ $actual.DependentOn.Count | Assert-Equal -Expected 0
+ $actual.Account | Assert-Equal -Expected (
+ [System.Security.Principal.SecurityIdentifier]'S-1-5-18').Translate([System.Security.Principal.NTAccount]
+ )
+ $actual.DisplayName | Assert-Equal -Expected $serviceName
+ $actual.Description | Assert-Equal -Expected $null
+ $actual.FailureActions.ResetPeriod | Assert-Equal -Expected 0
+ $actual.FailureActions.RebootMsg | Assert-Equal -Expected $null
+ $actual.FailureActions.Command | Assert-Equal -Expected $null
+ $actual.FailureActions.Actions.Count | Assert-Equal -Expected 0
+ $actual.FailureActionsOnNonCrashFailures | Assert-Equal -Expected $false
+ $actual.ServiceSidInfo | Assert-Equal -Expected ([Ansible.Service.ServiceSidInfo]::None)
+ $actual.RequiredPrivileges.Count | Assert-Equal -Expected 0
+ # Cannot test default values as it differs per OS version
+ $null -ne $actual.PreShutdownTimeout | Assert-Equal -Expected $true
+ $actual.Triggers.Count | Assert-Equal -Expected 0
+ $actual.PreferredNode | Assert-Equal -Expected $null
+ if ([Environment]::OSVersion.Version -ge [Version]'6.3') {
+ $actual.LaunchProtection | Assert-Equal -Expected ([Ansible.Service.LaunchProtection]::None)
+ }
+ else {
+ $actual.LaunchProtection | Assert-Equal -Expected $null
+ }
+ $actual.State | Assert-Equal -Expected ([Ansible.Service.ServiceStatus]::Stopped)
+ $actual.Win32ExitCode | Assert-Equal -Expected 1077 # ERROR_SERVICE_NEVER_STARTED
+ $actual.ServiceExitCode | Assert-Equal -Expected 0
+ $actual.Checkpoint | Assert-Equal -Expected 0
+ $actual.WaitHint | Assert-Equal -Expected 0
+ $actual.ProcessId | Assert-Equal -Expected 0
+ $actual.ServiceFlags | Assert-Equal -Expected ([Ansible.Service.ServiceFlags]::None)
+ $actual.DependedBy.Count | Assert-Equal 0
+ }
+
+ "Service creation through util" = {
+ $testName = "$($serviceName)_2"
+ $actual = [Ansible.Service.Service]::Create($testName, '"{0}"' -f $path)
+
+ try {
+ $cmdletService = Get-Service -Name $testName -ErrorAction SilentlyContinue
+ $null -ne $cmdletService | Assert-Equal -Expected $true
+
+ $actual.ServiceName | Assert-Equal -Expected $testName
+ $actual.ServiceType | Assert-Equal -Expected ([Ansible.Service.ServiceType]::Win32OwnProcess)
+ $actual.StartType | Assert-Equal -Expected ([Ansible.Service.ServiceStartType]::DemandStart)
+ $actual.ErrorControl | Assert-Equal -Expected ([Ansible.Service.ErrorControl]::Normal)
+ $actual.Path | Assert-Equal -Expected ('"{0}"' -f $path)
+ $actual.LoadOrderGroup | Assert-Equal -Expected ""
+ $actual.DependentOn.Count | Assert-Equal -Expected 0
+ $actual.Account | Assert-Equal -Expected (
+ [System.Security.Principal.SecurityIdentifier]'S-1-5-18').Translate([System.Security.Principal.NTAccount]
+ )
+ $actual.DisplayName | Assert-Equal -Expected $testName
+ $actual.Description | Assert-Equal -Expected $null
+ $actual.FailureActions.ResetPeriod | Assert-Equal -Expected 0
+ $actual.FailureActions.RebootMsg | Assert-Equal -Expected $null
+ $actual.FailureActions.Command | Assert-Equal -Expected $null
+ $actual.FailureActions.Actions.Count | Assert-Equal -Expected 0
+ $actual.FailureActionsOnNonCrashFailures | Assert-Equal -Expected $false
+ $actual.ServiceSidInfo | Assert-Equal -Expected ([Ansible.Service.ServiceSidInfo]::None)
+ $actual.RequiredPrivileges.Count | Assert-Equal -Expected 0
+ $null -ne $actual.PreShutdownTimeout | Assert-Equal -Expected $true
+ $actual.Triggers.Count | Assert-Equal -Expected 0
+ $actual.PreferredNode | Assert-Equal -Expected $null
+ if ([Environment]::OSVersion.Version -ge [Version]'6.3') {
+ $actual.LaunchProtection | Assert-Equal -Expected ([Ansible.Service.LaunchProtection]::None)
+ }
+ else {
+ $actual.LaunchProtection | Assert-Equal -Expected $null
+ }
+ $actual.State | Assert-Equal -Expected ([Ansible.Service.ServiceStatus]::Stopped)
+ $actual.Win32ExitCode | Assert-Equal -Expected 1077 # ERROR_SERVICE_NEVER_STARTED
+ $actual.ServiceExitCode | Assert-Equal -Expected 0
+ $actual.Checkpoint | Assert-Equal -Expected 0
+ $actual.WaitHint | Assert-Equal -Expected 0
+ $actual.ProcessId | Assert-Equal -Expected 0
+ $actual.ServiceFlags | Assert-Equal -Expected ([Ansible.Service.ServiceFlags]::None)
+ $actual.DependedBy.Count | Assert-Equal 0
+ }
+ finally {
+ $actual.Delete()
+ }
+ }
+
+ "Fail to open non-existing service" = {
+ $failed = $false
+ try {
+ $null = New-Object -TypeName Ansible.Service.Service -ArgumentList 'fake_service'
+ }
+ catch [Ansible.Service.ServiceManagerException] {
+ # 1060 == ERROR_SERVICE_DOES_NOT_EXIST
+ $_.Exception.Message -like '*Win32ErrorCode 1060 - 0x00000424*' | Assert-Equal -Expected $true
+ $failed = $true
+ }
+
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "Open with specific access rights" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList @(
+ $serviceName, [Ansible.Service.ServiceRights]'QueryConfig, QueryStatus'
+ )
+
+ # QueryStatus can get the status
+ $service.State | Assert-Equal -Expected ([Ansible.Service.ServiceStatus]::Stopped)
+
+ # Should fail to get the config because we did not request that right
+ $failed = $false
+ try {
+ $service.Path = 'fail'
+ }
+ catch [Ansible.Service.ServiceManagerException] {
+ # 5 == ERROR_ACCESS_DENIED
+ $_.Exception.Message -like '*Win32ErrorCode 5 - 0x00000005*' | Assert-Equal -Expected $true
+ $failed = $true
+ }
+
+ $failed | Assert-Equal -Expected $true
+
+ }
+
+ "Modfiy ServiceType" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.ServiceType = [Ansible.Service.ServiceType]::Win32ShareProcess
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.ServiceType | Assert-Equal -Expected ([Ansible.Service.ServiceType]::Win32ShareProcess)
+ $actual.TYPE | Assert-Equal -Expected "20 WIN32_SHARE_PROCESS"
+
+ $null = Invoke-Sc -Action config -Name $serviceName -Arguments @{type = "own" }
+ $service.Refresh()
+ $service.ServiceType | Assert-Equal -Expected ([Ansible.Service.ServiceType]::Win32OwnProcess)
+ }
+
+ "Create desktop interactive service" = {
+ $service = New-Object -Typename Ansible.Service.Service -ArgumentList $serviceName
+ $service.ServiceType = [Ansible.Service.ServiceType]'Win32OwnProcess, InteractiveProcess'
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $actual.TYPE | Assert-Equal -Expected "110 WIN32_OWN_PROCESS (interactive)"
+ $service.ServiceType | Assert-Equal -Expected ([Ansible.Service.ServiceType]'Win32OwnProcess, InteractiveProcess')
+
+ # Change back from interactive process
+ $service.ServiceType = [Ansible.Service.ServiceType]::Win32OwnProcess
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $actual.TYPE | Assert-Equal -Expected "10 WIN32_OWN_PROCESS"
+ $service.ServiceType | Assert-Equal -Expected ([Ansible.Service.ServiceType]::Win32OwnProcess)
+
+ $service.Account = [System.Security.Principal.SecurityIdentifier]'S-1-5-20'
+
+ $failed = $false
+ try {
+ $service.ServiceType = [Ansible.Service.ServiceType]'Win32OwnProcess, InteractiveProcess'
+ }
+ catch [Ansible.Service.ServiceManagerException] {
+ $failed = $true
+ $_.Exception.NativeErrorCode | Assert-Equal -Expected 87 # ERROR_INVALID_PARAMETER
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $actual.TYPE | Assert-Equal -Expected "10 WIN32_OWN_PROCESS"
+ }
+
+ "Modify StartType" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.StartType = [Ansible.Service.ServiceStartType]::Disabled
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.StartType | Assert-Equal -Expected ([Ansible.Service.ServiceStartType]::Disabled)
+ $actual.START_TYPE | Assert-Equal -Expected "4 DISABLED"
+
+ $null = Invoke-Sc -Action config -Name $serviceName -Arguments @{start = "demand" }
+ $service.Refresh()
+ $service.StartType | Assert-Equal -Expected ([Ansible.Service.ServiceStartType]::DemandStart)
+ }
+
+ "Modify StartType auto delayed" = {
+ # Delayed start type is a modifier of the AutoStart type. It uses a separate config entry to define and this
+ # makes sure the util does that correctly from various types and back.
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.StartType = [Ansible.Service.ServiceStartType]::Disabled # Start from Disabled
+
+ # Disabled -> Auto Start Delayed
+ $service.StartType = [Ansible.Service.ServiceStartType]::AutoStartDelayed
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.StartType | Assert-Equal -Expected ([Ansible.Service.ServiceStartType]::AutoStartDelayed)
+ $actual.START_TYPE | Assert-Equal -Expected "2 AUTO_START (DELAYED)"
+
+ # Auto Start Delayed -> Auto Start
+ $service.StartType = [Ansible.Service.ServiceStartType]::AutoStart
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.StartType | Assert-Equal -Expected ([Ansible.Service.ServiceStartType]::AutoStart)
+ $actual.START_TYPE | Assert-Equal -Expected "2 AUTO_START"
+
+ # Auto Start -> Auto Start Delayed
+ $service.StartType = [Ansible.Service.ServiceStartType]::AutoStartDelayed
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.StartType | Assert-Equal -Expected ([Ansible.Service.ServiceStartType]::AutoStartDelayed)
+ $actual.START_TYPE | Assert-Equal -Expected "2 AUTO_START (DELAYED)"
+
+ # Auto Start Delayed -> Manual
+ $service.StartType = [Ansible.Service.ServiceStartType]::DemandStart
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.StartType | Assert-Equal -Expected ([Ansible.Service.ServiceStartType]::DemandStart)
+ $actual.START_TYPE | Assert-Equal -Expected "3 DEMAND_START"
+ }
+
+ "Modify ErrorControl" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.ErrorControl = [Ansible.Service.ErrorControl]::Severe
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.ErrorControl | Assert-Equal -Expected ([Ansible.Service.ErrorControl]::Severe)
+ $actual.ERROR_CONTROL | Assert-Equal -Expected "2 SEVERE"
+
+ $null = Invoke-Sc -Action config -Name $serviceName -Arguments @{error = "ignore" }
+ $service.Refresh()
+ $service.ErrorControl | Assert-Equal -Expected ([Ansible.Service.ErrorControl]::Ignore)
+ }
+
+ "Modify Path" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.Path = "Fake path"
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.Path | Assert-Equal -Expected "Fake path"
+ $actual.BINARY_PATH_NAME | Assert-Equal -Expected "Fake path"
+
+ $null = Invoke-Sc -Action config -Name $serviceName -Arguments @{binpath = "other fake path" }
+ $service.Refresh()
+ $service.Path | Assert-Equal -Expected "other fake path"
+ }
+
+ "Modify LoadOrderGroup" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.LoadOrderGroup = "my group"
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.LoadOrderGroup | Assert-Equal -Expected "my group"
+ $actual.LOAD_ORDER_GROUP | Assert-Equal -Expected "my group"
+
+ $null = Invoke-Sc -Action config -Name $serviceName -Arguments @{group = "" }
+ $service.Refresh()
+ $service.LoadOrderGroup | Assert-Equal -Expected ""
+ }
+
+ "Modify DependentOn" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.DependentOn = @("HTTP", "WinRM")
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ @(, $service.DependentOn) | Assert-Equal -Expected @("HTTP", "WinRM")
+ @(, $actual.DEPENDENCIES) | Assert-Equal -Expected @("HTTP", "WinRM")
+
+ $null = Invoke-Sc -Action config -Name $serviceName -Arguments @{depend = "" }
+ $service.Refresh()
+ $service.DependentOn.Count | Assert-Equal -Expected 0
+ }
+
+ "Modify Account - service account" = {
+ $systemSid = [System.Security.Principal.SecurityIdentifier]'S-1-5-18'
+ $systemName = $systemSid.Translate([System.Security.Principal.NTAccount])
+ $localSid = [System.Security.Principal.SecurityIdentifier]'S-1-5-19'
+ $localName = $localSid.Translate([System.Security.Principal.NTAccount])
+ $networkSid = [System.Security.Principal.SecurityIdentifier]'S-1-5-20'
+ $networkName = $networkSid.Translate([System.Security.Principal.NTAccount])
+
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.Account = $networkSid
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.Account | Assert-Equal -Expected $networkName
+ $actual.SERVICE_START_NAME | Assert-Equal -Expected $networkName.Value
+
+ $null = Invoke-Sc -Action config -Name $serviceName -Arguments @{obj = $localName.Value }
+ $service.Refresh()
+ $service.Account | Assert-Equal -Expected $localName
+
+ $service.Account = $systemSid
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.Account | Assert-Equal -Expected $systemName
+ $actual.SERVICE_START_NAME | Assert-Equal -Expected "LocalSystem"
+ }
+
+ "Modify Account - user" = {
+ $currentSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
+
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.Account = $currentSid
+ $service.Password = 'password'
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+
+ # When running tests in CI this seems to become .\Administrator
+ if ($service.Account.Value.StartsWith('.\')) {
+ $username = $service.Account.Value.Substring(2, $service.Account.Value.Length - 2)
+ $actualSid = ([System.Security.Principal.NTAccount]"$env:COMPUTERNAME\$username").Translate(
+ [System.Security.Principal.SecurityIdentifier]
+ )
+ }
+ else {
+ $actualSid = $service.Account.Translate([System.Security.Principal.SecurityIdentifier])
+ }
+ $actualSid.Value | Assert-Equal -Expected $currentSid.Value
+ $actual.SERVICE_START_NAME | Assert-Equal -Expected $service.Account.Value
+
+ # Go back to SYSTEM from account
+ $systemSid = [System.Security.Principal.SecurityIdentifier]'S-1-5-18'
+ $service.Account = $systemSid
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.Account | Assert-Equal -Expected $systemSid.Translate([System.Security.Principal.NTAccount])
+ $actual.SERVICE_START_NAME | Assert-Equal -Expected "LocalSystem"
+ }
+
+ "Modify Account - virtual account" = {
+ $account = [System.Security.Principal.NTAccount]"NT SERVICE\$serviceName"
+
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.Account = $account
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.Account | Assert-Equal -Expected $account
+ $actual.SERVICE_START_NAME | Assert-Equal -Expected $account.Value
+ }
+
+ "Modify Account - gMSA" = {
+ # This cannot be tested through CI, only done on manual tests.
+ return
+
+ $gmsaName = [System.Security.Principal.NTAccount]'gMSA$@DOMAIN.LOCAL' # Make sure this is UPN.
+ $gmsaSid = $gmsaName.Translate([System.Security.Principal.SecurityIdentifier])
+ $gmsaNetlogon = $gmsaSid.Translate([System.Security.Principal.NTAccount])
+
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.Account = $gmsaName
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.Account | Assert-Equal -Expected $gmsaName
+ $actual.SERVICE_START_NAME | Assert-Equal -Expected $gmsaName
+
+ # Go from gMSA to account and back to verify the Password doesn't matter.
+ $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
+ $service.Account = $currentUser
+ $service.Password = 'fake password'
+ $service.Password = 'fake password2'
+
+ # Now test in the Netlogon format.
+ $service.Account = $gmsaSid
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.Account | Assert-Equal -Expected $gmsaNetlogon
+ $actual.SERVICE_START_NAME | Assert-Equal -Expected $gmsaNetlogon.Value
+ }
+
+ "Modify DisplayName" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.DisplayName = "Custom Service Name"
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.DisplayName | Assert-Equal -Expected "Custom Service Name"
+ $actual.DISPLAY_NAME | Assert-Equal -Expected "Custom Service Name"
+
+ $null = Invoke-Sc -Action config -Name $serviceName -Arguments @{displayname = "New Service Name" }
+ $service.Refresh()
+ $service.DisplayName | Assert-Equal -Expected "New Service Name"
+ }
+
+ "Modify Description" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.Description = "My custom service description"
+
+ $actual = Invoke-Sc -Action qdescription -Name $serviceName
+ $service.Description | Assert-Equal -Expected "My custom service description"
+ $actual.DESCRIPTION | Assert-Equal -Expected "My custom service description"
+
+ $null = Invoke-Sc -Action description -Name $serviceName -Arguments @(, "new description")
+ $service.Description | Assert-Equal -Expected "new description"
+
+ $service.Description = $null
+
+ $actual = Invoke-Sc -Action qdescription -Name $serviceName
+ $service.Description | Assert-Equal -Expected $null
+ $actual.DESCRIPTION | Assert-Equal -Expected ""
+ }
+
+ "Modify FailureActions" = {
+ $newAction = [Ansible.Service.FailureActions]@{
+ ResetPeriod = 86400
+ RebootMsg = 'Reboot msg'
+ Command = 'Command line'
+ Actions = @(
+ [Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::RunCommand; Delay = 1000 },
+ [Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::RunCommand; Delay = 2000 },
+ [Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::Restart; Delay = 1000 },
+ [Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::Reboot; Delay = 1000 }
+ )
+ }
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.FailureActions = $newAction
+
+ $actual = Invoke-Sc -Action qfailure -Name $serviceName
+ $actual.'RESET_PERIOD (in seconds)' | Assert-Equal -Expected 86400
+ $actual.REBOOT_MESSAGE | Assert-Equal -Expected 'Reboot msg'
+ $actual.COMMAND_LINE | Assert-Equal -Expected 'Command line'
+ $actual.FAILURE_ACTIONS.Count | Assert-Equal -Expected 4
+ $actual.FAILURE_ACTIONS[0] | Assert-Equal -Expected "RUN PROCESS -- Delay = 1000 milliseconds."
+ $actual.FAILURE_ACTIONS[1] | Assert-Equal -Expected "RUN PROCESS -- Delay = 2000 milliseconds."
+ $actual.FAILURE_ACTIONS[2] | Assert-Equal -Expected "RESTART -- Delay = 1000 milliseconds."
+ $actual.FAILURE_ACTIONS[3] | Assert-Equal -Expected "REBOOT -- Delay = 1000 milliseconds."
+ $service.FailureActions.Actions.Count | Assert-Equal -Expected 4
+
+ # Test that we can change individual settings and it doesn't change all
+ $service.FailureActions = [Ansible.Service.FailureActions]@{ResetPeriod = 172800 }
+
+ $actual = Invoke-Sc -Action qfailure -Name $serviceName
+ $actual.'RESET_PERIOD (in seconds)' | Assert-Equal -Expected 172800
+ $actual.REBOOT_MESSAGE | Assert-Equal -Expected 'Reboot msg'
+ $actual.COMMAND_LINE | Assert-Equal -Expected 'Command line'
+ $actual.FAILURE_ACTIONS.Count | Assert-Equal -Expected 4
+ $service.FailureActions.Actions.Count | Assert-Equal -Expected 4
+
+ $service.FailureActions = [Ansible.Service.FailureActions]@{RebootMsg = "New reboot msg" }
+
+ $actual = Invoke-Sc -Action qfailure -Name $serviceName
+ $actual.'RESET_PERIOD (in seconds)' | Assert-Equal -Expected 172800
+ $actual.REBOOT_MESSAGE | Assert-Equal -Expected 'New reboot msg'
+ $actual.COMMAND_LINE | Assert-Equal -Expected 'Command line'
+ $actual.FAILURE_ACTIONS.Count | Assert-Equal -Expected 4
+ $service.FailureActions.Actions.Count | Assert-Equal -Expected 4
+
+ $service.FailureActions = [Ansible.Service.FailureActions]@{Command = "New command line" }
+
+ $actual = Invoke-Sc -Action qfailure -Name $serviceName
+ $actual.'RESET_PERIOD (in seconds)' | Assert-Equal -Expected 172800
+ $actual.REBOOT_MESSAGE | Assert-Equal -Expected 'New reboot msg'
+ $actual.COMMAND_LINE | Assert-Equal -Expected 'New command line'
+ $actual.FAILURE_ACTIONS.Count | Assert-Equal -Expected 4
+ $service.FailureActions.Actions.Count | Assert-Equal -Expected 4
+
+ # Test setting both ResetPeriod and Actions together
+ $service.FailureActions = [Ansible.Service.FailureActions]@{
+ ResetPeriod = 86400
+ Actions = @(
+ [Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::RunCommand; Delay = 5000 },
+ [Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::None; Delay = 0 }
+ )
+ }
+
+ $actual = Invoke-Sc -Action qfailure -Name $serviceName
+ $actual.'RESET_PERIOD (in seconds)' | Assert-Equal -Expected 86400
+ $actual.REBOOT_MESSAGE | Assert-Equal -Expected 'New reboot msg'
+ $actual.COMMAND_LINE | Assert-Equal -Expected 'New command line'
+ # sc.exe does not show the None action it just ends the list, so we verify from get_FailureActions
+ $actual.FAILURE_ACTIONS | Assert-Equal -Expected "RUN PROCESS -- Delay = 5000 milliseconds."
+ $service.FailureActions.Actions.Count | Assert-Equal -Expected 2
+ $service.FailureActions.Actions[1].Type | Assert-Equal -Expected ([Ansible.Service.FailureAction]::None)
+
+ # Test setting just Actions without ResetPeriod
+ $service.FailureActions = [Ansible.Service.FailureActions]@{
+ Actions = [Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::RunCommand; Delay = 10000 }
+ }
+ $actual = Invoke-Sc -Action qfailure -Name $serviceName
+ $actual.'RESET_PERIOD (in seconds)' | Assert-Equal -Expected 86400
+ $actual.REBOOT_MESSAGE | Assert-Equal -Expected 'New reboot msg'
+ $actual.COMMAND_LINE | Assert-Equal -Expected 'New command line'
+ $actual.FAILURE_ACTIONS | Assert-Equal -Expected "RUN PROCESS -- Delay = 10000 milliseconds."
+ $service.FailureActions.Actions.Count | Assert-Equal -Expected 1
+
+ # Test removing all actions
+ $service.FailureActions = [Ansible.Service.FailureActions]@{
+ Actions = @()
+ }
+ $actual = Invoke-Sc -Action qfailure -Name $serviceName
+ $actual.'RESET_PERIOD (in seconds)' | Assert-Equal -Expected 0 # ChangeServiceConfig2W resets this back to 0.
+ $actual.REBOOT_MESSAGE | Assert-Equal -Expected 'New reboot msg'
+ $actual.COMMAND_LINE | Assert-Equal -Expected 'New command line'
+ $actual.PSObject.Properties.Name.Contains('FAILURE_ACTIONS') | Assert-Equal -Expected $false
+ $service.FailureActions.Actions.Count | Assert-Equal -Expected 0
+
+ # Test that we are reading the right values
+ $null = Invoke-Sc -Action failure -Name $serviceName -Arguments @{
+ reset = 172800
+ reboot = "sc reboot msg"
+ command = "sc command line"
+ actions = "run/5000/reboot/800"
+ }
+
+ $actual = $service.FailureActions
+ $actual.ResetPeriod | Assert-Equal -Expected 172800
+ $actual.RebootMsg | Assert-Equal -Expected "sc reboot msg"
+ $actual.Command | Assert-Equal -Expected "sc command line"
+ $actual.Actions.Count | Assert-Equal -Expected 2
+ $actual.Actions[0].Type | Assert-Equal -Expected ([Ansible.Service.FailureAction]::RunCommand)
+ $actual.Actions[0].Delay | Assert-Equal -Expected 5000
+ $actual.Actions[1].Type | Assert-Equal -Expected ([Ansible.Service.FailureAction]::Reboot)
+ $actual.Actions[1].Delay | Assert-Equal -Expected 800
+ }
+
+ "Modify FailureActionsOnNonCrashFailures" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.FailureActionsOnNonCrashFailures = $true
+
+ $actual = Invoke-Sc -Action qfailureflag -Name $serviceName
+ $service.FailureActionsOnNonCrashFailures | Assert-Equal -Expected $true
+ $actual.FAILURE_ACTIONS_ON_NONCRASH_FAILURES | Assert-Equal -Expected "TRUE"
+
+ $null = Invoke-Sc -Action failureflag -Name $serviceName -Arguments @(, 0)
+ $service.FailureActionsOnNonCrashFailures | Assert-Equal -Expected $false
+ }
+
+ "Modify ServiceSidInfo" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.ServiceSidInfo = [Ansible.Service.ServiceSidInfo]::None
+
+ $actual = Invoke-Sc -Action qsidtype -Name $serviceName
+ $service.ServiceSidInfo | Assert-Equal -Expected ([Ansible.Service.ServiceSidInfo]::None)
+ $actual.SERVICE_SID_TYPE | Assert-Equal -Expected 'NONE'
+
+ $null = Invoke-Sc -Action sidtype -Name $serviceName -Arguments @(, 'unrestricted')
+ $service.ServiceSidInfo | Assert-Equal -Expected ([Ansible.Service.ServiceSidInfo]::Unrestricted)
+
+ $service.ServiceSidInfo = [Ansible.Service.ServiceSidInfo]::Restricted
+
+ $actual = Invoke-Sc -Action qsidtype -Name $serviceName
+ $service.ServiceSidInfo | Assert-Equal -Expected ([Ansible.Service.ServiceSidInfo]::Restricted)
+ $actual.SERVICE_SID_TYPE | Assert-Equal -Expected 'RESTRICTED'
+ }
+
+ "Modify RequiredPrivileges" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.RequiredPrivileges = @("SeBackupPrivilege", "SeTcbPrivilege")
+
+ $actual = Invoke-Sc -Action qprivs -Name $serviceName
+ , $service.RequiredPrivileges | Assert-Equal -Expected @("SeBackupPrivilege", "SeTcbPrivilege")
+ , $actual.PRIVILEGES | Assert-Equal -Expected @("SeBackupPrivilege", "SeTcbPrivilege")
+
+ # Ensure setting to $null is the same as an empty array
+ $service.RequiredPrivileges = $null
+
+ $actual = Invoke-Sc -Action qprivs -Name $serviceName
+ , $service.RequiredPrivileges | Assert-Equal -Expected @()
+ , $actual.PRIVILEGES | Assert-Equal -Expected @()
+
+ $service.RequiredPrivileges = @("SeBackupPrivilege", "SeTcbPrivilege")
+ $service.RequiredPrivileges = @()
+
+ $actual = Invoke-Sc -Action qprivs -Name $serviceName
+ , $service.RequiredPrivileges | Assert-Equal -Expected @()
+ , $actual.PRIVILEGES | Assert-Equal -Expected @()
+
+ $null = Invoke-Sc -Action privs -Name $serviceName -Arguments @(, "SeCreateTokenPrivilege/SeRestorePrivilege")
+ , $service.RequiredPrivileges | Assert-Equal -Expected @("SeCreateTokenPrivilege", "SeRestorePrivilege")
+ }
+
+ "Modify PreShutdownTimeout" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.PreShutdownTimeout = 60000
+
+ # sc.exe doesn't seem to have a query argument for this, just get it from the registry
+ $actual = (
+ Get-ItemProperty -LiteralPath "HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName" -Name PreshutdownTimeout
+ ).PreshutdownTimeout
+ $actual | Assert-Equal -Expected 60000
+ }
+
+ "Modify Triggers" = {
+ $service = [Ansible.Service.Service]$serviceName
+ $service.Triggers = @(
+ [Ansible.Service.Trigger]@{
+ Type = [Ansible.Service.TriggerType]::DomainJoin
+ Action = [Ansible.Service.TriggerAction]::ServiceStop
+ SubType = [Guid][Ansible.Service.Trigger]::DOMAIN_JOIN_GUID
+ },
+ [Ansible.Service.Trigger]@{
+ Type = [Ansible.Service.TriggerType]::NetworkEndpoint
+ Action = [Ansible.Service.TriggerAction]::ServiceStart
+ SubType = [Guid][Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID
+ DataItems = [Ansible.Service.TriggerItem]@{
+ Type = [Ansible.Service.TriggerDataType]::String
+ Data = 'my named pipe'
+ }
+ },
+ [Ansible.Service.Trigger]@{
+ Type = [Ansible.Service.TriggerType]::NetworkEndpoint
+ Action = [Ansible.Service.TriggerAction]::ServiceStart
+ SubType = [Guid][Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID
+ DataItems = [Ansible.Service.TriggerItem]@{
+ Type = [Ansible.Service.TriggerDataType]::String
+ Data = 'my named pipe 2'
+ }
+ },
+ [Ansible.Service.Trigger]@{
+ Type = [Ansible.Service.TriggerType]::Custom
+ Action = [Ansible.Service.TriggerAction]::ServiceStart
+ SubType = [Guid]'9bf04e57-05dc-4914-9ed9-84bf992db88c'
+ DataItems = @(
+ [Ansible.Service.TriggerItem]@{
+ Type = [Ansible.Service.TriggerDataType]::Binary
+ Data = [byte[]]@(1, 2, 3, 4)
+ },
+ [Ansible.Service.TriggerItem]@{
+ Type = [Ansible.Service.TriggerDataType]::Binary
+ Data = [byte[]]@(5, 6, 7, 8, 9)
+ }
+ )
+ }
+ [Ansible.Service.Trigger]@{
+ Type = [Ansible.Service.TriggerType]::Custom
+ Action = [Ansible.Service.TriggerAction]::ServiceStart
+ SubType = [Guid]'9fbcfc7e-7581-4d46-913b-53bb15c80c51'
+ DataItems = @(
+ [Ansible.Service.TriggerItem]@{
+ Type = [Ansible.Service.TriggerDataType]::String
+ Data = 'entry 1'
+ },
+ [Ansible.Service.TriggerItem]@{
+ Type = [Ansible.Service.TriggerDataType]::String
+ Data = 'entry 2'
+ }
+ )
+ },
+ [Ansible.Service.Trigger]@{
+ Type = [Ansible.Service.TriggerType]::FirewallPortEvent
+ Action = [Ansible.Service.TriggerAction]::ServiceStop
+ SubType = [Guid][Ansible.Service.Trigger]::FIREWALL_PORT_CLOSE_GUID
+ DataItems = [Ansible.Service.TriggerItem]@{
+ Type = [Ansible.Service.TriggerDataType]::String
+ Data = [System.Collections.Generic.List[String]]@("1234", "tcp", "imagepath", "servicename")
+ }
+ }
+ )
+
+ $actual = Invoke-Sc -Action qtriggerinfo -Name $serviceName
+
+ $actual.Triggers.Count | Assert-Equal -Expected 6
+ $actual.Triggers[0].Type | Assert-Equal -Expected 'DOMAIN JOINED STATUS'
+ $actual.Triggers[0].Action | Assert-Equal -Expected 'STOP SERVICE'
+ $actual.Triggers[0].SubType | Assert-Equal -Expected "$([Ansible.Service.Trigger]::DOMAIN_JOIN_GUID) [DOMAIN JOINED]"
+ $actual.Triggers[0].Data.Count | Assert-Equal -Expected 0
+
+ $actual.Triggers[1].Type | Assert-Equal -Expected 'NETWORK EVENT'
+ $actual.Triggers[1].Action | Assert-Equal -Expected 'START SERVICE'
+ $actual.Triggers[1].SubType | Assert-Equal -Expected "$([Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID) [NAMED PIPE EVENT]"
+ $actual.Triggers[1].Data.Count | Assert-Equal -Expected 1
+ $actual.Triggers[1].Data[0] | Assert-Equal -Expected 'my named pipe'
+
+ $actual.Triggers[2].Type | Assert-Equal -Expected 'NETWORK EVENT'
+ $actual.Triggers[2].Action | Assert-Equal -Expected 'START SERVICE'
+ $actual.Triggers[2].SubType | Assert-Equal -Expected "$([Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID) [NAMED PIPE EVENT]"
+ $actual.Triggers[2].Data.Count | Assert-Equal -Expected 1
+ $actual.Triggers[2].Data[0] | Assert-Equal -Expected 'my named pipe 2'
+
+ $actual.Triggers[3].Type | Assert-Equal -Expected 'CUSTOM'
+ $actual.Triggers[3].Action | Assert-Equal -Expected 'START SERVICE'
+ $actual.Triggers[3].SubType | Assert-Equal -Expected '9bf04e57-05dc-4914-9ed9-84bf992db88c [ETW PROVIDER UUID]'
+ $actual.Triggers[3].Data.Count | Assert-Equal -Expected 2
+ $actual.Triggers[3].Data[0] | Assert-Equal -Expected '01 02 03 04'
+ $actual.Triggers[3].Data[1] | Assert-Equal -Expected '05 06 07 08 09'
+
+ $actual.Triggers[4].Type | Assert-Equal -Expected 'CUSTOM'
+ $actual.Triggers[4].Action | Assert-Equal -Expected 'START SERVICE'
+ $actual.Triggers[4].SubType | Assert-Equal -Expected '9fbcfc7e-7581-4d46-913b-53bb15c80c51 [ETW PROVIDER UUID]'
+ $actual.Triggers[4].Data.Count | Assert-Equal -Expected 2
+ $actual.Triggers[4].Data[0] | Assert-Equal -Expected "entry 1"
+ $actual.Triggers[4].Data[1] | Assert-Equal -Expected "entry 2"
+
+ $actual.Triggers[5].Type | Assert-Equal -Expected 'FIREWALL PORT EVENT'
+ $actual.Triggers[5].Action | Assert-Equal -Expected 'STOP SERVICE'
+ $actual.Triggers[5].SubType | Assert-Equal -Expected "$([Ansible.Service.Trigger]::FIREWALL_PORT_CLOSE_GUID) [PORT CLOSE]"
+ $actual.Triggers[5].Data.Count | Assert-Equal -Expected 1
+ $actual.Triggers[5].Data[0] | Assert-Equal -Expected '1234;tcp;imagepath;servicename'
+
+ # Remove trigger with $null
+ $service.Triggers = $null
+
+ $actual = Invoke-Sc -Action qtriggerinfo -Name $serviceName
+ $actual.Triggers.Count | Assert-Equal -Expected 0
+
+ # Add a single trigger
+ $service.Triggers = [Ansible.Service.Trigger]@{
+ Type = [Ansible.Service.TriggerType]::GroupPolicy
+ Action = [Ansible.Service.TriggerAction]::ServiceStart
+ SubType = [Guid][Ansible.Service.Trigger]::MACHINE_POLICY_PRESENT_GUID
+ }
+
+ $actual = Invoke-Sc -Action qtriggerinfo -Name $serviceName
+ $actual.Triggers.Count | Assert-Equal -Expected 1
+ $actual.Triggers[0].Type | Assert-Equal -Expected 'GROUP POLICY'
+ $actual.Triggers[0].Action | Assert-Equal -Expected 'START SERVICE'
+ $actual.Triggers[0].SubType | Assert-Equal -Expected "$([Ansible.Service.Trigger]::MACHINE_POLICY_PRESENT_GUID) [MACHINE POLICY PRESENT]"
+ $actual.Triggers[0].Data.Count | Assert-Equal -Expected 0
+
+ # Remove trigger with empty list
+ $service.Triggers = @()
+
+ $actual = Invoke-Sc -Action qtriggerinfo -Name $serviceName
+ $actual.Triggers.Count | Assert-Equal -Expected 0
+
+ # Add triggers through sc and check we get the values correctly
+ $null = Invoke-Sc -Action triggerinfo -Name $serviceName -Arguments @(
+ 'start/namedpipe/abc',
+ 'start/namedpipe/def',
+ 'start/custom/d4497e12-ac36-4823-af61-92db0dbd4a76/11223344/aabbccdd',
+ 'start/strcustom/435a1742-22c5-4234-9db3-e32dafde695c/11223344/aabbccdd',
+ 'stop/portclose/1234;tcp;imagepath;servicename',
+ 'stop/networkoff'
+ )
+
+ $actual = $service.Triggers
+ $actual.Count | Assert-Equal -Expected 6
+
+ $actual[0].Type | Assert-Equal -Expected ([Ansible.Service.TriggerType]::NetworkEndpoint)
+ $actual[0].Action | Assert-Equal -Expected ([Ansible.Service.TriggerAction]::ServiceStart)
+ $actual[0].SubType = [Guid][Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID
+ $actual[0].DataItems.Count | Assert-Equal -Expected 1
+ $actual[0].DataItems[0].Type | Assert-Equal -Expected ([Ansible.Service.TriggerDataType]::String)
+ $actual[0].DataItems[0].Data | Assert-Equal -Expected 'abc'
+
+ $actual[1].Type | Assert-Equal -Expected ([Ansible.Service.TriggerType]::NetworkEndpoint)
+ $actual[1].Action | Assert-Equal -Expected ([Ansible.Service.TriggerAction]::ServiceStart)
+ $actual[1].SubType = [Guid][Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID
+ $actual[1].DataItems.Count | Assert-Equal -Expected 1
+ $actual[1].DataItems[0].Type | Assert-Equal -Expected ([Ansible.Service.TriggerDataType]::String)
+ $actual[1].DataItems[0].Data | Assert-Equal -Expected 'def'
+
+ $actual[2].Type | Assert-Equal -Expected ([Ansible.Service.TriggerType]::Custom)
+ $actual[2].Action | Assert-Equal -Expected ([Ansible.Service.TriggerAction]::ServiceStart)
+ $actual[2].SubType = [Guid]'d4497e12-ac36-4823-af61-92db0dbd4a76'
+ $actual[2].DataItems.Count | Assert-Equal -Expected 2
+ $actual[2].DataItems[0].Type | Assert-Equal -Expected ([Ansible.Service.TriggerDataType]::Binary)
+ , $actual[2].DataItems[0].Data | Assert-Equal -Expected ([byte[]]@(17, 34, 51, 68))
+ $actual[2].DataItems[1].Type | Assert-Equal -Expected ([Ansible.Service.TriggerDataType]::Binary)
+ , $actual[2].DataItems[1].Data | Assert-Equal -Expected ([byte[]]@(170, 187, 204, 221))
+
+ $actual[3].Type | Assert-Equal -Expected ([Ansible.Service.TriggerType]::Custom)
+ $actual[3].Action | Assert-Equal -Expected ([Ansible.Service.TriggerAction]::ServiceStart)
+ $actual[3].SubType = [Guid]'435a1742-22c5-4234-9db3-e32dafde695c'
+ $actual[3].DataItems.Count | Assert-Equal -Expected 2
+ $actual[3].DataItems[0].Type | Assert-Equal -Expected ([Ansible.Service.TriggerDataType]::String)
+ $actual[3].DataItems[0].Data | Assert-Equal -Expected '11223344'
+ $actual[3].DataItems[1].Type | Assert-Equal -Expected ([Ansible.Service.TriggerDataType]::String)
+ $actual[3].DataItems[1].Data | Assert-Equal -Expected 'aabbccdd'
+
+ $actual[4].Type | Assert-Equal -Expected ([Ansible.Service.TriggerType]::FirewallPortEvent)
+ $actual[4].Action | Assert-Equal -Expected ([Ansible.Service.TriggerAction]::ServiceStop)
+ $actual[4].SubType = [Guid][Ansible.Service.Trigger]::FIREWALL_PORT_CLOSE_GUID
+ $actual[4].DataItems.Count | Assert-Equal -Expected 1
+ $actual[4].DataItems[0].Type | Assert-Equal -Expected ([Ansible.Service.TriggerDataType]::String)
+ , $actual[4].DataItems[0].Data | Assert-Equal -Expected @('1234', 'tcp', 'imagepath', 'servicename')
+
+ $actual[5].Type | Assert-Equal -Expected ([Ansible.Service.TriggerType]::IpAddressAvailability)
+ $actual[5].Action | Assert-Equal -Expected ([Ansible.Service.TriggerAction]::ServiceStop)
+ $actual[5].SubType = [Guid][Ansible.Service.Trigger]::NETWORK_MANAGER_LAST_IP_ADDRESS_REMOVAL_GUID
+ $actual[5].DataItems.Count | Assert-Equal -Expected 0
+ }
+
+ # Cannot test PreferredNode as we can't guarantee CI is set up with NUMA support.
+ # Cannot test LaunchProtection as once set we cannot remove unless rebooting
+}
+
+# setup and teardown should favour native tools to create and delete the service and not the util we are testing.
+foreach ($testImpl in $tests.GetEnumerator()) {
+ $serviceName = "ansible_$([System.IO.Path]::GetRandomFileName())"
+ $null = New-Service -Name $serviceName -BinaryPathName ('"{0}"' -f $path) -StartupType Manual
+
+ try {
+ $test = $testImpl.Key
+ &$testImpl.Value
+ }
+ finally {
+ $null = Invoke-Sc -Action delete -Name $serviceName
+ }
+}
+
+$module.Result.data = "success"
+$module.ExitJson()
diff --git a/test/integration/targets/module_utils_Ansible.Service/tasks/main.yml b/test/integration/targets/module_utils_Ansible.Service/tasks/main.yml
new file mode 100644
index 0000000..78f91e1
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Service/tasks/main.yml
@@ -0,0 +1,9 @@
+---
+- name: test Ansible.Service.cs
+ ansible_service_tests:
+ register: ansible_service_test
+
+- name: assert test Ansible.Service.cs
+ assert:
+ that:
+ - ansible_service_test.data == "success"
diff --git a/test/integration/targets/module_utils_ansible_release/aliases b/test/integration/targets/module_utils_ansible_release/aliases
new file mode 100644
index 0000000..7ae73ab
--- /dev/null
+++ b/test/integration/targets/module_utils_ansible_release/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group1
+context/target
diff --git a/test/integration/targets/module_utils_ansible_release/library/ansible_release.py b/test/integration/targets/module_utils_ansible_release/library/ansible_release.py
new file mode 100644
index 0000000..528465d
--- /dev/null
+++ b/test/integration/targets/module_utils_ansible_release/library/ansible_release.py
@@ -0,0 +1,40 @@
+#!/usr/bin/python
+
+# Copyright: (c) 2021, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = r'''
+---
+module: ansible_release
+short_description: Get ansible_release info from module_utils
+description: Get ansible_release info from module_utils
+author:
+- Ansible Project
+'''
+
+EXAMPLES = r'''
+#
+'''
+
+RETURN = r'''
+#
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.ansible_release import __version__, __author__, __codename__
+
+
+def main():
+ module = AnsibleModule(argument_spec={})
+ result = {
+ 'version': __version__,
+ 'author': __author__,
+ 'codename': __codename__,
+ }
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_utils_ansible_release/tasks/main.yml b/test/integration/targets/module_utils_ansible_release/tasks/main.yml
new file mode 100644
index 0000000..4d20b84
--- /dev/null
+++ b/test/integration/targets/module_utils_ansible_release/tasks/main.yml
@@ -0,0 +1,9 @@
+- name: Get module_utils ansible_release vars
+ ansible_release:
+ register: ansible_release
+
+- assert:
+ that:
+ - ansible_release['version'][0]|int != 0
+ - ansible_release['author'] == 'Ansible, Inc.'
+ - ansible_release['codename']|length > 0
diff --git a/test/integration/targets/module_utils_common.respawn/aliases b/test/integration/targets/module_utils_common.respawn/aliases
new file mode 100644
index 0000000..a6dafcf
--- /dev/null
+++ b/test/integration/targets/module_utils_common.respawn/aliases
@@ -0,0 +1 @@
+shippable/posix/group1
diff --git a/test/integration/targets/module_utils_common.respawn/library/respawnme.py b/test/integration/targets/module_utils_common.respawn/library/respawnme.py
new file mode 100644
index 0000000..6471dba
--- /dev/null
+++ b/test/integration/targets/module_utils_common.respawn/library/respawnme.py
@@ -0,0 +1,44 @@
+#!/usr/bin/python
+
+# Copyright: (c) 2021, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+import sys
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.respawn import respawn_module, has_respawned
+
+
+def main():
+ mod = AnsibleModule(argument_spec=dict(
+ mode=dict(required=True, choices=['multi_respawn', 'no_respawn', 'respawn'])
+ ))
+
+ # just return info about what interpreter we're currently running under
+ if mod.params['mode'] == 'no_respawn':
+ mod.exit_json(interpreter_path=sys.executable)
+ elif mod.params['mode'] == 'respawn':
+ if not has_respawned():
+ new_interpreter = os.path.join(mod.tmpdir, 'anotherpython')
+ os.symlink(sys.executable, new_interpreter)
+ respawn_module(interpreter_path=new_interpreter)
+
+ # respawn should always exit internally- if we continue executing here, it's a bug
+ raise Exception('FAIL, should never reach this line')
+ else:
+ # return the current interpreter, as well as a signal that we created a different one
+ mod.exit_json(created_interpreter=sys.executable, interpreter_path=sys.executable)
+ elif mod.params['mode'] == 'multi_respawn':
+ # blindly respawn with the current interpreter, the second time should bomb
+ respawn_module(sys.executable)
+
+ # shouldn't be any way for us to fall through, but just in case, that's also a bug
+ raise Exception('FAIL, should never reach this code')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_utils_common.respawn/tasks/main.yml b/test/integration/targets/module_utils_common.respawn/tasks/main.yml
new file mode 100644
index 0000000..50178df
--- /dev/null
+++ b/test/integration/targets/module_utils_common.respawn/tasks/main.yml
@@ -0,0 +1,24 @@
+- name: collect the default interpreter
+ respawnme:
+ mode: no_respawn
+ register: default_python
+
+- name: cause a respawn
+ respawnme:
+ mode: respawn
+ register: respawned_python
+
+- name: cause multiple respawns (should fail)
+ respawnme:
+ mode: multi_respawn
+ ignore_errors: true
+ register: multi_respawn
+
+- assert:
+ that:
+ # the respawn task should report the path of the interpreter it created, and that it's running under the new interpreter
+ - respawned_python.created_interpreter == respawned_python.interpreter_path
+ # ensure that the respawned interpreter is not the same as the default
+ - default_python.interpreter_path != respawned_python.interpreter_path
+ # multiple nested respawns should fail
+ - multi_respawn is failed and (multi_respawn.module_stdout + multi_respawn.module_stderr) is search('has already been respawned')
diff --git a/test/integration/targets/module_utils_distro/aliases b/test/integration/targets/module_utils_distro/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/module_utils_distro/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/module_utils_distro/meta/main.yml b/test/integration/targets/module_utils_distro/meta/main.yml
new file mode 100644
index 0000000..1810d4b
--- /dev/null
+++ b/test/integration/targets/module_utils_distro/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/module_utils_distro/runme.sh b/test/integration/targets/module_utils_distro/runme.sh
new file mode 100755
index 0000000..e5d3d05
--- /dev/null
+++ b/test/integration/targets/module_utils_distro/runme.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# Ensure that when a non-distro 'distro' package is in PYTHONPATH, we fallback
+# to our bundled one.
+new_pythonpath="$OUTPUT_DIR/pythonpath"
+mkdir -p "$new_pythonpath/distro"
+touch "$new_pythonpath/distro/__init__.py"
+
+export PYTHONPATH="$new_pythonpath:$PYTHONPATH"
+
+# Sanity test to make sure the above worked
+set +e
+distro_id_fail="$(python -c 'import distro; distro.id' 2>&1)"
+set -e
+grep -q "AttributeError:.*has no attribute 'id'" <<< "$distro_id_fail"
+
+# ansible.module_utils.common.sys_info imports distro, and itself gets imported
+# in DataLoader, so all we have to do to test the fallback is run `ansible`.
+ansirun="$(ansible -i ../../inventory -a "echo \$PYTHONPATH" localhost)"
+grep -q "$new_pythonpath" <<< "$ansirun"
+
+rm -rf "$new_pythonpath"
diff --git a/test/integration/targets/module_utils_facts.system.selinux/aliases b/test/integration/targets/module_utils_facts.system.selinux/aliases
new file mode 100644
index 0000000..a6dafcf
--- /dev/null
+++ b/test/integration/targets/module_utils_facts.system.selinux/aliases
@@ -0,0 +1 @@
+shippable/posix/group1
diff --git a/test/integration/targets/module_utils_facts.system.selinux/tasks/main.yml b/test/integration/targets/module_utils_facts.system.selinux/tasks/main.yml
new file mode 100644
index 0000000..1717239
--- /dev/null
+++ b/test/integration/targets/module_utils_facts.system.selinux/tasks/main.yml
@@ -0,0 +1,38 @@
+- name: check selinux config
+ shell: |
+ command -v getenforce &&
+ getenforce | grep -E 'Enforcing|Permissive'
+ ignore_errors: yes
+ register: selinux_state
+
+- name: explicitly collect selinux facts
+ setup:
+ gather_subset:
+ - '!all'
+ - '!any'
+ - selinux
+ register: selinux_facts
+
+- set_fact:
+ selinux_policytype: "unknown"
+
+- name: check selinux policy type
+ shell: grep '^SELINUXTYPE=' /etc/selinux/config | cut -d'=' -f2
+ ignore_errors: yes
+ register: r
+
+- set_fact:
+ selinux_policytype: "{{ r.stdout_lines[0] }}"
+ when: r is success and r.stdout_lines
+
+- assert:
+ that:
+ - selinux_facts is success and selinux_facts.ansible_facts.ansible_selinux is defined
+ - (selinux_facts.ansible_facts.ansible_selinux.status in ['disabled', 'Missing selinux Python library'] if selinux_state is not success else True)
+ - (selinux_facts.ansible_facts.ansible_selinux.status == 'enabled' if selinux_state is success else True)
+ - (selinux_facts.ansible_facts.ansible_selinux.mode in ['enforcing', 'permissive'] if selinux_state is success else True)
+ - (selinux_facts.ansible_facts.ansible_selinux.type == selinux_policytype if selinux_state is success else True)
+
+- name: run selinux tests
+ include_tasks: selinux.yml
+ when: selinux_state is success
diff --git a/test/integration/targets/module_utils_facts.system.selinux/tasks/selinux.yml b/test/integration/targets/module_utils_facts.system.selinux/tasks/selinux.yml
new file mode 100644
index 0000000..6a2b159
--- /dev/null
+++ b/test/integration/targets/module_utils_facts.system.selinux/tasks/selinux.yml
@@ -0,0 +1,93 @@
+- name: collect selinux facts
+ setup:
+ gather_subset: ['!all', '!min', selinux]
+ register: fact_output
+
+- debug:
+ var: fact_output
+
+- name: create tempdir container in home
+ file:
+ path: ~/.selinux_tmp
+ state: directory
+
+- name: create tempdir
+ tempfile:
+ path: ~/.selinux_tmp
+ prefix: selinux_test
+ state: directory
+ register: tempdir
+
+- name: ls -1Zd tempdir to capture context from FS
+ shell: ls -1Zd '{{ tempdir.path }}'
+ register: tempdir_context_output
+
+- name: create a file under the tempdir with no context info specified (it should inherit parent context)
+ file:
+ path: '{{ tempdir.path }}/file_inherited_context'
+ state: touch
+ register: file_inherited_context
+
+- name: ls -1Z inherited file to capture context from FS
+ shell: ls -1Z '{{ tempdir.path }}/file_inherited_context'
+ register: inherited_context_output
+
+- name: copy the file with explicit overrides on all context values
+ copy:
+ remote_src: yes
+ src: '{{ tempdir.path }}/file_inherited_context'
+ dest: '{{ tempdir.path }}/file_explicit_context'
+ seuser: system_u
+ serole: system_r
+ setype: user_tmp_t
+ # default configs don't have MLS levels defined, so we can't test that yet
+ # selevel: s1
+ register: file_explicit_context
+
+- name: ls -1Z explicit file to capture context from FS
+ shell: ls -1Z '{{ tempdir.path }}/file_explicit_context'
+ register: explicit_context_output
+
+- name: alter the tempdir context
+ file:
+ path: '{{ tempdir.path }}'
+ seuser: system_u
+ serole: system_r
+ setype: user_tmp_t
+ # default configs don't have MLS levels defined, so we can't test that yet
+ # selevel: s1
+ register: tempdir_altered
+
+- name: ls -1Z tempdir to capture context from FS
+ shell: ls -1Z '{{ tempdir.path }}/file_explicit_context'
+ register: tempdir_altered_context_output
+
+- name: copy the explicit context file with default overrides on all context values
+ copy:
+ remote_src: yes
+ src: '{{ tempdir.path }}/file_explicit_context'
+ dest: '{{ tempdir.path }}/file_default_context'
+ seuser: _default
+ serole: _default
+ setype: _default
+ selevel: _default
+ register: file_default_context
+
+- name: see what matchpathcon thinks the context of default_file_context should be
+ shell: matchpathcon {{ file_default_context.dest }} | awk '{ print $2 }'
+ register: expected_default_context
+
+- assert:
+ that:
+ - fact_output.ansible_facts.ansible_selinux.config_mode in ['enforcing','permissive']
+ - fact_output.ansible_facts.ansible_selinux.mode in ['enforcing','permissive']
+ - fact_output.ansible_facts.ansible_selinux.status == 'enabled'
+ - fact_output.ansible_facts.ansible_selinux_python_present == true
+ # assert that secontext is set on the file results (injected by basic.py, for better or worse)
+ - tempdir.secontext is match('.+:.+:.+') and tempdir.secontext in tempdir_context_output.stdout
+ - file_inherited_context.secontext is match('.+:.+:.+') and file_inherited_context.secontext in inherited_context_output.stdout
+ - file_inherited_context.secontext == tempdir.secontext # should've been inherited from the parent dir since not set explicitly
+ - file_explicit_context.secontext == 'system_u:system_r:user_tmp_t:s0' and file_explicit_context.secontext in explicit_context_output.stdout
+ - tempdir_altered.secontext == 'system_u:system_r:user_tmp_t:s0' and tempdir_altered.secontext in tempdir_altered_context_output.stdout
+ # the one with reset defaults should match the original tempdir context, not the altered one (ie, it was set by the original policy context, not inherited from the parent dir)
+ - file_default_context.secontext == expected_default_context.stdout_lines[0]
diff --git a/test/integration/targets/module_utils_urls/aliases b/test/integration/targets/module_utils_urls/aliases
new file mode 100644
index 0000000..3c4491b
--- /dev/null
+++ b/test/integration/targets/module_utils_urls/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group1
+needs/httptester
diff --git a/test/integration/targets/module_utils_urls/library/test_peercert.py b/test/integration/targets/module_utils_urls/library/test_peercert.py
new file mode 100644
index 0000000..ecb7d20
--- /dev/null
+++ b/test/integration/targets/module_utils_urls/library/test_peercert.py
@@ -0,0 +1,98 @@
+#!/usr/bin/python
+
+# Copyright: (c) 2020, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = r'''
+---
+module: test_perrcert
+short_description: Test getting the peer certificate of a HTTP response
+description: Test getting the peer certificate of a HTTP response.
+options:
+ url:
+ description: The endpoint to get the peer cert for
+ required: true
+ type: str
+author:
+- Ansible Project
+'''
+
+EXAMPLES = r'''
+#
+'''
+
+RETURN = r'''
+#
+'''
+
+import base64
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.text.converters import to_text
+from ansible.module_utils.urls import getpeercert, Request
+
+
+def get_x509_shorthand(name, value):
+ prefix = {
+ 'countryName': 'C',
+ 'stateOrProvinceName': 'ST',
+ 'localityName': 'L',
+ 'organizationName': 'O',
+ 'commonName': 'CN',
+ 'organizationalUnitName': 'OU',
+ }[name]
+
+ return '%s=%s' % (prefix, value)
+
+
+def main():
+ module_args = dict(
+ url=dict(type='str', required=True),
+ )
+ module = AnsibleModule(
+ argument_spec=module_args,
+ supports_check_mode=True,
+ )
+ result = {
+ 'changed': False,
+ 'cert': None,
+ 'raw_cert': None,
+ }
+
+ req = Request().get(module.params['url'])
+ try:
+ cert = getpeercert(req)
+ b_cert = getpeercert(req, binary_form=True)
+
+ finally:
+ req.close()
+
+ if cert:
+ processed_cert = {
+ 'issuer': '',
+ 'not_after': cert.get('notAfter', None),
+ 'not_before': cert.get('notBefore', None),
+ 'serial_number': cert.get('serialNumber', None),
+ 'subject': '',
+ 'version': cert.get('version', None),
+ }
+
+ for field in ['issuer', 'subject']:
+ field_values = []
+ for x509_part in cert.get(field, []):
+ field_values.append(get_x509_shorthand(x509_part[0][0], x509_part[0][1]))
+
+ processed_cert[field] = ",".join(field_values)
+
+ result['cert'] = processed_cert
+
+ if b_cert:
+ result['raw_cert'] = to_text(base64.b64encode(b_cert))
+
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_utils_urls/meta/main.yml b/test/integration/targets/module_utils_urls/meta/main.yml
new file mode 100644
index 0000000..f3a332d
--- /dev/null
+++ b/test/integration/targets/module_utils_urls/meta/main.yml
@@ -0,0 +1,3 @@
+dependencies:
+- prepare_http_tests
+- setup_remote_tmp_dir
diff --git a/test/integration/targets/module_utils_urls/tasks/main.yml b/test/integration/targets/module_utils_urls/tasks/main.yml
new file mode 100644
index 0000000..ca76a7d
--- /dev/null
+++ b/test/integration/targets/module_utils_urls/tasks/main.yml
@@ -0,0 +1,32 @@
+- name: get peercert for HTTP connection
+ test_peercert:
+ url: http://{{ httpbin_host }}/get
+ register: cert_http
+
+- name: assert get peercert for HTTP connection
+ assert:
+ that:
+ - cert_http.raw_cert == None
+
+- name: get peercert for HTTPS connection
+ test_peercert:
+ url: https://{{ httpbin_host }}/get
+ register: cert_https
+
+# Alpine does not have openssl, just make sure the text was actually set instead
+- name: check if openssl is installed
+ command: which openssl
+ ignore_errors: yes
+ register: openssl
+
+- name: get actual certificate from endpoint
+ shell: echo | openssl s_client -connect {{ httpbin_host }}:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p'
+ register: cert_https_actual
+ changed_when: no
+ when: openssl is successful
+
+- name: assert get peercert for HTTPS connection
+ assert:
+ that:
+ - cert_https.raw_cert != None
+ - openssl is failed or cert_https.raw_cert == cert_https_actual.stdout_lines[1:-1] | join("")
diff --git a/test/integration/targets/network_cli/aliases b/test/integration/targets/network_cli/aliases
new file mode 100644
index 0000000..6a739c9
--- /dev/null
+++ b/test/integration/targets/network_cli/aliases
@@ -0,0 +1,3 @@
+# Keeping incidental for efficiency, to avoid spinning up another VM
+shippable/vyos/incidental
+network/vyos
diff --git a/test/integration/targets/network_cli/passworded_user.yml b/test/integration/targets/network_cli/passworded_user.yml
new file mode 100644
index 0000000..5538684
--- /dev/null
+++ b/test/integration/targets/network_cli/passworded_user.yml
@@ -0,0 +1,14 @@
+- hosts: vyos
+ gather_facts: false
+
+ tasks:
+ - name: Run whoami
+ vyos.vyos.vyos_command:
+ commands:
+ - whoami
+ register: whoami
+
+ - assert:
+ that:
+ - whoami is successful
+ - whoami.stdout_lines[0][0] == 'atester'
diff --git a/test/integration/targets/network_cli/runme.sh b/test/integration/targets/network_cli/runme.sh
new file mode 100755
index 0000000..156674f
--- /dev/null
+++ b/test/integration/targets/network_cli/runme.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+set -eux
+export ANSIBLE_ROLES_PATH=../
+
+function cleanup {
+ ansible-playbook teardown.yml -i "$INVENTORY_PATH" "$@"
+}
+
+trap cleanup EXIT
+
+ansible-playbook setup.yml -i "$INVENTORY_PATH" "$@"
+
+# We need a nonempty file to override key with (empty file gives a
+# lovely "list index out of range" error)
+foo=$(mktemp)
+echo hello > "$foo"
+
+# We want to ensure that passwords make it to the network connection plugins
+# because they follow a different path than the rest of the codebase.
+# In setup.yml, we create a passworded user, and now we connect as that user
+# to make sure the password we pass here successfully makes it to the plugin.
+ansible-playbook \
+ -i "$INVENTORY_PATH" \
+ -e ansible_user=atester \
+ -e ansible_password=testymctest \
+ -e ansible_ssh_private_key_file="$foo" \
+ passworded_user.yml
diff --git a/test/integration/targets/network_cli/setup.yml b/test/integration/targets/network_cli/setup.yml
new file mode 100644
index 0000000..d862406
--- /dev/null
+++ b/test/integration/targets/network_cli/setup.yml
@@ -0,0 +1,14 @@
+- hosts: vyos
+ connection: ansible.netcommon.network_cli
+ become: true
+ gather_facts: false
+
+ tasks:
+ - name: Create user with password
+ register: result
+ vyos.vyos.vyos_config:
+ lines:
+ - set system login user atester full-name "Ansible Tester"
+ - set system login user atester authentication plaintext-password testymctest
+ - set system login user jsmith level admin
+ - delete service ssh disable-password-authentication
diff --git a/test/integration/targets/network_cli/teardown.yml b/test/integration/targets/network_cli/teardown.yml
new file mode 100644
index 0000000..c47f3e8
--- /dev/null
+++ b/test/integration/targets/network_cli/teardown.yml
@@ -0,0 +1,14 @@
+- hosts: vyos
+ connection: ansible.netcommon.network_cli
+ become: true
+ gather_facts: false
+
+ tasks:
+ - name: Get rid of user (undo everything from setup.yml)
+ register: result
+ vyos.vyos.vyos_config:
+ lines:
+ - delete system login user atester full-name "Ansible Tester"
+ - delete system login user atester authentication plaintext-password testymctest
+ - delete system login user jsmith level admin
+ - set service ssh disable-password-authentication
diff --git a/test/integration/targets/no_log/aliases b/test/integration/targets/no_log/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/no_log/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/no_log/dynamic.yml b/test/integration/targets/no_log/dynamic.yml
new file mode 100644
index 0000000..4a1123d
--- /dev/null
+++ b/test/integration/targets/no_log/dynamic.yml
@@ -0,0 +1,27 @@
+- name: test dynamic no log
+ hosts: testhost
+ gather_facts: no
+ ignore_errors: yes
+ tasks:
+ - name: no loop, task fails, dynamic no_log
+ debug:
+ msg: "SHOW {{ var_does_not_exist }}"
+ no_log: "{{ not (unsafe_show_logs|bool) }}"
+
+ - name: loop, task succeeds, dynamic does no_log
+ debug:
+ msg: "SHOW {{ item }}"
+ loop:
+ - a
+ - b
+ - c
+ no_log: "{{ not (unsafe_show_logs|bool) }}"
+
+ - name: loop, task fails, dynamic no_log
+ debug:
+ msg: "SHOW {{ var_does_not_exist }}"
+ loop:
+ - a
+ - b
+ - c
+ no_log: "{{ not (unsafe_show_logs|bool) }}"
diff --git a/test/integration/targets/no_log/library/module.py b/test/integration/targets/no_log/library/module.py
new file mode 100644
index 0000000..d4f3c56
--- /dev/null
+++ b/test/integration/targets/no_log/library/module.py
@@ -0,0 +1,45 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec={
+ 'state': {},
+ 'secret': {'no_log': True},
+ 'subopt_dict': {
+ 'type': 'dict',
+ 'options': {
+ 'str_sub_opt1': {'no_log': True},
+ 'str_sub_opt2': {},
+ 'nested_subopt': {
+ 'type': 'dict',
+ 'options': {
+ 'n_subopt1': {'no_log': True},
+ }
+ }
+ }
+ },
+ 'subopt_list': {
+ 'type': 'list',
+ 'elements': 'dict',
+ 'options': {
+ 'subopt1': {'no_log': True},
+ 'subopt2': {},
+ }
+ }
+
+ }
+ )
+ module.exit_json(msg='done')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/no_log/no_log_local.yml b/test/integration/targets/no_log/no_log_local.yml
new file mode 100644
index 0000000..aacf7de
--- /dev/null
+++ b/test/integration/targets/no_log/no_log_local.yml
@@ -0,0 +1,92 @@
+# TODO: test against real connection plugins to ensure they're not leaking module args
+
+- name: normal play
+ hosts: testhost
+ gather_facts: no
+ tasks:
+ - name: args should be logged in the absence of no_log
+ shell: echo "LOG_ME_TASK_SUCCEEDED"
+
+ - name: failed args should be logged in the absence of no_log
+ shell: echo "LOG_ME_TASK_FAILED"
+ failed_when: true
+ ignore_errors: true
+
+ - name: item args should be logged in the absence of no_log
+ shell: echo {{ item }}
+ with_items: [ "LOG_ME_ITEM", "LOG_ME_SKIPPED", "LOG_ME_ITEM_FAILED" ]
+ when: item != "LOG_ME_SKIPPED"
+ failed_when: item == "LOG_ME_ITEM_FAILED"
+ ignore_errors: true
+
+ - name: args should not be logged when task-level no_log set
+ shell: echo "DO_NOT_LOG_TASK_SUCCEEDED"
+ no_log: true
+
+ - name: failed args should not be logged when task-level no_log set
+ shell: echo "DO_NOT_LOG_TASK_FAILED"
+ no_log: true
+ failed_when: true
+ ignore_errors: true
+
+ - name: skipped task args should be suppressed with no_log
+ shell: echo "DO_NOT_LOG_TASK_SKIPPED"
+ no_log: true
+ when: false
+
+ - name: items args should be suppressed with no_log in every state
+ shell: echo {{ item }}
+ no_log: true
+ with_items: [ "DO_NOT_LOG_ITEM", "DO_NOT_LOG_ITEM_SKIPPED", "DO_NOT_LOG_ITEM_FAILED" ]
+ when: item != "DO_NOT_LOG_ITEM_SKIPPED"
+ failed_when: item == "DO_NOT_LOG_ITEM_FAILED"
+ ignore_errors: yes
+
+ - name: async task args should suppressed with no_log
+ async: 10
+ poll: 1
+ shell: echo "DO_NOT_LOG_ASYNC_TASK_SUCCEEDED"
+ no_log: true
+
+- name: play-level no_log set
+ hosts: testhost
+ gather_facts: no
+ no_log: true
+ tasks:
+ - name: args should not be logged when play-level no_log set
+ shell: echo "DO_NOT_LOG_PLAY"
+
+ - name: args should not be logged when both play- and task-level no_log set
+ shell: echo "DO_NOT_LOG_TASK_AND_PLAY"
+ no_log: true
+
+ - name: args should be logged when task-level no_log overrides play-level
+ shell: echo "LOG_ME_OVERRIDE"
+ no_log: false
+
+ - name: Add a fake host for next play
+ add_host:
+ hostname: fake
+
+- name: use 'fake' unreachable host to force unreachable error
+ hosts: fake
+ gather_facts: no
+ connection: ssh
+ tasks:
+ - name: 'EXPECTED FAILURE: Fail to run a lineinfile task'
+ vars:
+ logins:
+ - machine: foo
+ login: bar
+ password: DO_NOT_LOG_UNREACHABLE_ITEM
+ - machine: two
+ login: three
+ password: DO_NOT_LOG_UNREACHABLE_ITEM
+ lineinfile:
+ path: /dev/null
+ mode: 0600
+ create: true
+ insertafter: EOF
+ line: "machine {{ item.machine }} login {{ item.login }} password {{ item.password }}"
+ loop: "{{ logins }}"
+ no_log: true
diff --git a/test/integration/targets/no_log/no_log_suboptions.yml b/test/integration/targets/no_log/no_log_suboptions.yml
new file mode 100644
index 0000000..e67ecfe
--- /dev/null
+++ b/test/integration/targets/no_log/no_log_suboptions.yml
@@ -0,0 +1,24 @@
+- name: test no log with suboptions
+ hosts: testhost
+ gather_facts: no
+
+ tasks:
+ - name: Task with suboptions
+ module:
+ secret: GLAMOROUS
+ subopt_dict:
+ str_sub_opt1: AFTERMATH
+ str_sub_opt2: otherstring
+ nested_subopt:
+ n_subopt1: MANPOWER
+
+ subopt_list:
+ - subopt1: UNTAPPED
+ subopt2: thridstring
+
+ - subopt1: CONCERNED
+
+ - name: Task with suboptions as string
+ module:
+ secret: MARLIN
+ subopt_dict: str_sub_opt1=FLICK
diff --git a/test/integration/targets/no_log/no_log_suboptions_invalid.yml b/test/integration/targets/no_log/no_log_suboptions_invalid.yml
new file mode 100644
index 0000000..933a8a9
--- /dev/null
+++ b/test/integration/targets/no_log/no_log_suboptions_invalid.yml
@@ -0,0 +1,45 @@
+- name: test no log with suboptions
+ hosts: testhost
+ gather_facts: no
+ ignore_errors: yes
+
+ tasks:
+ - name: Task with suboptions and invalid parameter
+ module:
+ secret: SUPREME
+ invalid: param
+ subopt_dict:
+ str_sub_opt1: IDIOM
+ str_sub_opt2: otherstring
+ nested_subopt:
+ n_subopt1: MOCKUP
+
+ subopt_list:
+ - subopt1: EDUCATED
+ subopt2: thridstring
+ - subopt1: FOOTREST
+
+ - name: Task with suboptions as string with invalid parameter
+ module:
+ secret: FOOTREST
+ invalid: param
+ subopt_dict: str_sub_opt1=CRAFTY
+
+ - name: Task with suboptions with dict instead of list
+ module:
+ secret: FELINE
+ subopt_dict:
+ str_sub_opt1: CRYSTAL
+ str_sub_opt2: otherstring
+ nested_subopt:
+ n_subopt1: EXPECTANT
+ subopt_list:
+ foo: bar
+
+ - name: Task with suboptions with incorrect data type
+ module:
+ secret: AGROUND
+ subopt_dict: 9068.21361
+ subopt_list:
+ - subopt1: GOLIATH
+ - subopt1: FREEFALL
diff --git a/test/integration/targets/no_log/runme.sh b/test/integration/targets/no_log/runme.sh
new file mode 100755
index 0000000..bb5c048
--- /dev/null
+++ b/test/integration/targets/no_log/runme.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# This test expects 7 loggable vars and 0 non-loggable ones.
+# If either mismatches it fails, run the ansible-playbook command to debug.
+[ "$(ansible-playbook no_log_local.yml -i ../../inventory -vvvvv "$@" | awk \
+'BEGIN { logme = 0; nolog = 0; } /LOG_ME/ { logme += 1;} /DO_NOT_LOG/ { nolog += 1;} END { printf "%d/%d", logme, nolog; }')" = "26/0" ]
+
+# deal with corner cases with no log and loops
+# no log enabled, should produce 6 censored messages
+[ "$(ansible-playbook dynamic.yml -i ../../inventory -vvvvv "$@" -e unsafe_show_logs=no|grep -c 'output has been hidden')" = "6" ]
+
+# no log disabled, should produce 0 censored
+[ "$(ansible-playbook dynamic.yml -i ../../inventory -vvvvv "$@" -e unsafe_show_logs=yes|grep -c 'output has been hidden')" = "0" ]
+
+# test no log for sub options
+[ "$(ansible-playbook no_log_suboptions.yml -i ../../inventory -vvvvv "$@" | grep -Ec '(MANPOWER|UNTAPPED|CONCERNED|MARLIN|FLICK)')" = "0" ]
+
+# test invalid data passed to a suboption
+[ "$(ansible-playbook no_log_suboptions_invalid.yml -i ../../inventory -vvvvv "$@" | grep -Ec '(SUPREME|IDIOM|MOCKUP|EDUCATED|FOOTREST|CRAFTY|FELINE|CRYSTAL|EXPECTANT|AGROUND|GOLIATH|FREEFALL)')" = "0" ]
diff --git a/test/integration/targets/noexec/aliases b/test/integration/targets/noexec/aliases
new file mode 100644
index 0000000..e420d4b
--- /dev/null
+++ b/test/integration/targets/noexec/aliases
@@ -0,0 +1,4 @@
+shippable/posix/group4
+context/controller
+skip/docker
+skip/macos
diff --git a/test/integration/targets/noexec/inventory b/test/integration/targets/noexec/inventory
new file mode 100644
index 0000000..ab9b62c
--- /dev/null
+++ b/test/integration/targets/noexec/inventory
@@ -0,0 +1 @@
+not_empty # avoid empty empty hosts list warning without defining explicit localhost
diff --git a/test/integration/targets/noexec/runme.sh b/test/integration/targets/noexec/runme.sh
new file mode 100755
index 0000000..ff70655
--- /dev/null
+++ b/test/integration/targets/noexec/runme.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+set -eux
+
+trap 'umount "${OUTPUT_DIR}/ramdisk"' EXIT
+
+mkdir "${OUTPUT_DIR}/ramdisk"
+mount -t tmpfs -o size=32m,noexec,rw tmpfs "${OUTPUT_DIR}/ramdisk"
+ANSIBLE_REMOTE_TMP="${OUTPUT_DIR}/ramdisk" ansible-playbook -i inventory "$@" test-noexec.yml
diff --git a/test/integration/targets/noexec/test-noexec.yml b/test/integration/targets/noexec/test-noexec.yml
new file mode 100644
index 0000000..3c7d756
--- /dev/null
+++ b/test/integration/targets/noexec/test-noexec.yml
@@ -0,0 +1,8 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - ping:
+
+ - command: sleep 1
+ async: 2
+ poll: 1
diff --git a/test/integration/targets/old_style_cache_plugins/aliases b/test/integration/targets/old_style_cache_plugins/aliases
new file mode 100644
index 0000000..3777383
--- /dev/null
+++ b/test/integration/targets/old_style_cache_plugins/aliases
@@ -0,0 +1,6 @@
+destructive
+needs/root
+shippable/posix/group5
+context/controller
+skip/osx
+skip/macos
diff --git a/test/integration/targets/old_style_cache_plugins/cleanup.yml b/test/integration/targets/old_style_cache_plugins/cleanup.yml
new file mode 100644
index 0000000..93f5cc5
--- /dev/null
+++ b/test/integration/targets/old_style_cache_plugins/cleanup.yml
@@ -0,0 +1,41 @@
+---
+- hosts: localhost
+ gather_facts: no
+ ignore_errors: yes
+ tasks:
+ - command: redis-cli keys
+
+ - name: delete cache keys
+ command: redis-cli del {{ item }}
+ loop:
+ - ansible_facts_localhost
+ - ansible_inventory_localhost
+ - ansible_cache_keys
+
+ - name: shutdown the server
+ command: redis-cli shutdown
+
+ - name: cleanup set up files
+ file:
+ path: "{{ item }}"
+ state: absent
+ loop:
+ - redis-stable.tar.gz
+
+ - name: remove executables
+ file:
+ state: absent
+ path: "/usr/local/bin/{{ item }}"
+ follow: no
+ become: yes
+ loop:
+ - redis-server
+ - redis-cli
+
+ - name: clean the rest of the files
+ file:
+ path: "{{ item }}"
+ state: absent
+ loop:
+ - ./redis-stable.tar.gz
+ - ./redis-stable
diff --git a/test/integration/targets/old_style_cache_plugins/inspect_cache.yml b/test/integration/targets/old_style_cache_plugins/inspect_cache.yml
new file mode 100644
index 0000000..72810e1
--- /dev/null
+++ b/test/integration/targets/old_style_cache_plugins/inspect_cache.yml
@@ -0,0 +1,36 @@
+---
+- hosts: localhost
+ gather_facts: no
+ vars:
+ json_cache: "{{ cache.stdout | from_json }}"
+ tasks:
+ - command: redis-cli get ansible_facts_localhost
+ register: cache
+ tags:
+ - always
+
+ - name: test that the cache only contains the set_fact var
+ assert:
+ that:
+ - "json_cache | length == 1"
+ - "json_cache.foo == ansible_facts.foo"
+ tags:
+ - set_fact
+
+ - name: test that the cache contains gathered facts and the var
+ assert:
+ that:
+ - "json_cache | length > 1"
+ - "json_cache.foo == 'bar'"
+ - "json_cache.ansible_distribution is defined"
+ tags:
+ - additive_gather_facts
+
+ - name: test that the cache contains only gathered facts
+ assert:
+ that:
+ - "json_cache | length > 1"
+ - "json_cache.foo is undefined"
+ - "json_cache.ansible_distribution is defined"
+ tags:
+ - gather_facts
diff --git a/test/integration/targets/old_style_cache_plugins/inventory_config b/test/integration/targets/old_style_cache_plugins/inventory_config
new file mode 100644
index 0000000..d87c2a9
--- /dev/null
+++ b/test/integration/targets/old_style_cache_plugins/inventory_config
@@ -0,0 +1 @@
+# inventory config file for consistent source
diff --git a/test/integration/targets/old_style_cache_plugins/plugins/cache/configurable_redis.py b/test/integration/targets/old_style_cache_plugins/plugins/cache/configurable_redis.py
new file mode 100644
index 0000000..44b6cf9
--- /dev/null
+++ b/test/integration/targets/old_style_cache_plugins/plugins/cache/configurable_redis.py
@@ -0,0 +1,147 @@
+# (c) 2014, Brian Coca, Josh Drake, et al
+# (c) 2017 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ cache: configurable_redis
+ short_description: Use Redis DB for cache
+ description:
+ - This cache uses JSON formatted, per host records saved in Redis.
+ version_added: "1.9"
+ requirements:
+ - redis>=2.4.5 (python lib)
+ options:
+ _uri:
+ description:
+ - A colon separated string of connection information for Redis.
+ required: True
+ env:
+ - name: ANSIBLE_CACHE_PLUGIN_CONNECTION
+ ini:
+ - key: fact_caching_connection
+ section: defaults
+ _prefix:
+ description: User defined prefix to use when creating the DB entries
+ default: ansible_facts
+ env:
+ - name: ANSIBLE_CACHE_PLUGIN_PREFIX
+ ini:
+ - key: fact_caching_prefix
+ section: defaults
+ _timeout:
+ default: 86400
+ description: Expiration timeout for the cache plugin data
+ env:
+ - name: ANSIBLE_CACHE_PLUGIN_TIMEOUT
+ ini:
+ - key: fact_caching_timeout
+ section: defaults
+ type: integer
+'''
+
+import time
+import json
+
+from ansible import constants as C
+from ansible.errors import AnsibleError
+from ansible.parsing.ajson import AnsibleJSONEncoder, AnsibleJSONDecoder
+from ansible.plugins.cache import BaseCacheModule
+from ansible.utils.display import Display
+
+try:
+ from redis import StrictRedis, VERSION
+except ImportError:
+ raise AnsibleError("The 'redis' python module (version 2.4.5 or newer) is required for the redis fact cache, 'pip install redis'")
+
+display = Display()
+
+
+class CacheModule(BaseCacheModule):
+ """
+ A caching module backed by redis.
+ Keys are maintained in a zset with their score being the timestamp
+ when they are inserted. This allows for the usage of 'zremrangebyscore'
+ to expire keys. This mechanism is used or a pattern matched 'scan' for
+ performance.
+ """
+ def __init__(self, *args, **kwargs):
+ connection = []
+
+ super(CacheModule, self).__init__(*args, **kwargs)
+ if self.get_option('_uri'):
+ connection = self.get_option('_uri').split(':')
+ self._timeout = float(self.get_option('_timeout'))
+ self._prefix = self.get_option('_prefix')
+
+ self._cache = {}
+ self._db = StrictRedis(*connection)
+ self._keys_set = 'ansible_cache_keys'
+
+ def _make_key(self, key):
+ return self._prefix + key
+
+ def get(self, key):
+
+ if key not in self._cache:
+ value = self._db.get(self._make_key(key))
+ # guard against the key not being removed from the zset;
+ # this could happen in cases where the timeout value is changed
+ # between invocations
+ if value is None:
+ self.delete(key)
+ raise KeyError
+ self._cache[key] = json.loads(value, cls=AnsibleJSONDecoder)
+
+ return self._cache.get(key)
+
+ def set(self, key, value):
+
+ value2 = json.dumps(value, cls=AnsibleJSONEncoder, sort_keys=True, indent=4)
+ if self._timeout > 0: # a timeout of 0 is handled as meaning 'never expire'
+ self._db.setex(self._make_key(key), int(self._timeout), value2)
+ else:
+ self._db.set(self._make_key(key), value2)
+
+ if VERSION[0] == 2:
+ self._db.zadd(self._keys_set, time.time(), key)
+ else:
+ self._db.zadd(self._keys_set, {key: time.time()})
+ self._cache[key] = value
+
+ def _expire_keys(self):
+ if self._timeout > 0:
+ expiry_age = time.time() - self._timeout
+ self._db.zremrangebyscore(self._keys_set, 0, expiry_age)
+
+ def keys(self):
+ self._expire_keys()
+ return self._db.zrange(self._keys_set, 0, -1)
+
+ def contains(self, key):
+ self._expire_keys()
+ return (self._db.zrank(self._keys_set, key) is not None)
+
+ def delete(self, key):
+ if key in self._cache:
+ del self._cache[key]
+ self._db.delete(self._make_key(key))
+ self._db.zrem(self._keys_set, key)
+
+ def flush(self):
+ for key in self.keys():
+ self.delete(key)
+
+ def copy(self):
+ # TODO: there is probably a better way to do this in redis
+ ret = dict()
+ for key in self.keys():
+ ret[key] = self.get(key)
+ return ret
+
+ def __getstate__(self):
+ return dict()
+
+ def __setstate__(self, data):
+ self.__init__()
diff --git a/test/integration/targets/old_style_cache_plugins/plugins/cache/legacy_redis.py b/test/integration/targets/old_style_cache_plugins/plugins/cache/legacy_redis.py
new file mode 100644
index 0000000..9879dec
--- /dev/null
+++ b/test/integration/targets/old_style_cache_plugins/plugins/cache/legacy_redis.py
@@ -0,0 +1,141 @@
+# (c) 2014, Brian Coca, Josh Drake, et al
+# (c) 2017 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ cache: redis
+ short_description: Use Redis DB for cache
+ description:
+ - This cache uses JSON formatted, per host records saved in Redis.
+ version_added: "1.9"
+ requirements:
+ - redis>=2.4.5 (python lib)
+ options:
+ _uri:
+ description:
+ - A colon separated string of connection information for Redis.
+ required: True
+ env:
+ - name: ANSIBLE_CACHE_PLUGIN_CONNECTION
+ ini:
+ - key: fact_caching_connection
+ section: defaults
+ _prefix:
+ description: User defined prefix to use when creating the DB entries
+ env:
+ - name: ANSIBLE_CACHE_PLUGIN_PREFIX
+ ini:
+ - key: fact_caching_prefix
+ section: defaults
+ _timeout:
+ default: 86400
+ description: Expiration timeout for the cache plugin data
+ env:
+ - name: ANSIBLE_CACHE_PLUGIN_TIMEOUT
+ ini:
+ - key: fact_caching_timeout
+ section: defaults
+ type: integer
+'''
+
+import time
+import json
+
+from ansible import constants as C
+from ansible.errors import AnsibleError
+from ansible.plugins.cache import BaseCacheModule
+
+try:
+ from redis import StrictRedis, VERSION
+except ImportError:
+ raise AnsibleError("The 'redis' python module (version 2.4.5 or newer) is required for the redis fact cache, 'pip install redis'")
+
+
+class CacheModule(BaseCacheModule):
+ """
+ A caching module backed by redis.
+ Keys are maintained in a zset with their score being the timestamp
+ when they are inserted. This allows for the usage of 'zremrangebyscore'
+ to expire keys. This mechanism is used or a pattern matched 'scan' for
+ performance.
+ """
+ def __init__(self, *args, **kwargs):
+ if C.CACHE_PLUGIN_CONNECTION:
+ connection = C.CACHE_PLUGIN_CONNECTION.split(':')
+ else:
+ connection = []
+
+ self._timeout = float(C.CACHE_PLUGIN_TIMEOUT)
+ self._prefix = C.CACHE_PLUGIN_PREFIX
+ self._cache = {}
+ self._db = StrictRedis(*connection)
+ self._keys_set = 'ansible_cache_keys'
+
+ def _make_key(self, key):
+ return self._prefix + key
+
+ def get(self, key):
+
+ if key not in self._cache:
+ value = self._db.get(self._make_key(key))
+ # guard against the key not being removed from the zset;
+ # this could happen in cases where the timeout value is changed
+ # between invocations
+ if value is None:
+ self.delete(key)
+ raise KeyError
+ self._cache[key] = json.loads(value)
+
+ return self._cache.get(key)
+
+ def set(self, key, value):
+
+ value2 = json.dumps(value)
+ if self._timeout > 0: # a timeout of 0 is handled as meaning 'never expire'
+ self._db.setex(self._make_key(key), int(self._timeout), value2)
+ else:
+ self._db.set(self._make_key(key), value2)
+
+ if VERSION[0] == 2:
+ self._db.zadd(self._keys_set, time.time(), key)
+ else:
+ self._db.zadd(self._keys_set, {key: time.time()})
+ self._cache[key] = value
+
+ def _expire_keys(self):
+ if self._timeout > 0:
+ expiry_age = time.time() - self._timeout
+ self._db.zremrangebyscore(self._keys_set, 0, expiry_age)
+
+ def keys(self):
+ self._expire_keys()
+ return self._db.zrange(self._keys_set, 0, -1)
+
+ def contains(self, key):
+ self._expire_keys()
+ return (self._db.zrank(self._keys_set, key) is not None)
+
+ def delete(self, key):
+ if key in self._cache:
+ del self._cache[key]
+ self._db.delete(self._make_key(key))
+ self._db.zrem(self._keys_set, key)
+
+ def flush(self):
+ for key in self.keys():
+ self.delete(key)
+
+ def copy(self):
+ # TODO: there is probably a better way to do this in redis
+ ret = dict()
+ for key in self.keys():
+ ret[key] = self.get(key)
+ return ret
+
+ def __getstate__(self):
+ return dict()
+
+ def __setstate__(self, data):
+ self.__init__()
diff --git a/test/integration/targets/old_style_cache_plugins/plugins/inventory/test.py b/test/integration/targets/old_style_cache_plugins/plugins/inventory/test.py
new file mode 100644
index 0000000..7e59195
--- /dev/null
+++ b/test/integration/targets/old_style_cache_plugins/plugins/inventory/test.py
@@ -0,0 +1,59 @@
+# Copyright (c) 2019 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ name: test
+ plugin_type: inventory
+ short_description: test inventory source
+ extends_documentation_fragment:
+ - inventory_cache
+'''
+
+from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable
+
+
+class InventoryModule(BaseInventoryPlugin, Cacheable):
+
+ NAME = 'test'
+
+ def populate(self, hosts):
+ for host in list(hosts.keys()):
+ self.inventory.add_host(host, group='all')
+ for hostvar, hostval in hosts[host].items():
+ self.inventory.set_variable(host, hostvar, hostval)
+
+ def get_hosts(self):
+ return {'host1': {'one': 'two'}, 'host2': {'three': 'four'}}
+
+ def parse(self, inventory, loader, path, cache=True):
+ super(InventoryModule, self).parse(inventory, loader, path)
+
+ self.load_cache_plugin()
+
+ cache_key = self.get_cache_key(path)
+
+ # cache may be True or False at this point to indicate if the inventory is being refreshed
+ # get the user's cache option
+ cache_setting = self.get_option('cache')
+
+ attempt_to_read_cache = cache_setting and cache
+ cache_needs_update = cache_setting and not cache
+
+ # attempt to read the cache if inventory isn't being refreshed and the user has caching enabled
+ if attempt_to_read_cache:
+ try:
+ results = self._cache[cache_key]
+ except KeyError:
+ # This occurs if the cache_key is not in the cache or if the cache_key expired, so the cache needs to be updated
+ cache_needs_update = True
+
+ if cache_needs_update:
+ results = self.get_hosts()
+
+ # set the cache
+ self._cache[cache_key] = results
+
+ self.populate(results)
diff --git a/test/integration/targets/old_style_cache_plugins/runme.sh b/test/integration/targets/old_style_cache_plugins/runme.sh
new file mode 100755
index 0000000..ffa6723
--- /dev/null
+++ b/test/integration/targets/old_style_cache_plugins/runme.sh
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+
+set -eux
+
+source virtualenv.sh
+
+trap 'ansible-playbook cleanup.yml' EXIT
+
+export PATH="$PATH:/usr/local/bin"
+
+ansible-playbook setup_redis_cache.yml "$@"
+
+# Cache should start empty
+redis-cli keys ansible_
+[ "$(redis-cli keys ansible_)" = "" ]
+
+export ANSIBLE_CACHE_PLUGINS=./plugins/cache
+export ANSIBLE_CACHE_PLUGIN_CONNECTION=localhost:6379:0
+export ANSIBLE_CACHE_PLUGIN_PREFIX='ansible_facts_'
+
+# Test legacy cache plugins (that use ansible.constants) and
+# new cache plugins that use config manager both work for facts.
+for fact_cache in legacy_redis configurable_redis; do
+
+ export ANSIBLE_CACHE_PLUGIN="$fact_cache"
+
+ # test set_fact with cacheable: true
+ ansible-playbook test_fact_gathering.yml --tags set_fact "$@"
+ [ "$(redis-cli keys ansible_facts_localhost | wc -l)" -eq 1 ]
+ ansible-playbook inspect_cache.yml --tags set_fact "$@"
+
+ # cache gathered facts in addition
+ ansible-playbook test_fact_gathering.yml --tags gather_facts "$@"
+ ansible-playbook inspect_cache.yml --tags additive_gather_facts "$@"
+
+ # flush cache and only cache gathered facts
+ ansible-playbook test_fact_gathering.yml --flush-cache --tags gather_facts --tags flush "$@"
+ ansible-playbook inspect_cache.yml --tags gather_facts "$@"
+
+ redis-cli del ansible_facts_localhost
+ unset ANSIBLE_CACHE_PLUGIN
+
+done
+
+# Legacy cache plugins need to be updated to use set_options/get_option to be compatible with inventory plugins.
+# Inventory plugins load cache options with the config manager.
+ansible-playbook test_inventory_cache.yml "$@"
diff --git a/test/integration/targets/old_style_cache_plugins/setup_redis_cache.yml b/test/integration/targets/old_style_cache_plugins/setup_redis_cache.yml
new file mode 100644
index 0000000..8aad37a
--- /dev/null
+++ b/test/integration/targets/old_style_cache_plugins/setup_redis_cache.yml
@@ -0,0 +1,51 @@
+---
+- hosts: localhost
+ vars:
+ make: "{{ ( ansible_distribution != 'FreeBSD' ) | ternary('make', 'gmake') }}"
+ tasks:
+ - name: name ensure make is available
+ command: "which {{ make }}"
+ register: has_make
+ ignore_errors: yes
+
+ - command: apk add --no-cache make
+ when: "has_make is failed and ansible_distribution == 'Alpine'"
+ become: yes
+
+ - package:
+ name: "{{ make }}"
+ state: present
+ become: yes
+ when: "has_make is failed and ansible_distribution != 'Alpine'"
+
+ - name: get the latest stable redis server release
+ get_url:
+ url: http://download.redis.io/redis-stable.tar.gz
+ dest: ./
+
+ - name: unzip download
+ unarchive:
+ src: redis-stable.tar.gz
+ dest: ./
+
+ - command: "{{ make }}"
+ args:
+ chdir: redis-stable
+
+ - name: copy the executable into the path
+ copy:
+ src: "redis-stable/src/{{ item }}"
+ dest: /usr/local/bin/
+ mode: 755
+ become: yes
+ loop:
+ - redis-server
+ - redis-cli
+
+ - name: start the redis server in the background
+ command: redis-server --daemonize yes
+
+ - name: install dependency for the cache plugin
+ pip:
+ name: redis>2.4.5
+ state: present
diff --git a/test/integration/targets/old_style_cache_plugins/test_fact_gathering.yml b/test/integration/targets/old_style_cache_plugins/test_fact_gathering.yml
new file mode 100644
index 0000000..2c77f0d
--- /dev/null
+++ b/test/integration/targets/old_style_cache_plugins/test_fact_gathering.yml
@@ -0,0 +1,22 @@
+---
+- hosts: localhost
+ gather_facts: no
+ tags:
+ - flush
+ tasks:
+ - meta: clear_facts
+
+- hosts: localhost
+ gather_facts: yes
+ gather_subset: min
+ tags:
+ - gather_facts
+
+- hosts: localhost
+ gather_facts: no
+ tags:
+ - set_fact
+ tasks:
+ - set_fact:
+ foo: bar
+ cacheable: true
diff --git a/test/integration/targets/old_style_cache_plugins/test_inventory_cache.yml b/test/integration/targets/old_style_cache_plugins/test_inventory_cache.yml
new file mode 100644
index 0000000..83b7983
--- /dev/null
+++ b/test/integration/targets/old_style_cache_plugins/test_inventory_cache.yml
@@ -0,0 +1,45 @@
+- hosts: localhost
+ gather_facts: no
+ vars:
+ reset_color: '\x1b\[0m'
+ color: '\x1b\[[0-9];[0-9]{2}m'
+ base_environment:
+ ANSIBLE_INVENTORY_PLUGINS: ./plugins/inventory
+ ANSIBLE_INVENTORY_ENABLED: test
+ ANSIBLE_INVENTORY_CACHE: true
+ ANSIBLE_CACHE_PLUGINS: ./plugins/cache
+ ANSIBLE_CACHE_PLUGIN_CONNECTION: localhost:6379:0
+ ANSIBLE_CACHE_PLUGIN_PREFIX: 'ansible_inventory_'
+ legacy_cache:
+ ANSIBLE_INVENTORY_CACHE_PLUGIN: legacy_redis
+ updated_cache:
+ ANSIBLE_INVENTORY_CACHE_PLUGIN: configurable_redis
+ tasks:
+ - name: legacy-style cache plugin should cause a warning
+ command: ansible-inventory -i inventory_config --graph
+ register: result
+ environment: "{{ base_environment | combine(legacy_cache) }}"
+
+ - name: test warning message
+ assert:
+ that:
+ - expected_warning in warning
+ - "'No inventory was parsed, only implicit localhost is available' in warning"
+ vars:
+ warning: "{{ result.stderr | regex_replace(reset_color) | regex_replace(color) | regex_replace('\\n', ' ') }}"
+ expected_warning: "Cache options were provided but may not reconcile correctly unless set via set_options"
+
+ - name: cache plugin updated to use config manager should work
+ command: ansible-inventory -i inventory_config --graph
+ register: result
+ environment: "{{ base_environment | combine(updated_cache) }}"
+
+ - name: test warning message
+ assert:
+ that:
+ - unexpected_warning not in warning
+ - "'No inventory was parsed, only implicit localhost is available' not in warning"
+ - '"host1" in result.stdout'
+ vars:
+ warning: "{{ result.stderr | regex_replace(reset_color) | regex_replace(color) | regex_replace('\\n', ' ') }}"
+ unexpected_warning: "Cache options were provided but may not reconcile correctly unless set via set_options"
diff --git a/test/integration/targets/old_style_modules_posix/aliases b/test/integration/targets/old_style_modules_posix/aliases
new file mode 100644
index 0000000..6452e6d
--- /dev/null
+++ b/test/integration/targets/old_style_modules_posix/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group2
+context/target
diff --git a/test/integration/targets/old_style_modules_posix/library/helloworld.sh b/test/integration/targets/old_style_modules_posix/library/helloworld.sh
new file mode 100644
index 0000000..c1108a8
--- /dev/null
+++ b/test/integration/targets/old_style_modules_posix/library/helloworld.sh
@@ -0,0 +1,29 @@
+#!/bin/sh
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+if [ -f "$1" ]; then
+ . "$1"
+else
+ echo '{"msg": "No argument file provided", "failed": true}'
+ exit 1
+fi
+
+salutation=${salutation:=Hello}
+name=${name:=World}
+
+cat << EOF
+{"msg": "${salutation}, ${name}!"}
+EOF
diff --git a/test/integration/targets/old_style_modules_posix/meta/main.yml b/test/integration/targets/old_style_modules_posix/meta/main.yml
new file mode 100644
index 0000000..1810d4b
--- /dev/null
+++ b/test/integration/targets/old_style_modules_posix/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/old_style_modules_posix/tasks/main.yml b/test/integration/targets/old_style_modules_posix/tasks/main.yml
new file mode 100644
index 0000000..a788217
--- /dev/null
+++ b/test/integration/targets/old_style_modules_posix/tasks/main.yml
@@ -0,0 +1,44 @@
+- name: Hello, World!
+ helloworld:
+ register: hello_world
+
+- assert:
+ that:
+ - 'hello_world.msg == "Hello, World!"'
+
+- name: Hello, Ansible!
+ helloworld:
+ args:
+ name: Ansible
+ register: hello_ansible
+
+- assert:
+ that:
+ - 'hello_ansible.msg == "Hello, Ansible!"'
+
+- name: Goodbye, Ansible!
+ helloworld:
+ args:
+ salutation: Goodbye
+ name: Ansible
+ register: goodbye_ansible
+
+- assert:
+ that:
+ - 'goodbye_ansible.msg == "Goodbye, Ansible!"'
+
+- name: Copy module to remote
+ copy:
+ src: "{{ role_path }}/library/helloworld.sh"
+ dest: "{{ remote_tmp_dir }}/helloworld.sh"
+
+- name: Execute module directly
+ command: '/bin/sh {{ remote_tmp_dir }}/helloworld.sh'
+ register: direct
+ ignore_errors: true
+
+- assert:
+ that:
+ - direct is failed
+ - |
+ direct.stdout == '{"msg": "No argument file provided", "failed": true}'
diff --git a/test/integration/targets/old_style_vars_plugins/aliases b/test/integration/targets/old_style_vars_plugins/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/old_style_vars_plugins/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/old_style_vars_plugins/deprecation_warning/vars.py b/test/integration/targets/old_style_vars_plugins/deprecation_warning/vars.py
new file mode 100644
index 0000000..d5c9a42
--- /dev/null
+++ b/test/integration/targets/old_style_vars_plugins/deprecation_warning/vars.py
@@ -0,0 +1,8 @@
+from ansible.plugins.vars import BaseVarsPlugin
+
+
+class VarsModule(BaseVarsPlugin):
+ REQUIRES_WHITELIST = False
+
+ def get_vars(self, loader, path, entities):
+ return {}
diff --git a/test/integration/targets/old_style_vars_plugins/runme.sh b/test/integration/targets/old_style_vars_plugins/runme.sh
new file mode 100755
index 0000000..4cd1916
--- /dev/null
+++ b/test/integration/targets/old_style_vars_plugins/runme.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+
+set -eux
+
+export ANSIBLE_VARS_PLUGINS=./vars_plugins
+
+# Test vars plugin without REQUIRES_ENABLED class attr and vars plugin with REQUIRES_ENABLED = False run by default
+[ "$(ansible-inventory -i localhost, --list --yaml all "$@" | grep -Ec '(implicitly|explicitly)_auto_enabled')" = "2" ]
+
+# Test vars plugin with REQUIRES_ENABLED=True only runs when enabled
+[ "$(ansible-inventory -i localhost, --list --yaml all "$@" | grep -Ec 'require_enabled')" = "0" ]
+export ANSIBLE_VARS_ENABLED=require_enabled
+[ "$(ansible-inventory -i localhost, --list --yaml all "$@" | grep -c 'require_enabled')" = "1" ]
+
+# Test the deprecated class attribute
+export ANSIBLE_VARS_PLUGINS=./deprecation_warning
+WARNING="The VarsModule class variable 'REQUIRES_WHITELIST' is deprecated. Use 'REQUIRES_ENABLED' instead."
+ANSIBLE_DEPRECATION_WARNINGS=True ANSIBLE_NOCOLOR=True ANSIBLE_FORCE_COLOR=False \
+ ansible-inventory -i localhost, --list all 2> err.txt
+ansible localhost -m debug -a "msg={{ lookup('file', 'err.txt') | regex_replace('\n', '') }}" | grep "$WARNING"
diff --git a/test/integration/targets/old_style_vars_plugins/vars_plugins/auto_enabled.py b/test/integration/targets/old_style_vars_plugins/vars_plugins/auto_enabled.py
new file mode 100644
index 0000000..a91d94d
--- /dev/null
+++ b/test/integration/targets/old_style_vars_plugins/vars_plugins/auto_enabled.py
@@ -0,0 +1,8 @@
+from ansible.plugins.vars import BaseVarsPlugin
+
+
+class VarsModule(BaseVarsPlugin):
+ REQUIRES_ENABLED = False
+
+ def get_vars(self, loader, path, entities):
+ return {'explicitly_auto_enabled': True}
diff --git a/test/integration/targets/old_style_vars_plugins/vars_plugins/implicitly_auto_enabled.py b/test/integration/targets/old_style_vars_plugins/vars_plugins/implicitly_auto_enabled.py
new file mode 100644
index 0000000..4f407b4
--- /dev/null
+++ b/test/integration/targets/old_style_vars_plugins/vars_plugins/implicitly_auto_enabled.py
@@ -0,0 +1,7 @@
+from ansible.plugins.vars import BaseVarsPlugin
+
+
+class VarsModule(BaseVarsPlugin):
+
+ def get_vars(self, loader, path, entities):
+ return {'implicitly_auto_enabled': True}
diff --git a/test/integration/targets/old_style_vars_plugins/vars_plugins/require_enabled.py b/test/integration/targets/old_style_vars_plugins/vars_plugins/require_enabled.py
new file mode 100644
index 0000000..a251447
--- /dev/null
+++ b/test/integration/targets/old_style_vars_plugins/vars_plugins/require_enabled.py
@@ -0,0 +1,8 @@
+from ansible.plugins.vars import BaseVarsPlugin
+
+
+class VarsModule(BaseVarsPlugin):
+ REQUIRES_ENABLED = True
+
+ def get_vars(self, loader, path, entities):
+ return {'require_enabled': True}
diff --git a/test/integration/targets/omit/48673.yml b/test/integration/targets/omit/48673.yml
new file mode 100644
index 0000000..d25c8cf
--- /dev/null
+++ b/test/integration/targets/omit/48673.yml
@@ -0,0 +1,4 @@
+- hosts: testhost
+ serial: "{{ testing_omitted_variable | default(omit) }}"
+ tasks:
+ - debug:
diff --git a/test/integration/targets/omit/75692.yml b/test/integration/targets/omit/75692.yml
new file mode 100644
index 0000000..b4000c9
--- /dev/null
+++ b/test/integration/targets/omit/75692.yml
@@ -0,0 +1,31 @@
+- name: omit should reset to 'absent' or same context, not just 'default' value
+ hosts: testhost
+ gather_facts: false
+ become: yes
+ become_user: nobody
+ roles:
+ - name: setup_test_user
+ become: yes
+ become_user: root
+ tasks:
+ - shell: whoami
+ register: inherited
+
+ - shell: whoami
+ register: explicit_no
+ become: false
+
+ - shell: whoami
+ register: omited_inheritance
+ become: '{{ omit }}'
+
+ - shell: whoami
+ register: explicit_yes
+ become: yes
+
+ - name: ensure omit works with inheritance
+ assert:
+ that:
+ - inherited.stdout == omited_inheritance.stdout
+ - inherited.stdout == explicit_yes.stdout
+ - inherited.stdout != explicit_no.stdout
diff --git a/test/integration/targets/omit/C75692.yml b/test/integration/targets/omit/C75692.yml
new file mode 100644
index 0000000..6e1215f
--- /dev/null
+++ b/test/integration/targets/omit/C75692.yml
@@ -0,0 +1,44 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: Make sure foo is gone
+ file:
+ path: foo
+ state: absent
+ - name: Create foo - should only be changed in first iteration
+ copy:
+ dest: foo
+ content: foo
+ check_mode: '{{ omit }}'
+ register: cmode
+ loop:
+ - 1
+ - 2
+
+ - when: ansible_check_mode
+ block:
+ - name: stat foo
+ stat: path=foo
+ register: foo
+ check_mode: off
+ - debug: var=foo
+ - name: validate expected outcomes when in check mode and file does not exist
+ assert:
+ that:
+ - cmode['results'][0] is changed
+ - cmode['results'][1] is changed
+ when: not foo['stat']['exists']
+
+ - name: validate expected outcomes when in check mode and file exists
+ assert:
+ that:
+ - cmode['results'][0] is not changed
+ - cmode['results'][1] is not changed
+ when: foo['stat']['exists']
+
+ - name: validate expected outcomes when not in check mode (file is always deleted)
+ assert:
+ that:
+ - cmode['results'][0] is changed
+ - cmode['results'][1] is not changed
+ when: not ansible_check_mode
diff --git a/test/integration/targets/omit/aliases b/test/integration/targets/omit/aliases
new file mode 100644
index 0000000..1bff31c
--- /dev/null
+++ b/test/integration/targets/omit/aliases
@@ -0,0 +1,3 @@
+shippable/posix/group5
+needs/target/setup_test_user
+context/controller
diff --git a/test/integration/targets/omit/runme.sh b/test/integration/targets/omit/runme.sh
new file mode 100755
index 0000000..e2f3c02
--- /dev/null
+++ b/test/integration/targets/omit/runme.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# positive inheritance works
+ANSIBLE_ROLES_PATH=../ ansible-playbook 48673.yml 75692.yml -i ../../inventory -v "$@"
+
+# ensure negative also works
+ansible-playbook -C C75692.yml -i ../../inventory -v "$@" # expects 'foo' not to exist
+ansible-playbook C75692.yml -i ../../inventory -v "$@" # creates 'foo'
+ansible-playbook -C C75692.yml -i ../../inventory -v "$@" # expects 'foo' does exist
diff --git a/test/integration/targets/order/aliases b/test/integration/targets/order/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/order/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/order/inventory b/test/integration/targets/order/inventory
new file mode 100644
index 0000000..11f322a
--- /dev/null
+++ b/test/integration/targets/order/inventory
@@ -0,0 +1,9 @@
+[incremental]
+hostB
+hostA
+hostD
+hostC
+
+[incremental:vars]
+ansible_connection=local
+ansible_python_interpreter='{{ansible_playbook_python}}'
diff --git a/test/integration/targets/order/order.yml b/test/integration/targets/order/order.yml
new file mode 100644
index 0000000..62176b1
--- /dev/null
+++ b/test/integration/targets/order/order.yml
@@ -0,0 +1,39 @@
+- name: just plain order
+ hosts: all
+ gather_facts: false
+ order: '{{ myorder | default("inventory") }}'
+ tasks:
+ - shell: "echo '{{ inventory_hostname }}' >> hostlist.txt"
+
+- name: with serial
+ hosts: all
+ gather_facts: false
+ serial: 1
+ order: '{{ myorder | default("inventory")}}'
+ tasks:
+ - shell: "echo '{{ inventory_hostname }}' >> shostlist.txt"
+
+- name: ensure everything works
+ hosts: localhost
+ gather_facts: false
+ tasks:
+ - assert:
+ that:
+ - item.1 == hostlist[item.0]
+ - item.1 == shostlist[item.0]
+ loop: '{{ lookup("indexed_items", inputlist) }}'
+ vars:
+ hostlist: '{{ lookup("file", "hostlist.txt").splitlines() }}'
+ shostlist: '{{ lookup("file", "shostlist.txt").splitlines() }}'
+ when: myorder | default('inventory') != 'shuffle'
+
+ - name: Assert that shuffle worked
+ assert:
+ that:
+ - item.1 != hostlist[item.0] or item.1 in hostlist
+ - item.1 != hostlist[item.0] or item.1 in hostlist
+ loop: '{{ lookup("indexed_items", inputlist) }}'
+ vars:
+ hostlist: '{{ lookup("file", "hostlist.txt").splitlines() }}'
+ shostlist: '{{ lookup("file", "shostlist.txt").splitlines() }}'
+ when: myorder | default('inventory') == 'shuffle'
diff --git a/test/integration/targets/order/runme.sh b/test/integration/targets/order/runme.sh
new file mode 100755
index 0000000..9a01c21
--- /dev/null
+++ b/test/integration/targets/order/runme.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+
+set -eux
+
+cleanup () {
+ files="shostlist.txt hostlist.txt"
+ for file in $files; do
+ if [[ -f "$file" ]]; then
+ rm -f "$file"
+ fi
+ done
+}
+
+for EXTRA in '{"inputlist": ["hostB", "hostA", "hostD", "hostC"]}' \
+ '{"myorder": "inventory", "inputlist": ["hostB", "hostA", "hostD", "hostC"]}' \
+ '{"myorder": "sorted", "inputlist": ["hostA", "hostB", "hostC", "hostD"]}' \
+ '{"myorder": "reverse_sorted", "inputlist": ["hostD", "hostC", "hostB", "hostA"]}' \
+ '{"myorder": "reverse_inventory", "inputlist": ["hostC", "hostD", "hostA", "hostB"]}' \
+ '{"myorder": "shuffle", "inputlist": ["hostC", "hostD", "hostA", "hostB"]}'
+do
+ cleanup
+ ansible-playbook order.yml --forks 1 -i inventory -e "$EXTRA" "$@"
+done
+cleanup
diff --git a/test/integration/targets/package/aliases b/test/integration/targets/package/aliases
new file mode 100644
index 0000000..6eae8bd
--- /dev/null
+++ b/test/integration/targets/package/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group1
+destructive
diff --git a/test/integration/targets/package/meta/main.yml b/test/integration/targets/package/meta/main.yml
new file mode 100644
index 0000000..07faa21
--- /dev/null
+++ b/test/integration/targets/package/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - prepare_tests
diff --git a/test/integration/targets/package/tasks/main.yml b/test/integration/targets/package/tasks/main.yml
new file mode 100644
index 0000000..c17525d
--- /dev/null
+++ b/test/integration/targets/package/tasks/main.yml
@@ -0,0 +1,242 @@
+# Test code for the package module.
+# (c) 2017, James Tanner <tanner.jc@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Verify correct default package manager for Fedora
+# Validates: https://github.com/ansible/ansible/issues/34014
+- block:
+ - name: install apt
+ dnf:
+ name: apt
+ state: present
+ - name: gather facts again
+ setup:
+ - name: validate output
+ assert:
+ that:
+ - 'ansible_pkg_mgr == "dnf"'
+ always:
+ - name: remove apt
+ dnf:
+ name: apt
+ state: absent
+ - name: gather facts again
+ setup:
+ when: ansible_distribution == "Fedora"
+
+# Verify correct default package manager for Debian/Ubuntu when Zypper installed
+- block:
+ # Just make an executable file called "zypper" - installing zypper itself
+ # consistently is hard - and we're not going to use it
+ - name: install fake zypper
+ file:
+ state: touch
+ mode: 0755
+ path: /usr/bin/zypper
+ - name: gather facts again
+ setup:
+ - name: validate output
+ assert:
+ that:
+ - 'ansible_pkg_mgr == "apt"'
+ always:
+ - name: remove fake zypper
+ file:
+ path: /usr/bin/zypper
+ state: absent
+ - name: gather facts again
+ setup:
+ when: ansible_os_family == "Debian"
+
+##
+## package
+##
+
+# Verify module_defaults for package and the underlying module are utilized
+# Validates: https://github.com/ansible/ansible/issues/72918
+- block:
+ # 'name' is required
+ - name: install apt with package defaults
+ package:
+ module_defaults:
+ package:
+ name: apt
+ state: present
+
+ - name: install apt with dnf defaults (auto)
+ package:
+ module_defaults:
+ dnf:
+ name: apt
+ state: present
+
+ - name: install apt with dnf defaults (use dnf)
+ package:
+ use: dnf
+ module_defaults:
+ dnf:
+ name: apt
+ state: present
+ always:
+ - name: remove apt
+ dnf:
+ name: apt
+ state: absent
+ when: ansible_distribution == "Fedora"
+
+- name: define distros to attempt installing at on
+ set_fact:
+ package_distros:
+ - RedHat
+ - CentOS
+ - ScientificLinux
+ - Fedora
+ - Ubuntu
+ - Debian
+
+- block:
+ - name: remove at package
+ package:
+ name: at
+ state: absent
+ register: at_check0
+
+ - name: verify at command is missing
+ shell: which at
+ register: at_check1
+ failed_when: at_check1.rc == 0
+
+ - name: reinstall at package
+ package:
+ name: at
+ state: present
+ register: at_install0
+ - debug: var=at_install0
+ - name: validate results
+ assert:
+ that:
+ - 'at_install0.changed is defined'
+ - 'at_install0.changed'
+
+ - name: verify at command is installed
+ shell: which at
+
+ - name: remove at package
+ package:
+ name: at
+ state: absent
+ register: at_install0
+
+ - name: validate package removal
+ assert:
+ that:
+ - "at_install0 is changed"
+
+ when: ansible_distribution in package_distros
+
+##
+## yum
+##
+#Validation for new parameter 'use' in yum action plugin which aliases to 'use_backend'
+#Issue: https://github.com/ansible/ansible/issues/70774
+- block:
+ - name: verify if using both the parameters 'use' and 'use_backend' throw error
+ yum:
+ name: at
+ state: present
+ use_backend: yum
+ use: yum
+ ignore_errors: yes
+ register: result
+
+ - name: verify error
+ assert:
+ that:
+ - "'parameters are mutually exclusive' in result.msg"
+ - "not result is changed"
+
+ - name: verify if package installation is successful using 'use' parameter
+ yum:
+ name: at
+ state: present
+ use: dnf
+ register: result
+
+ - name: verify the result
+ assert:
+ that:
+ - "result is changed"
+
+ - name: remove at package
+ yum:
+ name: at
+ state: absent
+ use: dnf
+ register: result
+
+ - name: verify package removal
+ assert:
+ that:
+ - "result is changed"
+
+ - name: verify if package installation is successful using 'use_backend' parameter
+ yum:
+ name: at
+ state: present
+ use_backend: dnf
+ register: result
+
+ - name: verify the result
+ assert:
+ that:
+ - "result is changed"
+
+ - name: remove at package
+ yum:
+ name: at
+ state: absent
+ use_backend: dnf
+ register: result
+
+ - name: verify package removal
+ assert:
+ that:
+ - "result is changed"
+
+ - name: verify if package installation is successful without using 'use_backend' and 'use' parameters
+ yum:
+ name: at
+ state: present
+ register: result
+
+ - name: verify the result
+ assert:
+ that:
+ - "result is changed"
+
+ - name: remove at package
+ yum:
+ name: at
+ state: absent
+ register: result
+
+ - name: verify package removal
+ assert:
+ that:
+ - "result is changed"
+
+ when: ansible_distribution == "Fedora" \ No newline at end of file
diff --git a/test/integration/targets/package_facts/aliases b/test/integration/targets/package_facts/aliases
new file mode 100644
index 0000000..5a5e464
--- /dev/null
+++ b/test/integration/targets/package_facts/aliases
@@ -0,0 +1,3 @@
+shippable/posix/group2
+skip/osx
+skip/macos
diff --git a/test/integration/targets/package_facts/tasks/main.yml b/test/integration/targets/package_facts/tasks/main.yml
new file mode 100644
index 0000000..12dfcf0
--- /dev/null
+++ b/test/integration/targets/package_facts/tasks/main.yml
@@ -0,0 +1,115 @@
+# Test playbook for the package_facts module
+# (c) 2017, Adam Miller <admiller@redhat.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: Prep package_fact tests - Debian Family
+ block:
+ - name: install python apt bindings - python2
+ package: name="python-apt" state=present
+ when: ansible_python.version.major|int == 2
+
+ - name: install python apt bindings - python3
+ package: name="python3-apt" state=present
+ when: ansible_python.version.major|int == 3
+
+ - name: Gather package facts
+ package_facts:
+ manager: apt
+
+ - name: check for ansible_facts.packages exists
+ assert:
+ that: ansible_facts.packages is defined
+ when: ansible_os_family == "Debian"
+
+- name: Run package_fact tests - Red Hat Family
+ block:
+ - name: Gather package facts
+ package_facts:
+ manager: rpm
+
+ - name: check for ansible_facts.packages exists
+ assert:
+ that: ansible_facts.packages is defined
+ when: (ansible_os_family == "RedHat")
+
+- name: Run package_fact tests - SUSE/OpenSUSE Family
+ block:
+ - name: install python rpm bindings - python2
+ package: name="rpm-python" state=present
+ when: ansible_python.version.major|int == 2
+
+ - name: install python rpm bindings - python3
+ package: name="python3-rpm" state=present
+ when: ansible_python.version.major|int == 3
+
+ - name: Gather package facts
+ package_facts:
+ manager: rpm
+
+ - name: check for ansible_facts.packages exists
+ assert:
+ that: ansible_facts.packages is defined
+ when: (ansible_os_family == "openSUSE Leap") or (ansible_os_family == "Suse")
+
+# Check that auto detection works also
+- name: Gather package facts
+ package_facts:
+ manager: auto
+
+- name: check for ansible_facts.packages exists
+ assert:
+ that: ansible_facts.packages is defined
+
+- name: Run package_fact tests - FreeBSD
+ block:
+ - name: Gather package facts
+ package_facts:
+ manager: pkg
+
+ - name: check for ansible_facts.packages exists
+ assert:
+ that: ansible_facts.packages is defined
+
+ - name: check there is at least one package not flagged vital nor automatic
+ command: pkg query -e "%a = 0 && %V = 0" %n
+ register: not_vital_nor_automatic
+ failed_when: not not_vital_nor_automatic.stdout
+
+ - vars:
+ pkg_name: "{{ not_vital_nor_automatic.stdout_lines[0].strip() }}"
+ block:
+ - name: check the selected package is not vital
+ assert:
+ that:
+ - 'not ansible_facts.packages[pkg_name][0].vital'
+ - 'not ansible_facts.packages[pkg_name][0].automatic'
+
+ - name: flag the selected package as vital and automatic
+ command: 'pkg set --yes -v 1 -A 1 {{ pkg_name }}'
+
+ - name: Gather package facts (again)
+ package_facts:
+
+ - name: check the selected package is flagged vital and automatic
+ assert:
+ that:
+ - 'ansible_facts.packages[pkg_name][0].vital|bool'
+ - 'ansible_facts.packages[pkg_name][0].automatic|bool'
+ always:
+ - name: restore previous flags for the selected package
+ command: 'pkg set --yes -v 0 -A 0 {{ pkg_name }}'
+ when: ansible_os_family == "FreeBSD"
diff --git a/test/integration/targets/parsing/aliases b/test/integration/targets/parsing/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/parsing/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/parsing/bad_parsing.yml b/test/integration/targets/parsing/bad_parsing.yml
new file mode 100644
index 0000000..953ec07
--- /dev/null
+++ b/test/integration/targets/parsing/bad_parsing.yml
@@ -0,0 +1,12 @@
+- hosts: testhost
+
+ # the following commands should all parse fine and execute fine
+ # and represent quoting scenarios that should be legit
+
+ gather_facts: False
+
+ roles:
+
+ # this one has a lot of things that should fail, see makefile for operation w/ tags
+
+ - { role: test_bad_parsing }
diff --git a/test/integration/targets/parsing/good_parsing.yml b/test/integration/targets/parsing/good_parsing.yml
new file mode 100644
index 0000000..b68d911
--- /dev/null
+++ b/test/integration/targets/parsing/good_parsing.yml
@@ -0,0 +1,9 @@
+- hosts: testhost
+
+ # the following commands should all parse fine and execute fine
+ # and represent quoting scenarios that should be legit
+
+ gather_facts: False
+
+ roles:
+ - { role: test_good_parsing, tags: test_good_parsing }
diff --git a/test/integration/targets/parsing/roles/test_bad_parsing/tasks/main.yml b/test/integration/targets/parsing/roles/test_bad_parsing/tasks/main.yml
new file mode 100644
index 0000000..f1b2ec6
--- /dev/null
+++ b/test/integration/targets/parsing/roles/test_bad_parsing/tasks/main.yml
@@ -0,0 +1,60 @@
+# test code for the ping module
+# (c) 2014, Michael DeHaan <michael@ansible.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# the following tests all raise errors, to use them in a Makefile, we run them with different flags, as
+# otherwise ansible stops at the first one and we want to ensure STOP conditions for each
+
+- set_fact:
+ test_file: "{{ output_dir }}/ansible_test_file" # FIXME, use set tempdir
+ test_input: "owner=test"
+ bad_var: "{{ output_dir }}' owner=test"
+ chdir: "mom chdir=/tmp"
+ tags: common
+
+- file: name={{test_file}} state=touch
+ tags: common
+
+- name: remove touched file
+ file: name={{test_file}} state=absent
+ tags: common
+
+- name: include test that we cannot insert arguments
+ include: scenario1.yml
+ tags: scenario1
+
+- name: include test that we cannot duplicate arguments
+ include: scenario2.yml
+ tags: scenario2
+
+- name: include test that we can't do this for the shell module
+ include: scenario3.yml
+ tags: scenario3
+
+- name: include test that we can't go all Little Bobby Droptables on a quoted var to add more
+ include: scenario4.yml
+ tags: scenario4
+
+- name: test that a missing/malformed jinja2 filter fails
+ debug: msg="{{output_dir|badfiltername}}"
+ tags: scenario5
+ register: filter_fail
+ ignore_errors: yes
+
+- assert:
+ that:
+ - filter_fail is failed
diff --git a/test/integration/targets/parsing/roles/test_bad_parsing/tasks/scenario1.yml b/test/integration/targets/parsing/roles/test_bad_parsing/tasks/scenario1.yml
new file mode 100644
index 0000000..8a82fb9
--- /dev/null
+++ b/test/integration/targets/parsing/roles/test_bad_parsing/tasks/scenario1.yml
@@ -0,0 +1,4 @@
+- name: test that we cannot insert arguments
+ file: path={{ test_file }} {{ test_input }}
+ failed_when: False # ignore the module, just test the parser
+ tags: scenario1
diff --git a/test/integration/targets/parsing/roles/test_bad_parsing/tasks/scenario2.yml b/test/integration/targets/parsing/roles/test_bad_parsing/tasks/scenario2.yml
new file mode 100644
index 0000000..c3b4b13
--- /dev/null
+++ b/test/integration/targets/parsing/roles/test_bad_parsing/tasks/scenario2.yml
@@ -0,0 +1,4 @@
+- name: test that we cannot duplicate arguments
+ file: path={{ test_file }} owner=test2 {{ test_input }}
+ failed_when: False # ignore the module, just test the parser
+ tags: scenario2
diff --git a/test/integration/targets/parsing/roles/test_bad_parsing/tasks/scenario3.yml b/test/integration/targets/parsing/roles/test_bad_parsing/tasks/scenario3.yml
new file mode 100644
index 0000000..a228f70
--- /dev/null
+++ b/test/integration/targets/parsing/roles/test_bad_parsing/tasks/scenario3.yml
@@ -0,0 +1,4 @@
+- name: test that we can't do this for the shell module
+ shell: echo hi {{ chdir }}
+ failed_when: False
+ tags: scenario3
diff --git a/test/integration/targets/parsing/roles/test_bad_parsing/tasks/scenario4.yml b/test/integration/targets/parsing/roles/test_bad_parsing/tasks/scenario4.yml
new file mode 100644
index 0000000..2845adc
--- /dev/null
+++ b/test/integration/targets/parsing/roles/test_bad_parsing/tasks/scenario4.yml
@@ -0,0 +1,4 @@
+- name: test that we can't go all Little Bobby Droptables on a quoted var to add more
+ file: "name={{ bad_var }}"
+ failed_when: False
+ tags: scenario4
diff --git a/test/integration/targets/parsing/roles/test_bad_parsing/vars/main.yml b/test/integration/targets/parsing/roles/test_bad_parsing/vars/main.yml
new file mode 100644
index 0000000..1aaeac7
--- /dev/null
+++ b/test/integration/targets/parsing/roles/test_bad_parsing/vars/main.yml
@@ -0,0 +1,2 @@
+---
+output_dir: .
diff --git a/test/integration/targets/parsing/roles/test_good_parsing/tasks/main.yml b/test/integration/targets/parsing/roles/test_good_parsing/tasks/main.yml
new file mode 100644
index 0000000..d225c0f
--- /dev/null
+++ b/test/integration/targets/parsing/roles/test_good_parsing/tasks/main.yml
@@ -0,0 +1,217 @@
+# test code for the ping module
+# (c) 2014, Michael DeHaan <michael@ansible.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# various tests of things that should not cause parsing problems
+
+- set_fact:
+ test_input: "a=1 a=2 a=3"
+
+- set_fact:
+ multi_line: |
+ echo old
+ echo mcdonald
+ echo had
+ echo a
+ echo farm
+
+- shell: echo "dog"
+ register: result
+
+- assert:
+ that:
+ result.cmd == 'echo "dog"'
+
+- shell: echo 'dog'
+ register: result
+
+- assert:
+ that:
+ result.cmd == 'echo \'dog\''
+
+- name: a quoted argument is not sent to the shell module as anything but a string parameter
+ shell: echo 'dog' 'executable=/usr/bin/python'
+ register: result
+
+- debug: var=result.cmd
+
+- assert:
+ that:
+ result.cmd == "echo 'dog' 'executable=/usr/bin/python'"
+
+- name: it is valid to pass multiple key=value arguments because the shell doesn't check key=value arguments
+ shell: echo quackquack=here quackquack=everywhere
+ register: result
+
+- assert:
+ that:
+ result.cmd == 'echo quackquack=here quackquack=everywhere'
+
+- name: the same is true with quoting
+ shell: echo "quackquack=here quackquack=everywhere"
+ register: result
+
+- assert:
+ that:
+ result.cmd == 'echo "quackquack=here quackquack=everywhere"'
+
+- name: the same is true with quoting (B)
+ shell: echo "quackquack=here" "quackquack=everywhere"
+ register: result
+
+- name: the same is true with quoting (C)
+ shell: echo "quackquack=here" 'quackquack=everywhere'
+ register: result
+
+- name: the same is true with quoting (D)
+ shell: echo "quackquack=here" 'quackquack=everywhere'
+ register: result
+
+- name: the same is true with quoting (E)
+ shell: echo {{ test_input }}
+ register: result
+
+- assert:
+ that:
+ result.cmd == "echo a=1 a=2 a=3"
+
+- name: more shell duplicates
+ shell: echo foo=bar foo=bar
+ register: result
+
+- assert:
+ that:
+ result.cmd == "echo foo=bar foo=bar"
+
+- name: raw duplicates, noop
+ raw: env true foo=bar foo=bar
+
+- name: multi-line inline shell commands (should use script module but hey) are a thing
+ shell: "{{ multi_line }}"
+ register: result
+
+- debug: var=result
+
+- assert:
+ that:
+ result.stdout_lines == [ 'old', 'mcdonald', 'had', 'a', 'farm' ]
+
+- name: passing same arg to shell command is legit
+ shell: echo foo --arg=a --arg=b
+ failed_when: False # just catch the exit code, parse error is what I care about, but should register and compare result
+ register: result
+
+- assert:
+ that:
+ # command shouldn't end in spaces, amend test once fixed
+ - result.cmd == "echo foo --arg=a --arg=b"
+
+- name: test includes with params
+ include: test_include.yml fact_name=include_params param="{{ test_input }}"
+
+- name: assert the include set the correct fact for the param
+ assert:
+ that:
+ - include_params == test_input
+
+- name: test includes with quoted params
+ include: test_include.yml fact_name=double_quoted_param param="this is a param with double quotes"
+
+- name: assert the include set the correct fact for the double quoted param
+ assert:
+ that:
+ - double_quoted_param == "this is a param with double quotes"
+
+- name: test includes with single quoted params
+ include: test_include.yml fact_name=single_quoted_param param='this is a param with single quotes'
+
+- name: assert the include set the correct fact for the single quoted param
+ assert:
+ that:
+ - single_quoted_param == "this is a param with single quotes"
+
+- name: test includes with quoted params in complex args
+ include: test_include.yml
+ vars:
+ fact_name: complex_param
+ param: "this is a param in a complex arg with double quotes"
+
+- name: assert the include set the correct fact for the params in complex args
+ assert:
+ that:
+ - complex_param == "this is a param in a complex arg with double quotes"
+
+- name: test variable module name
+ action: "{{ variable_module_name }} msg='this should be debugged'"
+ register: result
+
+- name: assert the task with variable module name ran
+ assert:
+ that:
+ - result.msg == "this should be debugged"
+
+- name: test conditional includes
+ include: test_include_conditional.yml
+ when: false
+
+- name: assert the nested include from test_include_conditional was not set
+ assert:
+ that:
+ - nested_include_var is undefined
+
+- name: test omit in complex args
+ set_fact:
+ foo: bar
+ spam: "{{ omit }}"
+ should_not_omit: "prefix{{ omit }}"
+
+- assert:
+ that:
+ - foo == 'bar'
+ - spam is undefined
+ - should_not_omit is defined
+
+- name: test omit in module args
+ set_fact: >
+ yo=whatsup
+ eggs="{{ omit }}"
+ default_omitted="{{ not_exists|default(omit) }}"
+ should_not_omit_1="prefix{{ omit }}"
+ should_not_omit_2="{{ omit }}suffix"
+ should_not_omit_3="__omit_place_holder__afb6b9bc3d20bfeaa00a1b23a5930f89"
+
+- assert:
+ that:
+ - yo == 'whatsup'
+ - eggs is undefined
+ - default_omitted is undefined
+ - should_not_omit_1 is defined
+ - should_not_omit_2 is defined
+ - should_not_omit_3 == "__omit_place_holder__afb6b9bc3d20bfeaa00a1b23a5930f89"
+
+- name: Ensure module names are stripped of extra spaces (local_action)
+ local_action: set_fact b="b"
+ register: la_set_fact_b
+
+- name: Ensure module names are stripped of extra spaces (action)
+ action: set_fact c="c"
+ register: la_set_fact_c
+
+- assert:
+ that:
+ - b == "b"
+ - c == "c"
diff --git a/test/integration/targets/parsing/roles/test_good_parsing/tasks/test_include.yml b/test/integration/targets/parsing/roles/test_good_parsing/tasks/test_include.yml
new file mode 100644
index 0000000..4ba5035
--- /dev/null
+++ b/test/integration/targets/parsing/roles/test_good_parsing/tasks/test_include.yml
@@ -0,0 +1 @@
+- set_fact: "{{fact_name}}='{{param}}'"
diff --git a/test/integration/targets/parsing/roles/test_good_parsing/tasks/test_include_conditional.yml b/test/integration/targets/parsing/roles/test_good_parsing/tasks/test_include_conditional.yml
new file mode 100644
index 0000000..070888d
--- /dev/null
+++ b/test/integration/targets/parsing/roles/test_good_parsing/tasks/test_include_conditional.yml
@@ -0,0 +1 @@
+- include: test_include_nested.yml
diff --git a/test/integration/targets/parsing/roles/test_good_parsing/tasks/test_include_nested.yml b/test/integration/targets/parsing/roles/test_good_parsing/tasks/test_include_nested.yml
new file mode 100644
index 0000000..f1f6fcc
--- /dev/null
+++ b/test/integration/targets/parsing/roles/test_good_parsing/tasks/test_include_nested.yml
@@ -0,0 +1,2 @@
+- name: set the nested include fact
+ set_fact: nested_include_var=1
diff --git a/test/integration/targets/parsing/roles/test_good_parsing/vars/main.yml b/test/integration/targets/parsing/roles/test_good_parsing/vars/main.yml
new file mode 100644
index 0000000..ea7a0b8
--- /dev/null
+++ b/test/integration/targets/parsing/roles/test_good_parsing/vars/main.yml
@@ -0,0 +1,2 @@
+---
+variable_module_name: debug
diff --git a/test/integration/targets/parsing/runme.sh b/test/integration/targets/parsing/runme.sh
new file mode 100755
index 0000000..022ce4c
--- /dev/null
+++ b/test/integration/targets/parsing/runme.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook bad_parsing.yml -i ../../inventory -vvv "$@" --tags prepare,common,scenario5
+ansible-playbook good_parsing.yml -i ../../inventory -v "$@"
diff --git a/test/integration/targets/path_lookups/aliases b/test/integration/targets/path_lookups/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/path_lookups/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/path_lookups/play.yml b/test/integration/targets/path_lookups/play.yml
new file mode 100644
index 0000000..233f972
--- /dev/null
+++ b/test/integration/targets/path_lookups/play.yml
@@ -0,0 +1,49 @@
+- name: setup state
+ hosts: localhost
+ gather_facts: false
+ tasks:
+ - file: path={{playbook_dir}}/files state=directory
+ - file: path={{playbook_dir}}/roles/showfile/files state=directory
+ - copy: dest={{playbook_dir}}/roles/showfile/files/testfile content='in role files'
+ - copy: dest={{playbook_dir}}/roles/showfile/testfile content='in role'
+ - copy: dest={{playbook_dir}}/roles/showfile/tasks/testfile content='in role tasks'
+ - copy: dest={{playbook_dir}}/files/testfile content='in files'
+ - copy: dest={{playbook_dir}}/testfile content='in local'
+
+- import_playbook: testplay.yml
+ vars:
+ remove: nothing
+ role_out: in role files
+ play_out: in files
+
+- import_playbook: testplay.yml
+ vars:
+ remove: roles/showfile/files/testfile
+ role_out: in role
+ play_out: in files
+
+- import_playbook: testplay.yml
+ vars:
+ remove: roles/showfile/testfile
+ role_out: in role tasks
+ play_out: in files
+
+- import_playbook: testplay.yml
+ vars:
+ remove: roles/showfile/tasks/testfile
+ role_out: in files
+ play_out: in files
+
+- import_playbook: testplay.yml
+ vars:
+ remove: files/testfile
+ role_out: in local
+ play_out: in local
+
+- name: cleanup
+ hosts: localhost
+ gather_facts: false
+ tasks:
+ - file: path={{playbook_dir}}/testfile state=absent
+ - file: path={{playbook_dir}}/files state=absent
+ - file: path={{playbook_dir}}/roles/showfile/files state=absent
diff --git a/test/integration/targets/path_lookups/roles/showfile/tasks/main.yml b/test/integration/targets/path_lookups/roles/showfile/tasks/main.yml
new file mode 100644
index 0000000..1b38057
--- /dev/null
+++ b/test/integration/targets/path_lookups/roles/showfile/tasks/main.yml
@@ -0,0 +1,2 @@
+- name: relative to role
+ set_fact: role_result="{{lookup('file', 'testfile')}}"
diff --git a/test/integration/targets/path_lookups/runme.sh b/test/integration/targets/path_lookups/runme.sh
new file mode 100755
index 0000000..754150b
--- /dev/null
+++ b/test/integration/targets/path_lookups/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook play.yml -i ../../inventory -v "$@"
diff --git a/test/integration/targets/path_lookups/testplay.yml b/test/integration/targets/path_lookups/testplay.yml
new file mode 100644
index 0000000..8bf4553
--- /dev/null
+++ b/test/integration/targets/path_lookups/testplay.yml
@@ -0,0 +1,20 @@
+- name: test initial state
+ hosts: localhost
+ gather_facts: false
+ pre_tasks:
+ - name: remove {{ remove }}
+ file: path={{ playbook_dir }}/{{ remove }} state=absent
+ roles:
+ - showfile
+ post_tasks:
+ - name: from play
+ set_fact: play_result="{{lookup('file', 'testfile')}}"
+
+ - name: output stage {{ remove }} removed
+ debug: msg="play> {{play_out}}, role> {{role_out}}"
+
+ - name: verify that result match expected
+ assert:
+ that:
+ - 'play_result == play_out'
+ - 'role_result == role_out'
diff --git a/test/integration/targets/path_with_comma_in_inventory/aliases b/test/integration/targets/path_with_comma_in_inventory/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/path_with_comma_in_inventory/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/path_with_comma_in_inventory/playbook.yml b/test/integration/targets/path_with_comma_in_inventory/playbook.yml
new file mode 100644
index 0000000..64c8368
--- /dev/null
+++ b/test/integration/targets/path_with_comma_in_inventory/playbook.yml
@@ -0,0 +1,9 @@
+---
+- hosts: all
+ gather_facts: false
+ tasks:
+ - name: Ensure we can see group_vars from path with comma
+ assert:
+ that:
+ - inventory_var_from_path_with_commas is defined
+ - inventory_var_from_path_with_commas == 'here'
diff --git a/test/integration/targets/path_with_comma_in_inventory/runme.sh b/test/integration/targets/path_with_comma_in_inventory/runme.sh
new file mode 100755
index 0000000..833e2ac
--- /dev/null
+++ b/test/integration/targets/path_with_comma_in_inventory/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -ux
+
+ansible-playbook -i this,path,has,commas/hosts playbook.yml -v "$@"
diff --git a/test/integration/targets/path_with_comma_in_inventory/this,path,has,commas/group_vars/all.yml b/test/integration/targets/path_with_comma_in_inventory/this,path,has,commas/group_vars/all.yml
new file mode 100644
index 0000000..df5b84d
--- /dev/null
+++ b/test/integration/targets/path_with_comma_in_inventory/this,path,has,commas/group_vars/all.yml
@@ -0,0 +1 @@
+inventory_var_from_path_with_commas: 'here'
diff --git a/test/integration/targets/path_with_comma_in_inventory/this,path,has,commas/hosts b/test/integration/targets/path_with_comma_in_inventory/this,path,has,commas/hosts
new file mode 100644
index 0000000..5219b90
--- /dev/null
+++ b/test/integration/targets/path_with_comma_in_inventory/this,path,has,commas/hosts
@@ -0,0 +1 @@
+localhost ansible_connect=local
diff --git a/test/integration/targets/pause/aliases b/test/integration/targets/pause/aliases
new file mode 100644
index 0000000..8597f01
--- /dev/null
+++ b/test/integration/targets/pause/aliases
@@ -0,0 +1,3 @@
+needs/target/setup_pexpect
+shippable/posix/group3
+context/controller # this is a controller-only action, the module is just for documentation
diff --git a/test/integration/targets/pause/pause-1.yml b/test/integration/targets/pause/pause-1.yml
new file mode 100644
index 0000000..44c9960
--- /dev/null
+++ b/test/integration/targets/pause/pause-1.yml
@@ -0,0 +1,11 @@
+- name: Test pause module in default state
+ hosts: localhost
+ become: no
+ gather_facts: no
+
+ tasks:
+ - name: EXPECTED FAILURE
+ pause:
+
+ - debug:
+ msg: Task after pause
diff --git a/test/integration/targets/pause/pause-2.yml b/test/integration/targets/pause/pause-2.yml
new file mode 100644
index 0000000..81a7fda
--- /dev/null
+++ b/test/integration/targets/pause/pause-2.yml
@@ -0,0 +1,12 @@
+- name: Test pause module with custom prompt
+ hosts: localhost
+ become: no
+ gather_facts: no
+
+ tasks:
+ - name: EXPECTED FAILURE
+ pause:
+ prompt: Custom prompt
+
+ - debug:
+ msg: Task after pause
diff --git a/test/integration/targets/pause/pause-3.yml b/test/integration/targets/pause/pause-3.yml
new file mode 100644
index 0000000..8f8c72e
--- /dev/null
+++ b/test/integration/targets/pause/pause-3.yml
@@ -0,0 +1,12 @@
+- name: Test pause module with pause
+ hosts: localhost
+ become: no
+ gather_facts: no
+
+ tasks:
+ - name: EXPECTED FAILURE
+ pause:
+ seconds: 2
+
+ - debug:
+ msg: Task after pause
diff --git a/test/integration/targets/pause/pause-4.yml b/test/integration/targets/pause/pause-4.yml
new file mode 100644
index 0000000..f16c7d6
--- /dev/null
+++ b/test/integration/targets/pause/pause-4.yml
@@ -0,0 +1,13 @@
+- name: Test pause module with pause and custom prompt
+ hosts: localhost
+ become: no
+ gather_facts: no
+
+ tasks:
+ - name: EXPECTED FAILURE
+ pause:
+ seconds: 2
+ prompt: Waiting for two seconds
+
+ - debug:
+ msg: Task after pause
diff --git a/test/integration/targets/pause/pause-5.yml b/test/integration/targets/pause/pause-5.yml
new file mode 100644
index 0000000..22955cd
--- /dev/null
+++ b/test/integration/targets/pause/pause-5.yml
@@ -0,0 +1,35 @@
+- name: Test pause module echo output
+ hosts: localhost
+ become: no
+ gather_facts: no
+
+ tasks:
+ - pause:
+ echo: yes
+ prompt: Enter some text
+ register: results
+
+ - name: Ensure that input was captured
+ assert:
+ that:
+ - results.user_input == 'hello there'
+
+ - pause:
+ echo: yes
+ prompt: Enter some text to edit
+ register: result
+
+ - name: Ensure edited input was captured
+ assert:
+ that:
+ - result.user_input == 'hello tommy boy'
+
+ - pause:
+ echo: no
+ prompt: Enter some text
+ register: result
+
+ - name: Ensure secret input was caputered
+ assert:
+ that:
+ - result.user_input == 'supersecretpancakes'
diff --git a/test/integration/targets/pause/runme.sh b/test/integration/targets/pause/runme.sh
new file mode 100755
index 0000000..eb2c6f7
--- /dev/null
+++ b/test/integration/targets/pause/runme.sh
@@ -0,0 +1,43 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ANSIBLE_ROLES_PATH=../ ansible-playbook setup.yml
+
+# Test pause module when no tty and non-interactive with no seconds parameter.
+# This is to prevent playbooks from hanging in cron and Tower jobs.
+/usr/bin/env bash << EOF
+ansible-playbook test-pause-no-tty.yml 2>&1 | \
+ grep '\[WARNING\]: Not waiting for response to prompt as stdin is not interactive' && {
+ echo 'Successfully skipped pause in no TTY mode' >&2
+ exit 0
+ } || {
+ echo 'Failed to skip pause module' >&2
+ exit 1
+ }
+EOF
+
+# Do not issue a warning when run in the background if a timeout is given
+# https://github.com/ansible/ansible/issues/73042
+if sleep 0 | ansible localhost -m pause -a 'seconds=1' 2>&1 | grep '\[WARNING\]: Not waiting for response'; then
+ echo "Incorrectly issued warning when run in the background"
+ exit 1
+else
+ echo "Succesfully ran in the background with no warning"
+fi
+
+# Test redirecting stdout
+# https://github.com/ansible/ansible/issues/41717
+if ansible-playbook pause-3.yml > /dev/null ; then
+ echo "Successfully redirected stdout"
+else
+ echo "Failure when attempting to redirect stdout"
+ exit 1
+fi
+
+
+# Test pause with seconds and minutes specified
+ansible-playbook test-pause.yml "$@"
+
+# Interactively test pause
+python test-pause.py "$@"
diff --git a/test/integration/targets/pause/setup.yml b/test/integration/targets/pause/setup.yml
new file mode 100644
index 0000000..9f6ab11
--- /dev/null
+++ b/test/integration/targets/pause/setup.yml
@@ -0,0 +1,4 @@
+- hosts: localhost
+ gather_facts: no
+ roles:
+ - setup_pexpect
diff --git a/test/integration/targets/pause/test-pause-background.yml b/test/integration/targets/pause/test-pause-background.yml
new file mode 100644
index 0000000..e480a77
--- /dev/null
+++ b/test/integration/targets/pause/test-pause-background.yml
@@ -0,0 +1,10 @@
+- name: Test pause in a background task
+ hosts: localhost
+ gather_facts: no
+ become: no
+
+ tasks:
+ - pause:
+
+ - pause:
+ seconds: 1
diff --git a/test/integration/targets/pause/test-pause-no-tty.yml b/test/integration/targets/pause/test-pause-no-tty.yml
new file mode 100644
index 0000000..6e0e402
--- /dev/null
+++ b/test/integration/targets/pause/test-pause-no-tty.yml
@@ -0,0 +1,7 @@
+- name: Test pause
+ hosts: localhost
+ gather_facts: no
+ become: no
+
+ tasks:
+ - pause:
diff --git a/test/integration/targets/pause/test-pause.py b/test/integration/targets/pause/test-pause.py
new file mode 100755
index 0000000..3703470
--- /dev/null
+++ b/test/integration/targets/pause/test-pause.py
@@ -0,0 +1,292 @@
+#!/usr/bin/env python
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+import pexpect
+import sys
+import termios
+
+from ansible.module_utils.six import PY2
+
+args = sys.argv[1:]
+
+env_vars = {
+ 'ANSIBLE_ROLES_PATH': './roles',
+ 'ANSIBLE_NOCOLOR': 'True',
+ 'ANSIBLE_RETRY_FILES_ENABLED': 'False'
+}
+
+try:
+ backspace = termios.tcgetattr(sys.stdin.fileno())[6][termios.VERASE]
+except Exception:
+ backspace = b'\x7f'
+
+if PY2:
+ log_buffer = sys.stdout
+else:
+ log_buffer = sys.stdout.buffer
+
+os.environ.update(env_vars)
+
+# -- Plain pause -- #
+playbook = 'pause-1.yml'
+
+# Case 1 - Contiune with enter
+pause_test = pexpect.spawn(
+ 'ansible-playbook',
+ args=[playbook] + args,
+ timeout=10,
+ env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Press enter to continue, Ctrl\+C to interrupt:')
+pause_test.send('\r')
+pause_test.expect('Task after pause')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+
+# Case 2 - Continue with C
+pause_test = pexpect.spawn(
+ 'ansible-playbook',
+ args=[playbook] + args,
+ timeout=10,
+ env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Press enter to continue, Ctrl\+C to interrupt:')
+pause_test.send('\x03')
+pause_test.expect("Press 'C' to continue the play or 'A' to abort")
+pause_test.send('C')
+pause_test.expect('Task after pause')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+
+# Case 3 - Abort with A
+pause_test = pexpect.spawn(
+ 'ansible-playbook',
+ args=[playbook] + args,
+ timeout=10,
+ env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Press enter to continue, Ctrl\+C to interrupt:')
+pause_test.send('\x03')
+pause_test.expect("Press 'C' to continue the play or 'A' to abort")
+pause_test.send('A')
+pause_test.expect('user requested abort!')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+# -- Custom Prompt -- #
+playbook = 'pause-2.yml'
+
+# Case 1 - Contiune with enter
+pause_test = pexpect.spawn(
+ 'ansible-playbook',
+ args=[playbook] + args,
+ timeout=10,
+ env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Custom prompt:')
+pause_test.send('\r')
+pause_test.expect('Task after pause')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+
+# Case 2 - Contiune with C
+pause_test = pexpect.spawn(
+ 'ansible-playbook',
+ args=[playbook] + args,
+ timeout=10,
+ env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Custom prompt:')
+pause_test.send('\x03')
+pause_test.expect("Press 'C' to continue the play or 'A' to abort")
+pause_test.send('C')
+pause_test.expect('Task after pause')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+
+# Case 3 - Abort with A
+pause_test = pexpect.spawn(
+ 'ansible-playbook',
+ args=[playbook] + args,
+ timeout=10,
+ env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Custom prompt:')
+pause_test.send('\x03')
+pause_test.expect("Press 'C' to continue the play or 'A' to abort")
+pause_test.send('A')
+pause_test.expect('user requested abort!')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+# -- Pause for N seconds -- #
+
+playbook = 'pause-3.yml'
+
+# Case 1 - Wait for task to continue after timeout
+pause_test = pexpect.spawn(
+ 'ansible-playbook',
+ args=[playbook] + args,
+ timeout=10,
+ env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Pausing for \d+ seconds')
+pause_test.expect(r"\(ctrl\+C then 'C' = continue early, ctrl\+C then 'A' = abort\)")
+pause_test.expect('Task after pause')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+# Case 2 - Contiune with Ctrl + C, C
+pause_test = pexpect.spawn(
+ 'ansible-playbook',
+ args=[playbook] + args,
+ timeout=10,
+ env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Pausing for \d+ seconds')
+pause_test.expect(r"\(ctrl\+C then 'C' = continue early, ctrl\+C then 'A' = abort\)")
+pause_test.send('\x03')
+pause_test.send('C')
+pause_test.expect('Task after pause')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+
+# Case 3 - Abort with Ctrl + C, A
+pause_test = pexpect.spawn(
+ 'ansible-playbook',
+ args=[playbook] + args,
+ timeout=10,
+ env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Pausing for \d+ seconds')
+pause_test.expect(r"\(ctrl\+C then 'C' = continue early, ctrl\+C then 'A' = abort\)")
+pause_test.send('\x03')
+pause_test.send('A')
+pause_test.expect('user requested abort!')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+# -- Pause for N seconds with custom prompt -- #
+
+playbook = 'pause-4.yml'
+
+# Case 1 - Wait for task to continue after timeout
+pause_test = pexpect.spawn(
+ 'ansible-playbook',
+ args=[playbook] + args,
+ timeout=10,
+ env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Pausing for \d+ seconds')
+pause_test.expect(r"\(ctrl\+C then 'C' = continue early, ctrl\+C then 'A' = abort\)")
+pause_test.expect(r"Waiting for two seconds:")
+pause_test.expect('Task after pause')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+# Case 2 - Contiune with Ctrl + C, C
+pause_test = pexpect.spawn(
+ 'ansible-playbook',
+ args=[playbook] + args,
+ timeout=10,
+ env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Pausing for \d+ seconds')
+pause_test.expect(r"\(ctrl\+C then 'C' = continue early, ctrl\+C then 'A' = abort\)")
+pause_test.expect(r"Waiting for two seconds:")
+pause_test.send('\x03')
+pause_test.send('C')
+pause_test.expect('Task after pause')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+
+# Case 3 - Abort with Ctrl + C, A
+pause_test = pexpect.spawn(
+ 'ansible-playbook',
+ args=[playbook] + args,
+ timeout=10,
+ env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Pausing for \d+ seconds')
+pause_test.expect(r"\(ctrl\+C then 'C' = continue early, ctrl\+C then 'A' = abort\)")
+pause_test.expect(r"Waiting for two seconds:")
+pause_test.send('\x03')
+pause_test.send('A')
+pause_test.expect('user requested abort!')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+# -- Enter input and ensure it's captured, echoed, and can be edited -- #
+
+playbook = 'pause-5.yml'
+
+pause_test = pexpect.spawn(
+ 'ansible-playbook',
+ args=[playbook] + args,
+ timeout=10,
+ env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Enter some text:')
+pause_test.send('hello there')
+pause_test.send('\r')
+pause_test.expect(r'Enter some text to edit:')
+pause_test.send('hello there')
+pause_test.send(backspace * 4)
+pause_test.send('ommy boy')
+pause_test.send('\r')
+pause_test.expect(r'Enter some text \(output is hidden\):')
+pause_test.send('supersecretpancakes')
+pause_test.send('\r')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+
+# Test that enter presses may not continue the play when a timeout is set.
+
+pause_test = pexpect.spawn(
+ 'ansible-playbook',
+ args=["pause-3.yml"] + args,
+ timeout=10,
+ env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r"\(ctrl\+C then 'C' = continue early, ctrl\+C then 'A' = abort\)")
+pause_test.send('\r')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
diff --git a/test/integration/targets/pause/test-pause.yml b/test/integration/targets/pause/test-pause.yml
new file mode 100644
index 0000000..1c8045b
--- /dev/null
+++ b/test/integration/targets/pause/test-pause.yml
@@ -0,0 +1,72 @@
+- name: Test pause
+ hosts: localhost
+ gather_facts: no
+ become: no
+
+ tasks:
+ - name: non-integer for duraction (EXPECTED FAILURE)
+ pause:
+ seconds: hello
+ register: result
+ ignore_errors: yes
+
+ - assert:
+ that:
+ - result is failed
+ - "'unable to convert to int' in result.msg"
+
+ - name: non-boolean for echo (EXPECTED FAILURE)
+ pause:
+ echo: hello
+ register: result
+ ignore_errors: yes
+
+ - assert:
+ that:
+ - result is failed
+ - "'not a valid boolean' in result.msg"
+
+ - name: Less than 1
+ pause:
+ seconds: 0.1
+ register: results
+
+ - assert:
+ that:
+ - results.stdout is search('Paused for \d+\.\d+ seconds')
+
+ - name: 1 second
+ pause:
+ seconds: 1
+ register: results
+
+ - assert:
+ that:
+ - results.stdout is search('Paused for \d+\.\d+ seconds')
+
+ - name: 1 minute
+ pause:
+ minutes: 1
+ register: results
+
+ - assert:
+ that:
+ - results.stdout is search('Paused for \d+\.\d+ minutes')
+
+ - name: minutes and seconds
+ pause:
+ minutes: 1
+ seconds: 1
+ register: exclusive
+ ignore_errors: yes
+
+ - name: invalid arg
+ pause:
+ foo: bar
+ register: invalid
+ ignore_errors: yes
+
+ - assert:
+ that:
+ - '"parameters are mutually exclusive: minutes|seconds" in exclusive.msg'
+ - '"Unsupported parameters for (pause) module: foo." in invalid.msg'
diff --git a/test/integration/targets/ping/aliases b/test/integration/targets/ping/aliases
new file mode 100644
index 0000000..765b70d
--- /dev/null
+++ b/test/integration/targets/ping/aliases
@@ -0,0 +1 @@
+shippable/posix/group2
diff --git a/test/integration/targets/ping/tasks/main.yml b/test/integration/targets/ping/tasks/main.yml
new file mode 100644
index 0000000..bc93f98
--- /dev/null
+++ b/test/integration/targets/ping/tasks/main.yml
@@ -0,0 +1,53 @@
+# test code for the ping module
+# (c) 2014, James Cammarata <jcammarata@ansible.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: ping the test
+ ping:
+ register: result
+
+- name: assert the ping worked
+ assert:
+ that:
+ - result is not failed
+ - result is not changed
+ - result.ping == 'pong'
+
+- name: ping with data
+ ping:
+ data: testing
+ register: result
+
+- name: assert the ping worked with data
+ assert:
+ that:
+ - result is not failed
+ - result is not changed
+ - result.ping == 'testing'
+
+- name: ping with data=crash
+ ping:
+ data: crash
+ register: result
+ ignore_errors: yes
+
+- name: assert the ping failed with data=boom
+ assert:
+ that:
+ - result is failed
+ - result is not changed
+ - "'Exception: boom' in result.module_stdout + result.module_stderr"
diff --git a/test/integration/targets/pip/aliases b/test/integration/targets/pip/aliases
new file mode 100644
index 0000000..aa159d9
--- /dev/null
+++ b/test/integration/targets/pip/aliases
@@ -0,0 +1,2 @@
+destructive
+shippable/posix/group2
diff --git a/test/integration/targets/pip/files/ansible_test_pip_chdir/__init__.py b/test/integration/targets/pip/files/ansible_test_pip_chdir/__init__.py
new file mode 100644
index 0000000..5d1f9ae
--- /dev/null
+++ b/test/integration/targets/pip/files/ansible_test_pip_chdir/__init__.py
@@ -0,0 +1,6 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+def main():
+ print("success")
diff --git a/test/integration/targets/pip/files/setup.py b/test/integration/targets/pip/files/setup.py
new file mode 100755
index 0000000..aaf2187
--- /dev/null
+++ b/test/integration/targets/pip/files/setup.py
@@ -0,0 +1,17 @@
+#!/usr/bin/env python
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from setuptools import setup, find_packages
+
+setup(
+ name="ansible_test_pip_chdir",
+ version="0",
+ packages=find_packages(),
+ entry_points={
+ 'console_scripts': [
+ 'ansible_test_pip_chdir = ansible_test_pip_chdir:main'
+ ]
+ }
+)
diff --git a/test/integration/targets/pip/meta/main.yml b/test/integration/targets/pip/meta/main.yml
new file mode 100644
index 0000000..2d78e29
--- /dev/null
+++ b/test/integration/targets/pip/meta/main.yml
@@ -0,0 +1,4 @@
+dependencies:
+ - prepare_tests
+ - setup_remote_tmp_dir
+ - setup_remote_constraints
diff --git a/test/integration/targets/pip/tasks/default_cleanup.yml b/test/integration/targets/pip/tasks/default_cleanup.yml
new file mode 100644
index 0000000..f2265c0
--- /dev/null
+++ b/test/integration/targets/pip/tasks/default_cleanup.yml
@@ -0,0 +1,5 @@
+- name: remove unwanted packages
+ package:
+ name: git
+ state: absent
+ when: git_install.changed
diff --git a/test/integration/targets/pip/tasks/freebsd_cleanup.yml b/test/integration/targets/pip/tasks/freebsd_cleanup.yml
new file mode 100644
index 0000000..fa224d8
--- /dev/null
+++ b/test/integration/targets/pip/tasks/freebsd_cleanup.yml
@@ -0,0 +1,6 @@
+- name: remove auto-installed packages from FreeBSD
+ pkgng:
+ name: git
+ state: absent
+ autoremove: yes
+ when: git_install.changed
diff --git a/test/integration/targets/pip/tasks/main.yml b/test/integration/targets/pip/tasks/main.yml
new file mode 100644
index 0000000..66992fd
--- /dev/null
+++ b/test/integration/targets/pip/tasks/main.yml
@@ -0,0 +1,54 @@
+# Current pip unconditionally uses md5.
+# We can re-enable if pip switches to a different hash or allows us to not check md5.
+
+- name: Python 2
+ when: ansible_python.version.major == 2
+ block:
+ - name: find virtualenv command
+ command: "which virtualenv virtualenv-{{ ansible_python.version.major }}.{{ ansible_python.version.minor }}"
+ register: command
+ ignore_errors: true
+
+ - name: is virtualenv available to python -m
+ command: '{{ ansible_python_interpreter }} -m virtualenv'
+ register: python_m
+ when: not command.stdout_lines
+ failed_when: python_m.rc != 2
+
+ - name: remember selected virtualenv command
+ set_fact:
+ virtualenv: "{{ command.stdout_lines[0] if command is successful else ansible_python_interpreter ~ ' -m virtualenv' }}"
+
+- name: Python 3+
+ when: ansible_python.version.major > 2
+ block:
+ - name: remember selected virtualenv command
+ set_fact:
+ virtualenv: "{{ ansible_python_interpreter ~ ' -m venv' }}"
+
+- block:
+ - name: install git, needed for repo installs
+ package:
+ name: git
+ state: present
+ when: ansible_distribution not in ["MacOSX", "Alpine"]
+ register: git_install
+
+ - name: ensure wheel is installed
+ pip:
+ name: wheel
+ extra_args: "-c {{ remote_constraints }}"
+
+ - include_tasks: pip.yml
+ always:
+ - name: platform specific cleanup
+ include_tasks: "{{ cleanup_filename }}"
+ with_first_found:
+ - "{{ ansible_distribution | lower }}_cleanup.yml"
+ - "default_cleanup.yml"
+ loop_control:
+ loop_var: cleanup_filename
+ when: ansible_fips|bool != True
+ module_defaults:
+ pip:
+ virtualenv_command: "{{ virtualenv }}"
diff --git a/test/integration/targets/pip/tasks/pip.yml b/test/integration/targets/pip/tasks/pip.yml
new file mode 100644
index 0000000..3948061
--- /dev/null
+++ b/test/integration/targets/pip/tasks/pip.yml
@@ -0,0 +1,601 @@
+# test code for the pip module
+# (c) 2014, Michael DeHaan <michael.dehaan@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# FIXME: replace the python test package
+
+# first some tests installed system-wide
+# verify things were not installed to start with
+
+- name: ensure packages are not installed (precondition setup)
+ pip:
+ name: "{{ pip_test_packages }}"
+ state: absent
+
+# verify that a package that is uninstalled being set to absent
+# results in an unchanged state and that the test package is not
+# installed
+
+- name: ensure packages are not installed
+ pip:
+ name: "{{ pip_test_packages }}"
+ state: absent
+ register: uninstall_result
+
+- name: removing unremoved packages should return unchanged
+ assert:
+ that:
+ - "not (uninstall_result is changed)"
+
+- command: "{{ ansible_python.executable }} -c 'import {{ item }}'"
+ register: absent_result
+ failed_when: "absent_result.rc == 0"
+ loop: '{{ pip_test_modules }}'
+
+# now we're going to install the test package knowing it is uninstalled
+# and check that installation was ok
+
+- name: ensure packages are installed
+ pip:
+ name: "{{ pip_test_packages }}"
+ state: present
+ register: install_result
+
+- name: verify we recorded a change
+ assert:
+ that:
+ - "install_result is changed"
+
+- command: "{{ ansible_python.executable }} -c 'import {{ item }}'"
+ loop: '{{ pip_test_modules }}'
+
+# now remove it to test uninstallation of a package we are sure is installed
+
+- name: now uninstall so we can see that a change occurred
+ pip:
+ name: "{{ pip_test_packages }}"
+ state: absent
+ register: absent2
+
+- name: assert a change occurred on uninstallation
+ assert:
+ that:
+ - "absent2 is changed"
+
+# put the test packages back
+
+- name: now put it back in case someone wanted it (like us!)
+ pip:
+ name: "{{ pip_test_packages }}"
+ state: present
+
+# Test virtualenv installations
+
+- name: "make sure the test env doesn't exist"
+ file:
+ state: absent
+ name: "{{ remote_tmp_dir }}/pipenv"
+
+- name: create a requirement file with an vcs url
+ copy:
+ dest: "{{ remote_tmp_dir }}/pipreq.txt"
+ content: "-e git+https://github.com/dvarrazzo/pyiso8601#egg=iso8601"
+
+- name: install the requirement file in a virtualenv
+ pip:
+ requirements: "{{ remote_tmp_dir}}/pipreq.txt"
+ virtualenv: "{{ remote_tmp_dir }}/pipenv"
+ register: req_installed
+
+- name: check that a change occurred
+ assert:
+ that:
+ - "req_installed is changed"
+
+- name: "repeat installation to check status didn't change"
+ pip:
+ requirements: "{{ remote_tmp_dir}}/pipreq.txt"
+ virtualenv: "{{ remote_tmp_dir }}/pipenv"
+ register: req_installed
+
+- name: "check that a change didn't occurr this time (bug ansible#1705)"
+ assert:
+ that:
+ - "not (req_installed is changed)"
+
+- name: install the same module from url
+ pip:
+ name: "git+https://github.com/dvarrazzo/pyiso8601#egg=iso8601"
+ virtualenv: "{{ remote_tmp_dir }}/pipenv"
+ editable: True
+ register: url_installed
+
+- name: "check that a change didn't occurr (bug ansible-modules-core#1645)"
+ assert:
+ that:
+ - "not (url_installed is changed)"
+
+# Test pip package in check mode doesn't always report changed.
+
+# Special case for pip
+- name: check for pip package
+ pip:
+ name: pip
+ virtualenv: "{{ remote_tmp_dir }}/pipenv"
+ state: present
+
+- name: check for pip package in check_mode
+ pip:
+ name: pip
+ virtualenv: "{{ remote_tmp_dir }}/pipenv"
+ state: present
+ check_mode: True
+ register: pip_check_mode
+
+- name: make sure pip in check_mode doesn't report changed
+ assert:
+ that:
+ - "not (pip_check_mode is changed)"
+
+# Special case for setuptools
+- name: check for setuptools package
+ pip:
+ name: setuptools
+ virtualenv: "{{ remote_tmp_dir }}/pipenv"
+ state: present
+
+- name: check for setuptools package in check_mode
+ pip:
+ name: setuptools
+ virtualenv: "{{ remote_tmp_dir }}/pipenv"
+ state: present
+ check_mode: True
+ register: setuptools_check_mode
+
+- name: make sure setuptools in check_mode doesn't report changed
+ assert:
+ that:
+ - "not (setuptools_check_mode is changed)"
+
+
+# Normal case
+- name: check for q package
+ pip:
+ name: q
+ virtualenv: "{{ remote_tmp_dir }}/pipenv"
+ state: present
+
+- name: check for q package in check_mode
+ pip:
+ name: q
+ virtualenv: "{{ remote_tmp_dir }}/pipenv"
+ state: present
+ check_mode: True
+ register: q_check_mode
+
+- name: make sure q in check_mode doesn't report changed
+ assert:
+ that:
+ - "not (q_check_mode is changed)"
+
+# Case with package name that has a different package name case and an
+# underscore instead of a hyphen
+- name: check for Junit-XML package
+ pip:
+ name: Junit-XML
+ virtualenv: "{{ remote_tmp_dir }}/pipenv"
+ state: present
+
+- name: check for Junit-XML package in check_mode
+ pip:
+ name: Junit-XML
+ virtualenv: "{{ remote_tmp_dir }}/pipenv"
+ state: present
+ check_mode: True
+ register: diff_case_check_mode
+
+- name: make sure Junit-XML in check_mode doesn't report changed
+ assert:
+ that:
+ - "diff_case_check_mode is not changed"
+
+# ansible#23204
+- name: ensure is a fresh virtualenv
+ file:
+ state: absent
+ name: "{{ remote_tmp_dir }}/pipenv"
+
+- name: install pip throught pip into fresh virtualenv
+ pip:
+ name: pip
+ virtualenv: "{{ remote_tmp_dir }}/pipenv"
+ register: pip_install_venv
+
+- name: make sure pip in fresh virtualenv report changed
+ assert:
+ that:
+ - "pip_install_venv is changed"
+
+# https://github.com/ansible/ansible/issues/37912
+# support chdir without virtualenv
+- name: create chdir test directories
+ file:
+ state: directory
+ name: "{{ remote_tmp_dir }}/{{ item }}"
+ loop:
+ - pip_module
+ - pip_root
+ - pip_module/ansible_test_pip_chdir
+
+- name: copy test module
+ copy:
+ src: "{{ item }}"
+ dest: "{{ remote_tmp_dir }}/pip_module/{{ item }}"
+ loop:
+ - setup.py
+ - ansible_test_pip_chdir/__init__.py
+
+- name: install test module
+ pip:
+ name: .
+ chdir: "{{ remote_tmp_dir }}/pip_module"
+ extra_args: --user --upgrade --root {{ remote_tmp_dir }}/pip_root
+
+- name: register python_site_lib
+ command: '{{ ansible_python.executable }} -c "import site; print(site.USER_SITE)"'
+ register: pip_python_site_lib
+
+- name: register python_user_base
+ command: '{{ ansible_python.executable }} -c "import site; print(site.USER_BASE)"'
+ register: pip_python_user_base
+
+- name: run test module
+ shell: "PYTHONPATH=$(echo {{ remote_tmp_dir }}/pip_root{{ pip_python_site_lib.stdout }}) {{ remote_tmp_dir }}/pip_root{{ pip_python_user_base.stdout }}/bin/ansible_test_pip_chdir"
+ register: pip_chdir_command
+
+- name: make sure command ran
+ assert:
+ that:
+ - pip_chdir_command.stdout == "success"
+
+# https://github.com/ansible/ansible/issues/25122
+- name: ensure is a fresh virtualenv
+ file:
+ state: absent
+ name: "{{ remote_tmp_dir }}/pipenv"
+
+- name: install requirements file into virtual + chdir
+ pip:
+ name: q
+ chdir: "{{ remote_tmp_dir }}/"
+ virtualenv: "pipenv"
+ state: present
+ register: venv_chdir
+
+- name: make sure fresh virtualenv + chdir report changed
+ assert:
+ that:
+ - "venv_chdir is changed"
+
+# ansible#38785
+- name: allow empty list of packages
+ pip:
+ name: []
+ register: pip_install_empty
+
+- name: ensure empty install is successful
+ assert:
+ that:
+ - "not (pip_install_empty is changed)"
+
+# https://github.com/ansible/ansible/issues/41043
+- block:
+ - name: Ensure previous virtualenv no longer exists
+ file:
+ state: absent
+ name: "{{ remote_tmp_dir }}/pipenv"
+
+ - name: do not consider an empty string as a version
+ pip:
+ name: q
+ state: present
+ version: ""
+ virtualenv: "{{ remote_tmp_dir }}/pipenv"
+ register: pip_empty_version_string
+
+ - name: test idempotency with empty string
+ pip:
+ name: q
+ state: present
+ version: ""
+ virtualenv: "{{ remote_tmp_dir }}/pipenv"
+ register: pip_empty_version_string_idempotency
+
+ - name: test idempotency without empty string
+ pip:
+ name: q
+ state: present
+ virtualenv: "{{ remote_tmp_dir }}/pipenv"
+ register: pip_no_empty_version_string_idempotency
+
+ # 'present' and version=="" is analogous to latest when first installed
+ - name: ensure we installed the latest version
+ pip:
+ name: q
+ state: latest
+ virtualenv: "{{ remote_tmp_dir }}/pipenv"
+ register: pip_empty_version_idempotency
+
+ - name: ensure that installation worked and is idempotent
+ assert:
+ that:
+ - pip_empty_version_string is changed
+ - pip_empty_version_string is successful
+ - pip_empty_version_idempotency is not changed
+ - pip_no_empty_version_string_idempotency is not changed
+ - pip_empty_version_string_idempotency is not changed
+
+# test version specifiers
+- name: make sure no test_package installed now
+ pip:
+ name: "{{ pip_test_packages }}"
+ state: absent
+
+- name: install package with version specifiers
+ pip:
+ name: "{{ pip_test_package }}"
+ version: "<100,!=1.0,>0.0.0"
+ register: version
+
+- name: assert package installed correctly
+ assert:
+ that: "version.changed"
+
+- name: reinstall package
+ pip:
+ name: "{{ pip_test_package }}"
+ version: "<100,!=1.0,>0.0.0"
+ register: version2
+
+- name: assert no changes ocurred
+ assert:
+ that: "not version2.changed"
+
+- name: test the check_mod
+ pip:
+ name: "{{ pip_test_package }}"
+ version: "<100,!=1.0,>0.0.0"
+ check_mode: yes
+ register: version3
+
+- name: assert no changes
+ assert:
+ that: "not version3.changed"
+
+- name: test the check_mod with unsatisfied version
+ pip:
+ name: "{{ pip_test_package }}"
+ version: ">100.0.0"
+ check_mode: yes
+ register: version4
+
+- name: assert changed
+ assert:
+ that: "version4.changed"
+
+- name: uninstall test packages for next test
+ pip:
+ name: "{{ pip_test_packages }}"
+ state: absent
+
+- name: test invalid combination of arguments
+ pip:
+ name: "{{ pip_test_pkg_ver }}"
+ version: "1.11.1"
+ ignore_errors: yes
+ register: version5
+
+- name: assert the invalid combination should fail
+ assert:
+ that: "version5 is failed"
+
+- name: another invalid combination of arguments
+ pip:
+ name: "{{ pip_test_pkg_ver[0] }}"
+ version: "<100.0.0"
+ ignore_errors: yes
+ register: version6
+
+- name: assert invalid combination should fail
+ assert:
+ that: "version6 is failed"
+
+- name: try to install invalid package
+ pip:
+ name: "{{ pip_test_pkg_ver_unsatisfied }}"
+ ignore_errors: yes
+ register: version7
+
+- name: assert install should fail
+ assert:
+ that: "version7 is failed"
+
+- name: test install multi-packages with version specifiers
+ pip:
+ name: "{{ pip_test_pkg_ver }}"
+ register: version8
+
+- name: assert packages installed correctly
+ assert:
+ that: "version8.changed"
+
+- name: test install multi-packages with check_mode
+ pip:
+ name: "{{ pip_test_pkg_ver }}"
+ check_mode: yes
+ register: version9
+
+- name: assert no change
+ assert:
+ that: "not version9.changed"
+
+- name: test install unsatisfied multi-packages with check_mode
+ pip:
+ name: "{{ pip_test_pkg_ver_unsatisfied }}"
+ check_mode: yes
+ register: version10
+
+- name: assert changes needed
+ assert:
+ that: "version10.changed"
+
+- name: uninstall packages for next test
+ pip:
+ name: "{{ pip_test_packages }}"
+ state: absent
+
+- name: test install multi package provided by one single string
+ pip:
+ name: "{{pip_test_pkg_ver[0]}},{{pip_test_pkg_ver[1]}}"
+ register: version11
+
+- name: assert the install ran correctly
+ assert:
+ that: "version11.changed"
+
+- name: test install multi package provided by one single string with check_mode
+ pip:
+ name: "{{pip_test_pkg_ver[0]}},{{pip_test_pkg_ver[1]}}"
+ check_mode: yes
+ register: version12
+
+- name: assert no changes needed
+ assert:
+ that: "not version12.changed"
+
+- name: test module can parse the combination of multi-packages one line and git url
+ pip:
+ name:
+ - git+https://github.com/dvarrazzo/pyiso8601#egg=iso8601
+ - "{{pip_test_pkg_ver[0]}},{{pip_test_pkg_ver[1]}}"
+
+- name: test the invalid package name
+ pip:
+ name: djan=+-~!@#$go>1.11.1,<1.11.3
+ ignore_errors: yes
+ register: version13
+
+- name: the invalid package should make module failed
+ assert:
+ that: "version13 is failed"
+
+- name: try install package with setuptools extras
+ pip:
+ name:
+ - "{{pip_test_package}}[test]"
+
+- name: clean up
+ pip:
+ name: "{{ pip_test_packages }}"
+ state: absent
+
+# https://github.com/ansible/ansible/issues/47198
+# distribute is a legacy package that will fail on newer Python 3 versions
+- block:
+ - name: make sure the virtualenv does not exist
+ file:
+ state: absent
+ name: "{{ remote_tmp_dir }}/pipenv"
+
+ - name: install distribute in the virtualenv
+ pip:
+ # using -c for constraints is not supported as long as tests are executed using the centos6 container
+ # since the pip version in the venv is not upgraded and is too old (6.0.8)
+ name:
+ - distribute
+ - setuptools<45 # setuptools 45 and later require python 3.5 or later
+ virtualenv: "{{ remote_tmp_dir }}/pipenv"
+ state: present
+
+ - name: try to remove distribute
+ pip:
+ state: "absent"
+ name: "distribute"
+ virtualenv: "{{ remote_tmp_dir }}/pipenv"
+ ignore_errors: yes
+ register: remove_distribute
+
+ - name: inspect the cmd
+ assert:
+ that: "'distribute' in remove_distribute.cmd"
+ when: ansible_python.version.major == 2
+
+### test virtualenv_command begin ###
+
+- name: Test virtualenv command with arguments
+ when: ansible_python.version.major == 2
+ block:
+ - name: make sure the virtualenv does not exist
+ file:
+ state: absent
+ name: "{{ remote_tmp_dir }}/pipenv"
+
+ # ref: https://github.com/ansible/ansible/issues/52275
+ - name: install using virtualenv_command with arguments
+ pip:
+ name: "{{ pip_test_package }}"
+ virtualenv: "{{ remote_tmp_dir }}/pipenv"
+ virtualenv_command: "{{ command.stdout_lines[0] | basename }} --verbose"
+ state: present
+ register: version13
+
+ - name: ensure install using virtualenv_command with arguments was successful
+ assert:
+ that:
+ - "version13 is success"
+
+### test virtualenv_command end ###
+
+# https://github.com/ansible/ansible/issues/68592
+# Handle pre-release version numbers in check_mode for already-installed
+# packages.
+- block:
+ - name: Install a pre-release version of a package
+ pip:
+ name: fallible
+ version: 0.0.1a2
+ state: present
+
+ - name: Use check_mode and ensure that the package is shown as installed
+ check_mode: true
+ pip:
+ name: fallible
+ state: present
+ register: pip_prereleases
+
+ - name: Uninstall the pre-release package if we need to
+ pip:
+ name: fallible
+ version: 0.0.1a2
+ state: absent
+ when: pip_prereleases is changed
+
+ - assert:
+ that:
+ - pip_prereleases is successful
+ - pip_prereleases is not changed
+ - '"fallible==0.0.1a2" in pip_prereleases.stdout_lines'
diff --git a/test/integration/targets/pip/vars/main.yml b/test/integration/targets/pip/vars/main.yml
new file mode 100644
index 0000000..2e87abc
--- /dev/null
+++ b/test/integration/targets/pip/vars/main.yml
@@ -0,0 +1,13 @@
+pip_test_package: sampleprojectpy2
+pip_test_packages:
+ - sampleprojectpy2
+ - jiphy
+pip_test_pkg_ver:
+ - sampleprojectpy2<=100, !=9.0.0,>=0.0.1
+ - jiphy<100 ,!=9,>=0.0.1
+pip_test_pkg_ver_unsatisfied:
+ - sampleprojectpy2>= 999.0.0
+ - jiphy >999.0
+pip_test_modules:
+ - sample
+ - jiphy
diff --git a/test/integration/targets/pkg_resources/aliases b/test/integration/targets/pkg_resources/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/pkg_resources/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/pkg_resources/lookup_plugins/check_pkg_resources.py b/test/integration/targets/pkg_resources/lookup_plugins/check_pkg_resources.py
new file mode 100644
index 0000000..9f1c5c0
--- /dev/null
+++ b/test/integration/targets/pkg_resources/lookup_plugins/check_pkg_resources.py
@@ -0,0 +1,23 @@
+"""
+This test case verifies that pkg_resources imports from ansible plugins are functional.
+
+If pkg_resources is not installed this test will succeed.
+If pkg_resources is installed but is unable to function, this test will fail.
+
+One known failure case this test can detect is when ansible declares a __requires__ and then tests are run without an egg-info directory.
+"""
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+# noinspection PyUnresolvedReferences
+try:
+ from pkg_resources import Requirement
+except ImportError:
+ Requirement = None
+
+from ansible.plugins.lookup import LookupBase
+
+
+class LookupModule(LookupBase):
+ def run(self, terms, variables=None, **kwargs):
+ return []
diff --git a/test/integration/targets/pkg_resources/tasks/main.yml b/test/integration/targets/pkg_resources/tasks/main.yml
new file mode 100644
index 0000000..b19d0eb
--- /dev/null
+++ b/test/integration/targets/pkg_resources/tasks/main.yml
@@ -0,0 +1,3 @@
+- name: Verify that pkg_resources imports are functional
+ debug:
+ msg: "{{ lookup('check_pkg_resources') }}"
diff --git a/test/integration/targets/play_iterator/aliases b/test/integration/targets/play_iterator/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/play_iterator/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/play_iterator/playbook.yml b/test/integration/targets/play_iterator/playbook.yml
new file mode 100644
index 0000000..76100c6
--- /dev/null
+++ b/test/integration/targets/play_iterator/playbook.yml
@@ -0,0 +1,10 @@
+---
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name:
+ debug:
+ msg: foo
+ - name: "task 2"
+ debug:
+ msg: bar
diff --git a/test/integration/targets/play_iterator/runme.sh b/test/integration/targets/play_iterator/runme.sh
new file mode 100755
index 0000000..9f30d9e
--- /dev/null
+++ b/test/integration/targets/play_iterator/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook playbook.yml --start-at-task 'task 2' "$@"
diff --git a/test/integration/targets/playbook/aliases b/test/integration/targets/playbook/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/playbook/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/playbook/empty.yml b/test/integration/targets/playbook/empty.yml
new file mode 100644
index 0000000..fe51488
--- /dev/null
+++ b/test/integration/targets/playbook/empty.yml
@@ -0,0 +1 @@
+[]
diff --git a/test/integration/targets/playbook/empty_hosts.yml b/test/integration/targets/playbook/empty_hosts.yml
new file mode 100644
index 0000000..c9493c0
--- /dev/null
+++ b/test/integration/targets/playbook/empty_hosts.yml
@@ -0,0 +1,4 @@
+- hosts: []
+ tasks:
+ - debug:
+ msg: does not run
diff --git a/test/integration/targets/playbook/malformed_post_tasks.yml b/test/integration/targets/playbook/malformed_post_tasks.yml
new file mode 100644
index 0000000..4c13411
--- /dev/null
+++ b/test/integration/targets/playbook/malformed_post_tasks.yml
@@ -0,0 +1,2 @@
+- hosts: localhost
+ post_tasks: 123
diff --git a/test/integration/targets/playbook/malformed_pre_tasks.yml b/test/integration/targets/playbook/malformed_pre_tasks.yml
new file mode 100644
index 0000000..6c58477
--- /dev/null
+++ b/test/integration/targets/playbook/malformed_pre_tasks.yml
@@ -0,0 +1,2 @@
+- hosts: localhost
+ pre_tasks: 123
diff --git a/test/integration/targets/playbook/malformed_roles.yml b/test/integration/targets/playbook/malformed_roles.yml
new file mode 100644
index 0000000..35db56e
--- /dev/null
+++ b/test/integration/targets/playbook/malformed_roles.yml
@@ -0,0 +1,2 @@
+- hosts: localhost
+ roles: 123
diff --git a/test/integration/targets/playbook/malformed_tasks.yml b/test/integration/targets/playbook/malformed_tasks.yml
new file mode 100644
index 0000000..123c059
--- /dev/null
+++ b/test/integration/targets/playbook/malformed_tasks.yml
@@ -0,0 +1,2 @@
+- hosts: localhost
+ tasks: 123
diff --git a/test/integration/targets/playbook/malformed_vars_prompt.yml b/test/integration/targets/playbook/malformed_vars_prompt.yml
new file mode 100644
index 0000000..5447197
--- /dev/null
+++ b/test/integration/targets/playbook/malformed_vars_prompt.yml
@@ -0,0 +1,3 @@
+- hosts: localhost
+ vars_prompt:
+ - foo: bar
diff --git a/test/integration/targets/playbook/old_style_role.yml b/test/integration/targets/playbook/old_style_role.yml
new file mode 100644
index 0000000..015f263
--- /dev/null
+++ b/test/integration/targets/playbook/old_style_role.yml
@@ -0,0 +1,3 @@
+- hosts: localhost
+ roles:
+ - foo,bar
diff --git a/test/integration/targets/playbook/remote_user_and_user.yml b/test/integration/targets/playbook/remote_user_and_user.yml
new file mode 100644
index 0000000..c9e2389
--- /dev/null
+++ b/test/integration/targets/playbook/remote_user_and_user.yml
@@ -0,0 +1,6 @@
+- hosts: localhost
+ remote_user: a
+ user: b
+ tasks:
+ - debug:
+ msg: did not run
diff --git a/test/integration/targets/playbook/roles_null.yml b/test/integration/targets/playbook/roles_null.yml
new file mode 100644
index 0000000..d06bcd1
--- /dev/null
+++ b/test/integration/targets/playbook/roles_null.yml
@@ -0,0 +1,3 @@
+- name: null roles is okay
+ hosts: localhost
+ roles: null
diff --git a/test/integration/targets/playbook/runme.sh b/test/integration/targets/playbook/runme.sh
new file mode 100755
index 0000000..cc8d495
--- /dev/null
+++ b/test/integration/targets/playbook/runme.sh
@@ -0,0 +1,92 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# run type tests
+ansible-playbook -i ../../inventory types.yml -v "$@"
+
+# test timeout
+ansible-playbook -i ../../inventory timeout.yml -v "$@"
+
+# our Play class allows for 'user' or 'remote_user', but not both.
+# first test that both user and remote_user work individually
+set +e
+result="$(ansible-playbook -i ../../inventory user.yml -v "$@" 2>&1)"
+set -e
+grep -q "worked with user" <<< "$result"
+grep -q "worked with remote_user" <<< "$result"
+
+# then test that the play errors if user and remote_user both exist
+echo "EXPECTED ERROR: Ensure we fail properly if a play has both user and remote_user."
+set +e
+result="$(ansible-playbook -i ../../inventory remote_user_and_user.yml -v "$@" 2>&1)"
+set -e
+grep -q "ERROR! both 'user' and 'remote_user' are set for this play." <<< "$result"
+
+# test that playbook errors if len(plays) == 0
+echo "EXPECTED ERROR: Ensure we fail properly if a playbook is an empty list."
+set +e
+result="$(ansible-playbook -i ../../inventory empty.yml -v "$@" 2>&1)"
+set -e
+grep -q "ERROR! A playbook must contain at least one play" <<< "$result"
+
+# test that play errors if len(hosts) == 0
+echo "EXPECTED ERROR: Ensure we fail properly if a play has 0 hosts."
+set +e
+result="$(ansible-playbook -i ../../inventory empty_hosts.yml -v "$@" 2>&1)"
+set -e
+grep -q "ERROR! Hosts list cannot be empty. Please check your playbook" <<< "$result"
+
+# test that play errors if tasks is malformed
+echo "EXPECTED ERROR: Ensure we fail properly if tasks is malformed."
+set +e
+result="$(ansible-playbook -i ../../inventory malformed_tasks.yml -v "$@" 2>&1)"
+set -e
+grep -q "ERROR! A malformed block was encountered while loading tasks: 123 should be a list or None" <<< "$result"
+
+# test that play errors if pre_tasks is malformed
+echo "EXPECTED ERROR: Ensure we fail properly if pre_tasks is malformed."
+set +e
+result="$(ansible-playbook -i ../../inventory malformed_pre_tasks.yml -v "$@" 2>&1)"
+set -e
+grep -q "ERROR! A malformed block was encountered while loading pre_tasks" <<< "$result"
+
+# test that play errors if post_tasks is malformed
+echo "EXPECTED ERROR: Ensure we fail properly if post_tasks is malformed."
+set +e
+result="$(ansible-playbook -i ../../inventory malformed_post_tasks.yml -v "$@" 2>&1)"
+set -e
+grep -q "ERROR! A malformed block was encountered while loading post_tasks" <<< "$result"
+
+# test roles: null -- it gets converted to [] internally
+ansible-playbook -i ../../inventory roles_null.yml -v "$@"
+
+# test roles: 123 -- errors
+echo "EXPECTED ERROR: Ensure we fail properly if roles is malformed."
+set +e
+result="$(ansible-playbook -i ../../inventory malformed_roles.yml -v "$@" 2>&1)"
+set -e
+grep -q "ERROR! A malformed role declaration was encountered." <<< "$result"
+
+# test roles: ["foo,bar"] -- errors about old style
+echo "EXPECTED ERROR: Ensure we fail properly if old style role is given."
+set +e
+result="$(ansible-playbook -i ../../inventory old_style_role.yml -v "$@" 2>&1)"
+set -e
+grep -q "ERROR! Invalid old style role requirement: foo,bar" <<< "$result"
+
+# test vars prompt that has no name
+echo "EXPECTED ERROR: Ensure we fail properly if vars_prompt has no name."
+set +e
+result="$(ansible-playbook -i ../../inventory malformed_vars_prompt.yml -v "$@" 2>&1)"
+set -e
+grep -q "ERROR! Invalid vars_prompt data structure, missing 'name' key" <<< "$result"
+
+# test vars_prompt: null
+ansible-playbook -i ../../inventory vars_prompt_null.yml -v "$@"
+
+# test vars_files: null
+ansible-playbook -i ../../inventory vars_files_null.yml -v "$@"
+
+# test vars_files: filename.yml
+ansible-playbook -i ../../inventory vars_files_string.yml -v "$@"
diff --git a/test/integration/targets/playbook/some_vars.yml b/test/integration/targets/playbook/some_vars.yml
new file mode 100644
index 0000000..7835365
--- /dev/null
+++ b/test/integration/targets/playbook/some_vars.yml
@@ -0,0 +1,2 @@
+a_variable: yep
+another: hi
diff --git a/test/integration/targets/playbook/timeout.yml b/test/integration/targets/playbook/timeout.yml
new file mode 100644
index 0000000..442e13a
--- /dev/null
+++ b/test/integration/targets/playbook/timeout.yml
@@ -0,0 +1,12 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - shell: sleep 100
+ timeout: 1
+ ignore_errors: true
+ register: time
+
+ - assert:
+ that:
+ - time is failed
+ - '"The shell action failed to execute in the expected time frame" in time["msg"]'
diff --git a/test/integration/targets/playbook/types.yml b/test/integration/targets/playbook/types.yml
new file mode 100644
index 0000000..dd8997b
--- /dev/null
+++ b/test/integration/targets/playbook/types.yml
@@ -0,0 +1,21 @@
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - name: try to set 'diff' a boolean
+ debug: msg="not important"
+ diff: yes
+ ignore_errors: True
+ register: good_diff
+
+ - name: try to set 'diff' a boolean to a string (. would make it non boolean)
+ debug: msg="not important"
+ diff: yes.
+ ignore_errors: True
+ register: bad_diff
+
+ - name: Check we did error out
+ assert:
+ that:
+ - good_diff is success
+ - bad_diff is failed
+ - "'is not a valid boolean' in bad_diff['msg']"
diff --git a/test/integration/targets/playbook/user.yml b/test/integration/targets/playbook/user.yml
new file mode 100644
index 0000000..8b4029b
--- /dev/null
+++ b/test/integration/targets/playbook/user.yml
@@ -0,0 +1,23 @@
+- hosts: localhost
+ tasks:
+ - command: whoami
+ register: whoami
+
+ - assert:
+ that:
+ - whoami is successful
+
+ - set_fact:
+ me: "{{ whoami.stdout }}"
+
+- hosts: localhost
+ user: "{{ me }}"
+ tasks:
+ - debug:
+ msg: worked with user ({{ me }})
+
+- hosts: localhost
+ remote_user: "{{ me }}"
+ tasks:
+ - debug:
+ msg: worked with remote_user ({{ me }})
diff --git a/test/integration/targets/playbook/vars_files_null.yml b/test/integration/targets/playbook/vars_files_null.yml
new file mode 100644
index 0000000..64c21c6
--- /dev/null
+++ b/test/integration/targets/playbook/vars_files_null.yml
@@ -0,0 +1,3 @@
+- name: null vars_files is okay
+ hosts: localhost
+ vars_files: null
diff --git a/test/integration/targets/playbook/vars_files_string.yml b/test/integration/targets/playbook/vars_files_string.yml
new file mode 100644
index 0000000..9191d3c
--- /dev/null
+++ b/test/integration/targets/playbook/vars_files_string.yml
@@ -0,0 +1,6 @@
+- hosts: localhost
+ vars_files: some_vars.yml
+ tasks:
+ - assert:
+ that:
+ - 'a_variable == "yep"'
diff --git a/test/integration/targets/playbook/vars_prompt_null.yml b/test/integration/targets/playbook/vars_prompt_null.yml
new file mode 100644
index 0000000..4fdfa7c
--- /dev/null
+++ b/test/integration/targets/playbook/vars_prompt_null.yml
@@ -0,0 +1,3 @@
+- name: null vars prompt is okay
+ hosts: localhost
+ vars_prompt: null
diff --git a/test/integration/targets/plugin_config_for_inventory/aliases b/test/integration/targets/plugin_config_for_inventory/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/plugin_config_for_inventory/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/plugin_config_for_inventory/cache_plugins/none.py b/test/integration/targets/plugin_config_for_inventory/cache_plugins/none.py
new file mode 100644
index 0000000..62a91c8
--- /dev/null
+++ b/test/integration/targets/plugin_config_for_inventory/cache_plugins/none.py
@@ -0,0 +1,62 @@
+# (c) 2014, Brian Coca, Josh Drake, et al
+# (c) 2017 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.cache import BaseCacheModule
+
+DOCUMENTATION = '''
+ cache: none
+ short_description: write-only cache (no cache)
+ description:
+ - No caching at all
+ version_added: historical
+ author: core team (@ansible-core)
+ options:
+ _timeout:
+ default: 86400
+ description: Expiration timeout for the cache plugin data
+ env:
+ - name: ANSIBLE_CACHE_PLUGIN_TIMEOUT
+ ini:
+ - key: fact_caching_timeout
+ section: defaults
+ type: integer
+'''
+
+
+class CacheModule(BaseCacheModule):
+ def __init__(self, *args, **kwargs):
+ super(CacheModule, self).__init__(*args, **kwargs)
+ self.empty = {}
+ self._timeout = self.get_option('_timeout')
+
+ def get(self, key):
+ return self.empty.get(key)
+
+ def set(self, key, value):
+ return value
+
+ def keys(self):
+ return self.empty.keys()
+
+ def contains(self, key):
+ return key in self.empty
+
+ def delete(self, key):
+ del self.emtpy[key]
+
+ def flush(self):
+ self.empty = {}
+
+ def copy(self):
+ return self.empty.copy()
+
+ def __getstate__(self):
+ return self.copy()
+
+ def __setstate__(self, data):
+ self.empty = data
diff --git a/test/integration/targets/plugin_config_for_inventory/config_with_parameter.yml b/test/integration/targets/plugin_config_for_inventory/config_with_parameter.yml
new file mode 100644
index 0000000..b9e367b
--- /dev/null
+++ b/test/integration/targets/plugin_config_for_inventory/config_with_parameter.yml
@@ -0,0 +1,5 @@
+plugin: test_inventory
+departments:
+ - paris
+cache: yes
+cache_timeout: 0
diff --git a/test/integration/targets/plugin_config_for_inventory/config_without_parameter.yml b/test/integration/targets/plugin_config_for_inventory/config_without_parameter.yml
new file mode 100644
index 0000000..787cf96
--- /dev/null
+++ b/test/integration/targets/plugin_config_for_inventory/config_without_parameter.yml
@@ -0,0 +1 @@
+plugin: test_inventory
diff --git a/test/integration/targets/plugin_config_for_inventory/runme.sh b/test/integration/targets/plugin_config_for_inventory/runme.sh
new file mode 100755
index 0000000..2a22325
--- /dev/null
+++ b/test/integration/targets/plugin_config_for_inventory/runme.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+
+set -o errexit -o nounset -o xtrace
+
+export ANSIBLE_INVENTORY_PLUGINS=./
+export ANSIBLE_INVENTORY_ENABLED=test_inventory
+
+# check default values
+ansible-inventory --list -i ./config_without_parameter.yml --export | \
+ env python -c "import json, sys; inv = json.loads(sys.stdin.read()); \
+ assert set(inv['_meta']['hostvars']['test_host']['departments']) == set(['seine-et-marne', 'haute-garonne'])"
+
+# check values
+ansible-inventory --list -i ./config_with_parameter.yml --export | \
+ env python -c "import json, sys; inv = json.loads(sys.stdin.read()); \
+ assert set(inv['_meta']['hostvars']['test_host']['departments']) == set(['paris'])"
+
+export ANSIBLE_CACHE_PLUGINS=cache_plugins/
+export ANSIBLE_CACHE_PLUGIN=none
+ansible-inventory --list -i ./config_with_parameter.yml --export | \
+ env python -c "import json, sys; inv = json.loads(sys.stdin.read()); \
+ assert inv['_meta']['hostvars']['test_host']['given_timeout'] == inv['_meta']['hostvars']['test_host']['cache_timeout']"
diff --git a/test/integration/targets/plugin_config_for_inventory/test_inventory.py b/test/integration/targets/plugin_config_for_inventory/test_inventory.py
new file mode 100644
index 0000000..f937c03
--- /dev/null
+++ b/test/integration/targets/plugin_config_for_inventory/test_inventory.py
@@ -0,0 +1,84 @@
+from __future__ import (absolute_import, division, print_function)
+
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ name: test_inventory
+ plugin_type: inventory
+ authors:
+ - Pierre-Louis Bonicoli (@pilou-)
+ short_description: test inventory
+ description:
+ - test inventory (fetch parameters using config API)
+ options:
+ departments:
+ description: test parameter
+ type: list
+ default:
+ - seine-et-marne
+ - haute-garonne
+ required: False
+ cache:
+ description: cache
+ type: bool
+ default: false
+ required: False
+ cache_plugin:
+ description: cache plugin
+ type: str
+ default: none
+ required: False
+ cache_timeout:
+ description: test cache parameter
+ type: integer
+ default: 7
+ required: False
+ cache_connection:
+ description: cache connection
+ type: str
+ default: /tmp/foo
+ required: False
+ cache_prefix:
+ description: cache prefix
+ type: str
+ default: prefix_
+ required: False
+'''
+
+EXAMPLES = '''
+# Example command line: ansible-inventory --list -i test_inventory.yml
+
+plugin: test_inventory
+departments:
+ - paris
+'''
+
+from ansible.plugins.inventory import BaseInventoryPlugin
+
+
+class InventoryModule(BaseInventoryPlugin):
+ NAME = 'test_inventory'
+
+ def verify_file(self, path):
+ return True
+
+ def parse(self, inventory, loader, path, cache=True):
+ super(InventoryModule, self).parse(inventory, loader, path)
+ config_data = self._read_config_data(path=path)
+ self._consume_options(config_data)
+
+ departments = self.get_option('departments')
+
+ group = 'test_group'
+ host = 'test_host'
+
+ self.inventory.add_group(group)
+ self.inventory.add_host(group=group, host=host)
+ self.inventory.set_variable(host, 'departments', departments)
+
+ # Ensure the timeout we're given gets sent to the cache plugin
+ if self.get_option('cache'):
+ given_timeout = self.get_option('cache_timeout')
+ cache_timeout = self._cache._plugin.get_option('_timeout')
+ self.inventory.set_variable(host, 'given_timeout', given_timeout)
+ self.inventory.set_variable(host, 'cache_timeout', cache_timeout)
diff --git a/test/integration/targets/plugin_filtering/aliases b/test/integration/targets/plugin_filtering/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/plugin_filtering/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/plugin_filtering/copy.yml b/test/integration/targets/plugin_filtering/copy.yml
new file mode 100644
index 0000000..083386a
--- /dev/null
+++ b/test/integration/targets/plugin_filtering/copy.yml
@@ -0,0 +1,10 @@
+---
+- hosts: testhost
+ gather_facts: False
+ tasks:
+ - copy:
+ content: 'Testing 1... 2... 3...'
+ dest: ./testing.txt
+ - file:
+ state: absent
+ path: ./testing.txt
diff --git a/test/integration/targets/plugin_filtering/filter_lookup.ini b/test/integration/targets/plugin_filtering/filter_lookup.ini
new file mode 100644
index 0000000..c14afad
--- /dev/null
+++ b/test/integration/targets/plugin_filtering/filter_lookup.ini
@@ -0,0 +1,4 @@
+[defaults]
+retry_files_enabled = False
+plugin_filters_cfg = ./filter_lookup.yml
+
diff --git a/test/integration/targets/plugin_filtering/filter_lookup.yml b/test/integration/targets/plugin_filtering/filter_lookup.yml
new file mode 100644
index 0000000..694ebfc
--- /dev/null
+++ b/test/integration/targets/plugin_filtering/filter_lookup.yml
@@ -0,0 +1,6 @@
+---
+filter_version: 1.0
+module_blacklist:
+ # Specify the name of a lookup plugin here. This should have no effect as
+ # this is only for filtering modules
+ - list
diff --git a/test/integration/targets/plugin_filtering/filter_modules.ini b/test/integration/targets/plugin_filtering/filter_modules.ini
new file mode 100644
index 0000000..97e672f
--- /dev/null
+++ b/test/integration/targets/plugin_filtering/filter_modules.ini
@@ -0,0 +1,4 @@
+[defaults]
+retry_files_enabled = False
+plugin_filters_cfg = ./filter_modules.yml
+
diff --git a/test/integration/targets/plugin_filtering/filter_modules.yml b/test/integration/targets/plugin_filtering/filter_modules.yml
new file mode 100644
index 0000000..6cffa67
--- /dev/null
+++ b/test/integration/targets/plugin_filtering/filter_modules.yml
@@ -0,0 +1,9 @@
+---
+filter_version: 1.0
+module_blacklist:
+ # A pure action plugin
+ - pause
+ # A hybrid action plugin with module
+ - copy
+ # A pure module
+ - tempfile
diff --git a/test/integration/targets/plugin_filtering/filter_ping.ini b/test/integration/targets/plugin_filtering/filter_ping.ini
new file mode 100644
index 0000000..b837bd7
--- /dev/null
+++ b/test/integration/targets/plugin_filtering/filter_ping.ini
@@ -0,0 +1,4 @@
+[defaults]
+retry_files_enabled = False
+plugin_filters_cfg = ./filter_ping.yml
+
diff --git a/test/integration/targets/plugin_filtering/filter_ping.yml b/test/integration/targets/plugin_filtering/filter_ping.yml
new file mode 100644
index 0000000..08e56f2
--- /dev/null
+++ b/test/integration/targets/plugin_filtering/filter_ping.yml
@@ -0,0 +1,5 @@
+---
+filter_version: 1.0
+module_blacklist:
+ # Ping is special
+ - ping
diff --git a/test/integration/targets/plugin_filtering/filter_stat.ini b/test/integration/targets/plugin_filtering/filter_stat.ini
new file mode 100644
index 0000000..7d18f11
--- /dev/null
+++ b/test/integration/targets/plugin_filtering/filter_stat.ini
@@ -0,0 +1,4 @@
+[defaults]
+retry_files_enabled = False
+plugin_filters_cfg = ./filter_stat.yml
+
diff --git a/test/integration/targets/plugin_filtering/filter_stat.yml b/test/integration/targets/plugin_filtering/filter_stat.yml
new file mode 100644
index 0000000..c1ce42e
--- /dev/null
+++ b/test/integration/targets/plugin_filtering/filter_stat.yml
@@ -0,0 +1,5 @@
+---
+filter_version: 1.0
+module_blacklist:
+ # Stat is special
+ - stat
diff --git a/test/integration/targets/plugin_filtering/lookup.yml b/test/integration/targets/plugin_filtering/lookup.yml
new file mode 100644
index 0000000..de6d1b4
--- /dev/null
+++ b/test/integration/targets/plugin_filtering/lookup.yml
@@ -0,0 +1,14 @@
+---
+- hosts: testhost
+ gather_facts: False
+ vars:
+ data:
+ - one
+ - two
+ tasks:
+ - debug:
+ msg: '{{ lookup("list", data) }}'
+
+ - debug:
+ msg: '{{ item }}'
+ with_list: '{{ data }}'
diff --git a/test/integration/targets/plugin_filtering/no_blacklist_module.ini b/test/integration/targets/plugin_filtering/no_blacklist_module.ini
new file mode 100644
index 0000000..65b51d6
--- /dev/null
+++ b/test/integration/targets/plugin_filtering/no_blacklist_module.ini
@@ -0,0 +1,3 @@
+[defaults]
+retry_files_enabled = False
+plugin_filters_cfg = ./no_blacklist_module.yml
diff --git a/test/integration/targets/plugin_filtering/no_blacklist_module.yml b/test/integration/targets/plugin_filtering/no_blacklist_module.yml
new file mode 100644
index 0000000..52a55df
--- /dev/null
+++ b/test/integration/targets/plugin_filtering/no_blacklist_module.yml
@@ -0,0 +1,3 @@
+---
+filter_version: 1.0
+module_blacklist:
diff --git a/test/integration/targets/plugin_filtering/no_filters.ini b/test/integration/targets/plugin_filtering/no_filters.ini
new file mode 100644
index 0000000..e4eed7b
--- /dev/null
+++ b/test/integration/targets/plugin_filtering/no_filters.ini
@@ -0,0 +1,4 @@
+[defaults]
+retry_files_enabled = False
+plugin_filters_cfg = ./empty.yml
+
diff --git a/test/integration/targets/plugin_filtering/pause.yml b/test/integration/targets/plugin_filtering/pause.yml
new file mode 100644
index 0000000..e2c1ef9
--- /dev/null
+++ b/test/integration/targets/plugin_filtering/pause.yml
@@ -0,0 +1,6 @@
+---
+- hosts: testhost
+ gather_facts: False
+ tasks:
+ - pause:
+ seconds: 1
diff --git a/test/integration/targets/plugin_filtering/ping.yml b/test/integration/targets/plugin_filtering/ping.yml
new file mode 100644
index 0000000..9e2214b
--- /dev/null
+++ b/test/integration/targets/plugin_filtering/ping.yml
@@ -0,0 +1,6 @@
+---
+- hosts: testhost
+ gather_facts: False
+ tasks:
+ - ping:
+ data: 'Testing 1... 2... 3...'
diff --git a/test/integration/targets/plugin_filtering/runme.sh b/test/integration/targets/plugin_filtering/runme.sh
new file mode 100755
index 0000000..aa0e2b0
--- /dev/null
+++ b/test/integration/targets/plugin_filtering/runme.sh
@@ -0,0 +1,137 @@
+#!/usr/bin/env bash
+
+set -ux
+
+#
+# Check that with no filters set, all of these modules run as expected
+#
+ANSIBLE_CONFIG=no_filters.ini ansible-playbook copy.yml -i ../../inventory -vvv "$@"
+if test $? != 0 ; then
+ echo "### Failed to run copy with no filters applied"
+ exit 1
+fi
+ANSIBLE_CONFIG=no_filters.ini ansible-playbook pause.yml -i ../../inventory -vvv "$@"
+if test $? != 0 ; then
+ echo "### Failed to run pause with no filters applied"
+ exit 1
+fi
+ANSIBLE_CONFIG=no_filters.ini ansible-playbook tempfile.yml -i ../../inventory -vvv "$@"
+if test $? != 0 ; then
+ echo "### Failed to run tempfile with no filters applied"
+ exit 1
+fi
+
+#
+# Check that if no modules are blacklisted then Ansible should not through traceback
+#
+ANSIBLE_CONFIG=no_blacklist_module.ini ansible-playbook tempfile.yml -i ../../inventory -vvv "$@"
+if test $? != 0 ; then
+ echo "### Failed to run tempfile with no modules blacklisted"
+ exit 1
+fi
+
+#
+# Check that with these modules filtered out, all of these modules fail to be found
+#
+ANSIBLE_CONFIG=filter_modules.ini ansible-playbook copy.yml -i ../../inventory -v "$@"
+if test $? = 0 ; then
+ echo "### Failed to prevent copy from running"
+ exit 1
+else
+ echo "### Copy was prevented from running as expected"
+fi
+ANSIBLE_CONFIG=filter_modules.ini ansible-playbook pause.yml -i ../../inventory -v "$@"
+if test $? = 0 ; then
+ echo "### Failed to prevent pause from running"
+ exit 1
+else
+ echo "### pause was prevented from running as expected"
+fi
+ANSIBLE_CONFIG=filter_modules.ini ansible-playbook tempfile.yml -i ../../inventory -v "$@"
+if test $? = 0 ; then
+ echo "### Failed to prevent tempfile from running"
+ exit 1
+else
+ echo "### tempfile was prevented from running as expected"
+fi
+
+#
+# ping is a special module as we test for its existence. Check it specially
+#
+
+# Check that ping runs with no filter
+ANSIBLE_CONFIG=no_filters.ini ansible-playbook ping.yml -i ../../inventory -vvv "$@"
+if test $? != 0 ; then
+ echo "### Failed to run ping with no filters applied"
+ exit 1
+fi
+
+# Check that other modules run with ping filtered
+ANSIBLE_CONFIG=filter_ping.ini ansible-playbook copy.yml -i ../../inventory -vvv "$@"
+if test $? != 0 ; then
+ echo "### Failed to run copy when a filter was applied to ping"
+ exit 1
+fi
+# Check that ping fails to run when it is filtered
+ANSIBLE_CONFIG=filter_ping.ini ansible-playbook ping.yml -i ../../inventory -v "$@"
+if test $? = 0 ; then
+ echo "### Failed to prevent ping from running"
+ exit 1
+else
+ echo "### Ping was prevented from running as expected"
+fi
+
+#
+# Check that specifying a lookup plugin in the filter has no effect
+#
+
+ANSIBLE_CONFIG=filter_lookup.ini ansible-playbook lookup.yml -i ../../inventory -vvv "$@"
+if test $? != 0 ; then
+ echo "### Failed to use a lookup plugin when it is incorrectly specified in the *module* blacklist"
+ exit 1
+fi
+
+#
+# stat is a special module as we use it to run nearly every other module. Check it specially
+#
+
+# Check that stat runs with no filter
+ANSIBLE_CONFIG=no_filters.ini ansible-playbook stat.yml -i ../../inventory -vvv "$@"
+if test $? != 0 ; then
+ echo "### Failed to run stat with no filters applied"
+ exit 1
+fi
+
+# Check that running another module when stat is filtered gives us our custom error message
+ANSIBLE_CONFIG=filter_stat.ini
+export ANSIBLE_CONFIG
+CAPTURE=$(ansible-playbook copy.yml -i ../../inventory -vvv "$@" 2>&1)
+if test $? = 0 ; then
+ echo "### Copy ran even though stat is in the module blacklist"
+ exit 1
+else
+ echo "$CAPTURE" | grep 'The stat module was specified in the module blacklist file,.*, but Ansible will not function without the stat module. Please remove stat from the blacklist.'
+ if test $? != 0 ; then
+ echo "### Stat did not give us our custom error message"
+ exit 1
+ fi
+ echo "### Filtering stat failed with our custom error message as expected"
+fi
+unset ANSIBLE_CONFIG
+
+# Check that running stat when stat is filtered gives our custom error message
+ANSIBLE_CONFIG=filter_stat.ini
+export ANSIBLE_CONFIG
+CAPTURE=$(ansible-playbook stat.yml -i ../../inventory -vvv "$@" 2>&1)
+if test $? = 0 ; then
+ echo "### Stat ran even though it is in the module blacklist"
+ exit 1
+else
+ echo "$CAPTURE" | grep 'The stat module was specified in the module blacklist file,.*, but Ansible will not function without the stat module. Please remove stat from the blacklist.'
+ if test $? != 0 ; then
+ echo "### Stat did not give us our custom error message"
+ exit 1
+ fi
+ echo "### Filtering stat failed with our custom error message as expected"
+fi
+unset ANSIBLE_CONFIG
diff --git a/test/integration/targets/plugin_filtering/stat.yml b/test/integration/targets/plugin_filtering/stat.yml
new file mode 100644
index 0000000..4f24baa
--- /dev/null
+++ b/test/integration/targets/plugin_filtering/stat.yml
@@ -0,0 +1,6 @@
+---
+- hosts: testhost
+ gather_facts: False
+ tasks:
+ - stat:
+ path: '/'
diff --git a/test/integration/targets/plugin_filtering/tempfile.yml b/test/integration/targets/plugin_filtering/tempfile.yml
new file mode 100644
index 0000000..0646354
--- /dev/null
+++ b/test/integration/targets/plugin_filtering/tempfile.yml
@@ -0,0 +1,9 @@
+---
+- hosts: testhost
+ gather_facts: False
+ tasks:
+ - tempfile:
+ register: temp_result
+ - file:
+ state: absent
+ path: '{{ temp_result["path"] }}'
diff --git a/test/integration/targets/plugin_loader/aliases b/test/integration/targets/plugin_loader/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/plugin_loader/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/plugin_loader/normal/action_plugins/self_referential.py b/test/integration/targets/plugin_loader/normal/action_plugins/self_referential.py
new file mode 100644
index 0000000..b4c8957
--- /dev/null
+++ b/test/integration/targets/plugin_loader/normal/action_plugins/self_referential.py
@@ -0,0 +1,29 @@
+# Copyright: Contributors to the Ansible project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.action import ActionBase
+
+import sys
+
+# reference our own module from sys.modules while it's being loaded to ensure the importer behaves properly
+try:
+ mod = sys.modules[__name__]
+except KeyError:
+ raise Exception(f'module {__name__} is not accessible via sys.modules, likely a pluginloader bug')
+
+
+class ActionModule(ActionBase):
+ TRANSFERS_FILES = False
+
+ def run(self, tmp=None, task_vars=None):
+ if task_vars is None:
+ task_vars = dict()
+
+ result = super(ActionModule, self).run(tmp, task_vars)
+ del tmp # tmp no longer has any effect
+
+ result['changed'] = False
+ result['msg'] = 'self-referential action loaded and ran successfully'
+ return result
diff --git a/test/integration/targets/plugin_loader/normal/filters.yml b/test/integration/targets/plugin_loader/normal/filters.yml
new file mode 100644
index 0000000..f9069be
--- /dev/null
+++ b/test/integration/targets/plugin_loader/normal/filters.yml
@@ -0,0 +1,13 @@
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - name: ensure filters work as shipped from core
+ assert:
+ that:
+ - a|flatten == [1, 2, 3, 4, 5]
+ - a|ternary('yes', 'no') == 'yes'
+ vars:
+ a:
+ - 1
+ - 2
+ - [3, 4, 5]
diff --git a/test/integration/targets/plugin_loader/normal/library/_underscore.py b/test/integration/targets/plugin_loader/normal/library/_underscore.py
new file mode 100644
index 0000000..7a416a6
--- /dev/null
+++ b/test/integration/targets/plugin_loader/normal/library/_underscore.py
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+
+def main():
+ print(json.dumps(dict(changed=False, source='legacy_library_dir')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/plugin_loader/normal/self_referential.yml b/test/integration/targets/plugin_loader/normal/self_referential.yml
new file mode 100644
index 0000000..d3eed21
--- /dev/null
+++ b/test/integration/targets/plugin_loader/normal/self_referential.yml
@@ -0,0 +1,5 @@
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - name: ensure a self-referential action plugin loads properly
+ self_referential:
diff --git a/test/integration/targets/plugin_loader/normal/underscore.yml b/test/integration/targets/plugin_loader/normal/underscore.yml
new file mode 100644
index 0000000..fb5bbad
--- /dev/null
+++ b/test/integration/targets/plugin_loader/normal/underscore.yml
@@ -0,0 +1,15 @@
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - name: Load a deprecated module
+ underscore:
+ register: res
+
+ - name: Load a deprecated module that is a symlink
+ symlink:
+ register: sym
+
+ - assert:
+ that:
+ - res.source == 'legacy_library_dir'
+ - sym.source == 'legacy_library_dir'
diff --git a/test/integration/targets/plugin_loader/override/filter_plugins/core.py b/test/integration/targets/plugin_loader/override/filter_plugins/core.py
new file mode 100644
index 0000000..f283dc3
--- /dev/null
+++ b/test/integration/targets/plugin_loader/override/filter_plugins/core.py
@@ -0,0 +1,18 @@
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+def do_flag(myval):
+ return 'flagged'
+
+
+class FilterModule(object):
+ ''' Ansible core jinja2 filters '''
+
+ def filters(self):
+ return {
+ # jinja2 overrides
+ 'flag': do_flag,
+ 'flatten': do_flag,
+ }
diff --git a/test/integration/targets/plugin_loader/override/filters.yml b/test/integration/targets/plugin_loader/override/filters.yml
new file mode 100644
index 0000000..e51ab4e
--- /dev/null
+++ b/test/integration/targets/plugin_loader/override/filters.yml
@@ -0,0 +1,15 @@
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - name: ensure local 'flag' filter works, 'flatten' is overriden and 'ternary' is still from core
+ assert:
+ that:
+ - a|flag == 'flagged'
+ - a|flatten != [1, 2, 3, 4, 5]
+ - a|flatten == "flagged"
+ - a|ternary('yes', 'no') == 'yes'
+ vars:
+ a:
+ - 1
+ - 2
+ - [3, 4, 5]
diff --git a/test/integration/targets/plugin_loader/runme.sh b/test/integration/targets/plugin_loader/runme.sh
new file mode 100755
index 0000000..e30f624
--- /dev/null
+++ b/test/integration/targets/plugin_loader/runme.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+
+set -ux
+
+cleanup() {
+ unlink normal/library/_symlink.py
+}
+
+pushd normal/library
+ln -s _underscore.py _symlink.py
+popd
+
+trap 'cleanup' EXIT
+
+# check normal execution
+for myplay in normal/*.yml
+do
+ ansible-playbook "${myplay}" -i ../../inventory -vvv "$@"
+ if test $? != 0 ; then
+ echo "### Failed to run ${myplay} normally"
+ exit 1
+ fi
+done
+
+# check overrides
+for myplay in override/*.yml
+do
+ ansible-playbook "${myplay}" -i ../../inventory -vvv "$@"
+ if test $? != 0 ; then
+ echo "### Failed to run ${myplay} override"
+ exit 1
+ fi
+done
+
+# test config loading
+ansible-playbook use_coll_name.yml -i ../../inventory -e 'ansible_connection=ansible.builtin.ssh' "$@"
diff --git a/test/integration/targets/plugin_loader/use_coll_name.yml b/test/integration/targets/plugin_loader/use_coll_name.yml
new file mode 100644
index 0000000..66507ce
--- /dev/null
+++ b/test/integration/targets/plugin_loader/use_coll_name.yml
@@ -0,0 +1,7 @@
+- name: ensure configuration is loaded when we use FQCN and have already loaded using 'short namne' (which is case will all builtin connection plugins)
+ hosts: all
+ gather_facts: false
+ tasks:
+ - name: relies on extra var being passed in with connection and fqcn
+ ping:
+ ignore_unreachable: True
diff --git a/test/integration/targets/plugin_namespace/aliases b/test/integration/targets/plugin_namespace/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/plugin_namespace/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/plugin_namespace/filter_plugins/test_filter.py b/test/integration/targets/plugin_namespace/filter_plugins/test_filter.py
new file mode 100644
index 0000000..dca094b
--- /dev/null
+++ b/test/integration/targets/plugin_namespace/filter_plugins/test_filter.py
@@ -0,0 +1,15 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+def filter_name(a):
+ return __name__
+
+
+class FilterModule(object):
+ def filters(self):
+ filters = {
+ 'filter_name': filter_name,
+ }
+
+ return filters
diff --git a/test/integration/targets/plugin_namespace/lookup_plugins/lookup_name.py b/test/integration/targets/plugin_namespace/lookup_plugins/lookup_name.py
new file mode 100644
index 0000000..d0af703
--- /dev/null
+++ b/test/integration/targets/plugin_namespace/lookup_plugins/lookup_name.py
@@ -0,0 +1,9 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.lookup import LookupBase
+
+
+class LookupModule(LookupBase):
+ def run(self, terms, variables, **kwargs):
+ return [__name__]
diff --git a/test/integration/targets/plugin_namespace/tasks/main.yml b/test/integration/targets/plugin_namespace/tasks/main.yml
new file mode 100644
index 0000000..19bdd3a
--- /dev/null
+++ b/test/integration/targets/plugin_namespace/tasks/main.yml
@@ -0,0 +1,11 @@
+- set_fact:
+ filter_name: "{{ 1 | filter_name }}"
+ lookup_name: "{{ lookup('lookup_name') }}"
+ test_name_ok: "{{ 1 is test_name_ok }}"
+
+- assert:
+ that:
+ # filter names are prefixed with a unique hash value to prevent shadowing of other plugins
+ - filter_name | regex_search('^ansible\.plugins\.filter\.[0-9]+_test_filter$')
+ - lookup_name == 'ansible.plugins.lookup.lookup_name'
+ - test_name_ok
diff --git a/test/integration/targets/plugin_namespace/test_plugins/test_test.py b/test/integration/targets/plugin_namespace/test_plugins/test_test.py
new file mode 100644
index 0000000..2a9d6ee
--- /dev/null
+++ b/test/integration/targets/plugin_namespace/test_plugins/test_test.py
@@ -0,0 +1,16 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import re
+
+
+def test_name_ok(value):
+ # test names are prefixed with a unique hash value to prevent shadowing of other plugins
+ return bool(re.match(r'^ansible\.plugins\.test\.[0-9]+_test_test$', __name__))
+
+
+class TestModule:
+ def tests(self):
+ return {
+ 'test_name_ok': test_name_ok,
+ }
diff --git a/test/integration/targets/preflight_encoding/aliases b/test/integration/targets/preflight_encoding/aliases
new file mode 100644
index 0000000..5a47349
--- /dev/null
+++ b/test/integration/targets/preflight_encoding/aliases
@@ -0,0 +1,2 @@
+context/controller
+shippable/posix/group3
diff --git a/test/integration/targets/preflight_encoding/tasks/main.yml b/test/integration/targets/preflight_encoding/tasks/main.yml
new file mode 100644
index 0000000..aa33b6c
--- /dev/null
+++ b/test/integration/targets/preflight_encoding/tasks/main.yml
@@ -0,0 +1,62 @@
+- name: find bash
+ shell: command -v bash
+ register: bash
+ ignore_errors: true
+
+- meta: end_host
+ when: bash is failed
+
+- name: get available locales
+ command: locale -a
+ register: locale_a
+ ignore_errors: true
+
+- set_fact:
+ non_utf8: '{{ locale_a.stdout_lines | select("contains", ".") | reject("search", "(?i)(\.UTF-?8$)") | default([None], true) | first }}'
+ has_cutf8: '{{ locale_a.stdout_lines | select("search", "(?i)C.UTF-?8") != [] }}'
+
+- name: Test successful encodings
+ shell: '{{ item }} ansible --version'
+ args:
+ executable: '{{ bash.stdout_lines|first }}'
+ loop:
+ - LC_ALL={{ utf8 }}
+ - LC_ALL={{ cutf8 }}
+ - LC_ALL= LC_CTYPE={{ utf8 }}
+ - LC_ALL= LC_CTYPE={{ cutf8 }}
+ when: cutf8 not in item or (cutf8 in item and has_cutf8)
+
+- name: test locales error
+ shell: LC_ALL=ham_sandwich LC_CTYPE={{ utf8 }} ansible --version
+ args:
+ executable: '{{ bash.stdout_lines|first }}'
+ ignore_errors: true
+ register: locales_error
+
+- assert:
+ that:
+ - locales_error is failed
+ - >-
+ 'ERROR: Ansible could not initialize the preferred locale' in locales_error.stderr
+
+- meta: end_host
+ when: non_utf8 is falsy
+
+- name: Test unsuccessful encodings
+ shell: '{{ item }} ansible --version'
+ args:
+ executable: '{{ bash.stdout_lines|first }}'
+ loop:
+ - LC_ALL={{ non_utf8 }}
+ - LC_ALL= LC_CTYPE={{ non_utf8 }}
+ ignore_errors: true
+ register: result
+
+- assert:
+ that:
+ - result is failed
+ - result.results | select('failed') | length == 2
+ - >-
+ 'ERROR: Ansible requires the locale encoding to be UTF-8' in result.results[0].stderr
+ - >-
+ 'ERROR: Ansible requires the locale encoding to be UTF-8' in result.results[1].stderr
diff --git a/test/integration/targets/preflight_encoding/vars/main.yml b/test/integration/targets/preflight_encoding/vars/main.yml
new file mode 100644
index 0000000..34eb2a6
--- /dev/null
+++ b/test/integration/targets/preflight_encoding/vars/main.yml
@@ -0,0 +1,2 @@
+utf8: en_US.UTF-8
+cutf8: C.UTF-8
diff --git a/test/integration/targets/prepare_http_tests/defaults/main.yml b/test/integration/targets/prepare_http_tests/defaults/main.yml
new file mode 100644
index 0000000..217b3db
--- /dev/null
+++ b/test/integration/targets/prepare_http_tests/defaults/main.yml
@@ -0,0 +1,5 @@
+badssl_host: wrong.host.badssl.com
+self_signed_host: self-signed.ansible.http.tests
+httpbin_host: httpbin.org
+sni_host: ci-files.testing.ansible.com
+badssl_host_substring: wrong.host.badssl.com
diff --git a/test/integration/targets/prepare_http_tests/handlers/main.yml b/test/integration/targets/prepare_http_tests/handlers/main.yml
new file mode 100644
index 0000000..172cab7
--- /dev/null
+++ b/test/integration/targets/prepare_http_tests/handlers/main.yml
@@ -0,0 +1,4 @@
+- name: Remove python gssapi
+ pip:
+ name: gssapi
+ state: absent
diff --git a/test/integration/targets/prepare_http_tests/library/httptester_kinit.py b/test/integration/targets/prepare_http_tests/library/httptester_kinit.py
new file mode 100644
index 0000000..4f7b7ad
--- /dev/null
+++ b/test/integration/targets/prepare_http_tests/library/httptester_kinit.py
@@ -0,0 +1,138 @@
+#!/usr/bin/python
+
+# Copyright: (c) 2020, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = r'''
+---
+module: httptester_kinit
+short_description: Get Kerberos ticket
+description: Get Kerberos ticket using kinit non-interactively.
+options:
+ username:
+ description: The username to get the ticket for.
+ required: true
+ type: str
+ password:
+ description: The password for I(username).
+ required; true
+ type: str
+author:
+- Ansible Project
+'''
+
+EXAMPLES = r'''
+#
+'''
+
+RETURN = r'''
+#
+'''
+
+import contextlib
+import errno
+import os
+import subprocess
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.text.converters import to_bytes, to_text
+
+try:
+ import configparser
+except ImportError:
+ import ConfigParser as configparser
+
+
+@contextlib.contextmanager
+def env_path(name, value, default_value):
+ """ Adds a value to a PATH-like env var and preserve the existing value if present. """
+ orig_value = os.environ.get(name, None)
+ os.environ[name] = '%s:%s' % (value, orig_value or default_value)
+ try:
+ yield
+
+ finally:
+ if orig_value:
+ os.environ[name] = orig_value
+
+ else:
+ del os.environ[name]
+
+
+@contextlib.contextmanager
+def krb5_conf(module, config):
+ """ Runs with a custom krb5.conf file that extends the existing config if present. """
+ if config:
+ ini_config = configparser.ConfigParser()
+ for section, entries in config.items():
+ ini_config.add_section(section)
+ for key, value in entries.items():
+ ini_config.set(section, key, value)
+
+ config_path = os.path.join(module.tmpdir, 'krb5.conf')
+ with open(config_path, mode='wt') as config_fd:
+ ini_config.write(config_fd)
+
+ with env_path('KRB5_CONFIG', config_path, '/etc/krb5.conf'):
+ yield
+
+ else:
+ yield
+
+
+def main():
+ module_args = dict(
+ username=dict(type='str', required=True),
+ password=dict(type='str', required=True, no_log=True),
+ )
+ module = AnsibleModule(
+ argument_spec=module_args,
+ required_together=[('username', 'password')],
+ )
+
+ # Heimdal has a few quirks that we want to paper over in this module
+ # 1. KRB5_TRACE does not work in any released version (<=7.7), we need to use a custom krb5.config to enable it
+ # 2. When reading the password it reads from the pty not stdin by default causing an issue with subprocess. We
+ # can control that behaviour with '--password-file=STDIN'
+ # Also need to set the custom path to krb5-config and kinit as FreeBSD relies on the newer Heimdal version in the
+ # port package.
+ sysname = os.uname()[0]
+ prefix = '/usr/local/bin/' if sysname == 'FreeBSD' else ''
+ is_heimdal = sysname in ['Darwin', 'FreeBSD']
+
+ # Debugging purposes, get the Kerberos version. On platforms like OpenSUSE this may not be on the PATH.
+ try:
+ process = subprocess.Popen(['%skrb5-config' % prefix, '--version'], stdout=subprocess.PIPE)
+ stdout, stderr = process.communicate()
+ version = to_text(stdout)
+ except OSError as e:
+ if e.errno != errno.ENOENT:
+ raise
+ version = 'Unknown (no krb5-config)'
+
+ kinit_args = ['%skinit' % prefix]
+ config = {}
+ if is_heimdal:
+ kinit_args.append('--password-file=STDIN')
+ config['logging'] = {'krb5': 'FILE:/dev/stdout'}
+ kinit_args.append(to_text(module.params['username'], errors='surrogate_or_strict'))
+
+ with krb5_conf(module, config):
+ # Weirdly setting KRB5_CONFIG in the modules environment block does not work unless we pass it in explicitly.
+ # Take a copy of the existing environment to make sure the process has the same env vars as ours. Also set
+ # KRB5_TRACE to output and debug logs helping to identify problems when calling kinit with MIT.
+ kinit_env = os.environ.copy()
+ kinit_env['KRB5_TRACE'] = '/dev/stdout'
+
+ process = subprocess.Popen(kinit_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ env=kinit_env)
+ stdout, stderr = process.communicate(to_bytes(module.params['password'], errors='surrogate_or_strict') + b'\n')
+ rc = process.returncode
+
+ module.exit_json(changed=True, stdout=to_text(stdout), stderr=to_text(stderr), rc=rc, version=version)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/prepare_http_tests/meta/main.yml b/test/integration/targets/prepare_http_tests/meta/main.yml
new file mode 100644
index 0000000..c2c543a
--- /dev/null
+++ b/test/integration/targets/prepare_http_tests/meta/main.yml
@@ -0,0 +1,3 @@
+dependencies:
+ - setup_remote_tmp_dir
+ - setup_remote_constraints
diff --git a/test/integration/targets/prepare_http_tests/tasks/default.yml b/test/integration/targets/prepare_http_tests/tasks/default.yml
new file mode 100644
index 0000000..2fb26a1
--- /dev/null
+++ b/test/integration/targets/prepare_http_tests/tasks/default.yml
@@ -0,0 +1,55 @@
+- name: RedHat - Enable the dynamic CA configuration feature
+ command: update-ca-trust force-enable
+ when: ansible_os_family == 'RedHat'
+
+- name: RedHat - Retrieve test cacert
+ get_url:
+ url: "http://ansible.http.tests/cacert.pem"
+ dest: "/etc/pki/ca-trust/source/anchors/ansible.pem"
+ when: ansible_os_family == 'RedHat'
+
+- name: Get client cert/key
+ get_url:
+ url: "http://ansible.http.tests/{{ item }}"
+ dest: "{{ remote_tmp_dir }}/{{ item }}"
+ with_items:
+ - client.pem
+ - client.key
+
+- name: Suse - Retrieve test cacert
+ get_url:
+ url: "http://ansible.http.tests/cacert.pem"
+ dest: "/etc/pki/trust/anchors/ansible.pem"
+ when: ansible_os_family == 'Suse'
+
+- name: Debian/Alpine - Retrieve test cacert
+ get_url:
+ url: "http://ansible.http.tests/cacert.pem"
+ dest: "/usr/local/share/ca-certificates/ansible.crt"
+ when: ansible_os_family in ['Debian', 'Alpine']
+
+- name: Redhat - Update ca trust
+ command: update-ca-trust extract
+ when: ansible_os_family == 'RedHat'
+
+- name: Debian/Alpine/Suse - Update ca certificates
+ command: update-ca-certificates
+ when: ansible_os_family in ['Debian', 'Alpine', 'Suse']
+
+- name: Update cacert
+ when: ansible_os_family in ['FreeBSD', 'Darwin']
+ block:
+ - name: Retrieve test cacert
+ uri:
+ url: "http://ansible.http.tests/cacert.pem"
+ return_content: true
+ register: cacert_pem
+
+ - name: Locate cacert
+ command: '{{ ansible_python_interpreter }} -c "import ssl; print(ssl.get_default_verify_paths().cafile)"'
+ register: cafile_path
+
+ - name: Update cacert
+ blockinfile:
+ path: "{{ cafile_path.stdout_lines|first }}"
+ block: "{{ cacert_pem.content }}"
diff --git a/test/integration/targets/prepare_http_tests/tasks/kerberos.yml b/test/integration/targets/prepare_http_tests/tasks/kerberos.yml
new file mode 100644
index 0000000..2678b46
--- /dev/null
+++ b/test/integration/targets/prepare_http_tests/tasks/kerberos.yml
@@ -0,0 +1,65 @@
+- set_fact:
+ krb5_config: '{{ remote_tmp_dir }}/krb5.conf'
+ krb5_realm: '{{ httpbin_host.split(".")[1:] | join(".") | upper }}'
+ krb5_provider: '{{ (ansible_facts.os_family == "FreeBSD" or ansible_facts.distribution == "MacOSX") | ternary("Heimdal", "MIT") }}'
+
+- set_fact:
+ krb5_username: admin@{{ krb5_realm }}
+
+- name: Create krb5.conf file
+ template:
+ src: krb5.conf.j2
+ dest: '{{ krb5_config }}'
+
+- name: Include distribution specific variables
+ include_vars: '{{ lookup("first_found", params) }}'
+ vars:
+ params:
+ files:
+ - '{{ ansible_facts.distribution }}-{{ ansible_facts.distribution_major_version }}.yml'
+ - '{{ ansible_facts.os_family }}-{{ ansible_facts.distribution_major_version }}.yml'
+ - '{{ ansible_facts.distribution }}.yml'
+ - '{{ ansible_facts.os_family }}.yml'
+ - default.yml
+ paths:
+ - '{{ role_path }}/vars'
+
+- name: Install Kerberos sytem packages
+ package:
+ name: '{{ krb5_packages }}'
+ state: present
+ when: ansible_facts.distribution not in ['Alpine', 'MacOSX']
+
+# apk isn't available on ansible-core so just call command
+- name: Alpine - Install Kerberos system packages
+ command: apk add {{ krb5_packages | join(' ') }}
+ when: ansible_facts.distribution == 'Alpine'
+
+- name: Install python gssapi
+ pip:
+ name:
+ - decorator < 5.0.0 ; python_version < '3.5' # decorator 5.0.5 and later require python 3.5 or later
+ - gssapi < 1.6.0 ; python_version <= '2.7' # gssapi 1.6.0 and later require python 3 or later
+ - gssapi ; python_version > '2.7'
+ - importlib ; python_version < '2.7'
+ state: present
+ extra_args: '-c {{ remote_constraints }}'
+ environment:
+ # Put /usr/local/bin for FreeBSD as we need to use the heimdal port over the builtin version
+ # https://github.com/pythongssapi/python-gssapi/issues/228
+ # Need the /usr/lib/mit/bin custom path for OpenSUSE as krb5-config is placed there
+ PATH: '/usr/local/bin:{{ ansible_facts.env.PATH }}:/usr/lib/mit/bin'
+ notify: Remove python gssapi
+
+- name: test the environment to make sure Kerberos is working properly
+ httptester_kinit:
+ username: '{{ krb5_username }}'
+ password: '{{ krb5_password }}'
+ environment:
+ KRB5_CONFIG: '{{ krb5_config }}'
+ KRB5CCNAME: FILE:{{ remote_tmp_dir }}/krb5.cc
+
+- name: remove test credential cache
+ file:
+ path: '{{ remote_tmp_dir }}/krb5.cc'
+ state: absent
diff --git a/test/integration/targets/prepare_http_tests/tasks/main.yml b/test/integration/targets/prepare_http_tests/tasks/main.yml
new file mode 100644
index 0000000..8d34a3c
--- /dev/null
+++ b/test/integration/targets/prepare_http_tests/tasks/main.yml
@@ -0,0 +1,35 @@
+# The docker --link functionality gives us an ENV var we can key off of to see if we have access to
+# the httptester container
+- set_fact:
+ has_httptester: "{{ lookup('env', 'HTTPTESTER') != '' }}"
+
+- name: make sure we have the ansible_os_family and ansible_distribution_version facts
+ setup:
+ gather_subset: distribution
+ when: ansible_facts == {}
+
+# If we are running with access to a httptester container, grab it's cacert and install it
+- block:
+ # Override hostname defaults with httptester linked names
+ - include_vars: httptester.yml
+
+ - include_tasks: "{{ lookup('first_found', files)}}"
+ vars:
+ files:
+ - "{{ ansible_os_family | lower }}-{{ ansible_distribution_major_version }}.yml"
+ - "{{ ansible_os_family | lower }}.yml"
+ - "default.yml"
+ when:
+ - has_httptester|bool
+ # skip the setup if running on Windows Server 2008 as httptester is not available
+ - ansible_os_family != 'Windows' or (ansible_os_family == 'Windows' and not ansible_distribution_version.startswith("6.0."))
+
+- set_fact:
+ krb5_password: "{{ lookup('env', 'KRB5_PASSWORD') }}"
+
+- name: setup Kerberos client
+ include_tasks: kerberos.yml
+ when:
+ - has_httptester|bool
+ - ansible_os_family != 'Windows'
+ - krb5_password != ''
diff --git a/test/integration/targets/prepare_http_tests/tasks/windows.yml b/test/integration/targets/prepare_http_tests/tasks/windows.yml
new file mode 100644
index 0000000..da8b0eb
--- /dev/null
+++ b/test/integration/targets/prepare_http_tests/tasks/windows.yml
@@ -0,0 +1,33 @@
+# Server 2008 R2 uses a 3rd party program to foward the ports and it may
+# not be ready straight away, we give it at least 5 minutes before
+# conceding defeat
+- name: Windows - make sure the port forwarder is active
+ win_wait_for:
+ host: ansible.http.tests
+ port: 80
+ state: started
+ timeout: 300
+
+- name: Windows - Get client cert/key
+ win_get_url:
+ url: http://ansible.http.tests/{{ item }}
+ dest: '{{ remote_tmp_dir }}\{{ item }}'
+ register: win_download
+ # Server 2008 R2 is slightly slower, we attempt 5 retries
+ retries: 5
+ until: win_download is successful
+ with_items:
+ - client.pem
+ - client.key
+
+- name: Windows - Retrieve test cacert
+ win_get_url:
+ url: http://ansible.http.tests/cacert.pem
+ dest: '{{ remote_tmp_dir }}\cacert.pem'
+
+- name: Windows - Update ca trust
+ win_certificate_store:
+ path: '{{ remote_tmp_dir }}\cacert.pem'
+ state: present
+ store_location: LocalMachine
+ store_name: Root
diff --git a/test/integration/targets/prepare_http_tests/templates/krb5.conf.j2 b/test/integration/targets/prepare_http_tests/templates/krb5.conf.j2
new file mode 100644
index 0000000..3ddfe5e
--- /dev/null
+++ b/test/integration/targets/prepare_http_tests/templates/krb5.conf.j2
@@ -0,0 +1,25 @@
+[libdefaults]
+ default_realm = {{ krb5_realm | upper }}
+ dns_lookup_realm = false
+ dns_lookup_kdc = false
+ rdns = false
+
+[realms]
+ {{ krb5_realm | upper }} = {
+{% if krb5_provider == 'Heimdal' %}
+{# Heimdal seems to only use UDP unless TCP is explicitly set and we must use TCP as the SSH tunnel only supports TCP. #}
+{# The hostname doesn't seem to work when using the alias, just use localhost as that works. #}
+ kdc = tcp/127.0.0.1
+ admin_server = tcp/127.0.0.1
+{% else %}
+ kdc = {{ httpbin_host }}
+ admin_server = {{ httpbin_host }}
+{% endif %}
+ }
+
+[domain_realm]
+ .{{ krb5_realm | lower }} = {{ krb5_realm | upper }}
+ {{ krb5_realm | lower }} = {{ krb5_realm | upper }}
+
+[logging]
+ krb5 = FILE:/dev/stdout
diff --git a/test/integration/targets/prepare_http_tests/vars/Alpine.yml b/test/integration/targets/prepare_http_tests/vars/Alpine.yml
new file mode 100644
index 0000000..2ac6a38
--- /dev/null
+++ b/test/integration/targets/prepare_http_tests/vars/Alpine.yml
@@ -0,0 +1,3 @@
+krb5_packages:
+- krb5
+- krb5-dev
diff --git a/test/integration/targets/prepare_http_tests/vars/Debian.yml b/test/integration/targets/prepare_http_tests/vars/Debian.yml
new file mode 100644
index 0000000..2b6f9b8
--- /dev/null
+++ b/test/integration/targets/prepare_http_tests/vars/Debian.yml
@@ -0,0 +1,3 @@
+krb5_packages:
+- krb5-user
+- libkrb5-dev
diff --git a/test/integration/targets/prepare_http_tests/vars/FreeBSD.yml b/test/integration/targets/prepare_http_tests/vars/FreeBSD.yml
new file mode 100644
index 0000000..752b536
--- /dev/null
+++ b/test/integration/targets/prepare_http_tests/vars/FreeBSD.yml
@@ -0,0 +1,2 @@
+krb5_packages:
+- heimdal
diff --git a/test/integration/targets/prepare_http_tests/vars/RedHat-9.yml b/test/integration/targets/prepare_http_tests/vars/RedHat-9.yml
new file mode 100644
index 0000000..2618233
--- /dev/null
+++ b/test/integration/targets/prepare_http_tests/vars/RedHat-9.yml
@@ -0,0 +1,4 @@
+krb5_packages:
+- krb5-devel
+- krb5-workstation
+- redhat-rpm-config # needed for gssapi install
diff --git a/test/integration/targets/prepare_http_tests/vars/Suse.yml b/test/integration/targets/prepare_http_tests/vars/Suse.yml
new file mode 100644
index 0000000..0e159c4
--- /dev/null
+++ b/test/integration/targets/prepare_http_tests/vars/Suse.yml
@@ -0,0 +1,3 @@
+krb5_packages:
+- krb5-client
+- krb5-devel
diff --git a/test/integration/targets/prepare_http_tests/vars/default.yml b/test/integration/targets/prepare_http_tests/vars/default.yml
new file mode 100644
index 0000000..5bc07d5
--- /dev/null
+++ b/test/integration/targets/prepare_http_tests/vars/default.yml
@@ -0,0 +1,3 @@
+krb5_packages:
+- krb5-devel
+- krb5-workstation
diff --git a/test/integration/targets/prepare_http_tests/vars/httptester.yml b/test/integration/targets/prepare_http_tests/vars/httptester.yml
new file mode 100644
index 0000000..26acf11
--- /dev/null
+++ b/test/integration/targets/prepare_http_tests/vars/httptester.yml
@@ -0,0 +1,6 @@
+# these are fake hostnames provided by docker link for the httptester container
+badssl_host: fail.ansible.http.tests
+httpbin_host: ansible.http.tests
+sni_host: sni1.ansible.http.tests
+badssl_host_substring: HTTP Client Testing Service
+self_signed_host: self-signed.ansible.http.tests
diff --git a/test/integration/targets/prepare_tests/tasks/main.yml b/test/integration/targets/prepare_tests/tasks/main.yml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/prepare_tests/tasks/main.yml
diff --git a/test/integration/targets/pyyaml/aliases b/test/integration/targets/pyyaml/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/pyyaml/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/pyyaml/runme.sh b/test/integration/targets/pyyaml/runme.sh
new file mode 100755
index 0000000..a664198
--- /dev/null
+++ b/test/integration/targets/pyyaml/runme.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+set -eu -o pipefail
+source virtualenv.sh
+set +x
+
+# deps are already installed, using --no-deps to avoid re-installing them
+# Install PyYAML without libyaml to validate ansible can run
+PYYAML_FORCE_LIBYAML=0 pip install --no-binary PyYAML --ignore-installed --no-cache-dir --no-deps PyYAML
+
+ansible --version | tee /dev/stderr | grep 'libyaml = False'
diff --git a/test/integration/targets/raw/aliases b/test/integration/targets/raw/aliases
new file mode 100644
index 0000000..f5bd1a5
--- /dev/null
+++ b/test/integration/targets/raw/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group1
+needs/target/setup_remote_tmp_dir
diff --git a/test/integration/targets/raw/meta/main.yml b/test/integration/targets/raw/meta/main.yml
new file mode 100644
index 0000000..cb6005d
--- /dev/null
+++ b/test/integration/targets/raw/meta/main.yml
@@ -0,0 +1,3 @@
+dependencies:
+ - prepare_tests
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/raw/runme.sh b/test/integration/targets/raw/runme.sh
new file mode 100755
index 0000000..2627599
--- /dev/null
+++ b/test/integration/targets/raw/runme.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+set -ux
+export ANSIBLE_BECOME_ALLOW_SAME_USER=1
+export ANSIBLE_ROLES_PATH=../
+ansible-playbook -i ../../inventory runme.yml -v "$@"
diff --git a/test/integration/targets/raw/runme.yml b/test/integration/targets/raw/runme.yml
new file mode 100644
index 0000000..ea865bc
--- /dev/null
+++ b/test/integration/targets/raw/runme.yml
@@ -0,0 +1,4 @@
+- hosts: testhost
+ gather_facts: no
+ roles:
+ - { role: raw }
diff --git a/test/integration/targets/raw/tasks/main.yml b/test/integration/targets/raw/tasks/main.yml
new file mode 100644
index 0000000..ce03c72
--- /dev/null
+++ b/test/integration/targets/raw/tasks/main.yml
@@ -0,0 +1,107 @@
+# Test code for the raw module.
+# (c) 2017, James Tanner <tanner.jc@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- set_fact: remote_tmp_dir_test={{remote_tmp_dir}}/test_command_raw
+
+- name: make sure our testing sub-directory does not exist
+ file: path="{{ remote_tmp_dir_test }}" state=absent
+
+- name: create our testing sub-directory
+ file: path="{{ remote_tmp_dir_test }}" state=directory
+
+##
+## raw
+##
+
+- name: touch a file
+ raw: "touch {{remote_tmp_dir_test | expanduser}}/test.txt"
+ register: raw_result0
+- debug: var=raw_result0
+- stat:
+ path: "{{remote_tmp_dir_test | expanduser}}/test.txt"
+ register: raw_result0_stat
+- debug: var=raw_result0_stat
+- name: ensure proper results
+ assert:
+ that:
+ - 'raw_result0.changed is defined'
+ - 'raw_result0.rc is defined'
+ - 'raw_result0.stderr is defined'
+ - 'raw_result0.stdout is defined'
+ - 'raw_result0.stdout_lines is defined'
+ - 'raw_result0.rc == 0'
+ - 'raw_result0_stat.stat.size == 0'
+
+- name: run a piped command
+ raw: "echo 'foo,bar,baz' | cut -d\\, -f2 | tr 'b' 'c'"
+ register: raw_result1
+- debug: var=raw_result1
+- name: ensure proper results
+ assert:
+ that:
+ - 'raw_result1.changed is defined'
+ - 'raw_result1.rc is defined'
+ - 'raw_result1.stderr is defined'
+ - 'raw_result1.stdout is defined'
+ - 'raw_result1.stdout_lines is defined'
+ - 'raw_result1.rc == 0'
+ - 'raw_result1.stdout_lines == ["car"]'
+
+- name: get the path to bash
+ shell: which bash
+ register: bash_path
+- name: run exmample non-posix command with bash
+ raw: "echo 'foobar' > {{remote_tmp_dir_test | expanduser}}/test.txt ; cat < {{remote_tmp_dir_test | expanduser}}/test.txt"
+ args:
+ executable: "{{ bash_path.stdout }}"
+ register: raw_result2
+- debug: var=raw_result2
+- name: ensure proper results
+ assert:
+ that:
+ - 'raw_result2.changed is defined'
+ - 'raw_result2.rc is defined'
+ - 'raw_result2.stderr is defined'
+ - 'raw_result2.stdout is defined'
+ - 'raw_result2.stdout_lines is defined'
+ - 'raw_result2.rc == 0'
+ - 'raw_result2.stdout_lines == ["foobar"]'
+# the following five tests added to test https://github.com/ansible/ansible/pull/68315
+- name: get the path to sh
+ shell: which sh
+ register: sh_path
+- name: use sh
+ raw: echo $0
+ args:
+ executable: "{{ sh_path.stdout }}"
+ become: true
+ become_method: su
+ register: sh_output
+- name: assert sh
+ assert:
+ that: "(sh_output.stdout | trim) == sh_path.stdout"
+- name: use bash
+ raw: echo $0
+ args:
+ executable: "{{ bash_path.stdout }}"
+ become: true
+ become_method: su
+ register: bash_output
+- name: assert bash
+ assert:
+ that: "(bash_output.stdout | trim) == bash_path.stdout"
diff --git a/test/integration/targets/reboot/aliases b/test/integration/targets/reboot/aliases
new file mode 100644
index 0000000..7f995fd
--- /dev/null
+++ b/test/integration/targets/reboot/aliases
@@ -0,0 +1,5 @@
+context/target
+destructive
+needs/root
+shippable/posix/group2
+skip/docker
diff --git a/test/integration/targets/reboot/handlers/main.yml b/test/integration/targets/reboot/handlers/main.yml
new file mode 100644
index 0000000..a40bac0
--- /dev/null
+++ b/test/integration/targets/reboot/handlers/main.yml
@@ -0,0 +1,4 @@
+- name: remove molly-guard
+ apt:
+ name: molly-guard
+ state: absent
diff --git a/test/integration/targets/reboot/tasks/check_reboot.yml b/test/integration/targets/reboot/tasks/check_reboot.yml
new file mode 100644
index 0000000..059c422
--- /dev/null
+++ b/test/integration/targets/reboot/tasks/check_reboot.yml
@@ -0,0 +1,10 @@
+- name: Get current boot time
+ command: "{{ _boot_time_command[ansible_facts['distribution'] | lower] | default('cat /proc/sys/kernel/random/boot_id') }}"
+ register: after_boot_time
+
+- name: Ensure system was actually rebooted
+ assert:
+ that:
+ - reboot_result is changed
+ - reboot_result.elapsed > 10
+ - before_boot_time.stdout != after_boot_time.stdout
diff --git a/test/integration/targets/reboot/tasks/get_boot_time.yml b/test/integration/targets/reboot/tasks/get_boot_time.yml
new file mode 100644
index 0000000..7f79770
--- /dev/null
+++ b/test/integration/targets/reboot/tasks/get_boot_time.yml
@@ -0,0 +1,3 @@
+- name: Get current boot time
+ command: "{{ _boot_time_command[ansible_facts['distribution'] | lower] | default('cat /proc/sys/kernel/random/boot_id') }}"
+ register: before_boot_time
diff --git a/test/integration/targets/reboot/tasks/main.yml b/test/integration/targets/reboot/tasks/main.yml
new file mode 100644
index 0000000..4884f10
--- /dev/null
+++ b/test/integration/targets/reboot/tasks/main.yml
@@ -0,0 +1,43 @@
+- name: Check split state
+ stat:
+ path: "{{ output_dir }}"
+ register: split
+ ignore_errors: yes
+
+- name: >-
+ Memorize whether we're in a containerized environment
+ and/or a split controller mode
+ set_fact:
+ in_container_env: >-
+ {{
+ ansible_facts.virtualization_type | default('')
+ in ['docker', 'container', 'containerd']
+ }}
+ in_split_controller_mode: >-
+ {{ split is not success or not split.stat.exists }}
+
+- name: Explain why testing against a container is not an option
+ debug:
+ msg: >-
+ This test is attempting to reboot the whole host operating system.
+ The current target is a containerized environment. Containers
+ cannot be reboot like VMs. This is why the test is being skipped.
+ when: in_container_env
+
+- name: Explain why testing against the same host is not an option
+ debug:
+ msg: >-
+ This test is attempting to reboot the whole host operating system.
+ This means it would interrupt itself trying to reboot own
+ environment. It needs to target a separate VM or machine to be
+ able to function so it's being skipped in the current invocation.
+ when: not in_split_controller_mode
+
+- name: Test reboot
+ when: not in_container_env and in_split_controller_mode
+ block:
+ - import_tasks: test_standard_scenarios.yml
+ - import_tasks: test_reboot_command.yml
+ - import_tasks: test_invalid_parameter.yml
+ - import_tasks: test_invalid_test_command.yml
+ - import_tasks: test_molly_guard.yml
diff --git a/test/integration/targets/reboot/tasks/test_invalid_parameter.yml b/test/integration/targets/reboot/tasks/test_invalid_parameter.yml
new file mode 100644
index 0000000..f8e1a8f
--- /dev/null
+++ b/test/integration/targets/reboot/tasks/test_invalid_parameter.yml
@@ -0,0 +1,11 @@
+- name: Use invalid parameter
+ reboot:
+ foo: bar
+ ignore_errors: yes
+ register: invalid_parameter
+
+- name: Ensure task fails with error
+ assert:
+ that:
+ - invalid_parameter is failed
+ - "invalid_parameter.msg == 'Invalid options for reboot: foo'"
diff --git a/test/integration/targets/reboot/tasks/test_invalid_test_command.yml b/test/integration/targets/reboot/tasks/test_invalid_test_command.yml
new file mode 100644
index 0000000..ea1db81
--- /dev/null
+++ b/test/integration/targets/reboot/tasks/test_invalid_test_command.yml
@@ -0,0 +1,8 @@
+- name: Reboot with test command that fails
+ reboot:
+ test_command: 'FAIL'
+ reboot_timeout: "{{ timeout }}"
+ register: reboot_fail_test
+ failed_when: "reboot_fail_test.msg != 'Timed out waiting for post-reboot test command (timeout=' ~ timeout ~ ')'"
+ vars:
+ timeout: "{{ _timeout_value[ansible_facts['distribution'] | lower] | default(60) }}"
diff --git a/test/integration/targets/reboot/tasks/test_molly_guard.yml b/test/integration/targets/reboot/tasks/test_molly_guard.yml
new file mode 100644
index 0000000..f91fd4f
--- /dev/null
+++ b/test/integration/targets/reboot/tasks/test_molly_guard.yml
@@ -0,0 +1,20 @@
+- name: Test molly-guard
+ when: ansible_facts.distribution in ['Debian', 'Ubuntu']
+ tags:
+ - molly-guard
+ block:
+ - import_tasks: get_boot_time.yml
+
+ - name: Install molly-guard
+ apt:
+ update_cache: yes
+ name: molly-guard
+ state: present
+ notify: remove molly-guard
+
+ - name: Reboot when molly-guard is installed
+ reboot:
+ search_paths: /lib/molly-guard
+ register: reboot_result
+
+ - import_tasks: check_reboot.yml
diff --git a/test/integration/targets/reboot/tasks/test_reboot_command.yml b/test/integration/targets/reboot/tasks/test_reboot_command.yml
new file mode 100644
index 0000000..779d380
--- /dev/null
+++ b/test/integration/targets/reboot/tasks/test_reboot_command.yml
@@ -0,0 +1,22 @@
+- import_tasks: get_boot_time.yml
+- name: Reboot with custom reboot_command using unqualified path
+ reboot:
+ reboot_command: reboot
+ register: reboot_result
+- import_tasks: check_reboot.yml
+
+
+- import_tasks: get_boot_time.yml
+- name: Reboot with custom reboot_command using absolute path
+ reboot:
+ reboot_command: /sbin/reboot
+ register: reboot_result
+- import_tasks: check_reboot.yml
+
+
+- import_tasks: get_boot_time.yml
+- name: Reboot with custom reboot_command with parameters
+ reboot:
+ reboot_command: shutdown -r now
+ register: reboot_result
+- import_tasks: check_reboot.yml
diff --git a/test/integration/targets/reboot/tasks/test_standard_scenarios.yml b/test/integration/targets/reboot/tasks/test_standard_scenarios.yml
new file mode 100644
index 0000000..fac85be
--- /dev/null
+++ b/test/integration/targets/reboot/tasks/test_standard_scenarios.yml
@@ -0,0 +1,32 @@
+- import_tasks: get_boot_time.yml
+- name: Reboot with default settings
+ reboot:
+ register: reboot_result
+- import_tasks: check_reboot.yml
+
+
+- import_tasks: get_boot_time.yml
+- name: Reboot with all options except reboot_command
+ reboot:
+ connect_timeout: 30
+ search_paths:
+ - /sbin
+ - /bin
+ - /usr/sbin
+ - /usr/bin
+ msg: Rebooting
+ post_reboot_delay: 1
+ pre_reboot_delay: 61
+ test_command: uptime
+ reboot_timeout: 500
+ register: reboot_result
+- import_tasks: check_reboot.yml
+
+
+- import_tasks: get_boot_time.yml
+- name: Test with negative values for delays
+ reboot:
+ post_reboot_delay: -0.5
+ pre_reboot_delay: -61
+ register: reboot_result
+- import_tasks: check_reboot.yml
diff --git a/test/integration/targets/reboot/vars/main.yml b/test/integration/targets/reboot/vars/main.yml
new file mode 100644
index 0000000..0e1997c
--- /dev/null
+++ b/test/integration/targets/reboot/vars/main.yml
@@ -0,0 +1,9 @@
+_boot_time_command:
+ freebsd: '/sbin/sysctl kern.boottime'
+ openbsd: '/sbin/sysctl kern.boottime'
+ macosx: 'who -b'
+ solaris: 'who -b'
+ sunos: 'who -b'
+
+_timeout_value:
+ solaris: 120
diff --git a/test/integration/targets/register/aliases b/test/integration/targets/register/aliases
new file mode 100644
index 0000000..b76d5f6
--- /dev/null
+++ b/test/integration/targets/register/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller # this "module" is actually an action that runs on the controller
diff --git a/test/integration/targets/register/can_register.yml b/test/integration/targets/register/can_register.yml
new file mode 100644
index 0000000..da61010
--- /dev/null
+++ b/test/integration/targets/register/can_register.yml
@@ -0,0 +1,21 @@
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - name: test registering
+ debug: msg='does nothing really but register this'
+ register: debug_msg
+
+ - name: validate registering
+ assert:
+ that:
+ - debug_msg is defined
+
+ - name: test registering skipped
+ debug: msg='does nothing really but register this'
+ when: false
+ register: debug_skipped
+
+ - name: validate registering
+ assert:
+ that:
+ - debug_skipped is defined
diff --git a/test/integration/targets/register/invalid.yml b/test/integration/targets/register/invalid.yml
new file mode 100644
index 0000000..bdca9d6
--- /dev/null
+++ b/test/integration/targets/register/invalid.yml
@@ -0,0 +1,11 @@
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - name: test registering
+ debug: msg='does nothing really but register this'
+ register: 200
+
+ - name: never gets here
+ assert:
+ that:
+ - 200 is not defined
diff --git a/test/integration/targets/register/invalid_skipped.yml b/test/integration/targets/register/invalid_skipped.yml
new file mode 100644
index 0000000..0ad31f5
--- /dev/null
+++ b/test/integration/targets/register/invalid_skipped.yml
@@ -0,0 +1,12 @@
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - name: test registering bad var when skipped
+ debug: msg='does nothing really but register this'
+ when: false
+ register: 200
+
+ - name: never gets here
+ assert:
+ that:
+ - 200 is not defined
diff --git a/test/integration/targets/register/runme.sh b/test/integration/targets/register/runme.sh
new file mode 100755
index 0000000..8adc504
--- /dev/null
+++ b/test/integration/targets/register/runme.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# does it work?
+ansible-playbook can_register.yml -i ../../inventory -v "$@"
+
+# ensure we do error when it its apprpos
+set +e
+result="$(ansible-playbook invalid.yml -i ../../inventory -v "$@" 2>&1)"
+set -e
+grep -q "Invalid variable name in " <<< "${result}"
diff --git a/test/integration/targets/rel_plugin_loading/aliases b/test/integration/targets/rel_plugin_loading/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/rel_plugin_loading/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/rel_plugin_loading/notyaml.yml b/test/integration/targets/rel_plugin_loading/notyaml.yml
new file mode 100644
index 0000000..23ab032
--- /dev/null
+++ b/test/integration/targets/rel_plugin_loading/notyaml.yml
@@ -0,0 +1,5 @@
+all:
+ hosts:
+ testhost:
+ ansible_connection: local
+ ansible_python_interpreter: "{{ansible_playbook_python}}"
diff --git a/test/integration/targets/rel_plugin_loading/runme.sh b/test/integration/targets/rel_plugin_loading/runme.sh
new file mode 100755
index 0000000..34e70fd
--- /dev/null
+++ b/test/integration/targets/rel_plugin_loading/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ANSIBLE_INVENTORY_ENABLED=notyaml ansible-playbook subdir/play.yml -i notyaml.yml "$@"
diff --git a/test/integration/targets/rel_plugin_loading/subdir/inventory_plugins/notyaml.py b/test/integration/targets/rel_plugin_loading/subdir/inventory_plugins/notyaml.py
new file mode 100644
index 0000000..e542913
--- /dev/null
+++ b/test/integration/targets/rel_plugin_loading/subdir/inventory_plugins/notyaml.py
@@ -0,0 +1,169 @@
+# Copyright (c) 2017 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ inventory: yaml
+ version_added: "2.4"
+ short_description: Uses a specific YAML file as an inventory source.
+ description:
+ - "YAML-based inventory, should start with the C(all) group and contain hosts/vars/children entries."
+ - Host entries can have sub-entries defined, which will be treated as variables.
+ - Vars entries are normal group vars.
+ - "Children are 'child groups', which can also have their own vars/hosts/children and so on."
+ - File MUST have a valid extension, defined in configuration.
+ notes:
+ - If you want to set vars for the C(all) group inside the inventory file, the C(all) group must be the first entry in the file.
+ - Whitelisted in configuration by default.
+ options:
+ yaml_extensions:
+ description: list of 'valid' extensions for files containing YAML
+ type: list
+ default: ['.yaml', '.yml', '.json']
+ env:
+ - name: ANSIBLE_YAML_FILENAME_EXT
+ - name: ANSIBLE_INVENTORY_PLUGIN_EXTS
+ ini:
+ - key: yaml_valid_extensions
+ section: defaults
+ - section: inventory_plugin_yaml
+ key: yaml_valid_extensions
+
+'''
+EXAMPLES = '''
+all: # keys must be unique, i.e. only one 'hosts' per group
+ hosts:
+ test1:
+ test2:
+ host_var: value
+ vars:
+ group_all_var: value
+ children: # key order does not matter, indentation does
+ other_group:
+ children:
+ group_x:
+ hosts:
+ test5
+ vars:
+ g2_var2: value3
+ hosts:
+ test4:
+ ansible_host: 127.0.0.1
+ last_group:
+ hosts:
+ test1 # same host as above, additional group membership
+ vars:
+ group_last_var: value
+'''
+
+import os
+
+from collections.abc import MutableMapping
+
+from ansible.errors import AnsibleError, AnsibleParserError
+from ansible.module_utils.six import string_types
+from ansible.module_utils._text import to_native, to_text
+from ansible.plugins.inventory import BaseFileInventoryPlugin
+
+NoneType = type(None)
+
+
+class InventoryModule(BaseFileInventoryPlugin):
+
+ NAME = 'yaml'
+
+ def __init__(self):
+
+ super(InventoryModule, self).__init__()
+
+ def verify_file(self, path):
+
+ valid = False
+ if super(InventoryModule, self).verify_file(path):
+ file_name, ext = os.path.splitext(path)
+ if not ext or ext in self.get_option('yaml_extensions'):
+ valid = True
+ return valid
+
+ def parse(self, inventory, loader, path, cache=True):
+ ''' parses the inventory file '''
+
+ super(InventoryModule, self).parse(inventory, loader, path)
+ self.set_options()
+
+ try:
+ data = self.loader.load_from_file(path, cache=False)
+ except Exception as e:
+ raise AnsibleParserError(e)
+
+ if not data:
+ raise AnsibleParserError('Parsed empty YAML file')
+ elif not isinstance(data, MutableMapping):
+ raise AnsibleParserError('YAML inventory has invalid structure, it should be a dictionary, got: %s' % type(data))
+ elif data.get('plugin'):
+ raise AnsibleParserError('Plugin configuration YAML file, not YAML inventory')
+
+ # We expect top level keys to correspond to groups, iterate over them
+ # to get host, vars and subgroups (which we iterate over recursivelly)
+ if isinstance(data, MutableMapping):
+ for group_name in data:
+ self._parse_group(group_name, data[group_name])
+ else:
+ raise AnsibleParserError("Invalid data from file, expected dictionary and got:\n\n%s" % to_native(data))
+
+ def _parse_group(self, group, group_data):
+
+ if isinstance(group_data, (MutableMapping, NoneType)):
+
+ try:
+ self.inventory.add_group(group)
+ except AnsibleError as e:
+ raise AnsibleParserError("Unable to add group %s: %s" % (group, to_text(e)))
+
+ if group_data is not None:
+ # make sure they are dicts
+ for section in ['vars', 'children', 'hosts']:
+ if section in group_data:
+ # convert strings to dicts as these are allowed
+ if isinstance(group_data[section], string_types):
+ group_data[section] = {group_data[section]: None}
+
+ if not isinstance(group_data[section], (MutableMapping, NoneType)):
+ raise AnsibleParserError('Invalid "%s" entry for "%s" group, requires a dictionary, found "%s" instead.' %
+ (section, group, type(group_data[section])))
+
+ for key in group_data:
+
+ if not isinstance(group_data[key], (MutableMapping, NoneType)):
+ self.display.warning('Skipping key (%s) in group (%s) as it is not a mapping, it is a %s' % (key, group, type(group_data[key])))
+ continue
+
+ if isinstance(group_data[key], NoneType):
+ self.display.vvv('Skipping empty key (%s) in group (%s)' % (key, group))
+ elif key == 'vars':
+ for var in group_data[key]:
+ self.inventory.set_variable(group, var, group_data[key][var])
+ elif key == 'children':
+ for subgroup in group_data[key]:
+ self._parse_group(subgroup, group_data[key][subgroup])
+ self.inventory.add_child(group, subgroup)
+
+ elif key == 'hosts':
+ for host_pattern in group_data[key]:
+ hosts, port = self._parse_host(host_pattern)
+ self._populate_host_vars(hosts, group_data[key][host_pattern] or {}, group, port)
+ else:
+ self.display.warning('Skipping unexpected key (%s) in group (%s), only "vars", "children" and "hosts" are valid' % (key, group))
+
+ else:
+ self.display.warning("Skipping '%s' as this is not a valid group definition" % group)
+
+ def _parse_host(self, host_pattern):
+ '''
+ Each host key can be a pattern, try to process it and add variables as needed
+ '''
+ (hostnames, port) = self._expand_hostpattern(host_pattern)
+
+ return hostnames, port
diff --git a/test/integration/targets/rel_plugin_loading/subdir/play.yml b/test/integration/targets/rel_plugin_loading/subdir/play.yml
new file mode 100644
index 0000000..2326b14
--- /dev/null
+++ b/test/integration/targets/rel_plugin_loading/subdir/play.yml
@@ -0,0 +1,6 @@
+- hosts: all
+ gather_facts: false
+ tasks:
+ - assert:
+ that:
+ - inventory_hostname == 'testhost'
diff --git a/test/integration/targets/remote_tmp/aliases b/test/integration/targets/remote_tmp/aliases
new file mode 100644
index 0000000..126e809
--- /dev/null
+++ b/test/integration/targets/remote_tmp/aliases
@@ -0,0 +1,3 @@
+shippable/posix/group2
+context/target
+needs/target/setup_remote_tmp_dir
diff --git a/test/integration/targets/remote_tmp/playbook.yml b/test/integration/targets/remote_tmp/playbook.yml
new file mode 100644
index 0000000..5adef62
--- /dev/null
+++ b/test/integration/targets/remote_tmp/playbook.yml
@@ -0,0 +1,59 @@
+- name: Test temp dir on de escalation
+ hosts: testhost
+ become: yes
+ tasks:
+ - name: create test user
+ user:
+ name: tmptest
+ state: present
+ group: '{{ "staff" if ansible_facts.distribution == "MacOSX" else omit }}'
+
+ - name: execute test case
+ become_user: tmptest
+ block:
+ - name: Test case from issue 41340
+ blockinfile:
+ create: yes
+ block: |
+ export foo=bar
+ marker: "# {mark} Here there be a marker"
+ dest: /tmp/testing.txt
+ mode: 0644
+ always:
+ - name: clean up file
+ file: path=/tmp/testing.txt state=absent
+
+ - name: clean up test user
+ user: name=tmptest state=absent
+ become_user: root
+
+- name: Test tempdir is removed
+ hosts: testhost
+ gather_facts: false
+ tasks:
+ - import_role:
+ name: ../setup_remote_tmp_dir
+
+ - file:
+ state: touch
+ path: "{{ remote_tmp_dir }}/65393"
+
+ - copy:
+ src: "{{ remote_tmp_dir }}/65393"
+ dest: "{{ remote_tmp_dir }}/65393.2"
+ remote_src: true
+
+ - find:
+ path: "~/.ansible/tmp"
+ use_regex: yes
+ patterns: 'AnsiballZ_.+\.py'
+ recurse: true
+ register: result
+
+ - debug:
+ var: result
+
+ - assert:
+ that:
+ # Should find nothing since pipelining is used
+ - result.files|length == 0
diff --git a/test/integration/targets/remote_tmp/runme.sh b/test/integration/targets/remote_tmp/runme.sh
new file mode 100755
index 0000000..69efd6e
--- /dev/null
+++ b/test/integration/targets/remote_tmp/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -ux
+
+ansible-playbook -i ../../inventory playbook.yml -v "$@"
diff --git a/test/integration/targets/replace/aliases b/test/integration/targets/replace/aliases
new file mode 100644
index 0000000..a6dafcf
--- /dev/null
+++ b/test/integration/targets/replace/aliases
@@ -0,0 +1 @@
+shippable/posix/group1
diff --git a/test/integration/targets/replace/meta/main.yml b/test/integration/targets/replace/meta/main.yml
new file mode 100644
index 0000000..cb6005d
--- /dev/null
+++ b/test/integration/targets/replace/meta/main.yml
@@ -0,0 +1,3 @@
+dependencies:
+ - prepare_tests
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/replace/tasks/main.yml b/test/integration/targets/replace/tasks/main.yml
new file mode 100644
index 0000000..d267b78
--- /dev/null
+++ b/test/integration/targets/replace/tasks/main.yml
@@ -0,0 +1,265 @@
+# setup
+- set_fact: remote_tmp_dir_test={{remote_tmp_dir}}/test_replace
+
+- name: make sure our testing sub-directory does not exist
+ file: path="{{ remote_tmp_dir_test }}" state=absent
+
+- name: create our testing sub-directory
+ file: path="{{ remote_tmp_dir_test }}" state=directory
+
+# tests
+- name: create test files
+ copy:
+ content: |-
+ The quick brown fox jumps over the lazy dog.
+ We promptly judged antique ivory buckles for the next prize.
+ Jinxed wizards pluck ivy from the big quilt.
+ Jaded zombies acted quaintly but kept driving their oxen forward.
+ dest: "{{ remote_tmp_dir_test }}/pangrams.{{ item }}.txt"
+ with_sequence: start=0 end=6 format=%02x #increment as needed
+
+
+## test `before` option
+- name: remove all spaces before "quilt"
+ replace:
+ path: "{{ remote_tmp_dir_test }}/pangrams.00.txt"
+ before: 'quilt'
+ regexp: ' '
+ register: replace_test0
+
+- command: "cat {{ remote_tmp_dir_test }}/pangrams.00.txt"
+ register: replace_cat0
+
+- name: validate before assertions
+ assert:
+ that:
+ - replace_test0 is successful
+ - replace_test0 is changed
+ - replace_cat0.stdout_lines[0] == 'Thequickbrownfoxjumpsoverthelazydog.'
+ - replace_cat0.stdout_lines[-1] == 'Jaded zombies acted quaintly but kept driving their oxen forward.'
+
+
+## test `after` option
+- name: remove all spaces after "promptly"
+ replace:
+ path: "{{ remote_tmp_dir_test }}/pangrams.01.txt"
+ after: 'promptly'
+ regexp: ' '
+ register: replace_test1
+
+- command: "cat {{ remote_tmp_dir_test }}/pangrams.01.txt"
+ register: replace_cat1
+
+- name: validate after assertions
+ assert:
+ that:
+ - replace_test1 is successful
+ - replace_test1 is changed
+ - replace_cat1.stdout_lines[0] == 'The quick brown fox jumps over the lazy dog.'
+ - replace_cat1.stdout_lines[-1] == 'Jadedzombiesactedquaintlybutkeptdrivingtheiroxenforward.'
+
+
+## test combined `before` and `after` options
+- name: before "promptly" but after "quilt", replace every "e" with a "3"
+ replace:
+ path: "{{ remote_tmp_dir_test }}/pangrams.02.txt"
+ before: 'promptly'
+ after: 'quilt'
+ regexp: 'e'
+ replace: '3'
+ register: replace_test2
+
+- name: validate after+before assertions
+ assert:
+ that:
+ - replace_test2 is successful
+ - not replace_test2 is changed
+ - replace_test2.msg.startswith("Pattern for before/after params did not match the given file")
+
+- name: before "quilt" but after "promptly", replace every "e" with a "3"
+ replace:
+ path: "{{ remote_tmp_dir_test }}/pangrams.03.txt"
+ before: 'quilt'
+ after: 'promptly'
+ regexp: 'e'
+ replace: '3'
+ register: replace_test3
+
+- command: "cat {{ remote_tmp_dir_test }}/pangrams.03.txt"
+ register: replace_cat3
+
+- name: validate before+after assertions
+ assert:
+ that:
+ - replace_test3 is successful
+ - replace_test3 is changed
+ - replace_cat3.stdout_lines[1] == 'We promptly judg3d antiqu3 ivory buckl3s for th3 n3xt priz3.'
+
+
+## test ^$ behavior in MULTILINE, and . behavior in absense of DOTALL
+- name: quote everything between bof and eof
+ replace:
+ path: "{{ remote_tmp_dir_test }}/pangrams.04.txt"
+ regexp: ^([\S\s]+)$
+ replace: '"\1"'
+ register: replace_test4_0
+
+- command: "cat {{ remote_tmp_dir_test }}/pangrams.04.txt"
+ register: replace_cat4_0
+
+- name: quote everything between bol and eol
+ replace:
+ path: "{{ remote_tmp_dir_test }}/pangrams.04.txt"
+ regexp: ^(.+)$
+ replace: '"\1"'
+ register: replace_test4_1
+
+- command: "cat {{ remote_tmp_dir_test }}/pangrams.04.txt"
+ register: replace_cat4_1
+
+- name: validate before+after assertions
+ assert:
+ that:
+ - replace_test4_0 is successful
+ - replace_test4_0 is changed
+ - replace_test4_1 is successful
+ - replace_test4_1 is changed
+ - replace_cat4_0.stdout_lines[0] == '"The quick brown fox jumps over the lazy dog.'
+ - replace_cat4_0.stdout_lines[-1] == 'Jaded zombies acted quaintly but kept driving their oxen forward."'
+ - replace_cat4_1.stdout_lines[0] == '""The quick brown fox jumps over the lazy dog."'
+ - replace_cat4_1.stdout_lines[-1] == '"Jaded zombies acted quaintly but kept driving their oxen forward.""'
+
+
+## test \b escaping in short and long form
+- name: short form with unescaped word boundaries
+ replace: path="{{ remote_tmp_dir_test }}/pangrams.05.txt" regexp='\b(.+)\b' replace='"\1"'
+ register: replace_test5_0
+
+- name: short form with escaped word boundaries
+ replace: path="{{ remote_tmp_dir_test }}/pangrams.05.txt" regexp='\\b(.+)\\b' replace='"\1"'
+ register: replace_test5_1
+
+- command: "cat {{ remote_tmp_dir_test }}/pangrams.05.txt"
+ register: replace_cat5_1
+
+- name: long form with unescaped word boundaries
+ replace:
+ path: "{{ remote_tmp_dir_test }}/pangrams.05.txt"
+ regexp: '\b(.+)\b'
+ replace: '"\1"'
+ register: replace_test5_2
+
+- command: "cat {{ remote_tmp_dir_test }}/pangrams.05.txt"
+ register: replace_cat5_2
+
+- name: long form with escaped word boundaries
+ replace:
+ path: "{{ remote_tmp_dir_test }}/pangrams.05.txt"
+ regexp: '\\b(.+)\\b'
+ replace: '"\1"'
+ register: replace_test5_3
+
+- name: validate before+after assertions
+ assert:
+ that:
+ - not replace_test5_0 is changed
+ - replace_test5_1 is changed
+ - replace_test5_2 is changed
+ - not replace_test5_3 is changed
+ - replace_cat5_1.stdout_lines[0] == '"The quick brown fox jumps over the lazy dog".'
+ - replace_cat5_1.stdout_lines[-1] == '"Jaded zombies acted quaintly but kept driving their oxen forward".'
+ - replace_cat5_2.stdout_lines[0] == '""The quick brown fox jumps over the lazy dog"".'
+ - replace_cat5_2.stdout_lines[-1] == '""Jaded zombies acted quaintly but kept driving their oxen forward"".'
+
+
+## test backup behaviors
+- name: replacement with backup
+ replace:
+ path: "{{ remote_tmp_dir_test }}/pangrams.06.txt"
+ regexp: ^(.+)$
+ replace: '"\1"'
+ backup: true
+ register: replace_test6
+
+- command: "cat {{ remote_tmp_dir_test }}/pangrams.06.txt"
+ register: replace_cat6_0
+
+- command: "cat {{ replace_test6.backup_file }}"
+ register: replace_cat6_1
+
+- name: validate backup
+ assert:
+ that:
+ - replace_test6 is successful
+ - replace_test6 is changed
+ - replace_test6.backup_file is search('/pangrams.06.txt.')
+ - replace_cat6_0.stdout != replace_cat6_1.stdout
+
+
+## test filesystem failures
+- name: fail on directory
+ replace:
+ path: "{{ remote_tmp_dir_test }}"
+ regexp: ^(.+)$
+ register: replace_test7_1
+ ignore_errors: true
+
+- name: fail on missing file
+ replace:
+ path: "{{ remote_tmp_dir_test }}/missing_file.txt"
+ regexp: ^(.+)$
+ register: replace_test7_2
+ ignore_errors: true
+
+- name: validate backup
+ assert:
+ that:
+ - replace_test7_1 is failure
+ - replace_test7_2 is failure
+ - replace_test7_1.msg.endswith(" is a directory !")
+ - replace_test7_2.msg.endswith(" does not exist !")
+
+
+## test subsection replacement when before/after potentially match more than once
+- name: test file for subsection replacement gone awry
+ copy:
+ content: |-
+ # start of group
+ 0.0.0.0
+ 127.0.0.1
+ 127.0.1.1
+ # end of group
+
+ # start of group
+ 0.0.0.0
+ 127.0.0.1
+ 127.0.1.1
+ # end of group
+
+ # start of group
+ 0.0.0.0
+ 127.0.0.1
+ 127.0.1.1
+ # end of group
+ dest: "{{ remote_tmp_dir_test }}/addresses.txt"
+
+- name: subsection madness
+ replace:
+ path: "{{ remote_tmp_dir_test }}/addresses.txt"
+ after: '# start of group'
+ before: '# end of group'
+ regexp: '0'
+ replace: '9'
+ register: replace_test8
+
+- command: "cat {{ remote_tmp_dir_test }}/addresses.txt"
+ register: replace_cat8
+
+- name: validate before+after assertions
+ assert:
+ that:
+ - replace_test8 is successful
+ - replace_test8 is changed
+ - replace_cat8.stdout_lines[1] == "9.9.9.9"
+ - replace_cat8.stdout_lines[7] == "0.0.0.0"
+ - replace_cat8.stdout_lines[13] == "0.0.0.0"
diff --git a/test/integration/targets/retry_task_name_in_callback/aliases b/test/integration/targets/retry_task_name_in_callback/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/retry_task_name_in_callback/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/retry_task_name_in_callback/runme.sh b/test/integration/targets/retry_task_name_in_callback/runme.sh
new file mode 100755
index 0000000..5f636cd
--- /dev/null
+++ b/test/integration/targets/retry_task_name_in_callback/runme.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# we are looking to verify the callback for v2_retry_runner gets a correct task name, include
+# if the value needs templating based on results of previous tasks
+OUTFILE="callback_retry_task_name.out"
+trap 'rm -rf "${OUTFILE}"' EXIT
+
+EXPECTED_REGEX="^.*TASK.*18236 callback task template fix OUTPUT 2"
+ansible-playbook "$@" -i ../../inventory test.yml | tee "${OUTFILE}"
+echo "Grepping for ${EXPECTED_REGEX} in stdout."
+grep -e "${EXPECTED_REGEX}" "${OUTFILE}"
diff --git a/test/integration/targets/retry_task_name_in_callback/test.yml b/test/integration/targets/retry_task_name_in_callback/test.yml
new file mode 100644
index 0000000..0e450cf
--- /dev/null
+++ b/test/integration/targets/retry_task_name_in_callback/test.yml
@@ -0,0 +1,28 @@
+---
+- hosts: testhost
+ gather_facts: False
+ vars:
+ foo: blippy
+ tasks:
+ - name: First run {{ foo }}
+ command: echo "18236 callback task template fix OUTPUT 1"
+ register: the_result_var
+
+ - block:
+ - name: "{{ the_result_var.stdout }}"
+ command: echo "18236 callback task template fix OUTPUT 2"
+ register: the_result_var
+ retries: 1
+ delay: 1
+ until: False
+ ignore_errors: true
+
+ # - name: assert task_name was
+
+ - name: "{{ the_result_var.stdout }}"
+ command: echo "18236 callback taskadfadf template fix OUTPUT 3"
+ register: the_result_var
+
+ - name: "{{ the_result_var.stdout }}"
+ debug:
+ msg: "nothing to see here."
diff --git a/test/integration/targets/roles/aliases b/test/integration/targets/roles/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/roles/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/roles/allowed_dupes.yml b/test/integration/targets/roles/allowed_dupes.yml
new file mode 100644
index 0000000..998950b
--- /dev/null
+++ b/test/integration/targets/roles/allowed_dupes.yml
@@ -0,0 +1,18 @@
+- name: test that import_role adds one (just one) execution of the role
+ hosts: localhost
+ gather_facts: false
+ tags: ['importrole']
+ roles:
+ - name: a
+ tasks:
+ - name: import role ignores dupe rule
+ import_role: name=a
+
+- name: test that include_role adds one (just one) execution of the role
+ hosts: localhost
+ gather_facts: false
+ tags: ['includerole']
+ roles:
+ - name: a
+ tasks:
+ - include_role: name=a
diff --git a/test/integration/targets/roles/data_integrity.yml b/test/integration/targets/roles/data_integrity.yml
new file mode 100644
index 0000000..5eb4fb3
--- /dev/null
+++ b/test/integration/targets/roles/data_integrity.yml
@@ -0,0 +1,4 @@
+- hosts: all
+ gather_facts: false
+ roles:
+ - data
diff --git a/test/integration/targets/roles/no_dupes.yml b/test/integration/targets/roles/no_dupes.yml
new file mode 100644
index 0000000..7e1ecb1
--- /dev/null
+++ b/test/integration/targets/roles/no_dupes.yml
@@ -0,0 +1,29 @@
+- name: play should only show 1 invocation of a, as dependencies in this play are deduped
+ hosts: testhost
+ gather_facts: false
+ tags: [ 'inroles' ]
+ roles:
+ - role: a
+ - role: b
+ - role: c
+
+- name: play should only show 1 invocation of a, as dependencies in this play are deduped even outside of roles
+ hosts: testhost
+ gather_facts: false
+ tags: [ 'acrossroles' ]
+ roles:
+ - role: a
+ - role: b
+ tasks:
+ - name: execute role c which depends on a
+ import_role: name=c
+
+- name: play should only show 1 invocation of a, as dependencies in this play are deduped by include_role
+ hosts: testhost
+ gather_facts: false
+ tags: [ 'intasks' ]
+ tasks:
+ - name: execute role b which depends on a
+ include_role: name=b
+ - name: execute role c which also depends on a
+ include_role: name=c
diff --git a/test/integration/targets/roles/no_outside.yml b/test/integration/targets/roles/no_outside.yml
new file mode 100644
index 0000000..cf6fe10
--- /dev/null
+++ b/test/integration/targets/roles/no_outside.yml
@@ -0,0 +1,7 @@
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - name: role attempts to load file from outside itself
+ include_role:
+ name: a
+ tasks_from: "{{ playbook_dir }}/tasks/dummy.yml"
diff --git a/test/integration/targets/roles/roles/a/tasks/main.yml b/test/integration/targets/roles/roles/a/tasks/main.yml
new file mode 100644
index 0000000..7fb1b48
--- /dev/null
+++ b/test/integration/targets/roles/roles/a/tasks/main.yml
@@ -0,0 +1 @@
+- debug: msg=A
diff --git a/test/integration/targets/roles/roles/b/meta/main.yml b/test/integration/targets/roles/roles/b/meta/main.yml
new file mode 100644
index 0000000..abe2dd4
--- /dev/null
+++ b/test/integration/targets/roles/roles/b/meta/main.yml
@@ -0,0 +1,4 @@
+dependencies:
+ - name: a
+
+argument_specs: {}
diff --git a/test/integration/targets/roles/roles/b/tasks/main.yml b/test/integration/targets/roles/roles/b/tasks/main.yml
new file mode 100644
index 0000000..57c1352
--- /dev/null
+++ b/test/integration/targets/roles/roles/b/tasks/main.yml
@@ -0,0 +1 @@
+- debug: msg=B
diff --git a/test/integration/targets/roles/roles/c/meta/main.yml b/test/integration/targets/roles/roles/c/meta/main.yml
new file mode 100644
index 0000000..04bd23b
--- /dev/null
+++ b/test/integration/targets/roles/roles/c/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - name: a
diff --git a/test/integration/targets/roles/roles/c/tasks/main.yml b/test/integration/targets/roles/roles/c/tasks/main.yml
new file mode 100644
index 0000000..190c429
--- /dev/null
+++ b/test/integration/targets/roles/roles/c/tasks/main.yml
@@ -0,0 +1 @@
+- debug: msg=C
diff --git a/test/integration/targets/roles/roles/data/defaults/main/00.yml b/test/integration/targets/roles/roles/data/defaults/main/00.yml
new file mode 100644
index 0000000..98c13a1
--- /dev/null
+++ b/test/integration/targets/roles/roles/data/defaults/main/00.yml
@@ -0,0 +1 @@
+defined_var: 1
diff --git a/test/integration/targets/roles/roles/data/defaults/main/01.yml b/test/integration/targets/roles/roles/data/defaults/main/01.yml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/roles/roles/data/defaults/main/01.yml
diff --git a/test/integration/targets/roles/roles/data/tasks/main.yml b/test/integration/targets/roles/roles/data/tasks/main.yml
new file mode 100644
index 0000000..8d85580
--- /dev/null
+++ b/test/integration/targets/roles/roles/data/tasks/main.yml
@@ -0,0 +1,5 @@
+- name: ensure data was correctly defind
+ assert:
+ that:
+ - defined_var is defined
+ - defined_var == 1
diff --git a/test/integration/targets/roles/runme.sh b/test/integration/targets/roles/runme.sh
new file mode 100755
index 0000000..bb98a93
--- /dev/null
+++ b/test/integration/targets/roles/runme.sh
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# test no dupes when dependencies in b and c point to a in roles:
+[ "$(ansible-playbook no_dupes.yml -i ../../inventory --tags inroles "$@" | grep -c '"msg": "A"')" = "1" ]
+[ "$(ansible-playbook no_dupes.yml -i ../../inventory --tags acrossroles "$@" | grep -c '"msg": "A"')" = "1" ]
+[ "$(ansible-playbook no_dupes.yml -i ../../inventory --tags intasks "$@" | grep -c '"msg": "A"')" = "1" ]
+
+# but still dupe across plays
+[ "$(ansible-playbook no_dupes.yml -i ../../inventory "$@" | grep -c '"msg": "A"')" = "3" ]
+
+# include/import can execute another instance of role
+[ "$(ansible-playbook allowed_dupes.yml -i ../../inventory --tags importrole "$@" | grep -c '"msg": "A"')" = "2" ]
+[ "$(ansible-playbook allowed_dupes.yml -i ../../inventory --tags includerole "$@" | grep -c '"msg": "A"')" = "2" ]
+
+
+# ensure role data is merged correctly
+ansible-playbook data_integrity.yml -i ../../inventory "$@"
+
+# ensure role fails when trying to load 'non role' in _from
+ansible-playbook no_outside.yml -i ../../inventory "$@" > role_outside_output.log 2>&1 || true
+if grep "as it is not inside the expected role path" role_outside_output.log >/dev/null; then
+ echo "Test passed (playbook failed with expected output, output not shown)."
+else
+ echo "Test failed, expected output from playbook failure is missing, output not shown)."
+ exit 1
+fi
diff --git a/test/integration/targets/roles/tasks/dummy.yml b/test/integration/targets/roles/tasks/dummy.yml
new file mode 100644
index 0000000..b168b7a
--- /dev/null
+++ b/test/integration/targets/roles/tasks/dummy.yml
@@ -0,0 +1 @@
+- debug: msg='this should not run'
diff --git a/test/integration/targets/roles_arg_spec/aliases b/test/integration/targets/roles_arg_spec/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/roles_arg_spec/collections/ansible_collections/foo/bar/MANIFEST.json b/test/integration/targets/roles_arg_spec/collections/ansible_collections/foo/bar/MANIFEST.json
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/collections/ansible_collections/foo/bar/MANIFEST.json
diff --git a/test/integration/targets/roles_arg_spec/collections/ansible_collections/foo/bar/roles/blah/meta/argument_specs.yml b/test/integration/targets/roles_arg_spec/collections/ansible_collections/foo/bar/roles/blah/meta/argument_specs.yml
new file mode 100644
index 0000000..3cb0a87
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/collections/ansible_collections/foo/bar/roles/blah/meta/argument_specs.yml
@@ -0,0 +1,8 @@
+argument_specs:
+ main:
+ short_description: "The foo.bar.blah role"
+ options:
+ blah_str:
+ type: "str"
+ required: true
+ description: "A string value"
diff --git a/test/integration/targets/roles_arg_spec/collections/ansible_collections/foo/bar/roles/blah/tasks/main.yml b/test/integration/targets/roles_arg_spec/collections/ansible_collections/foo/bar/roles/blah/tasks/main.yml
new file mode 100644
index 0000000..ecb4dac
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/collections/ansible_collections/foo/bar/roles/blah/tasks/main.yml
@@ -0,0 +1,3 @@
+- name: "First task of blah role"
+ debug:
+ msg: "The string is {{ blah_str }}"
diff --git a/test/integration/targets/roles_arg_spec/roles/a/meta/argument_specs.yml b/test/integration/targets/roles_arg_spec/roles/a/meta/argument_specs.yml
new file mode 100644
index 0000000..cfc1a37
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/roles/a/meta/argument_specs.yml
@@ -0,0 +1,17 @@
+argument_specs:
+ main:
+ short_description: Main entry point for role A.
+ options:
+ a_str:
+ type: "str"
+ required: true
+
+ alternate:
+ short_description: Alternate entry point for role A.
+ options:
+ a_int:
+ type: "int"
+ required: true
+
+ no_spec_entrypoint:
+ short_description: An entry point with no spec
diff --git a/test/integration/targets/roles_arg_spec/roles/a/meta/main.yml b/test/integration/targets/roles_arg_spec/roles/a/meta/main.yml
new file mode 100644
index 0000000..90920db
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/roles/a/meta/main.yml
@@ -0,0 +1,13 @@
+# This meta/main.yml exists to test that it is NOT read, with preference being
+# given to the meta/argument_specs.yml file. This spec contains an extra required
+# parameter, a_something, that argument_specs.yml does not.
+argument_specs:
+ main:
+ short_description: Main entry point for role A.
+ options:
+ a_str:
+ type: "str"
+ required: true
+ a_something:
+ type: "str"
+ required: true
diff --git a/test/integration/targets/roles_arg_spec/roles/a/tasks/alternate.yml b/test/integration/targets/roles_arg_spec/roles/a/tasks/alternate.yml
new file mode 100644
index 0000000..4d688be
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/roles/a/tasks/alternate.yml
@@ -0,0 +1,3 @@
+---
+- debug:
+ msg: "Role A (alternate) with {{ a_int }}"
diff --git a/test/integration/targets/roles_arg_spec/roles/a/tasks/main.yml b/test/integration/targets/roles_arg_spec/roles/a/tasks/main.yml
new file mode 100644
index 0000000..a74f37b
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/roles/a/tasks/main.yml
@@ -0,0 +1,3 @@
+---
+- debug:
+ msg: "Role A with {{ a_str }}"
diff --git a/test/integration/targets/roles_arg_spec/roles/a/tasks/no_spec_entrypoint.yml b/test/integration/targets/roles_arg_spec/roles/a/tasks/no_spec_entrypoint.yml
new file mode 100644
index 0000000..f1e600b
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/roles/a/tasks/no_spec_entrypoint.yml
@@ -0,0 +1,3 @@
+---
+- debug:
+ msg: "Role A no_spec_entrypoint"
diff --git a/test/integration/targets/roles_arg_spec/roles/b/meta/argument_specs.yaml b/test/integration/targets/roles_arg_spec/roles/b/meta/argument_specs.yaml
new file mode 100644
index 0000000..93663e9
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/roles/b/meta/argument_specs.yaml
@@ -0,0 +1,13 @@
+argument_specs:
+ main:
+ short_description: Main entry point for role B.
+ options:
+ b_str:
+ type: "str"
+ required: true
+ b_int:
+ type: "int"
+ required: true
+ b_bool:
+ type: "bool"
+ required: true
diff --git a/test/integration/targets/roles_arg_spec/roles/b/tasks/main.yml b/test/integration/targets/roles_arg_spec/roles/b/tasks/main.yml
new file mode 100644
index 0000000..b7e15cc
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/roles/b/tasks/main.yml
@@ -0,0 +1,9 @@
+---
+- debug:
+ msg: "Role B"
+- debug:
+ var: b_str
+- debug:
+ var: b_int
+- debug:
+ var: b_bool
diff --git a/test/integration/targets/roles_arg_spec/roles/c/meta/main.yml b/test/integration/targets/roles_arg_spec/roles/c/meta/main.yml
new file mode 100644
index 0000000..1a1ccbe
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/roles/c/meta/main.yml
@@ -0,0 +1,7 @@
+argument_specs:
+ main:
+ short_description: Main entry point for role C.
+ options:
+ c_int:
+ type: "int"
+ required: true
diff --git a/test/integration/targets/roles_arg_spec/roles/c/tasks/main.yml b/test/integration/targets/roles_arg_spec/roles/c/tasks/main.yml
new file mode 100644
index 0000000..78282be
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/roles/c/tasks/main.yml
@@ -0,0 +1,12 @@
+---
+- debug:
+ msg: "Role C that includes Role A with var {{ c_int }}"
+
+- name: "Role C import_role A with a_str {{ a_str }}"
+ import_role:
+ name: a
+
+- name: "Role C include_role A with a_int {{ a_int }}"
+ include_role:
+ name: a
+ tasks_from: "alternate"
diff --git a/test/integration/targets/roles_arg_spec/roles/empty_argspec/meta/argument_specs.yml b/test/integration/targets/roles_arg_spec/roles/empty_argspec/meta/argument_specs.yml
new file mode 100644
index 0000000..b592aa0
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/roles/empty_argspec/meta/argument_specs.yml
@@ -0,0 +1,2 @@
+---
+argument_specs:
diff --git a/test/integration/targets/roles_arg_spec/roles/empty_argspec/tasks/main.yml b/test/integration/targets/roles_arg_spec/roles/empty_argspec/tasks/main.yml
new file mode 100644
index 0000000..90aab0e
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/roles/empty_argspec/tasks/main.yml
@@ -0,0 +1,3 @@
+---
+- debug:
+ msg: "Role with empty argument_specs key"
diff --git a/test/integration/targets/roles_arg_spec/roles/empty_file/meta/argument_specs.yml b/test/integration/targets/roles_arg_spec/roles/empty_file/meta/argument_specs.yml
new file mode 100644
index 0000000..ed97d53
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/roles/empty_file/meta/argument_specs.yml
@@ -0,0 +1 @@
+---
diff --git a/test/integration/targets/roles_arg_spec/roles/empty_file/tasks/main.yml b/test/integration/targets/roles_arg_spec/roles/empty_file/tasks/main.yml
new file mode 100644
index 0000000..b77b835
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/roles/empty_file/tasks/main.yml
@@ -0,0 +1,3 @@
+---
+- debug:
+ msg: "Role with empty argument_specs.yml"
diff --git a/test/integration/targets/roles_arg_spec/roles/role_with_no_tasks/meta/argument_specs.yml b/test/integration/targets/roles_arg_spec/roles/role_with_no_tasks/meta/argument_specs.yml
new file mode 100644
index 0000000..55e4800
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/roles/role_with_no_tasks/meta/argument_specs.yml
@@ -0,0 +1,7 @@
+argument_specs:
+ main:
+ short_description: Main entry point for role role_with_no_tasks.
+ options:
+ a_str:
+ type: "str"
+ required: true
diff --git a/test/integration/targets/roles_arg_spec/roles/test1/defaults/main.yml b/test/integration/targets/roles_arg_spec/roles/test1/defaults/main.yml
new file mode 100644
index 0000000..5255f93
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/roles/test1/defaults/main.yml
@@ -0,0 +1,3 @@
+---
+# defaults file for test1
+test1_var1: 'THE_TEST1_VAR1_DEFAULT_VALUE'
diff --git a/test/integration/targets/roles_arg_spec/roles/test1/meta/argument_specs.yml b/test/integration/targets/roles_arg_spec/roles/test1/meta/argument_specs.yml
new file mode 100644
index 0000000..427946e
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/roles/test1/meta/argument_specs.yml
@@ -0,0 +1,112 @@
+---
+argument_specs:
+ main:
+ short_description: "EXPECTED FAILURE Validate the argument spec for the 'test1' role"
+ options:
+ test1_choices:
+ required: false
+ # required: true
+ choices:
+ - "this paddle game"
+ - "the astray"
+ - "this remote control"
+ - "the chair"
+ type: "str"
+ default: "this paddle game"
+ tidy_expected:
+ # required: false
+ # default: none
+ type: "list"
+ test1_var1:
+ # required: true
+ default: "THIS IS THE DEFAULT SURVEY ANSWER FOR test1_survey_test1_var1"
+ type: "str"
+ test1_var2:
+ required: false
+ default: "This IS THE DEFAULT fake band name / test1_var2 answer from survey_spec.yml"
+ type: "str"
+ bust_some_stuff:
+ # required: false
+ type: "int"
+ some_choices:
+ choices:
+ - "choice1"
+ - "choice2"
+ required: false
+ type: "str"
+ some_str:
+ type: "str"
+ some_list:
+ type: "list"
+ elements: "float"
+ some_dict:
+ type: "dict"
+ some_bool:
+ type: "bool"
+ some_int:
+ type: "int"
+ some_float:
+ type: "float"
+ some_path:
+ type: "path"
+ some_raw:
+ type: "raw"
+ some_jsonarg:
+ type: "jsonarg"
+ required: true
+ some_json:
+ type: "json"
+ required: true
+ some_bytes:
+ type: "bytes"
+ some_bits:
+ type: "bits"
+ some_str_aliases:
+ type: "str"
+ aliases:
+ - "some_str_nicknames"
+ - "some_str_akas"
+ - "some_str_formerly_known_as"
+ some_dict_options:
+ type: "dict"
+ options:
+ some_second_level:
+ type: "bool"
+ default: true
+ some_more_dict_options:
+ type: "dict"
+ options:
+ some_second_level:
+ type: "str"
+ some_str_removed_in:
+ type: "str"
+ removed_in: 2.10
+ some_tmp_path:
+ type: "path"
+ multi_level_option:
+ type: "dict"
+ options:
+ second_level:
+ type: "dict"
+ options:
+ third_level:
+ type: "int"
+ required: true
+
+ other:
+ short_description: "test1_simple_preset_arg_spec_other"
+ description: "A simpler set of required args for other tasks"
+ options:
+ test1_var1:
+ default: "This the default value for the other set of arg specs for test1 test1_var1"
+ type: "str"
+
+ test1_other:
+ description: "test1_other for role_that_includes_role"
+ options:
+ some_test1_other_arg:
+ default: "The some_test1_other_arg default value"
+ type: str
+ some_required_str:
+ type: str
+ required: true
diff --git a/test/integration/targets/roles_arg_spec/roles/test1/tasks/main.yml b/test/integration/targets/roles_arg_spec/roles/test1/tasks/main.yml
new file mode 100644
index 0000000..9ecf8b0
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/roles/test1/tasks/main.yml
@@ -0,0 +1,11 @@
+---
+# tasks file for test1
+- name: debug for task1 show test1_var1
+ debug:
+ var: test1_var1
+ tags: ["runme"]
+
+- name: debug for task1 show test1_var2
+ debug:
+ var: test1_var2
+ tags: ["runme"]
diff --git a/test/integration/targets/roles_arg_spec/roles/test1/tasks/other.yml b/test/integration/targets/roles_arg_spec/roles/test1/tasks/other.yml
new file mode 100644
index 0000000..b045813
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/roles/test1/tasks/other.yml
@@ -0,0 +1,11 @@
+---
+# "other" tasks file for test1
+- name: other tasks debug for task1 show test1_var1
+ debug:
+ var: test1_var1
+ tags: ["runme"]
+
+- name: other tasks debug for task1 show test1_var2
+ debug:
+ var: test1_var2
+ tags: ["runme"]
diff --git a/test/integration/targets/roles_arg_spec/roles/test1/tasks/test1_other.yml b/test/integration/targets/roles_arg_spec/roles/test1/tasks/test1_other.yml
new file mode 100644
index 0000000..8b1ec13
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/roles/test1/tasks/test1_other.yml
@@ -0,0 +1,11 @@
+---
+# "test1_other" tasks file for test1
+- name: "test1_other BLIPPY test1_other tasks debug for task1 show test1_var1"
+ debug:
+ var: test1_var1
+ tags: ["runme"]
+
+- name: "BLIPPY FOO test1_other tasks debug for task1 show test1_var2"
+ debug:
+ var: test1_var2
+ tags: ["runme"]
diff --git a/test/integration/targets/roles_arg_spec/roles/test1/vars/main.yml b/test/integration/targets/roles_arg_spec/roles/test1/vars/main.yml
new file mode 100644
index 0000000..3e72dd6
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/roles/test1/vars/main.yml
@@ -0,0 +1,4 @@
+---
+# vars file for test1
+test1_var1: 'THE_TEST1_VAR1_VARS_VALUE'
+test1_var2: 'THE_TEST1_VAR2_VARS_VALUE'
diff --git a/test/integration/targets/roles_arg_spec/roles/test1/vars/other.yml b/test/integration/targets/roles_arg_spec/roles/test1/vars/other.yml
new file mode 100644
index 0000000..a397bdc
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/roles/test1/vars/other.yml
@@ -0,0 +1,4 @@
+---
+# vars file for test1
+test1_var1: 'other_THE_TEST1_VAR1_VARS_VALUE'
+test1_var2: 'other_THE_TEST1_VAR2_VARS_VALUE'
diff --git a/test/integration/targets/roles_arg_spec/runme.sh b/test/integration/targets/roles_arg_spec/runme.sh
new file mode 100755
index 0000000..209a34e
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/runme.sh
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# This effectively disables junit callback output by directing the output to
+# a directory ansible-test will not look at.
+#
+# Since the failures in these tests are on the role arg spec validation and the
+# name for those tasks is fixed (we cannot add "EXPECTED FAILURE" to the name),
+# disabling the junit callback output is the easiest way to prevent these from
+# showing up in test run output.
+#
+# Longer term, an option can be added to the junit callback allowing a custom
+# regexp to be supplied rather than the hard coded "EXPECTED FAILURE".
+export JUNIT_OUTPUT_DIR="${OUTPUT_DIR}"
+
+# Various simple role scenarios
+ansible-playbook test.yml -i ../../inventory "$@"
+
+# More complex role test
+ansible-playbook test_complex_role_fails.yml -i ../../inventory "$@"
+
+# Test play level role will fail
+set +e
+ansible-playbook test_play_level_role_fails.yml -i ../../inventory "$@"
+test $? -ne 0
+set -e
+
+# Test the validation task is tagged with 'always' by specifying an unused tag.
+# The task is tagged with 'foo' but we use 'bar' in the call below and expect
+# the validation task to run anyway since it is tagged 'always'.
+ansible-playbook test_tags.yml -i ../../inventory "$@" --tags bar | grep "a : Validating arguments against arg spec 'main' - Main entry point for role A."
diff --git a/test/integration/targets/roles_arg_spec/test.yml b/test/integration/targets/roles_arg_spec/test.yml
new file mode 100644
index 0000000..5eca7c7
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/test.yml
@@ -0,0 +1,356 @@
+---
+- hosts: localhost
+ gather_facts: false
+ roles:
+ - { role: a, a_str: "roles" }
+
+ vars:
+ INT_VALUE: 42
+
+ tasks:
+
+ - name: "Valid simple role usage with include_role"
+ include_role:
+ name: a
+ vars:
+ a_str: "include_role"
+
+ - name: "Valid simple role usage with import_role"
+ import_role:
+ name: a
+ vars:
+ a_str: "import_role"
+
+ - name: "Valid role usage (more args)"
+ include_role:
+ name: b
+ vars:
+ b_str: "xyz"
+ b_int: 5
+ b_bool: true
+
+ - name: "Valid simple role usage with include_role of different entry point"
+ include_role:
+ name: a
+ tasks_from: "alternate"
+ vars:
+ a_int: 256
+
+ - name: "Valid simple role usage with import_role of different entry point"
+ import_role:
+ name: a
+ tasks_from: "alternate"
+ vars:
+ a_int: 512
+
+ - name: "Valid simple role usage with a templated value"
+ import_role:
+ name: a
+ vars:
+ a_int: "{{ INT_VALUE }}"
+
+ - name: "Call role entry point that is defined, but has no spec data"
+ import_role:
+ name: a
+ tasks_from: "no_spec_entrypoint"
+
+- name: "New play to reset vars: Test include_role fails"
+ hosts: localhost
+ gather_facts: false
+ vars:
+ expected_returned_spec:
+ b_bool:
+ required: true
+ type: "bool"
+ b_int:
+ required: true
+ type: "int"
+ b_str:
+ required: true
+ type: "str"
+
+ tasks:
+ - block:
+ - name: "Invalid role usage"
+ include_role:
+ name: b
+ vars:
+ b_bool: 7
+
+ - fail:
+ msg: "Should not get here"
+
+ rescue:
+ - debug:
+ var: ansible_failed_result
+
+ - name: "Validate failure"
+ assert:
+ that:
+ - ansible_failed_task.name == "Validating arguments against arg spec 'main' - Main entry point for role B."
+ - ansible_failed_result.argument_errors | length == 2
+ - "'missing required arguments: b_int, b_str' in ansible_failed_result.argument_errors"
+ - ansible_failed_result.validate_args_context.argument_spec_name == "main"
+ - ansible_failed_result.validate_args_context.name == "b"
+ - ansible_failed_result.validate_args_context.type == "role"
+ - "ansible_failed_result.validate_args_context.path is search('roles_arg_spec/roles/b')"
+ - ansible_failed_result.argument_spec_data == expected_returned_spec
+
+
+- name: "New play to reset vars: Test import_role fails"
+ hosts: localhost
+ gather_facts: false
+ vars:
+ expected_returned_spec:
+ b_bool:
+ required: true
+ type: "bool"
+ b_int:
+ required: true
+ type: "int"
+ b_str:
+ required: true
+ type: "str"
+
+ tasks:
+ - block:
+ - name: "Invalid role usage"
+ import_role:
+ name: b
+ vars:
+ b_bool: 7
+
+ - fail:
+ msg: "Should not get here"
+
+ rescue:
+ - debug:
+ var: ansible_failed_result
+
+ - name: "Validate failure"
+ assert:
+ that:
+ - ansible_failed_task.name == "Validating arguments against arg spec 'main' - Main entry point for role B."
+ - ansible_failed_result.argument_errors | length == 2
+ - "'missing required arguments: b_int, b_str' in ansible_failed_result.argument_errors"
+ - ansible_failed_result.validate_args_context.argument_spec_name == "main"
+ - ansible_failed_result.validate_args_context.name == "b"
+ - ansible_failed_result.validate_args_context.type == "role"
+ - "ansible_failed_result.validate_args_context.path is search('roles_arg_spec/roles/b')"
+ - ansible_failed_result.argument_spec_data == expected_returned_spec
+
+
+- name: "New play to reset vars: Test nested role including/importing role succeeds"
+ hosts: localhost
+ gather_facts: false
+ vars:
+ c_int: 1
+ a_str: "some string"
+ a_int: 42
+ tasks:
+ - name: "Test import_role of role C"
+ import_role:
+ name: c
+
+ - name: "Test include_role of role C"
+ include_role:
+ name: c
+
+
+- name: "New play to reset vars: Test nested role including/importing role fails"
+ hosts: localhost
+ gather_facts: false
+ vars:
+ main_expected_returned_spec:
+ a_str:
+ required: true
+ type: "str"
+ alternate_expected_returned_spec:
+ a_int:
+ required: true
+ type: "int"
+
+ tasks:
+ - block:
+ - name: "Test import_role of role C (missing a_str)"
+ import_role:
+ name: c
+ vars:
+ c_int: 100
+
+ - fail:
+ msg: "Should not get here"
+
+ rescue:
+ - debug:
+ var: ansible_failed_result
+ - name: "Validate import_role failure"
+ assert:
+ that:
+ # NOTE: a bug here that prevents us from getting ansible_failed_task
+ - ansible_failed_result.argument_errors | length == 1
+ - "'missing required arguments: a_str' in ansible_failed_result.argument_errors"
+ - ansible_failed_result.validate_args_context.argument_spec_name == "main"
+ - ansible_failed_result.validate_args_context.name == "a"
+ - ansible_failed_result.validate_args_context.type == "role"
+ - "ansible_failed_result.validate_args_context.path is search('roles_arg_spec/roles/a')"
+ - ansible_failed_result.argument_spec_data == main_expected_returned_spec
+
+ - block:
+ - name: "Test include_role of role C (missing a_int from `alternate` entry point)"
+ include_role:
+ name: c
+ vars:
+ c_int: 200
+ a_str: "some string"
+
+ - fail:
+ msg: "Should not get here"
+
+ rescue:
+ - debug:
+ var: ansible_failed_result
+ - name: "Validate include_role failure"
+ assert:
+ that:
+ # NOTE: a bug here that prevents us from getting ansible_failed_task
+ - ansible_failed_result.argument_errors | length == 1
+ - "'missing required arguments: a_int' in ansible_failed_result.argument_errors"
+ - ansible_failed_result.validate_args_context.argument_spec_name == "alternate"
+ - ansible_failed_result.validate_args_context.name == "a"
+ - ansible_failed_result.validate_args_context.type == "role"
+ - "ansible_failed_result.validate_args_context.path is search('roles_arg_spec/roles/a')"
+ - ansible_failed_result.argument_spec_data == alternate_expected_returned_spec
+
+- name: "New play to reset vars: Test role with no tasks can fail"
+ hosts: localhost
+ gather_facts: false
+ tasks:
+ - block:
+ - name: "Test import_role of role role_with_no_tasks (missing a_str)"
+ import_role:
+ name: role_with_no_tasks
+
+ - fail:
+ msg: "Should not get here"
+
+ rescue:
+ - debug:
+ var: ansible_failed_result
+ - name: "Validate import_role failure"
+ assert:
+ that:
+ # NOTE: a bug here that prevents us from getting ansible_failed_task
+ - ansible_failed_result.argument_errors | length == 1
+ - "'missing required arguments: a_str' in ansible_failed_result.argument_errors"
+ - ansible_failed_result.validate_args_context.argument_spec_name == "main"
+ - ansible_failed_result.validate_args_context.name == "role_with_no_tasks"
+ - ansible_failed_result.validate_args_context.type == "role"
+ - "ansible_failed_result.validate_args_context.path is search('roles_arg_spec/roles/role_with_no_tasks')"
+
+- name: "New play to reset vars: Test disabling role validation with rolespec_validate=False"
+ hosts: localhost
+ gather_facts: false
+ tasks:
+ - block:
+ - name: "Test import_role of role C (missing a_str), but validation turned off"
+ import_role:
+ name: c
+ rolespec_validate: False
+ - fail:
+ msg: "Should not get here"
+
+ rescue:
+ - debug:
+ var: ansible_failed_result
+ - name: "Validate import_role failure"
+ assert:
+ that:
+ # We expect the role to actually run, but will fail because an undefined variable was referenced
+ # and validation wasn't performed up front (thus not returning 'argument_errors').
+ - "'argument_errors' not in ansible_failed_result"
+ - "'The task includes an option with an undefined variable.' in ansible_failed_result.msg"
+
+- name: "New play to reset vars: Test collection-based role"
+ hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: "Valid collection-based role usage"
+ import_role:
+ name: "foo.bar.blah"
+ vars:
+ blah_str: "some string"
+
+
+- name: "New play to reset vars: Test collection-based role will fail"
+ hosts: localhost
+ gather_facts: false
+ tasks:
+ - block:
+ - name: "Invalid collection-based role usage"
+ import_role:
+ name: "foo.bar.blah"
+ - fail:
+ msg: "Should not get here"
+ rescue:
+ - debug: var=ansible_failed_result
+ - name: "Validate import_role failure for collection-based role"
+ assert:
+ that:
+ - ansible_failed_result.argument_errors | length == 1
+ - "'missing required arguments: blah_str' in ansible_failed_result.argument_errors"
+ - ansible_failed_result.validate_args_context.argument_spec_name == "main"
+ - ansible_failed_result.validate_args_context.name == "blah"
+ - ansible_failed_result.validate_args_context.type == "role"
+ - "ansible_failed_result.validate_args_context.path is search('roles_arg_spec/collections/ansible_collections/foo/bar/roles/blah')"
+
+- name: "New play to reset vars: Test templating succeeds"
+ hosts: localhost
+ gather_facts: false
+ vars:
+ value_some_choices: "choice2"
+ value_some_list: [1.5]
+ value_some_dict: {"some_key": "some_value"}
+ value_some_int: 1
+ value_some_float: 1.5
+ value_some_json: '{[1, 3, 3] 345345|45v<#!}'
+ value_some_jsonarg: {"foo": [1, 3, 3]}
+ value_some_second_level: True
+ value_third_level: 5
+ tasks:
+ - block:
+ - include_role:
+ name: test1
+ vars:
+ some_choices: "{{ value_some_choices }}"
+ some_list: "{{ value_some_list }}"
+ some_dict: "{{ value_some_dict }}"
+ some_int: "{{ value_some_int }}"
+ some_float: "{{ value_some_float }}"
+ some_json: "{{ value_some_json }}"
+ some_jsonarg: "{{ value_some_jsonarg }}"
+ some_dict_options:
+ some_second_level: "{{ value_some_second_level }}"
+ multi_level_option:
+ second_level:
+ third_level: "{{ value_third_level }}"
+ rescue:
+ - debug: var=ansible_failed_result
+ - fail:
+ msg: "Should not get here"
+
+- name: "New play to reset vars: Test empty argument_specs.yml"
+ hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: Import role with an empty argument_specs.yml
+ import_role:
+ name: empty_file
+
+- name: "New play to reset vars: Test empty argument_specs key"
+ hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: Import role with an empty argument_specs key
+ import_role:
+ name: empty_argspec
diff --git a/test/integration/targets/roles_arg_spec/test_complex_role_fails.yml b/test/integration/targets/roles_arg_spec/test_complex_role_fails.yml
new file mode 100644
index 0000000..81abdaa
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/test_complex_role_fails.yml
@@ -0,0 +1,197 @@
+---
+- name: "Running include_role test1"
+ hosts: localhost
+ gather_facts: false
+ vars:
+ ansible_unicode_type_match: "<type 'ansible.parsing.yaml.objects.AnsibleUnicode'>"
+ unicode_type_match: "<type 'unicode'>"
+ string_type_match: "<type 'str'>"
+ float_type_match: "<type 'float'>"
+ list_type_match: "<type 'list'>"
+ ansible_list_type_match: "<type 'ansible.parsing.yaml.objects.AnsibleSequence'>"
+ dict_type_match: "<type 'dict'>"
+ ansible_dict_type_match: "<type 'ansible.parsing.yaml.objects.AnsibleMapping'>"
+ ansible_unicode_class_match: "<class 'ansible.parsing.yaml.objects.AnsibleUnicode'>"
+ unicode_class_match: "<class 'unicode'>"
+ string_class_match: "<class 'str'>"
+ bytes_class_match: "<class 'bytes'>"
+ float_class_match: "<class 'float'>"
+ list_class_match: "<class 'list'>"
+ ansible_list_class_match: "<class 'ansible.parsing.yaml.objects.AnsibleSequence'>"
+ dict_class_match: "<class 'dict'>"
+ ansible_dict_class_match: "<class 'ansible.parsing.yaml.objects.AnsibleMapping'>"
+ expected:
+ test1_1:
+ argument_errors: [
+ "argument 'tidy_expected' is of type <class 'ansible.parsing.yaml.objects.AnsibleMapping'> and we were unable to convert to list: <class 'ansible.parsing.yaml.objects.AnsibleMapping'> cannot be converted to a list",
+ "argument 'bust_some_stuff' is of type <class 'str'> and we were unable to convert to int: <class 'str'> cannot be converted to an int",
+ "argument 'some_list' is of type <class 'ansible.parsing.yaml.objects.AnsibleMapping'> and we were unable to convert to list: <class 'ansible.parsing.yaml.objects.AnsibleMapping'> cannot be converted to a list",
+ "argument 'some_dict' is of type <class 'ansible.parsing.yaml.objects.AnsibleSequence'> and we were unable to convert to dict: <class 'ansible.parsing.yaml.objects.AnsibleSequence'> cannot be converted to a dict",
+ "argument 'some_int' is of type <class 'float'> and we were unable to convert to int: <class 'float'> cannot be converted to an int",
+ "argument 'some_float' is of type <class 'str'> and we were unable to convert to float: <class 'str'> cannot be converted to a float",
+ "argument 'some_bytes' is of type <class 'bytes'> and we were unable to convert to bytes: <class 'bytes'> cannot be converted to a Byte value",
+ "argument 'some_bits' is of type <class 'str'> and we were unable to convert to bits: <class 'str'> cannot be converted to a Bit value",
+ "value of test1_choices must be one of: this paddle game, the astray, this remote control, the chair, got: My dog",
+ "value of some_choices must be one of: choice1, choice2, got: choice4",
+ "argument 'some_second_level' is of type <class 'ansible.parsing.yaml.objects.AnsibleUnicode'> found in 'some_dict_options'. and we were unable to convert to bool: The value 'not-a-bool' is not a valid boolean. ",
+ "argument 'third_level' is of type <class 'ansible.parsing.yaml.objects.AnsibleUnicode'> found in 'multi_level_option -> second_level'. and we were unable to convert to int: <class 'ansible.parsing.yaml.objects.AnsibleUnicode'> cannot be converted to an int",
+ "argument 'some_more_dict_options' is of type <class 'ansible.parsing.yaml.objects.AnsibleUnicode'> and we were unable to convert to dict: dictionary requested, could not parse JSON or key=value",
+ "value of 'some_more_dict_options' must be of type dict or list of dicts",
+ "dictionary requested, could not parse JSON or key=value",
+ ]
+
+ tasks:
+ - name: include_role test1 since it has a arg_spec.yml
+ block:
+ - include_role:
+ name: test1
+ vars:
+ tidy_expected:
+ some_key: some_value
+ test1_var1: 37.4
+ test1_choices: "My dog"
+ bust_some_stuff: "some_string_that_is_not_an_int"
+ some_choices: "choice4"
+ some_str: 37.5
+ some_list: {'a': false}
+ some_dict:
+ - "foo"
+ - "bar"
+ some_int: 37.
+ some_float: "notafloatisit"
+ some_path: "anything_is_a_valid_path"
+ some_raw: {"anything_can_be": "a_raw_type"}
+ # not sure what would be an invalid jsonarg
+ # some_jsonarg: "not sure what this does yet"
+ some_json: |
+ '{[1, 3, 3] 345345|45v<#!}'
+ some_jsonarg: |
+ {"foo": [1, 3, 3]}
+ # not sure we can load binary in safe_load
+ some_bytes: !!binary |
+ R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5
+ OTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/+
+ +f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLC
+ AgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs=
+ some_bits: "foo"
+ # some_str_nicknames: []
+ # some_str_akas: {}
+ some_str_removed_in: "foo"
+ some_dict_options:
+ some_second_level: "not-a-bool"
+ some_more_dict_options: "not-a-dict"
+ multi_level_option:
+ second_level:
+ third_level: "should_be_int"
+
+ - fail:
+ msg: "Should not get here"
+
+ rescue:
+ - debug:
+ var: ansible_failed_result
+
+ - name: replace py version specific types with generic names so tests work on py2 and py3
+ set_fact:
+ # We want to compare if the actual failure messages and the expected failure messages
+ # are the same. But to compare and do set differences, we have to handle some
+ # differences between py2/py3.
+ # The validation failure messages include python type and class reprs, which are
+ # different between py2 and py3. For ex, "<type 'str'>" vs "<class 'str'>". Plus
+ # the usual py2/py3 unicode/str/bytes type shenanigans. The 'THE_FLOAT_REPR' is
+ # because py3 quotes the value in the error while py2 does not, so we just ignore
+ # the rest of the line.
+ actual_generic: "{{ ansible_failed_result.argument_errors|
+ map('replace', ansible_unicode_type_match, 'STR')|
+ map('replace', unicode_type_match, 'STR')|
+ map('replace', string_type_match, 'STR')|
+ map('replace', float_type_match, 'FLOAT')|
+ map('replace', list_type_match, 'LIST')|
+ map('replace', ansible_list_type_match, 'LIST')|
+ map('replace', dict_type_match, 'DICT')|
+ map('replace', ansible_dict_type_match, 'DICT')|
+ map('replace', ansible_unicode_class_match, 'STR')|
+ map('replace', unicode_class_match, 'STR')|
+ map('replace', string_class_match, 'STR')|
+ map('replace', bytes_class_match, 'STR')|
+ map('replace', float_class_match, 'FLOAT')|
+ map('replace', list_class_match, 'LIST')|
+ map('replace', ansible_list_class_match, 'LIST')|
+ map('replace', dict_class_match, 'DICT')|
+ map('replace', ansible_dict_class_match, 'DICT')|
+ map('regex_replace', '''float:.*$''', 'THE_FLOAT_REPR')|
+ map('regex_replace', 'Valid booleans include.*$', '')|
+ list }}"
+ expected_generic: "{{ expected.test1_1.argument_errors|
+ map('replace', ansible_unicode_type_match, 'STR')|
+ map('replace', unicode_type_match, 'STR')|
+ map('replace', string_type_match, 'STR')|
+ map('replace', float_type_match, 'FLOAT')|
+ map('replace', list_type_match, 'LIST')|
+ map('replace', ansible_list_type_match, 'LIST')|
+ map('replace', dict_type_match, 'DICT')|
+ map('replace', ansible_dict_type_match, 'DICT')|
+ map('replace', ansible_unicode_class_match, 'STR')|
+ map('replace', unicode_class_match, 'STR')|
+ map('replace', string_class_match, 'STR')|
+ map('replace', bytes_class_match, 'STR')|
+ map('replace', float_class_match, 'FLOAT')|
+ map('replace', list_class_match, 'LIST')|
+ map('replace', ansible_list_class_match, 'LIST')|
+ map('replace', dict_class_match, 'DICT')|
+ map('replace', ansible_dict_class_match, 'DICT')|
+ map('regex_replace', '''float:.*$''', 'THE_FLOAT_REPR')|
+ map('regex_replace', 'Valid booleans include.*$', '')|
+ list }}"
+
+ - name: figure out the difference between expected and actual validate_argument_spec failures
+ set_fact:
+ actual_not_in_expected: "{{ actual_generic| difference(expected_generic) | sort() }}"
+ expected_not_in_actual: "{{ expected_generic | difference(actual_generic) | sort() }}"
+
+ - name: assert that all actual validate_argument_spec failures were in expected
+ assert:
+ that:
+ - actual_not_in_expected | length == 0
+ msg: "Actual validate_argument_spec failures that were not expected: {{ actual_not_in_expected }}"
+
+ - name: assert that all expected validate_argument_spec failures were in expected
+ assert:
+ that:
+ - expected_not_in_actual | length == 0
+ msg: "Expected validate_argument_spec failures that were not in actual results: {{ expected_not_in_actual }}"
+
+ - name: assert that `validate_args_context` return value has what we expect
+ assert:
+ that:
+ - ansible_failed_result.validate_args_context.argument_spec_name == "main"
+ - ansible_failed_result.validate_args_context.name == "test1"
+ - ansible_failed_result.validate_args_context.type == "role"
+ - "ansible_failed_result.validate_args_context.path is search('roles_arg_spec/roles/test1')"
+
+ - name: test message for missing required parameters and invalid suboptions
+ block:
+ - include_role:
+ name: test1
+ vars:
+ some_json: '{}'
+ some_jsonarg: '{}'
+ multi_level_option:
+ second_level:
+ not_a_supported_suboption: true
+
+ - fail:
+ msg: "Should not get here"
+
+ rescue:
+ - debug:
+ var: ansible_failed_result
+
+ - assert:
+ that:
+ - ansible_failed_result.argument_errors | length == 2
+ - missing_required in ansible_failed_result.argument_errors
+ - got_unexpected in ansible_failed_result.argument_errors
+ vars:
+ missing_required: "missing required arguments: third_level found in multi_level_option -> second_level"
+ got_unexpected: "multi_level_option.second_level.not_a_supported_suboption. Supported parameters include: third_level."
diff --git a/test/integration/targets/roles_arg_spec/test_play_level_role_fails.yml b/test/integration/targets/roles_arg_spec/test_play_level_role_fails.yml
new file mode 100644
index 0000000..6c79569
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/test_play_level_role_fails.yml
@@ -0,0 +1,5 @@
+---
+- hosts: localhost
+ gather_facts: false
+ roles:
+ - { role: a, invalid_str: "roles" }
diff --git a/test/integration/targets/roles_arg_spec/test_tags.yml b/test/integration/targets/roles_arg_spec/test_tags.yml
new file mode 100644
index 0000000..b4ea188
--- /dev/null
+++ b/test/integration/targets/roles_arg_spec/test_tags.yml
@@ -0,0 +1,11 @@
+---
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: "Tag test #1"
+ import_role:
+ name: a
+ vars:
+ a_str: "tag test #1"
+ tags:
+ - foo
diff --git a/test/integration/targets/roles_var_inheritance/aliases b/test/integration/targets/roles_var_inheritance/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/roles_var_inheritance/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/roles_var_inheritance/play.yml b/test/integration/targets/roles_var_inheritance/play.yml
new file mode 100644
index 0000000..170eef5
--- /dev/null
+++ b/test/integration/targets/roles_var_inheritance/play.yml
@@ -0,0 +1,4 @@
+- hosts: localhost
+ roles:
+ - A
+ - B
diff --git a/test/integration/targets/roles_var_inheritance/roles/A/meta/main.yml b/test/integration/targets/roles_var_inheritance/roles/A/meta/main.yml
new file mode 100644
index 0000000..0e99e98
--- /dev/null
+++ b/test/integration/targets/roles_var_inheritance/roles/A/meta/main.yml
@@ -0,0 +1,4 @@
+dependencies:
+ - role: common_dep
+ vars:
+ test_var: A
diff --git a/test/integration/targets/roles_var_inheritance/roles/B/meta/main.yml b/test/integration/targets/roles_var_inheritance/roles/B/meta/main.yml
new file mode 100644
index 0000000..4da1740
--- /dev/null
+++ b/test/integration/targets/roles_var_inheritance/roles/B/meta/main.yml
@@ -0,0 +1,4 @@
+dependencies:
+ - role: common_dep
+ vars:
+ test_var: B
diff --git a/test/integration/targets/roles_var_inheritance/roles/child_nested_dep/vars/main.yml b/test/integration/targets/roles_var_inheritance/roles/child_nested_dep/vars/main.yml
new file mode 100644
index 0000000..6723fa0
--- /dev/null
+++ b/test/integration/targets/roles_var_inheritance/roles/child_nested_dep/vars/main.yml
@@ -0,0 +1 @@
+var_precedence: dependency
diff --git a/test/integration/targets/roles_var_inheritance/roles/common_dep/meta/main.yml b/test/integration/targets/roles_var_inheritance/roles/common_dep/meta/main.yml
new file mode 100644
index 0000000..1ede7be
--- /dev/null
+++ b/test/integration/targets/roles_var_inheritance/roles/common_dep/meta/main.yml
@@ -0,0 +1,4 @@
+dependencies:
+ - role: nested_dep
+ vars:
+ nested_var: "{{ test_var }}"
diff --git a/test/integration/targets/roles_var_inheritance/roles/common_dep/vars/main.yml b/test/integration/targets/roles_var_inheritance/roles/common_dep/vars/main.yml
new file mode 100644
index 0000000..87b6b58
--- /dev/null
+++ b/test/integration/targets/roles_var_inheritance/roles/common_dep/vars/main.yml
@@ -0,0 +1 @@
+var_precedence: parent
diff --git a/test/integration/targets/roles_var_inheritance/roles/nested_dep/meta/main.yml b/test/integration/targets/roles_var_inheritance/roles/nested_dep/meta/main.yml
new file mode 100644
index 0000000..231c6c1
--- /dev/null
+++ b/test/integration/targets/roles_var_inheritance/roles/nested_dep/meta/main.yml
@@ -0,0 +1,3 @@
+allow_duplicates: yes
+dependencies:
+ - child_nested_dep
diff --git a/test/integration/targets/roles_var_inheritance/roles/nested_dep/tasks/main.yml b/test/integration/targets/roles_var_inheritance/roles/nested_dep/tasks/main.yml
new file mode 100644
index 0000000..c69070c
--- /dev/null
+++ b/test/integration/targets/roles_var_inheritance/roles/nested_dep/tasks/main.yml
@@ -0,0 +1,5 @@
+- debug:
+ var: nested_var
+
+- debug:
+ var: var_precedence
diff --git a/test/integration/targets/roles_var_inheritance/runme.sh b/test/integration/targets/roles_var_inheritance/runme.sh
new file mode 100755
index 0000000..791155a
--- /dev/null
+++ b/test/integration/targets/roles_var_inheritance/runme.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook -i ../../inventory play.yml "$@" | tee out.txt
+
+test "$(grep out.txt -ce '"nested_var": "A"')" == 1
+test "$(grep out.txt -ce '"nested_var": "B"')" == 1
+test "$(grep out.txt -ce '"var_precedence": "dependency"')" == 2
diff --git a/test/integration/targets/rpm_key/aliases b/test/integration/targets/rpm_key/aliases
new file mode 100644
index 0000000..a4c92ef
--- /dev/null
+++ b/test/integration/targets/rpm_key/aliases
@@ -0,0 +1,2 @@
+destructive
+shippable/posix/group1
diff --git a/test/integration/targets/rpm_key/defaults/main.yaml b/test/integration/targets/rpm_key/defaults/main.yaml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/rpm_key/defaults/main.yaml
diff --git a/test/integration/targets/rpm_key/meta/main.yml b/test/integration/targets/rpm_key/meta/main.yml
new file mode 100644
index 0000000..1810d4b
--- /dev/null
+++ b/test/integration/targets/rpm_key/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/rpm_key/tasks/main.yaml b/test/integration/targets/rpm_key/tasks/main.yaml
new file mode 100644
index 0000000..6f71ca6
--- /dev/null
+++ b/test/integration/targets/rpm_key/tasks/main.yaml
@@ -0,0 +1,2 @@
+ - include_tasks: 'rpm_key.yaml'
+ when: ansible_os_family == "RedHat"
diff --git a/test/integration/targets/rpm_key/tasks/rpm_key.yaml b/test/integration/targets/rpm_key/tasks/rpm_key.yaml
new file mode 100644
index 0000000..89ed236
--- /dev/null
+++ b/test/integration/targets/rpm_key/tasks/rpm_key.yaml
@@ -0,0 +1,180 @@
+---
+#
+# Save initial state
+#
+- name: Retrieve a list of gpg keys are installed for package checking
+ shell: 'rpm -q gpg-pubkey | sort'
+ register: list_of_pubkeys
+
+- name: Retrieve the gpg keys used to verify packages
+ command: 'rpm -q --qf %{description} gpg-pubkey'
+ register: pubkeys
+
+- name: Save gpg keys to a file
+ copy:
+ content: "{{ pubkeys['stdout'] }}\n"
+ dest: '{{ remote_tmp_dir }}/pubkeys'
+ mode: 0600
+
+#
+# Tests start
+#
+- name: download EPEL GPG key
+ get_url:
+ url: https://ci-files.testing.ansible.com/test/integration/targets/rpm_key/RPM-GPG-KEY-EPEL-7
+ dest: /tmp/RPM-GPG-KEY-EPEL-7
+
+- name: download sl rpm
+ get_url:
+ url: https://ci-files.testing.ansible.com/test/integration/targets/rpm_key/sl-5.02-1.el7.x86_64.rpm
+ dest: /tmp/sl.rpm
+
+- name: remove EPEL GPG key from keyring
+ rpm_key:
+ state: absent
+ key: /tmp/RPM-GPG-KEY-EPEL-7
+
+- name: check GPG signature of sl. Should fail
+ shell: "rpm --checksig /tmp/sl.rpm"
+ register: sl_check
+ ignore_errors: yes
+
+- name: confirm that signature check failed
+ assert:
+ that:
+ - "'MISSING KEYS' in sl_check.stdout or 'SIGNATURES NOT OK' in sl_check.stdout"
+ - "sl_check.failed"
+
+- name: remove EPEL GPG key from keyring (idempotent)
+ rpm_key:
+ state: absent
+ key: /tmp/RPM-GPG-KEY-EPEL-7
+ register: idempotent_test
+
+- name: check idempontence
+ assert:
+ that: "not idempotent_test.changed"
+
+- name: add EPEL GPG key to key ring
+ rpm_key:
+ state: present
+ key: /tmp/RPM-GPG-KEY-EPEL-7
+
+- name: add EPEL GPG key to key ring (idempotent)
+ rpm_key:
+ state: present
+ key: /tmp/RPM-GPG-KEY-EPEL-7
+ register: key_idempotence
+
+- name: verify idempotence
+ assert:
+ that: "not key_idempotence.changed"
+
+- name: check GPG signature of sl. Should return okay
+ shell: "rpm --checksig /tmp/sl.rpm"
+ register: sl_check
+
+- name: confirm that signature check succeeded
+ assert:
+ that: "'rsa sha1 (md5) pgp md5 OK' in sl_check.stdout or 'digests signatures OK' in sl_check.stdout"
+
+- name: remove GPG key from url
+ rpm_key:
+ state: absent
+ key: https://ci-files.testing.ansible.com/test/integration/targets/rpm_key/RPM-GPG-KEY-EPEL-7
+
+- name: Confirm key is missing
+ shell: "rpm --checksig /tmp/sl.rpm"
+ register: sl_check
+ ignore_errors: yes
+
+- name: confirm that signature check failed
+ assert:
+ that:
+ - "'MISSING KEYS' in sl_check.stdout or 'SIGNATURES NOT OK' in sl_check.stdout"
+ - "sl_check.failed"
+
+- name: add GPG key from url
+ rpm_key:
+ state: present
+ key: https://ci-files.testing.ansible.com/test/integration/targets/rpm_key/RPM-GPG-KEY-EPEL-7
+
+- name: check GPG signature of sl. Should return okay
+ shell: "rpm --checksig /tmp/sl.rpm"
+ register: sl_check
+
+- name: confirm that signature check succeeded
+ assert:
+ that: "'rsa sha1 (md5) pgp md5 OK' in sl_check.stdout or 'digests signatures OK' in sl_check.stdout"
+
+- name: remove all keys from key ring
+ shell: "rpm -q gpg-pubkey | xargs rpm -e"
+
+- name: add very first key on system
+ rpm_key:
+ state: present
+ key: https://ci-files.testing.ansible.com/test/integration/targets/rpm_key/RPM-GPG-KEY-EPEL-7
+
+- name: check GPG signature of sl. Should return okay
+ shell: "rpm --checksig /tmp/sl.rpm"
+ register: sl_check
+
+- name: confirm that signature check succeeded
+ assert:
+ that: "'rsa sha1 (md5) pgp md5 OK' in sl_check.stdout or 'digests signatures OK' in sl_check.stdout"
+
+- name: Issue 20325 - Verify fingerprint of key, invalid fingerprint - EXPECTED FAILURE
+ rpm_key:
+ key: https://ci-files.testing.ansible.com/test/integration/targets/rpm_key/RPM-GPG-KEY.dag
+ fingerprint: 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111
+ register: result
+ failed_when: result is success
+
+- name: Issue 20325 - Assert Verify fingerprint of key, invalid fingerprint
+ assert:
+ that:
+ - result is success
+ - result is not changed
+ - "'does not match the key fingerprint' in result.msg"
+
+- name: Issue 20325 - Verify fingerprint of key, valid fingerprint
+ rpm_key:
+ key: https://ci-files.testing.ansible.com/test/integration/targets/rpm_key/RPM-GPG-KEY.dag
+ fingerprint: EBC6 E12C 62B1 C734 026B 2122 A20E 5214 6B8D 79E6
+ register: result
+
+- name: Issue 20325 - Assert Verify fingerprint of key, valid fingerprint
+ assert:
+ that:
+ - result is success
+ - result is changed
+
+- name: Issue 20325 - Verify fingerprint of key, valid fingerprint - Idempotent check
+ rpm_key:
+ key: https://ci-files.testing.ansible.com/test/integration/targets/rpm_key/RPM-GPG-KEY.dag
+ fingerprint: EBC6 E12C 62B1 C734 026B 2122 A20E 5214 6B8D 79E6
+ register: result
+
+- name: Issue 20325 - Assert Verify fingerprint of key, valid fingerprint - Idempotent check
+ assert:
+ that:
+ - result is success
+ - result is not changed
+
+#
+# Cleanup
+#
+- name: remove all keys from key ring
+ shell: "rpm -q gpg-pubkey | xargs rpm -e"
+
+- name: Restore the gpg keys normally installed on the system
+ command: 'rpm --import {{ remote_tmp_dir }}/pubkeys'
+
+- name: Retrieve a list of gpg keys are installed for package checking
+ shell: 'rpm -q gpg-pubkey | sort'
+ register: new_list_of_pubkeys
+
+- name: Confirm that we've restored all the pubkeys
+ assert:
+ that:
+ - 'list_of_pubkeys["stdout"] == new_list_of_pubkeys["stdout"]'
diff --git a/test/integration/targets/run_modules/aliases b/test/integration/targets/run_modules/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/run_modules/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/run_modules/args.json b/test/integration/targets/run_modules/args.json
new file mode 100644
index 0000000..c3abc21
--- /dev/null
+++ b/test/integration/targets/run_modules/args.json
@@ -0,0 +1 @@
+{ "ANSIBLE_MODULE_ARGS": {} }
diff --git a/test/integration/targets/run_modules/library/test.py b/test/integration/targets/run_modules/library/test.py
new file mode 100644
index 0000000..15a92e9
--- /dev/null
+++ b/test/integration/targets/run_modules/library/test.py
@@ -0,0 +1,10 @@
+#!/usr/bin/python
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+
+module = AnsibleModule(argument_spec=dict())
+
+module.exit_json(**{'tempdir': module._remote_tmp})
diff --git a/test/integration/targets/run_modules/runme.sh b/test/integration/targets/run_modules/runme.sh
new file mode 100755
index 0000000..34c245c
--- /dev/null
+++ b/test/integration/targets/run_modules/runme.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# test running module directly
+python.py library/test.py args.json
diff --git a/test/integration/targets/script/aliases b/test/integration/targets/script/aliases
new file mode 100644
index 0000000..765b70d
--- /dev/null
+++ b/test/integration/targets/script/aliases
@@ -0,0 +1 @@
+shippable/posix/group2
diff --git a/test/integration/targets/script/files/create_afile.sh b/test/integration/targets/script/files/create_afile.sh
new file mode 100755
index 0000000..e6fae44
--- /dev/null
+++ b/test/integration/targets/script/files/create_afile.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+echo "win" > "$1" \ No newline at end of file
diff --git a/test/integration/targets/script/files/no_shebang.py b/test/integration/targets/script/files/no_shebang.py
new file mode 100644
index 0000000..f2d386a
--- /dev/null
+++ b/test/integration/targets/script/files/no_shebang.py
@@ -0,0 +1,6 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import sys
+
+sys.stdout.write("Script with shebang omitted")
diff --git a/test/integration/targets/script/files/remove_afile.sh b/test/integration/targets/script/files/remove_afile.sh
new file mode 100755
index 0000000..4a7fea6
--- /dev/null
+++ b/test/integration/targets/script/files/remove_afile.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+rm "$1" \ No newline at end of file
diff --git a/test/integration/targets/script/files/space path/test.sh b/test/integration/targets/script/files/space path/test.sh
new file mode 100755
index 0000000..6f6334d
--- /dev/null
+++ b/test/integration/targets/script/files/space path/test.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+echo -n "Script with space in path" \ No newline at end of file
diff --git a/test/integration/targets/script/files/test.sh b/test/integration/targets/script/files/test.sh
new file mode 100755
index 0000000..ade17e9
--- /dev/null
+++ b/test/integration/targets/script/files/test.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+echo -n "win" \ No newline at end of file
diff --git a/test/integration/targets/script/files/test_with_args.sh b/test/integration/targets/script/files/test_with_args.sh
new file mode 100755
index 0000000..13dce4f
--- /dev/null
+++ b/test/integration/targets/script/files/test_with_args.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+for i in "$@"; do
+ echo "$i"
+done \ No newline at end of file
diff --git a/test/integration/targets/script/meta/main.yml b/test/integration/targets/script/meta/main.yml
new file mode 100644
index 0000000..cb6005d
--- /dev/null
+++ b/test/integration/targets/script/meta/main.yml
@@ -0,0 +1,3 @@
+dependencies:
+ - prepare_tests
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/script/tasks/main.yml b/test/integration/targets/script/tasks/main.yml
new file mode 100644
index 0000000..989513d
--- /dev/null
+++ b/test/integration/targets/script/tasks/main.yml
@@ -0,0 +1,241 @@
+# Test code for the script module and action_plugin.
+# (c) 2014, Richard Isaacson <richard.c.isaacson@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+##
+## prep
+##
+
+- set_fact:
+ remote_tmp_dir_test: "{{ remote_tmp_dir }}/test_script"
+
+- name: make sure our testing sub-directory does not exist
+ file:
+ path: "{{ remote_tmp_dir_test }}"
+ state: absent
+
+- name: create our testing sub-directory
+ file:
+ path: "{{ remote_tmp_dir_test }}"
+ state: directory
+
+##
+## script
+##
+
+- name: execute the test.sh script via command
+ script: test.sh
+ register: script_result0
+
+- name: assert that the script executed correctly
+ assert:
+ that:
+ - "script_result0.rc == 0"
+ - "script_result0.stdout == 'win'"
+
+- name: Execute a script with a space in the path
+ script: "'space path/test.sh'"
+ register: _space_path_test
+ tags:
+ - spacepath
+
+- name: Assert that script with space in path ran successfully
+ assert:
+ that:
+ - _space_path_test is success
+ - _space_path_test.stdout == 'Script with space in path'
+ tags:
+ - spacepath
+
+- name: Execute a script with arguments including a unicode character
+ script: test_with_args.sh -this -that -Ó¦ther
+ register: unicode_args
+
+- name: Assert that script with unicode character ran successfully
+ assert:
+ that:
+ - unicode_args is success
+ - unicode_args.stdout_lines[0] == '-this'
+ - unicode_args.stdout_lines[1] == '-that'
+ - unicode_args.stdout_lines[2] == '-Ó¦ther'
+
+# creates
+- name: verify that afile.txt is absent
+ file:
+ path: "{{ remote_tmp_dir_test }}/afile.txt"
+ state: absent
+
+- name: create afile.txt with create_afile.sh via command
+ script: create_afile.sh {{ remote_tmp_dir_test | expanduser }}/afile.txt
+ args:
+ creates: "{{ remote_tmp_dir_test | expanduser }}/afile.txt"
+ register: _create_test1
+
+- name: Check state of created file
+ stat:
+ path: "{{ remote_tmp_dir_test | expanduser }}/afile.txt"
+ register: _create_stat1
+
+- name: Run create_afile.sh again to ensure it is skipped
+ script: create_afile.sh {{ remote_tmp_dir_test | expanduser }}/afile.txt
+ args:
+ creates: "{{ remote_tmp_dir_test | expanduser }}/afile.txt"
+ register: _create_test2
+
+- name: Assert that script report a change, file was created, second run was skipped
+ assert:
+ that:
+ - _create_test1 is changed
+ - _create_stat1.stat.exists
+ - _create_test2 is skipped
+
+
+# removes
+- name: verify that afile.txt is present
+ file:
+ path: "{{ remote_tmp_dir_test }}/afile.txt"
+ state: file
+
+- name: remove afile.txt with remote_afile.sh via command
+ script: remove_afile.sh {{ remote_tmp_dir_test | expanduser }}/afile.txt
+ args:
+ removes: "{{ remote_tmp_dir_test | expanduser }}/afile.txt"
+ register: _remove_test1
+
+- name: Check state of removed file
+ stat:
+ path: "{{ remote_tmp_dir_test | expanduser }}/afile.txt"
+ register: _remove_stat1
+
+- name: Run remote_afile.sh again to enure it is skipped
+ script: remove_afile.sh {{ remote_tmp_dir_test | expanduser }}/afile.txt
+ args:
+ removes: "{{ remote_tmp_dir_test | expanduser }}/afile.txt"
+ register: _remove_test2
+
+- name: Assert that script report a change, file was removed, second run was skipped
+ assert:
+ that:
+ - _remove_test1 is changed
+ - not _remove_stat1.stat.exists
+ - _remove_test2 is skipped
+
+
+# async
+- name: verify that afile.txt is absent
+ file:
+ path: "{{ remote_tmp_dir_test }}/afile.txt"
+ state: absent
+
+- name: test task failure with async param
+ script: /some/script.sh
+ async: 2
+ ignore_errors: true
+ register: script_result3
+
+- name: assert task with async param failed
+ assert:
+ that:
+ - script_result3 is failed
+ - script_result3.msg == "async is not supported for this task."
+
+
+# check mode
+- name: Run script to create a file in check mode
+ script: create_afile.sh {{ remote_tmp_dir_test | expanduser }}/afile2.txt
+ check_mode: yes
+ register: _check_mode_test
+
+- debug:
+ var: _check_mode_test
+ verbosity: 2
+
+- name: Get state of file created by script
+ stat:
+ path: "{{ remote_tmp_dir_test | expanduser }}/afile2.txt"
+ register: _afile_stat
+
+- debug:
+ var: _afile_stat
+ verbosity: 2
+
+- name: Assert that a change was reported but the script did not make changes
+ assert:
+ that:
+ - _check_mode_test is not changed
+ - _check_mode_test is skipped
+ - not _afile_stat.stat.exists
+
+- name: Run script to create a file
+ script: create_afile.sh {{ remote_tmp_dir_test | expanduser }}/afile2.txt
+
+- name: Run script to create a file in check mode with 'creates' argument
+ script: create_afile.sh {{ remote_tmp_dir_test | expanduser }}/afile2.txt
+ args:
+ creates: "{{ remote_tmp_dir_test | expanduser }}/afile2.txt"
+ register: _check_mode_test2
+ check_mode: yes
+
+- debug:
+ var: _check_mode_test2
+ verbosity: 2
+
+- name: Assert that task was skipped and mesage was returned
+ assert:
+ that:
+ - _check_mode_test2 is skipped
+ - '_check_mode_test2.msg == "{{ remote_tmp_dir_test | expanduser }}/afile2.txt exists, matching creates option"'
+
+- name: Remove afile2.txt
+ file:
+ path: "{{ remote_tmp_dir_test | expanduser }}/afile2.txt"
+ state: absent
+
+- name: Run script to remove a file in check mode with 'removes' argument
+ script: remove_afile.sh {{ remote_tmp_dir_test | expanduser }}/afile2.txt
+ args:
+ removes: "{{ remote_tmp_dir_test | expanduser }}/afile2.txt"
+ register: _check_mode_test3
+ check_mode: yes
+
+- debug:
+ var: _check_mode_test3
+ verbosity: 2
+
+- name: Assert that task was skipped and message was returned
+ assert:
+ that:
+ - _check_mode_test3 is skipped
+ - '_check_mode_test3.msg == "{{ remote_tmp_dir_test | expanduser }}/afile2.txt does not exist, matching removes option"'
+
+# executable
+
+- name: Run script with shebang omitted
+ script: no_shebang.py
+ args:
+ executable: "{{ ansible_python_interpreter }}"
+ register: _shebang_omitted_test
+ tags:
+ - noshebang
+
+- name: Assert that script with shebang omitted succeeded
+ assert:
+ that:
+ - _shebang_omitted_test is success
+ - _shebang_omitted_test.stdout == 'Script with shebang omitted'
+ tags:
+ - noshebang
diff --git a/test/integration/targets/service/aliases b/test/integration/targets/service/aliases
new file mode 100644
index 0000000..f2f9ac9
--- /dev/null
+++ b/test/integration/targets/service/aliases
@@ -0,0 +1,4 @@
+destructive
+shippable/posix/group1
+skip/osx
+skip/macos
diff --git a/test/integration/targets/service/files/ansible-broken.upstart b/test/integration/targets/service/files/ansible-broken.upstart
new file mode 100644
index 0000000..4e9c669
--- /dev/null
+++ b/test/integration/targets/service/files/ansible-broken.upstart
@@ -0,0 +1,10 @@
+description "ansible test daemon"
+
+start on runlevel [345]
+stop on runlevel [!345]
+
+expect daemon
+
+exec ansible_test_service
+
+manual
diff --git a/test/integration/targets/service/files/ansible.rc b/test/integration/targets/service/files/ansible.rc
new file mode 100644
index 0000000..ec77d52
--- /dev/null
+++ b/test/integration/targets/service/files/ansible.rc
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+# PROVIDE: ansible_test_service
+# REQUIRE: FILESYSTEMS devfs
+# BEFORE: LOGIN
+# KEYWORD: nojail shutdown
+
+. /etc/rc.subr
+
+name="ansible_test_service"
+rcvar="ansible_test_service_enable"
+command="/usr/sbin/${name}"
+pidfile="/var/run/${name}.pid"
+extra_commands=reload
+load_rc_config $name
+run_rc_command "$1"
diff --git a/test/integration/targets/service/files/ansible.systemd b/test/integration/targets/service/files/ansible.systemd
new file mode 100644
index 0000000..3466f25
--- /dev/null
+++ b/test/integration/targets/service/files/ansible.systemd
@@ -0,0 +1,11 @@
+[Unit]
+Description=Ansible Test Service
+
+[Service]
+ExecStart=/usr/sbin/ansible_test_service "Test\nthat newlines in scripts\nwork"
+ExecReload=/bin/true
+Type=forking
+PIDFile=/var/run/ansible_test_service.pid
+
+[Install]
+WantedBy=multi-user.target
diff --git a/test/integration/targets/service/files/ansible.sysv b/test/integration/targets/service/files/ansible.sysv
new file mode 100755
index 0000000..1df0423
--- /dev/null
+++ b/test/integration/targets/service/files/ansible.sysv
@@ -0,0 +1,134 @@
+#!/bin/sh
+#
+
+# LSB header
+
+### BEGIN INIT INFO
+# Provides: ansible-test
+# Default-Start: 3 4 5
+# Default-Stop: 0 1 2 6
+# Short-Description: test daemon for ansible
+# Description: This is a test daemon used by ansible for testing only
+### END INIT INFO
+
+# chkconfig header
+
+# chkconfig: 345 99 99
+# description: This is a test daemon used by ansible for testing only
+#
+# processname: /usr/sbin/ansible_test_service
+
+# Sanity checks.
+[ -x /usr/sbin/ansible_test_service ] || exit 0
+
+DEBIAN_VERSION=/etc/debian_version
+SUSE_RELEASE=/etc/SuSE-release
+# Source function library.
+if [ -f $DEBIAN_VERSION ]; then
+ . /lib/lsb/init-functions
+elif [ -f $SUSE_RELEASE -a -r /etc/rc.status ]; then
+ . /etc/rc.status
+else
+ . /etc/rc.d/init.d/functions
+fi
+
+SERVICE=ansible_test_service
+PROCESS=ansible_test_service
+CONFIG_ARGS=" "
+if [ -f $DEBIAN_VERSION ]; then
+ LOCKFILE=/var/lock/$SERVICE
+else
+ LOCKFILE=/var/lock/subsys/$SERVICE
+fi
+
+RETVAL=0
+
+start() {
+ echo -n "Starting ansible test daemon: "
+ if [ -f $SUSE_RELEASE ]; then
+ startproc -p /var/run/${SERVICE}.pid -f /usr/sbin/ansible_test_service
+ rc_status -v
+ elif [ -e $DEBIAN_VERSION ]; then
+ if [ -f $LOCKFILE ]; then
+ echo -n "already started, lock file found"
+ RETVAL=1
+ elif /usr/sbin/ansible_test_service; then
+ echo -n "OK"
+ RETVAL=0
+ fi
+ else
+ daemon --check $SERVICE $PROCESS --daemonize $CONFIG_ARGS
+ fi
+ RETVAL=$?
+ echo
+ [ $RETVAL -eq 0 ] && touch $LOCKFILE
+ return $RETVAL
+}
+
+stop() {
+ echo -n "Stopping ansible test daemon: "
+ if [ -f $SUSE_RELEASE ]; then
+ killproc -TERM /usr/sbin/ansible_test_service
+ rc_status -v
+ elif [ -f $DEBIAN_VERSION ]; then
+ # Added this since Debian's start-stop-daemon doesn't support spawned processes
+ if ps -ef | grep "/usr/sbin/ansible_test_service" | grep -v grep | awk '{print $2}' | xargs kill &> /dev/null; then
+ echo -n "OK"
+ RETVAL=0
+ else
+ echo -n "Daemon is not started"
+ RETVAL=1
+ fi
+ else
+ killproc -p /var/run/${SERVICE}.pid
+ fi
+ RETVAL=$?
+ echo
+ if [ $RETVAL -eq 0 ]; then
+ rm -f $LOCKFILE
+ rm -f /var/run/$SERVICE.pid
+ fi
+}
+
+restart() {
+ stop
+ start
+}
+
+# See how we were called.
+case "$1" in
+ start|stop|restart)
+ $1
+ ;;
+ status)
+ if [ -f $SUSE_RELEASE ]; then
+ echo -n "Checking for ansible test service "
+ checkproc /usr/sbin/ansible_test_service
+ rc_status -v
+ elif [ -f $DEBIAN_VERSION ]; then
+ if [ -f $LOCKFILE ]; then
+ RETVAL=0
+ echo "ansible test is running."
+ else
+ RETVAL=1
+ echo "ansible test is stopped."
+ fi
+ else
+ status $PROCESS
+ RETVAL=$?
+ fi
+ ;;
+ condrestart)
+ [ -f $LOCKFILE ] && restart || :
+ ;;
+ reload)
+ echo "ok"
+ RETVAL=0
+ ;;
+ *)
+ echo "Usage: $0 {start|stop|status|restart|condrestart|reload}"
+ exit 1
+ ;;
+esac
+exit $RETVAL
+
diff --git a/test/integration/targets/service/files/ansible.upstart b/test/integration/targets/service/files/ansible.upstart
new file mode 100644
index 0000000..369f61a
--- /dev/null
+++ b/test/integration/targets/service/files/ansible.upstart
@@ -0,0 +1,9 @@
+description "ansible test daemon"
+
+start on runlevel [345]
+stop on runlevel [!345]
+
+expect daemon
+
+exec ansible_test_service
+
diff --git a/test/integration/targets/service/files/ansible_test_service.py b/test/integration/targets/service/files/ansible_test_service.py
new file mode 100644
index 0000000..522493f
--- /dev/null
+++ b/test/integration/targets/service/files/ansible_test_service.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python
+
+# this is mostly based off of the code found here:
+# http://code.activestate.com/recipes/278731-creating-a-daemon-the-python-way/
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+import resource
+import signal
+import sys
+import time
+
+UMASK = 0
+WORKDIR = "/"
+MAXFD = 1024
+
+if (hasattr(os, "devnull")):
+ REDIRECT_TO = os.devnull
+else:
+ REDIRECT_TO = "/dev/null"
+
+
+def createDaemon():
+ try:
+ pid = os.fork()
+ except OSError as e:
+ raise Exception("%s [%d]" % (e.strerror, e.errno))
+
+ if (pid == 0):
+ os.setsid()
+
+ try:
+ pid = os.fork()
+ except OSError as e:
+ raise Exception("%s [%d]" % (e.strerror, e.errno))
+
+ if (pid == 0):
+ os.chdir(WORKDIR)
+ os.umask(UMASK)
+ else:
+ f = open('/var/run/ansible_test_service.pid', 'w')
+ f.write("%d\n" % pid)
+ f.close()
+ os._exit(0)
+ else:
+ os._exit(0)
+
+ maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
+ if (maxfd == resource.RLIM_INFINITY):
+ maxfd = MAXFD
+
+ for fd in range(0, maxfd):
+ try:
+ os.close(fd)
+ except OSError: # ERROR, fd wasn't open to begin with (ignored)
+ pass
+
+ os.open(REDIRECT_TO, os.O_RDWR)
+ os.dup2(0, 1)
+ os.dup2(0, 2)
+
+ return (0)
+
+
+if __name__ == "__main__":
+
+ signal.signal(signal.SIGHUP, signal.SIG_IGN)
+
+ retCode = createDaemon()
+
+ while True:
+ time.sleep(1000)
diff --git a/test/integration/targets/service/meta/main.yml b/test/integration/targets/service/meta/main.yml
new file mode 100644
index 0000000..399f3fb
--- /dev/null
+++ b/test/integration/targets/service/meta/main.yml
@@ -0,0 +1,20 @@
+# test code for the service module
+# (c) 2014, James Cammarata <jcammarata@ansible.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+dependencies:
+ - prepare_tests
diff --git a/test/integration/targets/service/tasks/main.yml b/test/integration/targets/service/tasks/main.yml
new file mode 100644
index 0000000..4fc2ddf
--- /dev/null
+++ b/test/integration/targets/service/tasks/main.yml
@@ -0,0 +1,62 @@
+- name: skip unsupported distros
+ meta: end_host
+ when: ansible_distribution in ['Alpine']
+
+- name: install the test daemon script
+ copy:
+ src: ansible_test_service.py
+ dest: /usr/sbin/ansible_test_service
+ mode: '755'
+
+- name: rewrite shebang in the test daemon script
+ lineinfile:
+ path: /usr/sbin/ansible_test_service
+ line: "#!{{ ansible_python_interpreter | realpath }}"
+ insertbefore: BOF
+ firstmatch: yes
+
+- block:
+ # determine init system is in use
+ - name: detect sysv init system
+ set_fact:
+ service_type: sysv
+ when:
+ - ansible_distribution in ['RedHat', 'CentOS', 'ScientificLinux']
+ - ansible_distribution_version is version('6', '>=')
+ - ansible_distribution_version is version('7', '<')
+ - name: detect systemd init system
+ set_fact:
+ service_type: systemd
+ when: (ansible_distribution in ['RedHat', 'CentOS', 'ScientificLinux'] and ansible_distribution_major_version is version('7', '>=')) or ansible_distribution == 'Fedora' or (ansible_distribution == 'Ubuntu' and ansible_distribution_version is version('15.04', '>=')) or (ansible_distribution == 'Debian' and ansible_distribution_version is version('8', '>=')) or ansible_os_family == 'Suse'
+ - name: detect upstart init system
+ set_fact:
+ service_type: upstart
+ when:
+ - ansible_distribution == 'Ubuntu'
+ - ansible_distribution_version is version('15.04', '<')
+ - name: detect rc init system
+ set_fact:
+ service_type: rc
+ when:
+ - ansible_distribution.lower().endswith('bsd')
+
+
+ - name: display value of ansible_service_mgr
+ debug:
+ msg: 'ansible_service_mgr: {{ ansible_service_mgr }}'
+
+ - name: setup test service script
+ include_tasks: '{{ service_type }}_setup.yml'
+
+ - name: execute tests
+ import_tasks: tests.yml
+
+ always:
+ - name: disable and stop ansible test service
+ service:
+ name: ansible_test
+ state: stopped
+ enabled: false
+
+ # cleaning up changes made by this playbook
+ - include_tasks: '{{ service_type }}_cleanup.yml'
diff --git a/test/integration/targets/service/tasks/rc_cleanup.yml b/test/integration/targets/service/tasks/rc_cleanup.yml
new file mode 100644
index 0000000..47f470c
--- /dev/null
+++ b/test/integration/targets/service/tasks/rc_cleanup.yml
@@ -0,0 +1,9 @@
+- name: remove the rc init file
+ file: path=/etc/rc.d/ansible_test state=absent
+ register: remove_rc_result
+
+- name: assert that the rc init file was removed
+ assert:
+ that:
+ - "remove_rc_result.path == '/etc/rc.d/ansible_test'"
+ - "remove_rc_result.state == 'absent'"
diff --git a/test/integration/targets/service/tasks/rc_setup.yml b/test/integration/targets/service/tasks/rc_setup.yml
new file mode 100644
index 0000000..45d2c90
--- /dev/null
+++ b/test/integration/targets/service/tasks/rc_setup.yml
@@ -0,0 +1,21 @@
+- name: install the rc init file
+ copy: src=ansible.rc dest=/etc/rc.d/ansible_test mode=0755
+ register: install_rc_result
+
+- name: assert that the rc init file was installed
+ assert:
+ that:
+ - "install_rc_result.dest == '/etc/rc.d/ansible_test'"
+ - "install_rc_result.state == 'file'"
+ - "install_rc_result.mode == '0755'"
+ - "install_rc_result.checksum == '8526e4571d2ac685fa5a73af723183c194bda35d'"
+
+# FreeBSD (likely others as well) requires the command_interpreter to match the
+# shebang the script was started with as an extra caution against killing the
+# wrong thing. We add the line here.
+- name: add command_interpreter in rc init file
+ lineinfile:
+ path: /etc/rc.d/ansible_test
+ line: "command_interpreter={{ ansible_python_interpreter | realpath }}"
+ insertafter: '^pidfile.*'
+ firstmatch: yes
diff --git a/test/integration/targets/service/tasks/systemd_cleanup.yml b/test/integration/targets/service/tasks/systemd_cleanup.yml
new file mode 100644
index 0000000..e070726
--- /dev/null
+++ b/test/integration/targets/service/tasks/systemd_cleanup.yml
@@ -0,0 +1,25 @@
+- name: remove the systemd unit file
+ file: path=/usr/lib/systemd/system/ansible_test.service state=absent
+ register: remove_systemd_result
+
+- name: remove the systemd unit file
+ file: path=/usr/lib/systemd/system/ansible_test_broken.service state=absent
+ register: remove_systemd_broken_result
+
+- debug: var=remove_systemd_broken_result
+- name: assert that the systemd unit file was removed
+ assert:
+ that:
+ - "remove_systemd_result.path == '/usr/lib/systemd/system/ansible_test.service'"
+ - "remove_systemd_result.state == 'absent'"
+ - "remove_systemd_broken_result.path == '/usr/lib/systemd/system/ansible_test_broken.service'"
+ - "remove_systemd_broken_result.state == 'absent'"
+
+- name: make sure systemd is reloaded
+ shell: systemctl daemon-reload
+ register: restart_systemd_result
+
+- name: assert that systemd was reloaded
+ assert:
+ that:
+ - "restart_systemd_result.rc == 0"
diff --git a/test/integration/targets/service/tasks/systemd_setup.yml b/test/integration/targets/service/tasks/systemd_setup.yml
new file mode 100644
index 0000000..a9170a3
--- /dev/null
+++ b/test/integration/targets/service/tasks/systemd_setup.yml
@@ -0,0 +1,17 @@
+- name: install the systemd unit file
+ copy: src=ansible.systemd dest=/etc/systemd/system/ansible_test.service mode=0644
+ register: install_systemd_result
+
+- name: install a broken systemd unit file
+ file: src=ansible_test.service path=/etc/systemd/system/ansible_test_broken.service state=link
+ register: install_broken_systemd_result
+
+- name: assert that the systemd unit file was installed
+ assert:
+ that:
+ - "install_systemd_result.dest == '/etc/systemd/system/ansible_test.service'"
+ - "install_systemd_result.state == 'file'"
+ - "install_systemd_result.mode == '0644'"
+ - "install_systemd_result.checksum == '9e6320795a5c79c01230a6de1c343ea32097af52'"
+ - "install_broken_systemd_result.dest == '/etc/systemd/system/ansible_test_broken.service'"
+ - "install_broken_systemd_result.state == 'link'"
diff --git a/test/integration/targets/service/tasks/sysv_cleanup.yml b/test/integration/targets/service/tasks/sysv_cleanup.yml
new file mode 100644
index 0000000..dbdfcf8
--- /dev/null
+++ b/test/integration/targets/service/tasks/sysv_cleanup.yml
@@ -0,0 +1,9 @@
+- name: remove the sysV init file
+ file: path=/etc/init.d/ansible_test state=absent
+ register: remove_sysv_result
+
+- name: assert that the sysV init file was removed
+ assert:
+ that:
+ - "remove_sysv_result.path == '/etc/init.d/ansible_test'"
+ - "remove_sysv_result.state == 'absent'"
diff --git a/test/integration/targets/service/tasks/sysv_setup.yml b/test/integration/targets/service/tasks/sysv_setup.yml
new file mode 100644
index 0000000..7b648c2
--- /dev/null
+++ b/test/integration/targets/service/tasks/sysv_setup.yml
@@ -0,0 +1,11 @@
+- name: install the sysV init file
+ copy: src=ansible.sysv dest=/etc/init.d/ansible_test mode=0755
+ register: install_sysv_result
+
+- name: assert that the sysV init file was installed
+ assert:
+ that:
+ - "install_sysv_result.dest == '/etc/init.d/ansible_test'"
+ - "install_sysv_result.state == 'file'"
+ - "install_sysv_result.mode == '0755'"
+ - "install_sysv_result.checksum == '362899814c47d9aad6e93b2f64e39edd24e38797'"
diff --git a/test/integration/targets/service/tasks/tests.yml b/test/integration/targets/service/tasks/tests.yml
new file mode 100644
index 0000000..cfb4215
--- /dev/null
+++ b/test/integration/targets/service/tasks/tests.yml
@@ -0,0 +1,258 @@
+- name: disable the ansible test service
+ service: name=ansible_test enabled=no
+
+- name: (check mode run) enable the ansible test service
+ service: name=ansible_test enabled=yes
+ register: enable_in_check_mode_result
+ check_mode: yes
+
+- name: assert that changes reported for check mode run
+ assert:
+ that:
+ - "enable_in_check_mode_result is changed"
+
+- name: (check mode run) test that service defaults are used
+ service:
+ register: enable_in_check_mode_result
+ check_mode: yes
+ module_defaults:
+ service:
+ name: ansible_test
+ enabled: yes
+
+- name: assert that changes reported for check mode run
+ assert:
+ that:
+ - "enable_in_check_mode_result is changed"
+
+- name: (check mode run) test that specific module defaults are used
+ service:
+ register: enable_in_check_mode_result
+ check_mode: yes
+ when: "ansible_service_mgr in ['sysvinit', 'systemd']"
+ module_defaults:
+ sysvinit:
+ name: ansible_test
+ enabled: yes
+ systemd:
+ name: ansible_test
+ enabled: yes
+
+- name: assert that changes reported for check mode run
+ assert:
+ that:
+ - "enable_in_check_mode_result is changed"
+ when: "ansible_service_mgr in ['sysvinit', 'systemd']"
+
+- name: enable the ansible test service
+ service: name=ansible_test enabled=yes
+ register: enable_result
+
+- name: assert that the service was enabled and changes reported
+ assert:
+ that:
+ - "enable_result.enabled == true"
+ - "enable_result is changed"
+
+- name: start the ansible test service
+ service: name=ansible_test state=started
+ register: start_result
+
+- name: assert that the service was started
+ assert:
+ that:
+ - "start_result.state == 'started'"
+ - "start_result is changed"
+
+- name: check that the service was started
+ shell: 'cat /proc/$(cat /var/run/ansible_test_service.pid)/cmdline'
+ register: cmdline
+ failed_when: cmdline is failed or '\0/usr/sbin/ansible_test_service\0' not in cmdline.stdout
+ # No proc on BSD
+ when: not ansible_distribution.lower().endswith('bsd')
+
+- name: check that the service was started (*bsd)
+ shell: 'ps -p $(cat /var/run/ansible_test_service.pid)'
+ register: cmdline
+ failed_when: cmdline is failed or '/usr/sbin/ansible_test_service' not in cmdline.stdout
+ when: ansible_distribution.lower().endswith('bsd')
+
+- name: find the service with a pattern
+ service: name=ansible_test pattern="ansible_test_ser" state=started
+ register: start2_result
+
+- name: assert that the service was started via the pattern
+ assert:
+ that:
+ - "start2_result.name == 'ansible_test'"
+ - "start2_result.state == 'started'"
+ - "start2_result is not changed"
+
+- name: fetch PID for ansible_test service (before restart)
+ command: 'cat /var/run/ansible_test_service.pid'
+ register: pid_before_restart
+
+- name: restart the ansible test service
+ service: name=ansible_test state=restarted
+ register: restart_result
+
+- name: assert that the service was restarted
+ assert:
+ that:
+ - "restart_result.state == 'started'"
+ - "restart_result is changed"
+
+- name: fetch PID for ansible_test service (after restart)
+ command: 'cat /var/run/ansible_test_service.pid'
+ register: pid_after_restart
+
+- name: "check that PIDs aren't the same"
+ fail:
+ when: pid_before_restart.stdout == pid_after_restart.stdout
+
+- name: check that service is started
+ command: 'cat /proc/{{ pid_after_restart.stdout }}/cmdline'
+ register: cmdline
+ failed_when: cmdline is failed or '\0/usr/sbin/ansible_test_service\0' not in cmdline.stdout
+ # No proc on BSD
+ when: not ansible_distribution.lower().endswith('bsd')
+
+- name: check that the service is started (*bsd)
+ shell: 'ps -p {{ pid_after_restart.stdout }}'
+ register: cmdline
+ failed_when: cmdline is failed or '/usr/sbin/ansible_test_service' not in cmdline.stdout
+ when: ansible_distribution.lower().endswith('bsd')
+
+- name: restart the ansible test service with a sleep
+ service: name=ansible_test state=restarted sleep=2
+ register: restart_sleep_result
+
+- name: assert that the service was restarted with a sleep
+ assert:
+ that:
+ - "restart_sleep_result.state == 'started'"
+ - "restart_sleep_result is changed"
+
+- name: reload the ansible test service
+ service: name=ansible_test state=reloaded
+ register: reload_result
+ # don't do this on systems with systemd because it triggers error:
+ # Unable to reload service ansible_test: ansible_test.service is not active, cannot reload.
+ when: service_type != "systemd"
+
+- name: assert that the service was reloaded
+ assert:
+ that:
+ - "reload_result.state == 'started'"
+ - "reload_result is changed"
+ when: service_type != "systemd"
+
+- name: "test for #42786 (sysvinit)"
+ when: service_type == "sysv"
+ block:
+ - name: "sysvinit (#42786): check state, 'enable' parameter isn't set"
+ service: use=sysvinit name=ansible_test state=started
+
+ - name: "sysvinit (#42786): check that service is still enabled"
+ service: use=sysvinit name=ansible_test enabled=yes
+ register: result_enabled
+ failed_when: result_enabled is changed
+
+- name: fetch PID for ansible_test service
+ command: 'cat /var/run/ansible_test_service.pid'
+ register: ansible_test_pid
+
+- name: check that service is started
+ command: 'cat /proc/{{ ansible_test_pid.stdout }}/cmdline'
+ register: cmdline
+ failed_when: cmdline is failed or '\0/usr/sbin/ansible_test_service\0' not in cmdline.stdout
+ # No proc on BSD
+ when: not ansible_distribution.lower().endswith('bsd')
+
+- name: check that the service is started (*bsd)
+ shell: 'ps -p {{ ansible_test_pid.stdout }}'
+ register: cmdline
+ failed_when: cmdline is failed or '/usr/sbin/ansible_test_service' not in cmdline.stdout
+ when: ansible_distribution.lower().endswith('bsd')
+
+- name: stop the ansible test service
+ service: name=ansible_test state=stopped
+ register: stop_result
+
+- name: check that the service is stopped
+ command: 'cat /proc/{{ ansible_test_pid.stdout }}/cmdline'
+ register: cmdline
+ failed_when: cmdline is not failed or '\0/usr/sbin/ansible_test_service\0' in cmdline.stdout
+ # No proc on BSD
+ when: not ansible_distribution.lower().endswith('bsd')
+
+- name: check that the service is stopped (*bsd)
+ shell: 'ps -p {{ ansible_test_pid.stdout }}'
+ register: cmdline
+ failed_when: cmdline is not failed or '/usr/sbin/ansible_test_service' in cmdline.stdout
+ when: ansible_distribution.lower().endswith('bsd')
+
+- name: assert that the service was stopped
+ assert:
+ that:
+ - "stop_result.state == 'stopped'"
+ - "stop_result is changed"
+
+- name: disable the ansible test service
+ service: name=ansible_test enabled=no
+ register: disable_result
+
+- name: assert that the service was disabled
+ assert:
+ that:
+ - "disable_result.enabled == false"
+ - "disable_result is changed"
+
+- name: try to enable a broken service
+ service: name=ansible_broken_test enabled=yes
+ register: broken_enable_result
+ ignore_errors: True
+
+- name: assert that the broken test failed
+ assert:
+ that:
+ - "broken_enable_result is failed"
+
+- name: remove the test daemon script
+ file: path=/usr/sbin/ansible_test_service state=absent
+ register: remove_result
+
+- name: assert that the test daemon script was removed
+ assert:
+ that:
+ - "remove_result.path == '/usr/sbin/ansible_test_service'"
+ - "remove_result.state == 'absent'"
+
+- name: the module must fail when a service is not found
+ service:
+ name: 'nonexisting'
+ state: stopped
+ register: result
+ ignore_errors: yes
+ when: ansible_distribution != 'FreeBSD'
+
+- assert:
+ that:
+ - result is failed
+ - result is search("Could not find the requested service nonexisting")
+ when: ansible_distribution != 'FreeBSD'
+
+- name: the module must fail in check_mode as well when a service is not found
+ service:
+ name: 'nonexisting'
+ state: stopped
+ register: result
+ check_mode: yes
+ ignore_errors: yes
+ when: ansible_distribution != 'FreeBSD'
+
+- assert:
+ that:
+ - result is failed
+ - result is search("Could not find the requested service nonexisting")
+ when: ansible_distribution != 'FreeBSD'
diff --git a/test/integration/targets/service/tasks/upstart_cleanup.yml b/test/integration/targets/service/tasks/upstart_cleanup.yml
new file mode 100644
index 0000000..683fb10
--- /dev/null
+++ b/test/integration/targets/service/tasks/upstart_cleanup.yml
@@ -0,0 +1,17 @@
+- vars:
+ upstart_files:
+ - /etc/init/ansible_test.conf
+ - /etc/init/ansible_test.override
+ - /etc/init/ansible_test_broken.conf
+ block:
+ - name: remove upstart init files
+ file:
+ path: '{{ item }}'
+ state: absent
+ loop: '{{ upstart_files }}'
+
+ - name: assert that upstart init files were removed
+ raw: 'test -e {{ item }}'
+ loop: '{{ upstart_files }}'
+ register: file_exists
+ failed_when: file_exists is not failed
diff --git a/test/integration/targets/service/tasks/upstart_setup.yml b/test/integration/targets/service/tasks/upstart_setup.yml
new file mode 100644
index 0000000..e9607bb
--- /dev/null
+++ b/test/integration/targets/service/tasks/upstart_setup.yml
@@ -0,0 +1,19 @@
+- name: install the upstart init file
+ copy: src=ansible.upstart dest=/etc/init/ansible_test.conf mode=0644
+ register: install_upstart_result
+
+- name: install an upstart init file that will fail (manual in .conf)
+ copy: src=ansible-broken.upstart dest=/etc/init/ansible_broken_test.conf mode=0644
+ register: install_upstart_broken_result
+
+- name: assert that the upstart init file was installed
+ assert:
+ that:
+ - "install_upstart_result.dest == '/etc/init/ansible_test.conf'"
+ - "install_upstart_result.state == 'file'"
+ - "install_upstart_result.mode == '0644'"
+ - "install_upstart_result.checksum == '5c314837b6c4dd6c68d1809653a2974e9078e02a'"
+ - "install_upstart_broken_result.dest == '/etc/init/ansible_broken_test.conf'"
+ - "install_upstart_broken_result.state == 'file'"
+ - "install_upstart_broken_result.mode == '0644'"
+ - "install_upstart_broken_result.checksum == 'e66497894f2b2bf71e1380a196cc26089cc24a10'"
diff --git a/test/integration/targets/service/templates/main.yml b/test/integration/targets/service/templates/main.yml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/service/templates/main.yml
diff --git a/test/integration/targets/service_facts/aliases b/test/integration/targets/service_facts/aliases
new file mode 100644
index 0000000..17d3eb7
--- /dev/null
+++ b/test/integration/targets/service_facts/aliases
@@ -0,0 +1,4 @@
+shippable/posix/group2
+skip/freebsd
+skip/osx
+skip/macos
diff --git a/test/integration/targets/service_facts/files/ansible.systemd b/test/integration/targets/service_facts/files/ansible.systemd
new file mode 100644
index 0000000..3466f25
--- /dev/null
+++ b/test/integration/targets/service_facts/files/ansible.systemd
@@ -0,0 +1,11 @@
+[Unit]
+Description=Ansible Test Service
+
+[Service]
+ExecStart=/usr/sbin/ansible_test_service "Test\nthat newlines in scripts\nwork"
+ExecReload=/bin/true
+Type=forking
+PIDFile=/var/run/ansible_test_service.pid
+
+[Install]
+WantedBy=multi-user.target
diff --git a/test/integration/targets/service_facts/files/ansible_test_service.py b/test/integration/targets/service_facts/files/ansible_test_service.py
new file mode 100644
index 0000000..19f1e29
--- /dev/null
+++ b/test/integration/targets/service_facts/files/ansible_test_service.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python
+
+# this is mostly based off of the code found here:
+# http://code.activestate.com/recipes/278731-creating-a-daemon-the-python-way/
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import os
+import resource
+import signal
+import time
+
+UMASK = 0
+WORKDIR = "/"
+MAXFD = 1024
+
+if (hasattr(os, "devnull")):
+ REDIRECT_TO = os.devnull
+else:
+ REDIRECT_TO = "/dev/null"
+
+
+def createDaemon():
+ try:
+ pid = os.fork()
+ except OSError as e:
+ raise Exception("%s [%d]" % (e.strerror, e.errno))
+
+ if (pid == 0):
+ os.setsid()
+
+ try:
+ pid = os.fork()
+ except OSError as e:
+ raise Exception("%s [%d]" % (e.strerror, e.errno))
+
+ if (pid == 0):
+ os.chdir(WORKDIR)
+ os.umask(UMASK)
+ else:
+ f = open('/var/run/ansible_test_service.pid', 'w')
+ f.write("%d\n" % pid)
+ f.close()
+ os._exit(0)
+ else:
+ os._exit(0)
+
+ maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
+ if (maxfd == resource.RLIM_INFINITY):
+ maxfd = MAXFD
+
+ for fd in range(0, maxfd):
+ try:
+ os.close(fd)
+ except OSError: # ERROR, fd wasn't open to begin with (ignored)
+ pass
+
+ os.open(REDIRECT_TO, os.O_RDWR)
+ os.dup2(0, 1)
+ os.dup2(0, 2)
+
+ return (0)
+
+
+if __name__ == "__main__":
+
+ signal.signal(signal.SIGHUP, signal.SIG_IGN)
+
+ retCode = createDaemon()
+
+ while True:
+ time.sleep(1000)
diff --git a/test/integration/targets/service_facts/tasks/main.yml b/test/integration/targets/service_facts/tasks/main.yml
new file mode 100644
index 0000000..d2d6528
--- /dev/null
+++ b/test/integration/targets/service_facts/tasks/main.yml
@@ -0,0 +1,29 @@
+# Test playbook for the service_facts module
+# Copyright: (c) 2017, Adam Miller <admiller@redhat.com>
+# Copyright: (c) 2020, Abhijeet Kasurde <akasurde@redhat.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+- name: skip broken distros
+ meta: end_host
+ when: ansible_distribution == 'Alpine'
+
+- name: Gather service facts
+ service_facts:
+
+- name: check for ansible_facts.services exists
+ assert:
+ that: ansible_facts.services is defined
+
+- name: Test disabled service facts (https://github.com/ansible/ansible/issues/69144)
+ block:
+ - name: display value of ansible_service_mgr
+ debug:
+ msg: 'ansible_service_mgr: {{ ansible_service_mgr }}'
+
+ - name: setup test service script
+ include_tasks: 'systemd_setup.yml'
+
+ - name: execute tests
+ import_tasks: tests.yml
+
+ when: (ansible_distribution in ['RedHat', 'CentOS', 'ScientificLinux'] and ansible_distribution_major_version is version('7', '>=')) or ansible_distribution == 'Fedora' or (ansible_distribution == 'Ubuntu' and ansible_distribution_version is version('15.04', '>=')) or (ansible_distribution == 'Debian' and ansible_distribution_version is version('8', '>=')) or ansible_os_family == 'Suse'
diff --git a/test/integration/targets/service_facts/tasks/systemd_cleanup.yml b/test/integration/targets/service_facts/tasks/systemd_cleanup.yml
new file mode 100644
index 0000000..b68530b
--- /dev/null
+++ b/test/integration/targets/service_facts/tasks/systemd_cleanup.yml
@@ -0,0 +1,32 @@
+- name: remove the systemd unit file
+ file:
+ path: /usr/lib/systemd/system/ansible_test.service
+ state: absent
+ register: remove_systemd_result
+
+- name: assert that the systemd unit file was removed
+ assert:
+ that:
+ - "remove_systemd_result.path == '/usr/lib/systemd/system/ansible_test.service'"
+ - "remove_systemd_result.state == 'absent'"
+
+- name: remove python systemd test script file
+ file:
+ path: /usr/sbin/ansible_test_service
+ state: absent
+ register: remove_systemd_binary_result
+
+- name: assert that python systemd test script file was removed
+ assert:
+ that:
+ - "remove_systemd_binary_result.path == '/usr/sbin/ansible_test_service'"
+ - "remove_systemd_binary_result.state == 'absent'"
+
+- name: make sure systemd is reloaded
+ shell: systemctl daemon-reload
+ register: restart_systemd_result
+
+- name: assert that systemd was reloaded
+ assert:
+ that:
+ - "restart_systemd_result.rc == 0"
diff --git a/test/integration/targets/service_facts/tasks/systemd_setup.yml b/test/integration/targets/service_facts/tasks/systemd_setup.yml
new file mode 100644
index 0000000..85eeed0
--- /dev/null
+++ b/test/integration/targets/service_facts/tasks/systemd_setup.yml
@@ -0,0 +1,26 @@
+- name: install the test daemon script
+ copy:
+ src: ansible_test_service.py
+ dest: /usr/sbin/ansible_test_service
+ mode: '755'
+
+- name: rewrite shebang in the test daemon script
+ lineinfile:
+ path: /usr/sbin/ansible_test_service
+ line: "#!{{ ansible_python_interpreter | realpath }}"
+ insertbefore: BOF
+ firstmatch: yes
+
+- name: install the systemd unit file
+ copy:
+ src: ansible.systemd
+ dest: /etc/systemd/system/ansible_test.service
+ mode: '0644'
+ register: install_systemd_result
+
+- name: assert that the systemd unit file was installed
+ assert:
+ that:
+ - "install_systemd_result.dest == '/etc/systemd/system/ansible_test.service'"
+ - "install_systemd_result.state == 'file'"
+ - "install_systemd_result.mode == '0644'"
diff --git a/test/integration/targets/service_facts/tasks/tests.yml b/test/integration/targets/service_facts/tasks/tests.yml
new file mode 100644
index 0000000..495b71f
--- /dev/null
+++ b/test/integration/targets/service_facts/tasks/tests.yml
@@ -0,0 +1,36 @@
+- name: start the ansible test service
+ service:
+ name: ansible_test
+ enabled: yes
+ state: started
+ register: enable_result
+
+- name: assert that the service was enabled and changes reported
+ assert:
+ that:
+ - "enable_result.enabled == true"
+ - "enable_result is changed"
+
+- name: disable the ansible test service
+ service:
+ name: ansible_test
+ state: stopped
+ enabled: no
+ register: start_result
+
+- name: assert that the service was stopped
+ assert:
+ that:
+ - "start_result.state == 'stopped'"
+ - "start_result is changed"
+
+- name: Populate service facts
+ service_facts:
+
+- name: get ansible_test service's state
+ debug:
+ var: services['ansible_test.service'].state
+
+- name: ansible_test service's running state should be \"inactive\"
+ assert:
+ that: "services['ansible_test.service'].state == 'inactive'"
diff --git a/test/integration/targets/set_fact/aliases b/test/integration/targets/set_fact/aliases
new file mode 100644
index 0000000..a1b27a8
--- /dev/null
+++ b/test/integration/targets/set_fact/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller # this is a controller-only action, the module is just for documentation
diff --git a/test/integration/targets/set_fact/incremental.yml b/test/integration/targets/set_fact/incremental.yml
new file mode 100644
index 0000000..3f7aa6c
--- /dev/null
+++ b/test/integration/targets/set_fact/incremental.yml
@@ -0,0 +1,35 @@
+- name: test set_fact incremental https://github.com/ansible/ansible/issues/38271
+ hosts: testhost
+ gather_facts: no
+ tasks:
+ - name: Generate inline loop for set_fact
+ set_fact:
+ dig_list: "{{ dig_list + [ item ] }}"
+ loop:
+ - two
+ - three
+ - four
+ vars:
+ dig_list:
+ - one
+
+ - name: verify cumulative set fact worked
+ assert:
+ that:
+ - dig_list == ['one', 'two', 'three', 'four']
+
+ - name: Generate inline loop for set_fact (FQCN)
+ ansible.builtin.set_fact:
+ dig_list_fqcn: "{{ dig_list_fqcn + [ item ] }}"
+ loop:
+ - two
+ - three
+ - four
+ vars:
+ dig_list_fqcn:
+ - one
+
+ - name: verify cumulative set fact worked (FQCN)
+ assert:
+ that:
+ - dig_list_fqcn == ['one', 'two', 'three', 'four']
diff --git a/test/integration/targets/set_fact/inventory b/test/integration/targets/set_fact/inventory
new file mode 100644
index 0000000..b0c00d3
--- /dev/null
+++ b/test/integration/targets/set_fact/inventory
@@ -0,0 +1,3 @@
+[testgroup]
+testhost ansible_connection=local # no connection is actually established with this host
+localhost ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
diff --git a/test/integration/targets/set_fact/nowarn_clean_facts.yml b/test/integration/targets/set_fact/nowarn_clean_facts.yml
new file mode 100644
index 0000000..74f908d
--- /dev/null
+++ b/test/integration/targets/set_fact/nowarn_clean_facts.yml
@@ -0,0 +1,10 @@
+- name: Test no warnings ref "http://github.com/ansible/ansible/issues/37535"
+ hosts: testhost
+ gather_facts: false
+ tasks:
+ - name: set ssh jump host args
+ set_fact:
+ ansible_ssh_common_args: "-o ProxyCommand='ssh -W %h:%p -q root@localhost'"
+ - name: set ssh jump host args (FQCN)
+ ansible.builtin.set_fact:
+ ansible_ssh_common_args: "-o ProxyCommand='ssh -W %h:%p -q root@localhost'"
diff --git a/test/integration/targets/set_fact/runme.sh b/test/integration/targets/set_fact/runme.sh
new file mode 100755
index 0000000..9309359
--- /dev/null
+++ b/test/integration/targets/set_fact/runme.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+
+set -eux
+
+MYTMPDIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
+trap 'rm -rf "${MYTMPDIR}"' EXIT
+
+# ensure we can incrementally set fact via loopi, injection or not
+ANSIBLE_INJECT_FACT_VARS=0 ansible-playbook -i inventory incremental.yml
+ANSIBLE_INJECT_FACT_VARS=1 ansible-playbook -i inventory incremental.yml
+
+# ensure we dont have spurious warnings do to clean_facts
+ansible-playbook -i inventory nowarn_clean_facts.yml | grep '[WARNING]: Removed restricted key from module data: ansible_ssh_common_args' && exit 1
+
+# test cached feature
+export ANSIBLE_CACHE_PLUGIN=jsonfile ANSIBLE_CACHE_PLUGIN_CONNECTION="${MYTMPDIR}" ANSIBLE_CACHE_PLUGIN_PREFIX=prefix_
+ansible-playbook -i inventory "$@" set_fact_cached_1.yml
+ansible-playbook -i inventory "$@" set_fact_cached_2.yml
+
+# check contents of the fact cache directory before flushing it
+if [[ "$(find "${MYTMPDIR}" -type f)" != $MYTMPDIR/prefix_* ]]; then
+ echo "Unexpected cache file"
+ exit 1
+fi
+
+ansible-playbook -i inventory --flush-cache "$@" set_fact_no_cache.yml
+
+# Test boolean conversions in set_fact
+ANSIBLE_JINJA2_NATIVE=0 ansible-playbook -v set_fact_bool_conv.yml
+ANSIBLE_JINJA2_NATIVE=1 ansible-playbook -v set_fact_bool_conv_jinja2_native.yml
+
+# Test parsing of values when using an empty string as a key
+ansible-playbook -i inventory set_fact_empty_str_key.yml
+
+# https://github.com/ansible/ansible/issues/21088
+ansible-playbook -i inventory "$@" set_fact_auto_unsafe.yml
diff --git a/test/integration/targets/set_fact/set_fact_auto_unsafe.yml b/test/integration/targets/set_fact/set_fact_auto_unsafe.yml
new file mode 100644
index 0000000..b0fb4dc
--- /dev/null
+++ b/test/integration/targets/set_fact/set_fact_auto_unsafe.yml
@@ -0,0 +1,10 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - set_fact:
+ foo: bar
+ register: baz
+
+ - assert:
+ that:
+ - baz.ansible_facts.foo|type_debug != "AnsibleUnsafeText"
diff --git a/test/integration/targets/set_fact/set_fact_bool_conv.yml b/test/integration/targets/set_fact/set_fact_bool_conv.yml
new file mode 100644
index 0000000..8df249b
--- /dev/null
+++ b/test/integration/targets/set_fact/set_fact_bool_conv.yml
@@ -0,0 +1,35 @@
+- hosts: localhost
+ gather_facts: false
+ vars:
+ string_var: "no"
+ tasks:
+ - set_fact:
+ this_is_string: "yes"
+ this_is_not_string: yes
+ this_is_also_string: "{{ string_var }}"
+ this_is_another_string: !!str "{% set thing = '' + string_var + '' %}{{ thing }}"
+ this_is_more_strings: '{{ string_var + "" }}'
+
+ - assert:
+ that:
+ - string_var == 'no'
+ - this_is_string == True
+ - this_is_not_string == True
+ - this_is_also_string == False
+ - this_is_another_string == False
+ - this_is_more_strings == False
+
+ - ansible.builtin.set_fact:
+ this_is_string_fqcn: "yes"
+ this_is_not_string_fqcn: yes
+ this_is_also_string_fqcn: "{{ string_var }}"
+ this_is_another_string_fqcn: !!str "{% set thing = '' + string_var + '' %}{{ thing }}"
+ this_is_more_strings_fqcn: '{{ string_var + "" }}'
+
+ - assert:
+ that:
+ - this_is_string_fqcn == True
+ - this_is_not_string_fqcn == True
+ - this_is_also_string_fqcn == False
+ - this_is_another_string_fqcn == False
+ - this_is_more_strings_fqcn == False
diff --git a/test/integration/targets/set_fact/set_fact_bool_conv_jinja2_native.yml b/test/integration/targets/set_fact/set_fact_bool_conv_jinja2_native.yml
new file mode 100644
index 0000000..2642599
--- /dev/null
+++ b/test/integration/targets/set_fact/set_fact_bool_conv_jinja2_native.yml
@@ -0,0 +1,35 @@
+- hosts: localhost
+ gather_facts: false
+ vars:
+ string_var: "no"
+ tasks:
+ - set_fact:
+ this_is_string: "yes"
+ this_is_not_string: yes
+ this_is_also_string: "{{ string_var }}"
+ this_is_another_string: !!str "{% set thing = '' + string_var + '' %}{{ thing }}"
+ this_is_more_strings: '{{ string_var + "" }}'
+
+ - assert:
+ that:
+ - string_var == 'no'
+ - this_is_string == 'yes'
+ - this_is_not_string == True
+ - this_is_also_string == 'no'
+ - this_is_another_string == 'no'
+ - this_is_more_strings == 'no'
+
+ - ansible.builtin.set_fact:
+ this_is_string_fqcn: "yes"
+ this_is_not_string_fqcn: yes
+ this_is_also_string_fqcn: "{{ string_var }}"
+ this_is_another_string_fqcn: !!str "{% set thing = '' + string_var + '' %}{{ thing }}"
+ this_is_more_strings_fqcn: '{{ string_var + "" }}'
+
+ - assert:
+ that:
+ - this_is_string_fqcn == 'yes'
+ - this_is_not_string_fqcn == True
+ - this_is_also_string_fqcn == 'no'
+ - this_is_another_string_fqcn == 'no'
+ - this_is_more_strings_fqcn == 'no'
diff --git a/test/integration/targets/set_fact/set_fact_cached_1.yml b/test/integration/targets/set_fact/set_fact_cached_1.yml
new file mode 100644
index 0000000..01c9f1e
--- /dev/null
+++ b/test/integration/targets/set_fact/set_fact_cached_1.yml
@@ -0,0 +1,324 @@
+---
+- name: the first play
+ hosts: localhost
+ tasks:
+ - name: show foobar fact before
+ debug:
+ var: ansible_foobar
+
+ - name: set a persistent fact foobar
+ set_fact:
+ ansible_foobar: 'foobar_from_set_fact_cacheable'
+ cacheable: true
+
+ - name: show foobar fact after
+ debug:
+ var: ansible_foobar
+
+ - name: assert ansible_foobar is correct value
+ assert:
+ that:
+ - ansible_foobar == 'foobar_from_set_fact_cacheable'
+
+ - name: set a non persistent fact that will not be cached
+ set_fact:
+ ansible_foobar_not_cached: 'this_should_not_be_cached'
+
+ - name: show ansible_foobar_not_cached fact after being set
+ debug:
+ var: ansible_foobar_not_cached
+
+ - name: assert ansible_foobar_not_cached is correct value
+ assert:
+ that:
+ - ansible_foobar_not_cached == 'this_should_not_be_cached'
+
+ - name: set another non persistent fact that will not be cached
+ set_fact: "cacheable=no fact_not_cached='this_should_not_be_cached!'"
+
+ - name: show fact_not_cached fact after being set
+ debug:
+ var: fact_not_cached
+
+ - name: assert fact_not_cached is correct value
+ assert:
+ that:
+ - fact_not_cached == 'this_should_not_be_cached!'
+
+ - name: show foobar fact before (FQCN)
+ debug:
+ var: ansible_foobar_fqcn
+
+ - name: set a persistent fact foobar (FQCN)
+ set_fact:
+ ansible_foobar_fqcn: 'foobar_fqcn_from_set_fact_cacheable'
+ cacheable: true
+
+ - name: show foobar fact after (FQCN)
+ debug:
+ var: ansible_foobar_fqcn
+
+ - name: assert ansible_foobar_fqcn is correct value (FQCN)
+ assert:
+ that:
+ - ansible_foobar_fqcn == 'foobar_fqcn_from_set_fact_cacheable'
+
+ - name: set a non persistent fact that will not be cached (FQCN)
+ set_fact:
+ ansible_foobar_not_cached_fqcn: 'this_should_not_be_cached'
+
+ - name: show ansible_foobar_not_cached_fqcn fact after being set (FQCN)
+ debug:
+ var: ansible_foobar_not_cached_fqcn
+
+ - name: assert ansible_foobar_not_cached_fqcn is correct value (FQCN)
+ assert:
+ that:
+ - ansible_foobar_not_cached_fqcn == 'this_should_not_be_cached'
+
+ - name: set another non persistent fact that will not be cached (FQCN)
+ set_fact: "cacheable=no fact_not_cached_fqcn='this_should_not_be_cached!'"
+
+ - name: show fact_not_cached_fqcn fact after being set (FQCN)
+ debug:
+ var: fact_not_cached_fqcn
+
+ - name: assert fact_not_cached_fqcn is correct value (FQCN)
+ assert:
+ that:
+ - fact_not_cached_fqcn == 'this_should_not_be_cached!'
+
+- name: the second play
+ hosts: localhost
+ tasks:
+ - name: show foobar fact after second play
+ debug:
+ var: ansible_foobar
+
+ - name: assert ansible_foobar is correct value
+ assert:
+ that:
+ - ansible_foobar == 'foobar_from_set_fact_cacheable'
+
+ - name: show foobar fact after second play (FQCN)
+ debug:
+ var: ansible_foobar_fqcn
+
+ - name: assert ansible_foobar is correct value (FQCN)
+ assert:
+ that:
+ - ansible_foobar_fqcn == 'foobar_fqcn_from_set_fact_cacheable'
+
+- name: show ansible_nodename and ansible_os_family
+ hosts: localhost
+ tasks:
+ - name: show nodename fact after second play
+ debug:
+ var: ansible_nodename
+ - name: show os_family fact after second play (FQCN)
+ debug:
+ var: ansible_os_family
+
+- name: show ansible_nodename and ansible_os_family overridden with var
+ hosts: localhost
+ vars:
+ ansible_nodename: 'nodename_from_play_vars'
+ ansible_os_family: 'os_family_from_play_vars'
+ tasks:
+ - name: show nodename fact after second play
+ debug:
+ var: ansible_nodename
+ - name: show os_family fact after second play (FQCN)
+ debug:
+ var: ansible_os_family
+
+- name: verify ansible_nodename from vars overrides the fact
+ hosts: localhost
+ vars:
+ ansible_nodename: 'nodename_from_play_vars'
+ ansible_os_family: 'os_family_from_play_vars'
+ tasks:
+ - name: show nodename fact
+ debug:
+ var: ansible_nodename
+
+ - name: assert ansible_nodename is correct value
+ assert:
+ that:
+ - ansible_nodename == 'nodename_from_play_vars'
+
+ - name: show os_family fact (FQCN)
+ debug:
+ var: ansible_os_family
+
+ - name: assert ansible_os_family is correct value (FQCN)
+ assert:
+ that:
+ - ansible_os_family == 'os_family_from_play_vars'
+
+- name: set_fact ansible_nodename and ansible_os_family
+ hosts: localhost
+ tasks:
+ - name: set a persistent fact nodename
+ set_fact:
+ ansible_nodename: 'nodename_from_set_fact_cacheable'
+
+ - name: show nodename fact
+ debug:
+ var: ansible_nodename
+
+ - name: assert ansible_nodename is correct value
+ assert:
+ that:
+ - ansible_nodename == 'nodename_from_set_fact_cacheable'
+
+ - name: set a persistent fact os_family (FQCN)
+ ansible.builtin.set_fact:
+ ansible_os_family: 'os_family_from_set_fact_cacheable'
+
+ - name: show os_family fact (FQCN)
+ debug:
+ var: ansible_os_family
+
+ - name: assert ansible_os_family is correct value (FQCN)
+ assert:
+ that:
+ - ansible_os_family == 'os_family_from_set_fact_cacheable'
+
+- name: verify that set_fact ansible_xxx non_cacheable overrides ansible_xxx in vars
+ hosts: localhost
+ vars:
+ ansible_nodename: 'nodename_from_play_vars'
+ ansible_os_family: 'os_family_from_play_vars'
+ tasks:
+ - name: show nodename fact
+ debug:
+ var: ansible_nodename
+
+ - name: assert ansible_nodename is correct value
+ assert:
+ that:
+ - ansible_nodename == 'nodename_from_set_fact_cacheable'
+
+ - name: show os_family fact (FQCN)
+ debug:
+ var: ansible_os_family
+
+ - name: assert ansible_os_family is correct value (FQCN)
+ assert:
+ that:
+ - ansible_os_family == 'os_family_from_set_fact_cacheable'
+
+- name: verify that set_fact_cacheable in previous play overrides ansible_xxx in vars
+ hosts: localhost
+ vars:
+ ansible_nodename: 'nodename_from_play_vars'
+ ansible_os_family: 'os_family_from_play_vars'
+ tasks:
+ - name: show nodename fact
+ debug:
+ var: ansible_nodename
+
+ - name: assert ansible_nodename is correct value
+ assert:
+ that:
+ - ansible_nodename == 'nodename_from_set_fact_cacheable'
+
+ - name: show os_family fact (FQCN)
+ debug:
+ var: ansible_os_family
+
+ - name: assert ansible_os_family is correct value (FQCN)
+ assert:
+ that:
+ - ansible_os_family == 'os_family_from_set_fact_cacheable'
+
+- name: set_fact ansible_nodename and ansible_os_family cacheable
+ hosts: localhost
+ tasks:
+ - name: set a persistent fact nodename
+ set_fact:
+ ansible_nodename: 'nodename_from_set_fact_cacheable'
+ cacheable: true
+
+ - name: show nodename fact
+ debug:
+ var: ansible_nodename
+
+ - name: assert ansible_nodename is correct value
+ assert:
+ that:
+ - ansible_nodename == 'nodename_from_set_fact_cacheable'
+
+ - name: set a persistent fact os_family (FQCN)
+ ansible.builtin.set_fact:
+ ansible_os_family: 'os_family_from_set_fact_cacheable'
+ cacheable: true
+
+ - name: show os_family fact (FQCN)
+ debug:
+ var: ansible_os_family
+
+ - name: assert ansible_os_family is correct value (FQCN)
+ assert:
+ that:
+ - ansible_os_family == 'os_family_from_set_fact_cacheable'
+
+
+- name: verify that set_fact_cacheable in previous play overrides ansible_xxx in vars
+ hosts: localhost
+ vars:
+ ansible_nodename: 'nodename_from_play_vars'
+ ansible_os_family: 'os_family_from_play_vars'
+ tasks:
+ - name: show nodename fact
+ debug:
+ var: ansible_nodename
+
+ - name: assert ansible_nodename is correct value
+ assert:
+ that:
+ - ansible_nodename == 'nodename_from_set_fact_cacheable'
+
+ - name: show os_family fact (FQCN)
+ debug:
+ var: ansible_os_family
+
+ - name: assert ansible_os_family is correct value (FQCN)
+ assert:
+ that:
+ - ansible_os_family == 'os_family_from_set_fact_cacheable'
+
+- name: the fourth play
+ hosts: localhost
+ vars:
+ ansible_foobar: 'foobar_from_play_vars'
+ ansible_foobar_fqcn: 'foobar_fqcn_from_play_vars'
+ tasks:
+ - name: show example fact
+ debug:
+ var: ansible_example
+
+ - name: set a persistent fact example
+ set_fact:
+ ansible_example: 'foobar_from_set_fact_cacheable'
+ cacheable: true
+
+ - name: assert ansible_example is correct value
+ assert:
+ that:
+ - ansible_example == 'foobar_from_set_fact_cacheable'
+
+ - name: show example fact (FQCN)
+ debug:
+ var: ansible_example_fqcn
+
+ - name: set a persistent fact example (FQCN)
+ set_fact:
+ ansible_example_fqcn: 'foobar_fqcn_from_set_fact_cacheable'
+ cacheable: true
+
+ - name: assert ansible_example_fqcn is correct value (FQCN)
+ assert:
+ that:
+ - ansible_example_fqcn == 'foobar_fqcn_from_set_fact_cacheable'
diff --git a/test/integration/targets/set_fact/set_fact_cached_2.yml b/test/integration/targets/set_fact/set_fact_cached_2.yml
new file mode 100644
index 0000000..7df9224
--- /dev/null
+++ b/test/integration/targets/set_fact/set_fact_cached_2.yml
@@ -0,0 +1,57 @@
+---
+- name: A second playbook run with fact caching enabled
+ hosts: localhost
+ tasks:
+ - name: show ansible_foobar fact
+ debug:
+ var: ansible_foobar
+
+ - name: assert ansible_foobar is correct value when read from cache
+ assert:
+ that:
+ - ansible_foobar == 'foobar_from_set_fact_cacheable'
+
+ - name: show ansible_foobar_not_cached fact
+ debug:
+ var: ansible_foobar_not_cached
+
+ - name: assert ansible_foobar_not_cached is not cached
+ assert:
+ that:
+ - ansible_foobar_not_cached is undefined
+
+ - name: show fact_not_cached fact
+ debug:
+ var: fact_not_cached
+
+ - name: assert fact_not_cached is not cached
+ assert:
+ that:
+ - fact_not_cached is undefined
+
+ - name: show ansible_foobar_fqcn fact (FQCN)
+ debug:
+ var: ansible_foobar_fqcn
+
+ - name: assert ansible_foobar_fqcn is correct value when read from cache (FQCN)
+ assert:
+ that:
+ - ansible_foobar_fqcn == 'foobar_fqcn_from_set_fact_cacheable'
+
+ - name: show ansible_foobar_fqcn_not_cached fact (FQCN)
+ debug:
+ var: ansible_foobar_fqcn_not_cached
+
+ - name: assert ansible_foobar_fqcn_not_cached is not cached (FQCN)
+ assert:
+ that:
+ - ansible_foobar_fqcn_not_cached is undefined
+
+ - name: show fact_not_cached_fqcn fact (FQCN)
+ debug:
+ var: fact_not_cached_fqcn
+
+ - name: assert fact_not_cached_fqcn is not cached (FQCN)
+ assert:
+ that:
+ - fact_not_cached_fqcn is undefined
diff --git a/test/integration/targets/set_fact/set_fact_empty_str_key.yml b/test/integration/targets/set_fact/set_fact_empty_str_key.yml
new file mode 100644
index 0000000..2863190
--- /dev/null
+++ b/test/integration/targets/set_fact/set_fact_empty_str_key.yml
@@ -0,0 +1,15 @@
+- name: Test set_fact for empty string as a key
+ hosts: testhost
+ gather_facts: no
+ vars:
+ value: 1
+ tasks:
+ - name: Define fact with key as an empty string
+ set_fact:
+ foo:
+ "": "bar{{ value }}"
+
+ - name: Verify the parsed value of the key
+ assert:
+ that:
+ - foo == {"":"bar1"}
diff --git a/test/integration/targets/set_fact/set_fact_no_cache.yml b/test/integration/targets/set_fact/set_fact_no_cache.yml
new file mode 100644
index 0000000..f5a9979
--- /dev/null
+++ b/test/integration/targets/set_fact/set_fact_no_cache.yml
@@ -0,0 +1,39 @@
+---
+- name: Running with fact caching enabled but with cache flushed
+ hosts: localhost
+ tasks:
+ - name: show ansible_foobar fact
+ debug:
+ var: ansible_foobar
+
+ - name: assert ansible_foobar is correct value
+ assert:
+ that:
+ - ansible_foobar is undefined
+
+ - name: show ansible_foobar_not_cached fact
+ debug:
+ var: ansible_foobar_not_cached
+
+ - name: assert ansible_foobar_not_cached is not cached
+ assert:
+ that:
+ - ansible_foobar_not_cached is undefined
+
+ - name: show ansible_foobar fact (FQCN)
+ debug:
+ var: ansible_foobar_fqcn
+
+ - name: assert ansible_foobar is correct value (FQCN)
+ assert:
+ that:
+ - ansible_foobar_fqcn is undefined
+
+ - name: show ansible_foobar_not_cached fact (FQCN)
+ debug:
+ var: ansible_foobar_fqcn_not_cached
+
+ - name: assert ansible_foobar_not_cached is not cached (FQCN)
+ assert:
+ that:
+ - ansible_foobar_fqcn_not_cached is undefined
diff --git a/test/integration/targets/set_stats/aliases b/test/integration/targets/set_stats/aliases
new file mode 100644
index 0000000..2e198a7
--- /dev/null
+++ b/test/integration/targets/set_stats/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller # this is a controller-only action, the module is just for documentation
diff --git a/test/integration/targets/set_stats/runme.sh b/test/integration/targets/set_stats/runme.sh
new file mode 100755
index 0000000..27193dc
--- /dev/null
+++ b/test/integration/targets/set_stats/runme.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+set -eux
+
+export ANSIBLE_SHOW_CUSTOM_STATS=yes
+
+# Simple tests
+ansible-playbook test_simple.yml -i "${INVENTORY_PATH}"
+
+# This playbook does two set_stats calls setting my_int to 10 and 15.
+# The aggregated output should add to 25.
+output=$(ansible-playbook test_aggregate.yml -i "${INVENTORY_PATH}" | grep -c '"my_int": 25')
+test "$output" -eq 1
diff --git a/test/integration/targets/set_stats/test_aggregate.yml b/test/integration/targets/set_stats/test_aggregate.yml
new file mode 100644
index 0000000..7f12895
--- /dev/null
+++ b/test/integration/targets/set_stats/test_aggregate.yml
@@ -0,0 +1,13 @@
+---
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - name: First set_stats
+ set_stats:
+ data:
+ my_int: 10
+
+ - name: Second set_stats
+ set_stats:
+ data:
+ my_int: 15
diff --git a/test/integration/targets/set_stats/test_simple.yml b/test/integration/targets/set_stats/test_simple.yml
new file mode 100644
index 0000000..0f62120
--- /dev/null
+++ b/test/integration/targets/set_stats/test_simple.yml
@@ -0,0 +1,79 @@
+---
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - name: test simple data with defaults
+ set_stats:
+ data:
+ my_int: 42
+ my_string: "foo"
+ register: result
+
+ - name: assert simple data return
+ assert:
+ that:
+ - result is succeeded
+ - not result.changed
+ - '"ansible_stats" in result'
+ - '"data" in result.ansible_stats'
+ - result.ansible_stats.data.my_int == 42
+ - result.ansible_stats.data.my_string == "foo"
+ - '"per_host" in result.ansible_stats'
+ - not result.ansible_stats.per_host
+ - '"aggregate" in result.ansible_stats'
+ - result.ansible_stats.aggregate
+
+ - name: test per_host and aggregate settings
+ set_stats:
+ data:
+ my_int: 42
+ per_host: yes
+ aggregate: no
+ register: result
+
+ - name: assert per_host and aggregate changes
+ assert:
+ that:
+ - result is succeeded
+ - not result.changed
+ - '"ansible_stats" in result'
+ - '"per_host" in result.ansible_stats'
+ - result.ansible_stats.per_host
+ - '"aggregate" in result.ansible_stats'
+ - not result.ansible_stats.aggregate
+
+ - name: Test bad call
+ block:
+ - name: "EXPECTED FAILURE - test invalid data type"
+ set_stats:
+ data:
+ - 1
+ - 2
+
+ - fail:
+ msg: "should not get here"
+ rescue:
+ - assert:
+ that:
+ - ansible_failed_task.name == "EXPECTED FAILURE - test invalid data type"
+ - ansible_failed_result.msg == "The 'data' option needs to be a dictionary/hash"
+
+ - name: Test options from template
+ set_stats:
+ data:
+ my_string: "foo"
+ aggregate: "x"
+
+ - name: Test bad data
+ block:
+ - name: "EXPECTED FAILURE - bad data"
+ set_stats:
+ data:
+ .bad: 1
+ - fail:
+ msg: "should not get here"
+ rescue:
+ - assert:
+ that:
+ - ansible_failed_task.name == "EXPECTED FAILURE - bad data"
+ - ansible_failed_result.msg == "The variable name '.bad' is not valid. Variables must start with a letter or underscore character, and contain only letters, numbers and underscores."
diff --git a/test/integration/targets/setup_cron/defaults/main.yml b/test/integration/targets/setup_cron/defaults/main.yml
new file mode 100644
index 0000000..a6d1965
--- /dev/null
+++ b/test/integration/targets/setup_cron/defaults/main.yml
@@ -0,0 +1 @@
+remote_dir: "{{ remote_tmp_dir }}"
diff --git a/test/integration/targets/setup_cron/meta/main.yml b/test/integration/targets/setup_cron/meta/main.yml
new file mode 100644
index 0000000..1810d4b
--- /dev/null
+++ b/test/integration/targets/setup_cron/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/setup_cron/tasks/main.yml b/test/integration/targets/setup_cron/tasks/main.yml
new file mode 100644
index 0000000..730926d
--- /dev/null
+++ b/test/integration/targets/setup_cron/tasks/main.yml
@@ -0,0 +1,99 @@
+- name: Include distribution specific variables
+ include_vars: "{{ lookup('first_found', search) }}"
+ vars:
+ search:
+ files:
+ - '{{ ansible_distribution | lower }}.yml'
+ - '{{ ansible_os_family | lower }}.yml'
+ - '{{ ansible_system | lower }}.yml'
+ - default.yml
+ paths:
+ - vars
+
+- name: install cron package
+ package:
+ name: '{{ cron_pkg }}'
+ when: cron_pkg | default(false, true)
+ register: cron_package_installed
+ until: cron_package_installed is success
+
+- when: faketime_pkg | default(false, true)
+ block:
+ - name: install faketime packages
+ package:
+ name: '{{ faketime_pkg }}'
+ register: faketime_package_installed
+ until: faketime_package_installed is success
+ when: ansible_distribution != 'Alpine'
+
+ - name: install faketime packages - Alpine
+ # NOTE: The `faketime` package is currently only available in the
+ # NOTE: `edge` branch.
+ # FIXME: If it ever becomes available in the `main` repository for
+ # FIXME: currently tested Alpine versions, the `--repository=...`
+ # FIXME: option can be dropped.
+ command: apk add -U {{ faketime_pkg }} --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing
+ when: ansible_distribution == 'Alpine'
+
+ - name: Find libfaketime path
+ shell: '{{ list_pkg_files }} {{ faketime_pkg }} | grep -F libfaketime.so.1'
+ register: libfaketime_path
+ when: list_pkg_files is defined
+
+ - when: ansible_service_mgr == 'systemd'
+ block:
+ - name: create directory for cron drop-in file
+ file:
+ path: '/etc/systemd/system/{{ cron_service }}.service.d'
+ state: directory
+ owner: root
+ group: root
+ mode: 0755
+
+ - name: Use faketime with cron service
+ copy:
+ content: |-
+ [Service]
+ Environment=LD_PRELOAD={{ libfaketime_path.stdout_lines[0].strip() }}
+ Environment="FAKETIME=+0y x10"
+ Environment=RANDOM_DELAY=0
+ dest: '/etc/systemd/system/{{ cron_service }}.service.d/faketime.conf'
+ owner: root
+ group: root
+ mode: 0644
+
+ - when: ansible_system == 'FreeBSD'
+ name: Use faketime with cron service
+ copy:
+ content: |-
+ cron_env='LD_PRELOAD={{ libfaketime_path.stdout_lines[0].strip() }} FAKETIME="+0y x10"'
+ dest: '/etc/rc.conf.d/cron'
+ owner: root
+ group: wheel
+ mode: 0644
+
+- name: enable cron service
+ service:
+ daemon-reload: "{{ (ansible_service_mgr == 'systemd') | ternary(true, omit) }}"
+ name: '{{ cron_service }}'
+ state: restarted
+ when: ansible_distribution != 'Alpine'
+
+- name: enable cron service - Alpine
+ command: nohup crond
+ environment:
+ FAKETIME: "+0y x10"
+ LD_PRELOAD: "/usr/lib/faketime/libfaketime.so.1"
+ when: ansible_distribution == 'Alpine'
+
+- name: See if /etc/pam.d/crond exists
+ stat:
+ path: /etc/pam.d/crond
+ register: pamd
+
+# https://github.com/lxc/lxc/issues/661#issuecomment-222444916
+# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=726661
+- name: Work around containers not being able to write to /proc/self/loginuid
+ command: sed -i '/pam_loginuid\.so$/ s/required/optional/' /etc/pam.d/crond
+ when:
+ - pamd.stat.exists
diff --git a/test/integration/targets/setup_cron/vars/alpine.yml b/test/integration/targets/setup_cron/vars/alpine.yml
new file mode 100644
index 0000000..37e6fc3
--- /dev/null
+++ b/test/integration/targets/setup_cron/vars/alpine.yml
@@ -0,0 +1 @@
+faketime_pkg: libfaketime
diff --git a/test/integration/targets/setup_cron/vars/debian.yml b/test/integration/targets/setup_cron/vars/debian.yml
new file mode 100644
index 0000000..cd04871
--- /dev/null
+++ b/test/integration/targets/setup_cron/vars/debian.yml
@@ -0,0 +1,3 @@
+cron_pkg: cron
+cron_service: cron
+list_pkg_files: dpkg -L
diff --git a/test/integration/targets/setup_cron/vars/default.yml b/test/integration/targets/setup_cron/vars/default.yml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/setup_cron/vars/default.yml
diff --git a/test/integration/targets/setup_cron/vars/fedora.yml b/test/integration/targets/setup_cron/vars/fedora.yml
new file mode 100644
index 0000000..b80a51b
--- /dev/null
+++ b/test/integration/targets/setup_cron/vars/fedora.yml
@@ -0,0 +1,3 @@
+cron_pkg: cronie
+cron_service: crond
+list_pkg_files: rpm -ql
diff --git a/test/integration/targets/setup_cron/vars/freebsd.yml b/test/integration/targets/setup_cron/vars/freebsd.yml
new file mode 100644
index 0000000..41ed449
--- /dev/null
+++ b/test/integration/targets/setup_cron/vars/freebsd.yml
@@ -0,0 +1,3 @@
+cron_pkg:
+cron_service: cron
+list_pkg_files: pkg info --list-files
diff --git a/test/integration/targets/setup_cron/vars/redhat.yml b/test/integration/targets/setup_cron/vars/redhat.yml
new file mode 100644
index 0000000..2dff13d
--- /dev/null
+++ b/test/integration/targets/setup_cron/vars/redhat.yml
@@ -0,0 +1,4 @@
+cron_pkg: cronie
+cron_service: crond
+faketime_pkg:
+list_pkg_files: rpm -ql
diff --git a/test/integration/targets/setup_cron/vars/suse.yml b/test/integration/targets/setup_cron/vars/suse.yml
new file mode 100644
index 0000000..cd3677a
--- /dev/null
+++ b/test/integration/targets/setup_cron/vars/suse.yml
@@ -0,0 +1,3 @@
+cron_pkg: cron
+cron_service: cron
+list_pkg_files: rpm -ql
diff --git a/test/integration/targets/setup_deb_repo/files/package_specs/stable/foo-1.0.0 b/test/integration/targets/setup_deb_repo/files/package_specs/stable/foo-1.0.0
new file mode 100644
index 0000000..4206fba
--- /dev/null
+++ b/test/integration/targets/setup_deb_repo/files/package_specs/stable/foo-1.0.0
@@ -0,0 +1,10 @@
+Section: misc
+Priority: optional
+Standards-Version: 2.3.3
+
+Package: foo
+Version: 1.0.0
+Section: system
+Maintainer: John Doe <john@doe.com>
+Architecture: all
+Description: Dummy package
diff --git a/test/integration/targets/setup_deb_repo/files/package_specs/stable/foo-1.0.1 b/test/integration/targets/setup_deb_repo/files/package_specs/stable/foo-1.0.1
new file mode 100644
index 0000000..021f4d5
--- /dev/null
+++ b/test/integration/targets/setup_deb_repo/files/package_specs/stable/foo-1.0.1
@@ -0,0 +1,10 @@
+Section: misc
+Priority: optional
+Standards-Version: 2.3.3
+
+Package: foo
+Version: 1.0.1
+Section: system
+Maintainer: John Doe <john@doe.com>
+Architecture: all
+Description: Dummy package
diff --git a/test/integration/targets/setup_deb_repo/files/package_specs/stable/foobar-1.0.0 b/test/integration/targets/setup_deb_repo/files/package_specs/stable/foobar-1.0.0
new file mode 100644
index 0000000..0da0348
--- /dev/null
+++ b/test/integration/targets/setup_deb_repo/files/package_specs/stable/foobar-1.0.0
@@ -0,0 +1,11 @@
+Section: misc
+Priority: optional
+Standards-Version: 2.3.3
+
+Package: foobar
+Version: 1.0.0
+Section: system
+Depends: foo
+Maintainer: John Doe <john@doe.com>
+Architecture: all
+Description: Dummy package
diff --git a/test/integration/targets/setup_deb_repo/files/package_specs/stable/foobar-1.0.1 b/test/integration/targets/setup_deb_repo/files/package_specs/stable/foobar-1.0.1
new file mode 100644
index 0000000..b9fa830
--- /dev/null
+++ b/test/integration/targets/setup_deb_repo/files/package_specs/stable/foobar-1.0.1
@@ -0,0 +1,10 @@
+Section: misc
+Priority: optional
+Standards-Version: 2.3.3
+
+Package: foobar
+Version: 1.0.1
+Section: system
+Maintainer: John Doe <john@doe.com>
+Architecture: all
+Description: Dummy package
diff --git a/test/integration/targets/setup_deb_repo/files/package_specs/testing/foo-2.0.0 b/test/integration/targets/setup_deb_repo/files/package_specs/testing/foo-2.0.0
new file mode 100644
index 0000000..7e835f0
--- /dev/null
+++ b/test/integration/targets/setup_deb_repo/files/package_specs/testing/foo-2.0.0
@@ -0,0 +1,10 @@
+Section: misc
+Priority: optional
+Standards-Version: 2.3.3
+
+Package: foo
+Version: 2.0.0
+Section: system
+Maintainer: John Doe <john@doe.com>
+Architecture: all
+Description: Dummy package
diff --git a/test/integration/targets/setup_deb_repo/files/package_specs/testing/foo-2.0.1 b/test/integration/targets/setup_deb_repo/files/package_specs/testing/foo-2.0.1
new file mode 100644
index 0000000..c6e7b5b
--- /dev/null
+++ b/test/integration/targets/setup_deb_repo/files/package_specs/testing/foo-2.0.1
@@ -0,0 +1,10 @@
+Section: misc
+Priority: optional
+Standards-Version: 2.3.3
+
+Package: foo
+Version: 2.0.1
+Section: system
+Maintainer: John Doe <john@doe.com>
+Architecture: all
+Description: Dummy package
diff --git a/test/integration/targets/setup_deb_repo/meta/main.yml b/test/integration/targets/setup_deb_repo/meta/main.yml
new file mode 100644
index 0000000..1810d4b
--- /dev/null
+++ b/test/integration/targets/setup_deb_repo/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/setup_deb_repo/tasks/main.yml b/test/integration/targets/setup_deb_repo/tasks/main.yml
new file mode 100644
index 0000000..471fb2a
--- /dev/null
+++ b/test/integration/targets/setup_deb_repo/tasks/main.yml
@@ -0,0 +1,75 @@
+- block:
+ - name: Install needed packages
+ apt:
+ name: "{{ item }}"
+ with_items:
+ - dpkg-dev
+ - equivs
+ - libfile-fcntllock-perl # to silence warning by equivs-build
+
+ - set_fact:
+ repodir: /tmp/repo/
+
+ - name: Create repo dirs
+ file:
+ path: "{{ repodir }}/dists/{{ item }}/main/binary-all"
+ state: directory
+ mode: 0755
+ loop:
+ - stable
+ - testing
+
+ - name: Copy package specs to remote
+ copy:
+ src: package_specs
+ dest: "{{ remote_tmp_dir }}"
+
+ - name: Create deb files
+ shell: "find {{ remote_tmp_dir }}/package_specs/{{ item }} -type f -exec equivs-build {} \\;"
+ args:
+ chdir: "{{ repodir }}/dists/{{ item }}/main/binary-all"
+ loop:
+ - stable
+ - testing
+
+ - name: Create repo Packages
+ shell: dpkg-scanpackages --multiversion . /dev/null dists/{{ item }}/main/binary-all/ | gzip -9c > Packages.gz
+ args:
+ chdir: "{{ repodir }}/dists/{{ item }}/main/binary-all"
+ loop:
+ - stable
+ - testing
+
+ - name: Create repo Release
+ copy:
+ content: |
+ Codename: {{ item.0 }}
+ {% for k,v in item.1.items() %}
+ {{ k }}: {{ v }}
+ {% endfor %}
+ dest: "{{ repodir }}/dists/{{ item.0 }}/Release"
+ loop:
+ - [stable, {}]
+ - [testing, {NotAutomatic: "yes", ButAutomaticUpgrades: "yes"}]
+
+ - name: Install the repo
+ apt_repository:
+ repo: deb [trusted=yes arch=all] file:{{ repodir }} {{ item }} main
+ update_cache: false # interferes with task 'Test update_cache 1'
+ loop:
+ - stable
+ - testing
+
+ # Need to uncomment the deb-src for the universe component for build-dep state
+ - name: Ensure deb-src for the universe component
+ lineinfile:
+ path: /etc/apt/sources.list
+ backrefs: True
+ regexp: ^#\s*deb-src http://archive\.ubuntu\.com/ubuntu/ (\w*){{ item }} universe$
+ line: deb-src http://archive.ubuntu.com/ubuntu \1{{ item }} universe
+ state: present
+ with_items:
+ - ''
+ - -updates
+
+ when: ansible_distribution in ['Ubuntu', 'Debian']
diff --git a/test/integration/targets/setup_epel/tasks/main.yml b/test/integration/targets/setup_epel/tasks/main.yml
new file mode 100644
index 0000000..a8593bb
--- /dev/null
+++ b/test/integration/targets/setup_epel/tasks/main.yml
@@ -0,0 +1,10 @@
+- name: Enable RHEL7 extras
+ # EPEL 7 depends on RHEL 7 extras, which is not enabled by default on RHEL.
+ # See: https://docs.fedoraproject.org/en-US/epel/epel-policy/#_policy
+ command: yum-config-manager --enable rhel-7-server-rhui-extras-rpms
+ when: ansible_facts.distribution == 'RedHat' and ansible_facts.distribution_major_version == '7'
+- name: Install EPEL
+ yum:
+ name: https://ci-files.testing.ansible.com/test/integration/targets/setup_epel/epel-release-latest-{{ ansible_distribution_major_version }}.noarch.rpm
+ disable_gpg_check: true
+ when: ansible_facts.distribution in ['RedHat', 'CentOS']
diff --git a/test/integration/targets/setup_gnutar/handlers/main.yml b/test/integration/targets/setup_gnutar/handlers/main.yml
new file mode 100644
index 0000000..d3fa7c2
--- /dev/null
+++ b/test/integration/targets/setup_gnutar/handlers/main.yml
@@ -0,0 +1,6 @@
+- name: uninstall gnu-tar
+ command: brew uninstall gnu-tar
+ become: yes
+ become_user: "{{ brew_stat.stat.pw_name }}"
+ environment:
+ HOMEBREW_NO_AUTO_UPDATE: True
diff --git a/test/integration/targets/setup_gnutar/tasks/main.yml b/test/integration/targets/setup_gnutar/tasks/main.yml
new file mode 100644
index 0000000..b7d841c
--- /dev/null
+++ b/test/integration/targets/setup_gnutar/tasks/main.yml
@@ -0,0 +1,18 @@
+- when: ansible_facts.distribution == 'MacOSX'
+ block:
+ - name: MACOS | Find brew binary
+ command: which brew
+ register: brew_which
+
+ - name: MACOS | Get owner of brew binary
+ stat:
+ path: "{{ brew_which.stdout }}"
+ register: brew_stat
+
+ - command: brew install gnu-tar
+ become: yes
+ become_user: "{{ brew_stat.stat.pw_name }}"
+ environment:
+ HOMEBREW_NO_AUTO_UPDATE: True
+ notify:
+ - uninstall gnu-tar
diff --git a/test/integration/targets/setup_nobody/handlers/main.yml b/test/integration/targets/setup_nobody/handlers/main.yml
new file mode 100644
index 0000000..2d02efb
--- /dev/null
+++ b/test/integration/targets/setup_nobody/handlers/main.yml
@@ -0,0 +1,5 @@
+---
+- name: remove nobody user
+ user:
+ name: nobody
+ state: absent
diff --git a/test/integration/targets/setup_nobody/tasks/main.yml b/test/integration/targets/setup_nobody/tasks/main.yml
new file mode 100644
index 0000000..cc0e4fe
--- /dev/null
+++ b/test/integration/targets/setup_nobody/tasks/main.yml
@@ -0,0 +1,7 @@
+---
+- name: create nobody user
+ user:
+ name: nobody
+ create_home: no
+ state: present
+ notify: remove nobody user
diff --git a/test/integration/targets/setup_paramiko/aliases b/test/integration/targets/setup_paramiko/aliases
new file mode 100644
index 0000000..c49be25
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/aliases
@@ -0,0 +1 @@
+needs/target/setup_remote_tmp_dir
diff --git a/test/integration/targets/setup_paramiko/constraints.txt b/test/integration/targets/setup_paramiko/constraints.txt
new file mode 100644
index 0000000..c502ba0
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/constraints.txt
@@ -0,0 +1 @@
+cryptography >= 2.5, < 3.4
diff --git a/test/integration/targets/setup_paramiko/install-Alpine-3-python-3.yml b/test/integration/targets/setup_paramiko/install-Alpine-3-python-3.yml
new file mode 100644
index 0000000..f16d9b5
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/install-Alpine-3-python-3.yml
@@ -0,0 +1,9 @@
+- name: Setup remote constraints
+ include_tasks: setup-remote-constraints.yml
+- name: Install Paramiko for Python 3 on Alpine
+ pip: # no apk package manager in core, just use pip
+ name: paramiko
+ extra_args: "-c {{ remote_constraints }}"
+ environment:
+ # Not sure why this fixes the test, but it does.
+ SETUPTOOLS_USE_DISTUTILS: stdlib
diff --git a/test/integration/targets/setup_paramiko/install-CentOS-6-python-2.yml b/test/integration/targets/setup_paramiko/install-CentOS-6-python-2.yml
new file mode 100644
index 0000000..0c7b9e8
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/install-CentOS-6-python-2.yml
@@ -0,0 +1,3 @@
+- name: Install Paramiko for Python 2 on CentOS 6
+ yum:
+ name: python-paramiko
diff --git a/test/integration/targets/setup_paramiko/install-Darwin-python-3.yml b/test/integration/targets/setup_paramiko/install-Darwin-python-3.yml
new file mode 100644
index 0000000..8926fe3
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/install-Darwin-python-3.yml
@@ -0,0 +1,9 @@
+- name: Setup remote constraints
+ include_tasks: setup-remote-constraints.yml
+- name: Install Paramiko for Python 3 on MacOS
+ pip: # no homebrew package manager in core, just use pip
+ name: paramiko
+ extra_args: "-c {{ remote_constraints }}"
+ environment:
+ # Not sure why this fixes the test, but it does.
+ SETUPTOOLS_USE_DISTUTILS: stdlib
diff --git a/test/integration/targets/setup_paramiko/install-Fedora-35-python-3.yml b/test/integration/targets/setup_paramiko/install-Fedora-35-python-3.yml
new file mode 100644
index 0000000..bbe97a9
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/install-Fedora-35-python-3.yml
@@ -0,0 +1,9 @@
+- name: Install Paramiko and crypto policies scripts
+ dnf:
+ name:
+ - crypto-policies-scripts
+ - python3-paramiko
+ install_weak_deps: no
+
+- name: Drop the crypto-policy to LEGACY for these tests
+ command: update-crypto-policies --set LEGACY
diff --git a/test/integration/targets/setup_paramiko/install-FreeBSD-python-3.yml b/test/integration/targets/setup_paramiko/install-FreeBSD-python-3.yml
new file mode 100644
index 0000000..f737fe3
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/install-FreeBSD-python-3.yml
@@ -0,0 +1,8 @@
+- name: Setup remote constraints
+ include_tasks: setup-remote-constraints.yml
+- name: Install Paramiko for Python 3 on FreeBSD
+ pip: # no package in pkg, just use pip
+ name: paramiko
+ extra_args: "-c {{ remote_constraints }}"
+ environment:
+ SETUPTOOLS_USE_DISTUTILS: stdlib
diff --git a/test/integration/targets/setup_paramiko/install-RedHat-8-python-3.yml b/test/integration/targets/setup_paramiko/install-RedHat-8-python-3.yml
new file mode 100644
index 0000000..55677f2
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/install-RedHat-8-python-3.yml
@@ -0,0 +1,8 @@
+- name: Setup remote constraints
+ include_tasks: setup-remote-constraints.yml
+- name: Install Paramiko for Python 3 on RHEL 8
+ pip: # no python3-paramiko package exists for RHEL 8
+ name: paramiko
+ extra_args: "-c {{ remote_constraints }}"
+ environment:
+ SETUPTOOLS_USE_DISTUTILS: stdlib
diff --git a/test/integration/targets/setup_paramiko/install-RedHat-9-python-3.yml b/test/integration/targets/setup_paramiko/install-RedHat-9-python-3.yml
new file mode 100644
index 0000000..ca39155
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/install-RedHat-9-python-3.yml
@@ -0,0 +1,9 @@
+- name: Setup remote constraints
+ include_tasks: setup-remote-constraints.yml
+- name: Install Paramiko for Python 3 on RHEL 9
+ pip: # no python3-paramiko package exists for RHEL 9
+ name: paramiko
+ extra_args: "-c {{ remote_constraints }}"
+
+- name: Drop the crypto-policy to LEGACY for these tests
+ command: update-crypto-policies --set LEGACY
diff --git a/test/integration/targets/setup_paramiko/install-Ubuntu-16-python-2.yml b/test/integration/targets/setup_paramiko/install-Ubuntu-16-python-2.yml
new file mode 100644
index 0000000..8f76074
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/install-Ubuntu-16-python-2.yml
@@ -0,0 +1,3 @@
+- name: Install Paramiko for Python 2 on Ubuntu 16
+ apt:
+ name: python-paramiko
diff --git a/test/integration/targets/setup_paramiko/install-fail.yml b/test/integration/targets/setup_paramiko/install-fail.yml
new file mode 100644
index 0000000..b4ba464
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/install-fail.yml
@@ -0,0 +1,7 @@
+- name: Install Paramiko
+ fail:
+ msg: "Install of Paramiko on distribution '{{ ansible_distribution }}' with major version '{{ ansible_distribution_major_version }}'
+ with package manager '{{ ansible_pkg_mgr }}' on Python {{ ansible_python.version.major }} has not been implemented.
+ Use native OS packages if available, otherwise use pip.
+ Be sure to uninstall automatically installed dependencies when possible.
+ Do not implement a generic fallback to pip, as that would allow distributions not yet configured to go undetected."
diff --git a/test/integration/targets/setup_paramiko/install-python-2.yml b/test/integration/targets/setup_paramiko/install-python-2.yml
new file mode 100644
index 0000000..be337a1
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/install-python-2.yml
@@ -0,0 +1,3 @@
+- name: Install Paramiko for Python 2
+ package:
+ name: python2-paramiko
diff --git a/test/integration/targets/setup_paramiko/install-python-3.yml b/test/integration/targets/setup_paramiko/install-python-3.yml
new file mode 100644
index 0000000..ac2a1a2
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/install-python-3.yml
@@ -0,0 +1,3 @@
+- name: Install Paramiko for Python 3
+ package:
+ name: python3-paramiko
diff --git a/test/integration/targets/setup_paramiko/install.yml b/test/integration/targets/setup_paramiko/install.yml
new file mode 100644
index 0000000..1868987
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/install.yml
@@ -0,0 +1,19 @@
+- hosts: localhost
+ tasks:
+ - name: Detect Paramiko
+ detect_paramiko:
+ register: detect_paramiko
+ - name: Persist Result
+ copy:
+ content: "{{ detect_paramiko }}"
+ dest: "{{ lookup('env', 'OUTPUT_DIR') }}/detect-paramiko.json"
+ - name: Install Paramiko
+ when: not detect_paramiko.found
+ include_tasks: "{{ item }}"
+ with_first_found:
+ - "install-{{ ansible_distribution }}-{{ ansible_distribution_version }}-python-{{ ansible_python.version.major }}.yml"
+ - "install-{{ ansible_distribution }}-{{ ansible_distribution_major_version }}-python-{{ ansible_python.version.major }}.yml"
+ - "install-{{ ansible_os_family }}-{{ ansible_distribution_major_version }}-python-{{ ansible_python.version.major }}.yml"
+ - "install-{{ ansible_os_family }}-python-{{ ansible_python.version.major }}.yml"
+ - "install-python-{{ ansible_python.version.major }}.yml"
+ - "install-fail.yml"
diff --git a/test/integration/targets/setup_paramiko/inventory b/test/integration/targets/setup_paramiko/inventory
new file mode 100644
index 0000000..8618c72
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/inventory
@@ -0,0 +1 @@
+localhost ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
diff --git a/test/integration/targets/setup_paramiko/library/detect_paramiko.py b/test/integration/targets/setup_paramiko/library/detect_paramiko.py
new file mode 100644
index 0000000..e3a8158
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/library/detect_paramiko.py
@@ -0,0 +1,31 @@
+#!/usr/bin/python
+"""Ansible module to detect the presence of both the normal and Ansible-specific versions of Paramiko."""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+
+try:
+ import paramiko
+except ImportError:
+ paramiko = None
+
+try:
+ import ansible_paramiko
+except ImportError:
+ ansible_paramiko = None
+
+
+def main():
+ module = AnsibleModule(argument_spec={})
+ module.exit_json(**dict(
+ found=bool(paramiko or ansible_paramiko),
+ paramiko=bool(paramiko),
+ ansible_paramiko=bool(ansible_paramiko),
+ ))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/setup_paramiko/setup-remote-constraints.yml b/test/integration/targets/setup_paramiko/setup-remote-constraints.yml
new file mode 100644
index 0000000..a86d477
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/setup-remote-constraints.yml
@@ -0,0 +1,12 @@
+- name: Setup remote temporary directory
+ include_role:
+ name: setup_remote_tmp_dir
+
+- name: Record constraints.txt path on remote host
+ set_fact:
+ remote_constraints: "{{ remote_tmp_dir }}/constraints.txt"
+
+- name: Copy constraints.txt to remote host
+ copy:
+ src: "constraints.txt"
+ dest: "{{ remote_constraints }}"
diff --git a/test/integration/targets/setup_paramiko/setup.sh b/test/integration/targets/setup_paramiko/setup.sh
new file mode 100644
index 0000000..9f7afcb
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/setup.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+# Usage: source ../setup_paramiko/setup.sh
+
+set -eux
+
+source virtualenv.sh # for pip installs, if needed, otherwise unused
+ANSIBLE_ROLES_PATH=../ ansible-playbook ../setup_paramiko/install.yml -i ../setup_paramiko/inventory "$@"
+trap 'ansible-playbook ../setup_paramiko/uninstall.yml -i ../setup_paramiko/inventory "$@"' EXIT
diff --git a/test/integration/targets/setup_paramiko/uninstall-Alpine-3-python-3.yml b/test/integration/targets/setup_paramiko/uninstall-Alpine-3-python-3.yml
new file mode 100644
index 0000000..e9dcc62
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/uninstall-Alpine-3-python-3.yml
@@ -0,0 +1,4 @@
+- name: Uninstall Paramiko for Python 3 on Alpine
+ pip:
+ name: paramiko
+ state: absent
diff --git a/test/integration/targets/setup_paramiko/uninstall-Darwin-python-3.yml b/test/integration/targets/setup_paramiko/uninstall-Darwin-python-3.yml
new file mode 100644
index 0000000..69a68e4
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/uninstall-Darwin-python-3.yml
@@ -0,0 +1,4 @@
+- name: Uninstall Paramiko for Python 3 on MacOS
+ pip:
+ name: paramiko
+ state: absent
diff --git a/test/integration/targets/setup_paramiko/uninstall-Fedora-35-python-3.yml b/test/integration/targets/setup_paramiko/uninstall-Fedora-35-python-3.yml
new file mode 100644
index 0000000..6d0e9a1
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/uninstall-Fedora-35-python-3.yml
@@ -0,0 +1,5 @@
+- name: Revert the crypto-policy back to DEFAULT
+ command: update-crypto-policies --set DEFAULT
+
+- name: Uninstall Paramiko and crypto policies scripts using dnf history undo
+ command: dnf history undo last --assumeyes
diff --git a/test/integration/targets/setup_paramiko/uninstall-FreeBSD-python-3.yml b/test/integration/targets/setup_paramiko/uninstall-FreeBSD-python-3.yml
new file mode 100644
index 0000000..d3d3739
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/uninstall-FreeBSD-python-3.yml
@@ -0,0 +1,4 @@
+- name: Uninstall Paramiko for Python 3 on FreeBSD
+ pip:
+ name: paramiko
+ state: absent
diff --git a/test/integration/targets/setup_paramiko/uninstall-RedHat-8-python-3.yml b/test/integration/targets/setup_paramiko/uninstall-RedHat-8-python-3.yml
new file mode 100644
index 0000000..d3a9493
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/uninstall-RedHat-8-python-3.yml
@@ -0,0 +1,4 @@
+- name: Uninstall Paramiko for Python 3 on RHEL 8
+ pip: # no python3-paramiko package exists for RHEL 8
+ name: paramiko
+ state: absent
diff --git a/test/integration/targets/setup_paramiko/uninstall-RedHat-9-python-3.yml b/test/integration/targets/setup_paramiko/uninstall-RedHat-9-python-3.yml
new file mode 100644
index 0000000..f46ec55
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/uninstall-RedHat-9-python-3.yml
@@ -0,0 +1,7 @@
+- name: Uninstall Paramiko for Python 3 on RHEL 9
+ pip: # no python3-paramiko package exists for RHEL 9
+ name: paramiko
+ state: absent
+
+- name: Revert the crypto-policy back to DEFAULT
+ command: update-crypto-policies --set DEFAULT
diff --git a/test/integration/targets/setup_paramiko/uninstall-apt-python-2.yml b/test/integration/targets/setup_paramiko/uninstall-apt-python-2.yml
new file mode 100644
index 0000000..507d94c
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/uninstall-apt-python-2.yml
@@ -0,0 +1,5 @@
+- name: Uninstall Paramiko for Python 2 using apt
+ apt:
+ name: python-paramiko
+ state: absent
+ autoremove: yes
diff --git a/test/integration/targets/setup_paramiko/uninstall-apt-python-3.yml b/test/integration/targets/setup_paramiko/uninstall-apt-python-3.yml
new file mode 100644
index 0000000..d51fc92
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/uninstall-apt-python-3.yml
@@ -0,0 +1,5 @@
+- name: Uninstall Paramiko for Python 3 using apt
+ apt:
+ name: python3-paramiko
+ state: absent
+ autoremove: yes
diff --git a/test/integration/targets/setup_paramiko/uninstall-dnf.yml b/test/integration/targets/setup_paramiko/uninstall-dnf.yml
new file mode 100644
index 0000000..a56c0dd
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/uninstall-dnf.yml
@@ -0,0 +1,2 @@
+- name: Uninstall Paramiko using dnf history undo
+ command: dnf history undo last --assumeyes
diff --git a/test/integration/targets/setup_paramiko/uninstall-fail.yml b/test/integration/targets/setup_paramiko/uninstall-fail.yml
new file mode 100644
index 0000000..bc5e12f
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/uninstall-fail.yml
@@ -0,0 +1,7 @@
+- name: Uninstall Paramiko
+ fail:
+ msg: "Uninstall of Paramiko on distribution '{{ ansible_distribution }}' with major version '{{ ansible_distribution_major_version }}'
+ with package manager '{{ ansible_pkg_mgr }}' on Python {{ ansible_python.version.major }} has not been implemented.
+ Use native OS packages if available, otherwise use pip.
+ Be sure to uninstall automatically installed dependencies when possible.
+ Do not implement a generic fallback to pip, as that would allow distributions not yet configured to go undetected."
diff --git a/test/integration/targets/setup_paramiko/uninstall-yum.yml b/test/integration/targets/setup_paramiko/uninstall-yum.yml
new file mode 100644
index 0000000..de1c21f
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/uninstall-yum.yml
@@ -0,0 +1,2 @@
+- name: Uninstall Paramiko using yum history undo
+ command: yum history undo last --assumeyes
diff --git a/test/integration/targets/setup_paramiko/uninstall-zypper-python-2.yml b/test/integration/targets/setup_paramiko/uninstall-zypper-python-2.yml
new file mode 100644
index 0000000..adb26e5
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/uninstall-zypper-python-2.yml
@@ -0,0 +1,2 @@
+- name: Uninstall Paramiko for Python 2 using zypper
+ command: zypper --quiet --non-interactive remove --clean-deps python2-paramiko
diff --git a/test/integration/targets/setup_paramiko/uninstall-zypper-python-3.yml b/test/integration/targets/setup_paramiko/uninstall-zypper-python-3.yml
new file mode 100644
index 0000000..339be6f
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/uninstall-zypper-python-3.yml
@@ -0,0 +1,2 @@
+- name: Uninstall Paramiko for Python 3 using zypper
+ command: zypper --quiet --non-interactive remove --clean-deps python3-paramiko
diff --git a/test/integration/targets/setup_paramiko/uninstall.yml b/test/integration/targets/setup_paramiko/uninstall.yml
new file mode 100644
index 0000000..e517a2b
--- /dev/null
+++ b/test/integration/targets/setup_paramiko/uninstall.yml
@@ -0,0 +1,21 @@
+- hosts: localhost
+ vars:
+ detect_paramiko: '{{ lookup("file", lookup("env", "OUTPUT_DIR") + "/detect-paramiko.json") | from_json }}'
+ tasks:
+ - name: Uninstall Paramiko and Verify Results
+ when: not detect_paramiko.found
+ block:
+ - name: Uninstall Paramiko
+ include_tasks: "{{ item }}"
+ with_first_found:
+ - "uninstall-{{ ansible_distribution }}-{{ ansible_distribution_version }}-python-{{ ansible_python.version.major }}.yml"
+ - "uninstall-{{ ansible_distribution }}-{{ ansible_distribution_major_version }}-python-{{ ansible_python.version.major }}.yml"
+ - "uninstall-{{ ansible_os_family }}-{{ ansible_distribution_major_version }}-python-{{ ansible_python.version.major }}.yml"
+ - "uninstall-{{ ansible_os_family }}-python-{{ ansible_python.version.major }}.yml"
+ - "uninstall-{{ ansible_pkg_mgr }}-python-{{ ansible_python.version.major }}.yml"
+ - "uninstall-{{ ansible_pkg_mgr }}.yml"
+ - "uninstall-fail.yml"
+ - name: Verify Paramiko was uninstalled
+ detect_paramiko:
+ register: detect_paramiko
+ failed_when: detect_paramiko.found
diff --git a/test/integration/targets/setup_passlib/tasks/main.yml b/test/integration/targets/setup_passlib/tasks/main.yml
new file mode 100644
index 0000000..e4cd0d0
--- /dev/null
+++ b/test/integration/targets/setup_passlib/tasks/main.yml
@@ -0,0 +1,4 @@
+- name: Install passlib
+ pip:
+ name: passlib
+ state: present
diff --git a/test/integration/targets/setup_pexpect/files/constraints.txt b/test/integration/targets/setup_pexpect/files/constraints.txt
new file mode 100644
index 0000000..c78ecda
--- /dev/null
+++ b/test/integration/targets/setup_pexpect/files/constraints.txt
@@ -0,0 +1,2 @@
+pexpect == 4.8.0
+ptyprocess < 0.7.0 ; python_version < '2.7' # ptyprocess >= 0.7.0 not compatible with Python 2.6
diff --git a/test/integration/targets/setup_pexpect/meta/main.yml b/test/integration/targets/setup_pexpect/meta/main.yml
new file mode 100644
index 0000000..1810d4b
--- /dev/null
+++ b/test/integration/targets/setup_pexpect/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/setup_pexpect/tasks/main.yml b/test/integration/targets/setup_pexpect/tasks/main.yml
new file mode 100644
index 0000000..84b7bd1
--- /dev/null
+++ b/test/integration/targets/setup_pexpect/tasks/main.yml
@@ -0,0 +1,19 @@
+- name: Copy constraints file
+ copy:
+ src: constraints.txt
+ dest: "{{ remote_tmp_dir }}/pexpect-constraints.txt"
+
+- name: Install pexpect with --user
+ pip:
+ name: pexpect
+ extra_args: '--user --constraint "{{ remote_tmp_dir }}/pexpect-constraints.txt"'
+ state: present
+ ignore_errors: yes # fails when inside a virtual environment
+ register: pip_user
+
+- name: Install pexpect
+ pip:
+ name: pexpect
+ extra_args: '--constraint "{{ remote_tmp_dir }}/pexpect-constraints.txt"'
+ state: present
+ when: pip_user is failed
diff --git a/test/integration/targets/setup_remote_constraints/aliases b/test/integration/targets/setup_remote_constraints/aliases
new file mode 100644
index 0000000..18cc100
--- /dev/null
+++ b/test/integration/targets/setup_remote_constraints/aliases
@@ -0,0 +1 @@
+needs/file/test/lib/ansible_test/_data/requirements/constraints.txt
diff --git a/test/integration/targets/setup_remote_constraints/meta/main.yml b/test/integration/targets/setup_remote_constraints/meta/main.yml
new file mode 100644
index 0000000..1810d4b
--- /dev/null
+++ b/test/integration/targets/setup_remote_constraints/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/setup_remote_constraints/tasks/main.yml b/test/integration/targets/setup_remote_constraints/tasks/main.yml
new file mode 100644
index 0000000..eee09cc
--- /dev/null
+++ b/test/integration/targets/setup_remote_constraints/tasks/main.yml
@@ -0,0 +1,8 @@
+- name: record constraints.txt path on remote host
+ set_fact:
+ remote_constraints: "{{ remote_tmp_dir }}/constraints.txt"
+
+- name: copy constraints.txt to remote host
+ copy:
+ src: "{{ role_path }}/../../../lib/ansible_test/_data/requirements/constraints.txt"
+ dest: "{{ remote_constraints }}"
diff --git a/test/integration/targets/setup_remote_tmp_dir/defaults/main.yml b/test/integration/targets/setup_remote_tmp_dir/defaults/main.yml
new file mode 100644
index 0000000..3375fdf
--- /dev/null
+++ b/test/integration/targets/setup_remote_tmp_dir/defaults/main.yml
@@ -0,0 +1,2 @@
+setup_remote_tmp_dir_skip_cleanup: no
+setup_remote_tmp_dir_cache_path: no
diff --git a/test/integration/targets/setup_remote_tmp_dir/handlers/main.yml b/test/integration/targets/setup_remote_tmp_dir/handlers/main.yml
new file mode 100644
index 0000000..3c5b14f
--- /dev/null
+++ b/test/integration/targets/setup_remote_tmp_dir/handlers/main.yml
@@ -0,0 +1,7 @@
+- name: delete temporary directory
+ include_tasks: default-cleanup.yml
+ when: not setup_remote_tmp_dir_skip_cleanup | bool
+
+- name: delete temporary directory (windows)
+ include_tasks: windows-cleanup.yml
+ when: not setup_remote_tmp_dir_skip_cleanup | bool
diff --git a/test/integration/targets/setup_remote_tmp_dir/tasks/default-cleanup.yml b/test/integration/targets/setup_remote_tmp_dir/tasks/default-cleanup.yml
new file mode 100644
index 0000000..39872d7
--- /dev/null
+++ b/test/integration/targets/setup_remote_tmp_dir/tasks/default-cleanup.yml
@@ -0,0 +1,5 @@
+- name: delete temporary directory
+ file:
+ path: "{{ remote_tmp_dir }}"
+ state: absent
+ no_log: yes
diff --git a/test/integration/targets/setup_remote_tmp_dir/tasks/default.yml b/test/integration/targets/setup_remote_tmp_dir/tasks/default.yml
new file mode 100644
index 0000000..3be42ef
--- /dev/null
+++ b/test/integration/targets/setup_remote_tmp_dir/tasks/default.yml
@@ -0,0 +1,12 @@
+- name: create temporary directory
+ tempfile:
+ state: directory
+ suffix: .test
+ register: remote_tmp_dir
+ notify:
+ - delete temporary directory
+
+- name: record temporary directory
+ set_fact:
+ remote_tmp_dir: "{{ remote_tmp_dir.path }}"
+ cacheable: "{{ setup_remote_tmp_dir_cache_path | bool }}"
diff --git a/test/integration/targets/setup_remote_tmp_dir/tasks/main.yml b/test/integration/targets/setup_remote_tmp_dir/tasks/main.yml
new file mode 100644
index 0000000..f8df391
--- /dev/null
+++ b/test/integration/targets/setup_remote_tmp_dir/tasks/main.yml
@@ -0,0 +1,10 @@
+- name: make sure we have the ansible_os_family and ansible_distribution_version facts
+ setup:
+ gather_subset: distribution
+ when: ansible_facts == {}
+
+- include_tasks: "{{ lookup('first_found', files)}}"
+ vars:
+ files:
+ - "{{ ansible_os_family | lower }}.yml"
+ - "default.yml"
diff --git a/test/integration/targets/setup_remote_tmp_dir/tasks/windows-cleanup.yml b/test/integration/targets/setup_remote_tmp_dir/tasks/windows-cleanup.yml
new file mode 100644
index 0000000..1936b61
--- /dev/null
+++ b/test/integration/targets/setup_remote_tmp_dir/tasks/windows-cleanup.yml
@@ -0,0 +1,4 @@
+- name: delete temporary directory (windows)
+ win_file:
+ path: "{{ remote_tmp_dir }}"
+ state: absent
diff --git a/test/integration/targets/setup_remote_tmp_dir/tasks/windows.yml b/test/integration/targets/setup_remote_tmp_dir/tasks/windows.yml
new file mode 100644
index 0000000..afedc4e
--- /dev/null
+++ b/test/integration/targets/setup_remote_tmp_dir/tasks/windows.yml
@@ -0,0 +1,11 @@
+- name: create temporary directory
+ win_tempfile:
+ state: directory
+ suffix: .test
+ register: remote_tmp_dir
+ notify:
+ - delete temporary directory (windows)
+
+- name: record temporary directory
+ set_fact:
+ remote_tmp_dir: "{{ remote_tmp_dir.path }}"
diff --git a/test/integration/targets/setup_rpm_repo/aliases b/test/integration/targets/setup_rpm_repo/aliases
new file mode 100644
index 0000000..65e8315
--- /dev/null
+++ b/test/integration/targets/setup_rpm_repo/aliases
@@ -0,0 +1 @@
+needs/target/setup_epel
diff --git a/test/integration/targets/setup_rpm_repo/defaults/main.yml b/test/integration/targets/setup_rpm_repo/defaults/main.yml
new file mode 100644
index 0000000..19c033b
--- /dev/null
+++ b/test/integration/targets/setup_rpm_repo/defaults/main.yml
@@ -0,0 +1 @@
+install_repos: yes
diff --git a/test/integration/targets/setup_rpm_repo/files/comps.xml b/test/integration/targets/setup_rpm_repo/files/comps.xml
new file mode 100644
index 0000000..e939182
--- /dev/null
+++ b/test/integration/targets/setup_rpm_repo/files/comps.xml
@@ -0,0 +1,36 @@
+<!DOCTYPE comps PUBLIC "-//Red Hat, Inc.//DTD Comps info//EN" "comps.dtd">
+<comps>
+ <group>
+ <id>customgroup</id>
+ <name>Custom Group</name>
+ <description></description>
+ <default>false</default>
+ <uservisible>true</uservisible>
+ <display_order>1024</display_order>
+ <packagelist>
+ <packagereq type="mandatory">dinginessentail</packagereq>
+ </packagelist>
+ </group>
+
+ <group>
+ <id>customenvgroup</id>
+ <name>Custom Environment Group</name>
+ <description></description>
+ <default>false</default>
+ <uservisible>false</uservisible>
+ <display_order>1024</display_order>
+ <packagelist>
+ <packagereq type="mandatory">landsidescalping</packagereq>
+ </packagelist>
+ </group>
+
+ <environment>
+ <id>customenvgroup-environment</id>
+ <name>Custom Environment Group</name>
+ <description></description>
+ <display_order>1024</display_order>
+ <grouplist>
+ <groupid>customenvgroup</groupid>
+ </grouplist>
+ </environment>
+</comps>
diff --git a/test/integration/targets/setup_rpm_repo/handlers/main.yml b/test/integration/targets/setup_rpm_repo/handlers/main.yml
new file mode 100644
index 0000000..a0af3c9
--- /dev/null
+++ b/test/integration/targets/setup_rpm_repo/handlers/main.yml
@@ -0,0 +1,5 @@
+- name: remove repos
+ yum_repository:
+ state: absent
+ name: "{{ item }}"
+ loop: "{{ repos }}"
diff --git a/test/integration/targets/setup_rpm_repo/library/create_repo.py b/test/integration/targets/setup_rpm_repo/library/create_repo.py
new file mode 100644
index 0000000..e6a61ba
--- /dev/null
+++ b/test/integration/targets/setup_rpm_repo/library/create_repo.py
@@ -0,0 +1,125 @@
+#!/usr/bin/python
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import subprocess
+import sys
+import tempfile
+
+from collections import namedtuple
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.respawn import has_respawned, probe_interpreters_for_module, respawn_module
+
+HAS_RPMFLUFF = True
+can_use_rpm_weak_deps = None
+try:
+ from rpmfluff import SimpleRpmBuild
+ from rpmfluff import YumRepoBuild
+except ImportError:
+ try:
+ from rpmfluff.rpmbuild import SimpleRpmBuild
+ from rpmfluff.yumrepobuild import YumRepoBuild
+ except ImportError:
+ HAS_RPMFLUFF = False
+
+can_use_rpm_weak_deps = None
+if HAS_RPMFLUFF:
+ try:
+ from rpmfluff import can_use_rpm_weak_deps
+ except ImportError:
+ try:
+ from rpmfluff.utils import can_use_rpm_weak_deps
+ except ImportError:
+ pass
+
+
+RPM = namedtuple('RPM', ['name', 'version', 'release', 'epoch', 'recommends', 'arch'])
+
+
+SPECS = [
+ RPM('dinginessentail', '1.0', '1', None, None, None),
+ RPM('dinginessentail', '1.0', '2', '1', None, None),
+ RPM('dinginessentail', '1.1', '1', '1', None, None),
+ RPM('dinginessentail-olive', '1.0', '1', None, None, None),
+ RPM('dinginessentail-olive', '1.1', '1', None, None, None),
+ RPM('landsidescalping', '1.0', '1', None, None, None),
+ RPM('landsidescalping', '1.1', '1', None, None, None),
+ RPM('dinginessentail-with-weak-dep', '1.0', '1', None, ['dinginessentail-weak-dep'], None),
+ RPM('dinginessentail-weak-dep', '1.0', '1', None, None, None),
+ RPM('noarchfake', '1.0', '1', None, None, 'noarch'),
+]
+
+
+def create_repo(arch='x86_64'):
+ pkgs = []
+ for spec in SPECS:
+ pkg = SimpleRpmBuild(spec.name, spec.version, spec.release, [spec.arch or arch])
+ pkg.epoch = spec.epoch
+
+ if spec.recommends:
+ # Skip packages that require weak deps but an older version of RPM is being used
+ if not can_use_rpm_weak_deps or not can_use_rpm_weak_deps():
+ continue
+
+ for recommend in spec.recommends:
+ pkg.add_recommends(recommend)
+
+ pkgs.append(pkg)
+
+ # HACK: EPEL6 version of rpmfluff can't do multi-arch packaging, so we'll just build separately and copy
+ # the noarch stuff in, since we don't currently care about the repodata for noarch
+ if sys.version_info[0:2] == (2, 6):
+ noarch_repo = YumRepoBuild([p for p in pkgs if 'noarch' in p.get_build_archs()])
+ noarch_repo.make('noarch')
+
+ repo = YumRepoBuild([p for p in pkgs if arch in p.get_build_archs()])
+ repo.make(arch)
+
+ subprocess.call("cp {0}/*.rpm {1}".format(noarch_repo.repoDir, repo.repoDir), shell=True)
+ else:
+ repo = YumRepoBuild(pkgs)
+ repo.make(arch, 'noarch')
+
+ for pkg in pkgs:
+ pkg.clean()
+
+ return repo.repoDir
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec={
+ 'arch': {'required': True},
+ 'tempdir': {'type': 'path'},
+ }
+ )
+
+ if not HAS_RPMFLUFF:
+ system_interpreters = ['/usr/libexec/platform-python', '/usr/bin/python3', '/usr/bin/python']
+
+ interpreter = probe_interpreters_for_module(system_interpreters, 'rpmfluff')
+
+ if not interpreter or has_respawned():
+ module.fail_json('unable to find rpmfluff; tried {0}'.format(system_interpreters))
+
+ respawn_module(interpreter)
+
+ arch = module.params['arch']
+ tempdir = module.params['tempdir']
+
+ # Save current temp dir so we can set it back later
+ original_tempdir = tempfile.tempdir
+ tempfile.tempdir = tempdir
+
+ try:
+ repo_dir = create_repo(arch)
+ finally:
+ tempfile.tempdir = original_tempdir
+
+ module.exit_json(repo_dir=repo_dir, tmpfile=tempfile.gettempdir())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/test/integration/targets/setup_rpm_repo/meta/main.yml b/test/integration/targets/setup_rpm_repo/meta/main.yml
new file mode 100644
index 0000000..1810d4b
--- /dev/null
+++ b/test/integration/targets/setup_rpm_repo/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/setup_rpm_repo/tasks/main.yml b/test/integration/targets/setup_rpm_repo/tasks/main.yml
new file mode 100644
index 0000000..be20078
--- /dev/null
+++ b/test/integration/targets/setup_rpm_repo/tasks/main.yml
@@ -0,0 +1,100 @@
+- block:
+ - name: Install epel repo which is missing on rhel-7 and is needed for rpmfluff
+ include_role:
+ name: setup_epel
+ when:
+ - ansible_distribution in ['RedHat', 'CentOS']
+ - ansible_distribution_major_version is version('7', '==')
+
+ - name: Include distribution specific variables
+ include_vars: "{{ lookup('first_found', params) }}"
+ vars:
+ params:
+ files:
+ - "{{ ansible_facts.distribution }}-{{ ansible_facts.distribution_version }}.yml"
+ - "{{ ansible_facts.os_family }}-{{ ansible_facts.distribution_major_version }}.yml"
+ - "{{ ansible_facts.distribution }}.yml"
+ - "{{ ansible_facts.os_family }}.yml"
+ - default.yml
+ paths:
+ - "{{ role_path }}/vars"
+
+ - name: Install rpmfluff and deps
+ action: "{{ ansible_facts.pkg_mgr }}"
+ args:
+ name: "{{ rpm_repo_packages }}"
+
+ - name: Install rpmfluff via pip
+ pip:
+ name: rpmfluff
+ when: ansible_facts.os_family == 'RedHat' and ansible_distribution_major_version is version('9', '==')
+
+ - set_fact:
+ repos:
+ - "fake-{{ ansible_architecture }}"
+ - "fake-i686"
+ - "fake-ppc64"
+ changed_when: yes
+ notify: remove repos
+
+ - name: Create RPMs and put them into a repo
+ create_repo:
+ arch: "{{ ansible_architecture }}"
+ tempdir: "{{ remote_tmp_dir }}"
+ register: repo
+
+ - set_fact:
+ repodir: "{{ repo.repo_dir }}"
+
+ - name: Install the repo
+ yum_repository:
+ name: "fake-{{ ansible_architecture }}"
+ description: "fake-{{ ansible_architecture }}"
+ baseurl: "file://{{ repodir }}"
+ gpgcheck: no
+ when: install_repos | bool
+
+ - name: Copy comps.xml file
+ copy:
+ src: comps.xml
+ dest: "{{ repodir }}"
+ register: repodir_comps
+
+ - name: Register comps.xml on repo
+ command: createrepo -g {{ repodir_comps.dest | quote }} {{ repodir | quote }}
+
+ - name: Create RPMs and put them into a repo (i686)
+ create_repo:
+ arch: i686
+ tempdir: "{{ remote_tmp_dir }}"
+ register: repo_i686
+
+ - set_fact:
+ repodir_i686: "{{ repo_i686.repo_dir }}"
+
+ - name: Install the repo (i686)
+ yum_repository:
+ name: "fake-i686"
+ description: "fake-i686"
+ baseurl: "file://{{ repodir_i686 }}"
+ gpgcheck: no
+ when: install_repos | bool
+
+ - name: Create RPMs and put them into a repo (ppc64)
+ create_repo:
+ arch: ppc64
+ tempdir: "{{ remote_tmp_dir }}"
+ register: repo_ppc64
+
+ - set_fact:
+ repodir_ppc64: "{{ repo_ppc64.repo_dir }}"
+
+ - name: Install the repo (ppc64)
+ yum_repository:
+ name: "fake-ppc64"
+ description: "fake-ppc64"
+ baseurl: "file://{{ repodir_ppc64 }}"
+ gpgcheck: no
+ when: install_repos | bool
+
+ when: ansible_distribution in ['RedHat', 'CentOS', 'ScientificLinux', 'Fedora']
diff --git a/test/integration/targets/setup_rpm_repo/vars/Fedora.yml b/test/integration/targets/setup_rpm_repo/vars/Fedora.yml
new file mode 100644
index 0000000..004f42b
--- /dev/null
+++ b/test/integration/targets/setup_rpm_repo/vars/Fedora.yml
@@ -0,0 +1,4 @@
+rpm_repo_packages:
+ - "{{ 'python' ~ rpm_repo_python_major_version ~ '-rpmfluff' }}"
+ - createrepo
+ - rpm-build
diff --git a/test/integration/targets/setup_rpm_repo/vars/RedHat-6.yml b/test/integration/targets/setup_rpm_repo/vars/RedHat-6.yml
new file mode 100644
index 0000000..6edee17
--- /dev/null
+++ b/test/integration/targets/setup_rpm_repo/vars/RedHat-6.yml
@@ -0,0 +1,5 @@
+rpm_repo_packages:
+ - rpm-build
+ - python-rpmfluff
+ - createrepo_c
+ - createrepo
diff --git a/test/integration/targets/setup_rpm_repo/vars/RedHat-7.yml b/test/integration/targets/setup_rpm_repo/vars/RedHat-7.yml
new file mode 100644
index 0000000..6edee17
--- /dev/null
+++ b/test/integration/targets/setup_rpm_repo/vars/RedHat-7.yml
@@ -0,0 +1,5 @@
+rpm_repo_packages:
+ - rpm-build
+ - python-rpmfluff
+ - createrepo_c
+ - createrepo
diff --git a/test/integration/targets/setup_rpm_repo/vars/RedHat-8.yml b/test/integration/targets/setup_rpm_repo/vars/RedHat-8.yml
new file mode 100644
index 0000000..6e14933
--- /dev/null
+++ b/test/integration/targets/setup_rpm_repo/vars/RedHat-8.yml
@@ -0,0 +1,5 @@
+rpm_repo_packages:
+ - rpm-build
+ - createrepo_c
+ - createrepo
+ - python3-rpmfluff
diff --git a/test/integration/targets/setup_rpm_repo/vars/RedHat-9.yml b/test/integration/targets/setup_rpm_repo/vars/RedHat-9.yml
new file mode 100644
index 0000000..84849e2
--- /dev/null
+++ b/test/integration/targets/setup_rpm_repo/vars/RedHat-9.yml
@@ -0,0 +1,4 @@
+rpm_repo_packages:
+ - rpm-build
+ - createrepo_c
+ - createrepo
diff --git a/test/integration/targets/setup_rpm_repo/vars/main.yml b/test/integration/targets/setup_rpm_repo/vars/main.yml
new file mode 100644
index 0000000..8e924fc
--- /dev/null
+++ b/test/integration/targets/setup_rpm_repo/vars/main.yml
@@ -0,0 +1 @@
+rpm_repo_python_major_version: "{{ ansible_facts.python_version.split('.')[0] }}"
diff --git a/test/integration/targets/setup_test_user/handlers/main.yml b/test/integration/targets/setup_test_user/handlers/main.yml
new file mode 100644
index 0000000..dec4bd7
--- /dev/null
+++ b/test/integration/targets/setup_test_user/handlers/main.yml
@@ -0,0 +1,6 @@
+- name: delete test user
+ user:
+ name: "{{ test_user_name }}"
+ state: absent
+ remove: yes
+ force: yes
diff --git a/test/integration/targets/setup_test_user/tasks/default.yml b/test/integration/targets/setup_test_user/tasks/default.yml
new file mode 100644
index 0000000..83ee8f1
--- /dev/null
+++ b/test/integration/targets/setup_test_user/tasks/default.yml
@@ -0,0 +1,14 @@
+- name: set variables
+ set_fact:
+ test_user_name: ansibletest0
+ test_user_group: null
+
+- name: set plaintext password
+ no_log: yes
+ set_fact:
+ test_user_plaintext_password: "{{ lookup('password', '/dev/null') }}"
+
+- name: set hashed password
+ no_log: yes
+ set_fact:
+ test_user_hashed_password: "{{ test_user_plaintext_password | password_hash('sha512') }}"
diff --git a/test/integration/targets/setup_test_user/tasks/macosx.yml b/test/integration/targets/setup_test_user/tasks/macosx.yml
new file mode 100644
index 0000000..d33ab04
--- /dev/null
+++ b/test/integration/targets/setup_test_user/tasks/macosx.yml
@@ -0,0 +1,14 @@
+- name: set variables
+ set_fact:
+ test_user_name: ansibletest0
+ test_user_group: staff
+
+- name: set plaintext password
+ no_log: yes
+ set_fact:
+ test_user_plaintext_password: "{{ lookup('password', '/dev/null') }}"
+
+- name: set hashed password
+ no_log: yes
+ set_fact:
+ test_user_hashed_password: "{{ test_user_plaintext_password }}"
diff --git a/test/integration/targets/setup_test_user/tasks/main.yml b/test/integration/targets/setup_test_user/tasks/main.yml
new file mode 100644
index 0000000..5adfb13
--- /dev/null
+++ b/test/integration/targets/setup_test_user/tasks/main.yml
@@ -0,0 +1,37 @@
+- name: gather distribution facts
+ gather_facts:
+ gather_subset: distribution
+ when: ansible_distribution is not defined
+
+- name: include distribution specific tasks
+ include_tasks: "{{ lookup('first_found', params) }}"
+ vars:
+ params:
+ files:
+ - "{{ ansible_distribution | lower }}.yml"
+ - default.yml
+ paths:
+ - tasks
+
+- name: create test user
+ user:
+ name: "{{ test_user_name }}"
+ group: "{{ test_user_group or omit }}"
+ password: "{{ test_user_hashed_password or omit }}"
+ register: test_user
+ notify:
+ - delete test user
+
+- name: run whoami as the test user
+ shell: whoami
+ vars:
+ # ansible_become_method and ansible_become_flags are not set, allowing them to be provided by inventory
+ ansible_become: yes
+ ansible_become_user: "{{ test_user_name }}"
+ ansible_become_password: "{{ test_user_plaintext_password }}"
+ register: whoami
+
+- name: verify becoming the test user worked
+ assert:
+ that:
+ - whoami.stdout == test_user_name
diff --git a/test/integration/targets/setup_win_printargv/files/PrintArgv.cs b/test/integration/targets/setup_win_printargv/files/PrintArgv.cs
new file mode 100644
index 0000000..5ca3a8a
--- /dev/null
+++ b/test/integration/targets/setup_win_printargv/files/PrintArgv.cs
@@ -0,0 +1,13 @@
+using System;
+// This has been compiled to an exe and uploaded to S3 bucket for argv test
+
+namespace PrintArgv
+{
+ class Program
+ {
+ static void Main(string[] args)
+ {
+ Console.WriteLine(string.Join(System.Environment.NewLine, args));
+ }
+ }
+}
diff --git a/test/integration/targets/setup_win_printargv/meta/main.yml b/test/integration/targets/setup_win_printargv/meta/main.yml
new file mode 100644
index 0000000..e3dd5fb
--- /dev/null
+++ b/test/integration/targets/setup_win_printargv/meta/main.yml
@@ -0,0 +1,3 @@
+---
+dependencies:
+- setup_remote_tmp_dir
diff --git a/test/integration/targets/setup_win_printargv/tasks/main.yml b/test/integration/targets/setup_win_printargv/tasks/main.yml
new file mode 100644
index 0000000..3924931
--- /dev/null
+++ b/test/integration/targets/setup_win_printargv/tasks/main.yml
@@ -0,0 +1,9 @@
+---
+- name: download the PrintArgv.exe binary to temp location
+ win_get_url:
+ url: https://ci-files.testing.ansible.com/test/integration/targets/setup_win_printargv/PrintArgv.exe
+ dest: '{{ remote_tmp_dir }}\PrintArgv.exe'
+
+- name: set fact containing PrintArgv binary path
+ set_fact:
+ win_printargv_path: '{{ remote_tmp_dir }}\PrintArgv.exe'
diff --git a/test/integration/targets/shell/action_plugins/test_shell.py b/test/integration/targets/shell/action_plugins/test_shell.py
new file mode 100644
index 0000000..6e66ed0
--- /dev/null
+++ b/test/integration/targets/shell/action_plugins/test_shell.py
@@ -0,0 +1,19 @@
+# This file is part of Ansible
+
+# Copyright (c) 2019 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.action import ActionBase
+
+
+class ActionModule(ActionBase):
+
+ def run(self, tmp=None, task_vars=None):
+ result = super(ActionModule, self).run(tmp, task_vars)
+ del tmp # tmp no longer has any effect
+ result['shell'] = self._connection._shell.SHELL_FAMILY
+ return result
diff --git a/test/integration/targets/shell/aliases b/test/integration/targets/shell/aliases
new file mode 100644
index 0000000..a6dafcf
--- /dev/null
+++ b/test/integration/targets/shell/aliases
@@ -0,0 +1 @@
+shippable/posix/group1
diff --git a/test/integration/targets/shell/connection_plugins/test_connection_default.py b/test/integration/targets/shell/connection_plugins/test_connection_default.py
new file mode 100644
index 0000000..60feedd
--- /dev/null
+++ b/test/integration/targets/shell/connection_plugins/test_connection_default.py
@@ -0,0 +1,41 @@
+# Copyright (c) 2019 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+connection: test_connection_default
+short_description: test connection plugin used in tests
+description:
+- This is a test connection plugin used for shell testing
+author: ansible (@core)
+version_added: historical
+options:
+'''
+
+from ansible.plugins.connection import ConnectionBase
+
+
+class Connection(ConnectionBase):
+ ''' test connnection '''
+
+ transport = 'test_connection_default'
+
+ def __init__(self, *args, **kwargs):
+ super(Connection, self).__init__(*args, **kwargs)
+
+ def _connect(self):
+ pass
+
+ def exec_command(self, cmd, in_data=None, sudoable=True):
+ pass
+
+ def put_file(self, in_path, out_path):
+ pass
+
+ def fetch_file(self, in_path, out_path):
+ pass
+
+ def close(self):
+ pass
diff --git a/test/integration/targets/shell/connection_plugins/test_connection_override.py b/test/integration/targets/shell/connection_plugins/test_connection_override.py
new file mode 100644
index 0000000..d26d2b5
--- /dev/null
+++ b/test/integration/targets/shell/connection_plugins/test_connection_override.py
@@ -0,0 +1,42 @@
+# Copyright (c) 2019 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+connection: test_connection_override
+short_description: test connection plugin used in tests
+description:
+- This is a test connection plugin used for shell testing
+author: ansible (@core)
+version_added: historical
+options:
+'''
+
+from ansible.plugins.connection import ConnectionBase
+
+
+class Connection(ConnectionBase):
+ ''' test connection '''
+
+ transport = 'test_connection_override'
+
+ def __init__(self, *args, **kwargs):
+ self._shell_type = 'powershell' # Set a shell type that is not sh
+ super(Connection, self).__init__(*args, **kwargs)
+
+ def _connect(self):
+ pass
+
+ def exec_command(self, cmd, in_data=None, sudoable=True):
+ pass
+
+ def put_file(self, in_path, out_path):
+ pass
+
+ def fetch_file(self, in_path, out_path):
+ pass
+
+ def close(self):
+ pass
diff --git a/test/integration/targets/shell/tasks/main.yml b/test/integration/targets/shell/tasks/main.yml
new file mode 100644
index 0000000..d6f2a2b
--- /dev/null
+++ b/test/integration/targets/shell/tasks/main.yml
@@ -0,0 +1,36 @@
+---
+- name: get shell when shell_type is not defined
+ test_shell:
+ register: shell_type_default
+ failed_when: shell_type_default.shell != 'sh'
+ vars:
+ ansible_connection: test_connection_default
+
+- name: get shell when shell_type is not defined but is overridden
+ test_shell:
+ register: shell_type_default_override
+ failed_when: shell_type_default_override.shell != item
+ vars:
+ ansible_connection: test_connection_default
+ ansible_shell_type: '{{ item }}'
+ with_items:
+ - powershell
+ - sh
+
+- name: get shell when shell_type is defined
+ test_shell:
+ register: shell_type_defined
+ failed_when: shell_type_defined.shell != 'powershell'
+ vars:
+ ansible_connection: test_connection_override
+
+- name: get shell when shell_type is defined but is overridden
+ test_shell:
+ register: shell_type_defined_override
+ failed_when: shell_type_defined_override.shell != item
+ vars:
+ ansible_connection: test_connection_default
+ ansible_shell_type: '{{ item }}'
+ with_items:
+ - powershell
+ - sh
diff --git a/test/integration/targets/slurp/aliases b/test/integration/targets/slurp/aliases
new file mode 100644
index 0000000..6eae8bd
--- /dev/null
+++ b/test/integration/targets/slurp/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group1
+destructive
diff --git a/test/integration/targets/slurp/files/bar.bin b/test/integration/targets/slurp/files/bar.bin
new file mode 100644
index 0000000..38d4d8a
--- /dev/null
+++ b/test/integration/targets/slurp/files/bar.bin
Binary files differ
diff --git a/test/integration/targets/slurp/meta/main.yml b/test/integration/targets/slurp/meta/main.yml
new file mode 100644
index 0000000..3448ece
--- /dev/null
+++ b/test/integration/targets/slurp/meta/main.yml
@@ -0,0 +1,3 @@
+dependencies:
+ - setup_remote_tmp_dir
+ - setup_test_user
diff --git a/test/integration/targets/slurp/tasks/main.yml b/test/integration/targets/slurp/tasks/main.yml
new file mode 100644
index 0000000..9398594
--- /dev/null
+++ b/test/integration/targets/slurp/tasks/main.yml
@@ -0,0 +1,69 @@
+# test code for the slurp module. Based on win_slurp test cases
+# (c) 2014, Chris Church <chris@ninemoreminutes.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: Create a UTF-8 file to test with
+ copy:
+ content: 'We are at the café'
+ dest: '{{ remote_tmp_dir }}/foo.txt'
+
+- name: test slurping an existing file
+ slurp:
+ src: '{{ remote_tmp_dir }}/foo.txt'
+ register: slurp_existing
+
+- name: check slurp existing result
+ assert:
+ that:
+ - 'slurp_existing.content'
+ - 'slurp_existing.encoding == "base64"'
+ - 'slurp_existing is not changed'
+ - 'slurp_existing is not failed'
+ - '"{{ slurp_existing.content | b64decode }}" == "We are at the café"'
+
+- name: Create a binary file to test with
+ copy:
+ src: bar.bin
+ dest: '{{ remote_tmp_dir }}/bar.bin'
+
+- name: test slurping a binary file
+ slurp:
+ path: '{{ remote_tmp_dir }}/bar.bin'
+ register: slurp_binary
+ no_log: true
+
+- name: check slurp result of binary
+ assert:
+ that:
+ - "slurp_binary.content"
+ - "slurp_binary.encoding == 'base64'"
+ - "slurp_binary is not changed"
+ - "slurp_binary is not failed"
+
+- name: test slurp with missing argument
+ action: slurp
+ register: slurp_no_args
+ ignore_errors: true
+
+- name: check slurp with missing argument result
+ assert:
+ that:
+ - "slurp_no_args is failed"
+ - "slurp_no_args.msg"
+ - "slurp_no_args is not changed"
+
+- import_tasks: test_unreadable.yml
diff --git a/test/integration/targets/slurp/tasks/test_unreadable.yml b/test/integration/targets/slurp/tasks/test_unreadable.yml
new file mode 100644
index 0000000..cab80cf
--- /dev/null
+++ b/test/integration/targets/slurp/tasks/test_unreadable.yml
@@ -0,0 +1,74 @@
+- name: test slurping a non-existent file
+ slurp:
+ src: '{{ remote_tmp_dir }}/i_do_not_exist'
+ register: slurp_missing
+ ignore_errors: yes
+
+- name: Create a directory to test with
+ file:
+ path: '{{ remote_tmp_dir }}/baz/'
+ state: directory
+
+- name: test slurping a directory
+ slurp:
+ src: '{{ remote_tmp_dir }}/baz'
+ register: slurp_dir
+ ignore_errors: yes
+
+# Ensure unreadable file and directory handling and error messages
+# https://github.com/ansible/ansible/issues/67340
+
+- name: create unreadable file
+ copy:
+ content: "Hello, World!"
+ dest: "{{ remote_tmp_dir }}/qux.txt"
+ mode: '0600'
+ owner: root
+
+- name: test slurp unreadable file
+ slurp:
+ src: "{{ remote_tmp_dir }}/qux.txt"
+ register: slurp_unreadable_file
+ vars: &test_user_become
+ ansible_become: yes
+ ansible_become_user: "{{ test_user_name }}"
+ ansible_become_password: "{{ test_user_plaintext_password }}"
+ ignore_errors: yes
+
+- name: create unreadable directory
+ file:
+ path: "{{ remote_tmp_dir }}/test_data"
+ state: directory
+ mode: '0700'
+ owner: root
+
+- name: test slurp unreadable directory
+ slurp:
+ src: "{{ remote_tmp_dir }}/test_data"
+ register: slurp_unreadable_dir
+ vars: *test_user_become
+ ignore_errors: yes
+
+- name: Try to access file as directory
+ slurp:
+ src: "{{ remote_tmp_dir }}/qux.txt/somefile"
+ ignore_errors: yes
+ register: slurp_path_file_as_dir
+
+- name: check slurp failures
+ assert:
+ that:
+ - slurp_missing is failed
+ - slurp_missing.msg is search('file not found')
+ - slurp_missing is not changed
+ - slurp_unreadable_file is failed
+ - slurp_unreadable_file.msg is regex('^file is not readable:')
+ - slurp_unreadable_file is not changed
+ - slurp_unreadable_dir is failed
+ - slurp_unreadable_dir.msg is regex('^file is not readable:')
+ - slurp_unreadable_dir is not changed
+ - slurp_path_file_as_dir is failed
+ - slurp_path_file_as_dir is search('unable to slurp file')
+ - slurp_dir is failed
+ - slurp_dir.msg is search('source is a directory and must be a file')
+ - slurp_dir is not changed
diff --git a/test/integration/targets/special_vars/aliases b/test/integration/targets/special_vars/aliases
new file mode 100644
index 0000000..0010517
--- /dev/null
+++ b/test/integration/targets/special_vars/aliases
@@ -0,0 +1,3 @@
+shippable/posix/group4
+needs/target/include_parent_role_vars
+context/controller
diff --git a/test/integration/targets/special_vars/meta/main.yml b/test/integration/targets/special_vars/meta/main.yml
new file mode 100644
index 0000000..07faa21
--- /dev/null
+++ b/test/integration/targets/special_vars/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - prepare_tests
diff --git a/test/integration/targets/special_vars/tasks/main.yml b/test/integration/targets/special_vars/tasks/main.yml
new file mode 100644
index 0000000..0e71f1d
--- /dev/null
+++ b/test/integration/targets/special_vars/tasks/main.yml
@@ -0,0 +1,100 @@
+# test code for the template module
+# (c) 2015, Brian Coca <bcoca@ansible.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: verify ansible_managed
+ template: src=foo.j2 dest={{output_dir}}/special_vars.yaml
+
+- name: read the file into facts
+ include_vars: "{{output_dir}}/special_vars.yaml"
+
+
+- name: verify all test vars are defined
+ assert:
+ that:
+ - 'item in hostvars[inventory_hostname].keys()'
+ with_items:
+ - test_template_host
+ - test_template_path
+ - test_template_mtime
+ - test_template_uid
+ - test_template_fullpath
+ - test_template_run_date
+ - test_ansible_managed
+
+- name: ensure that role_name exists in role_names, ansible_play_role_names, ansible_role_names, and not in ansible_dependent_role_names
+ assert:
+ that:
+ - "role_name in role_names"
+ - "role_name in ansible_play_role_names"
+ - "role_name in ansible_role_names"
+ - "role_name not in ansible_dependent_role_names"
+
+- name: ensure that our dependency (prepare_tests) exists in ansible_role_names and ansible_dependent_role_names, but not in role_names or ansible_play_role_names
+ assert:
+ that:
+ - "'prepare_tests' in ansible_role_names"
+ - "'prepare_tests' in ansible_dependent_role_names"
+ - "'prepare_tests' not in role_names"
+ - "'prepare_tests' not in ansible_play_role_names"
+
+- name: ensure that ansible_role_names is the sum of ansible_play_role_names and ansible_dependent_role_names
+ assert:
+ that:
+ - "(ansible_play_role_names + ansible_dependent_role_names)|unique|sort|list == ansible_role_names|sort|list"
+
+- name: check that ansible_parent_role_names is normally unset when not included/imported (before including other roles)
+ assert:
+ that:
+ - "ansible_parent_role_names is undefined"
+ - "ansible_parent_role_paths is undefined"
+
+- name: ansible_parent_role_names - test functionality by including another role
+ include_role:
+ name: include_parent_role_vars
+ tasks_from: included_by_other_role.yml
+
+- name: check that ansible_parent_role_names is normally unset when not included/imported (after including other role)
+ assert:
+ that:
+ - "ansible_parent_role_names is undefined"
+ - "ansible_parent_role_paths is undefined"
+
+- name: ansible_parent_role_names - test functionality by importing another role
+ import_role:
+ name: include_parent_role_vars
+ tasks_from: included_by_other_role.yml
+
+- name: check that ansible_parent_role_names is normally unset when not included/imported (after importing other role)
+ assert:
+ that:
+ - "ansible_parent_role_names is undefined"
+ - "ansible_parent_role_paths is undefined"
+
+- name: ansible_parent_role_names - test functionality by including another role
+ include_role:
+ name: include_parent_role_vars
+
+- name: check that ansible_parent_role_names is normally unset when not included/imported (after both import and inlcude)
+ assert:
+ that:
+ - "ansible_parent_role_names is undefined"
+ - "ansible_parent_role_paths is undefined"
+
+- name: ansible_parent_role_names - test functionality by importing another role
+ import_role:
+ name: include_parent_role_vars
diff --git a/test/integration/targets/special_vars/templates/foo.j2 b/test/integration/targets/special_vars/templates/foo.j2
new file mode 100644
index 0000000..0f6db2a
--- /dev/null
+++ b/test/integration/targets/special_vars/templates/foo.j2
@@ -0,0 +1,7 @@
+test_template_host: "{{template_host}}"
+test_template_path: "{{template_path}}"
+test_template_mtime: "{{template_mtime}}"
+test_template_uid: "{{template_uid}}"
+test_template_fullpath: "{{template_fullpath}}"
+test_template_run_date: "{{template_run_date}}"
+test_ansible_managed: "{{ansible_managed}}"
diff --git a/test/integration/targets/special_vars/vars/main.yml b/test/integration/targets/special_vars/vars/main.yml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/special_vars/vars/main.yml
diff --git a/test/integration/targets/special_vars_hosts/aliases b/test/integration/targets/special_vars_hosts/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/special_vars_hosts/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/special_vars_hosts/inventory b/test/integration/targets/special_vars_hosts/inventory
new file mode 100644
index 0000000..8d69e57
--- /dev/null
+++ b/test/integration/targets/special_vars_hosts/inventory
@@ -0,0 +1,3 @@
+successful ansible_connection=local ansible_host=127.0.0.1 ansible_python_interpreter="{{ ansible_playbook_python }}"
+failed ansible_connection=local ansible_host=127.0.0.1 ansible_python_interpreter="{{ ansible_playbook_python }}"
+unreachable ansible_connection=ssh ansible_host=127.0.0.1 ansible_port=1011 # IANA Reserved port
diff --git a/test/integration/targets/special_vars_hosts/playbook.yml b/test/integration/targets/special_vars_hosts/playbook.yml
new file mode 100644
index 0000000..e3d9e43
--- /dev/null
+++ b/test/integration/targets/special_vars_hosts/playbook.yml
@@ -0,0 +1,53 @@
+---
+- hosts: all
+ gather_facts: no
+ tasks:
+ - name: test magic vars for hosts without any failed/unreachable (no serial)
+ assert:
+ that:
+ - ansible_play_batch | length == 3
+ - ansible_play_hosts | length == 3
+ - ansible_play_hosts_all | length == 3
+ run_once: True
+
+ - ping:
+ failed_when: "inventory_hostname == 'failed'"
+
+ - meta: clear_host_errors
+
+- hosts: all
+ gather_facts: no
+ tasks:
+ - name: test host errors were cleared
+ assert:
+ that:
+ - ansible_play_batch | length == 3
+ - ansible_play_hosts | length == 3
+ - ansible_play_hosts_all | length == 3
+ run_once: True
+
+ - ping:
+ failed_when: "inventory_hostname == 'failed'"
+
+ - name: test magic vars exclude failed/unreachable hosts
+ assert:
+ that:
+ - ansible_play_batch | length == 1
+ - ansible_play_hosts | length == 1
+ - "ansible_play_batch == ['successful']"
+ - "ansible_play_hosts == ['successful']"
+ - ansible_play_hosts_all | length == 3
+ run_once: True
+
+- hosts: all
+ gather_facts: no
+ tasks:
+ - name: test failed/unreachable persists between plays
+ assert:
+ that:
+ - ansible_play_batch | length == 1
+ - ansible_play_hosts | length == 1
+ - "ansible_play_batch == ['successful']"
+ - "ansible_play_hosts == ['successful']"
+ - ansible_play_hosts_all | length == 3
+ run_once: True
diff --git a/test/integration/targets/special_vars_hosts/runme.sh b/test/integration/targets/special_vars_hosts/runme.sh
new file mode 100755
index 0000000..81c1d9b
--- /dev/null
+++ b/test/integration/targets/special_vars_hosts/runme.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook -i ./inventory playbook.yml "$@" | tee out.txt
+grep 'unreachable=2' out.txt
+grep 'failed=2' out.txt
diff --git a/test/integration/targets/split/aliases b/test/integration/targets/split/aliases
new file mode 100644
index 0000000..7907b28
--- /dev/null
+++ b/test/integration/targets/split/aliases
@@ -0,0 +1,3 @@
+context/target
+shippable/posix/group1
+gather_facts/no
diff --git a/test/integration/targets/split/tasks/main.yml b/test/integration/targets/split/tasks/main.yml
new file mode 100644
index 0000000..599ef41
--- /dev/null
+++ b/test/integration/targets/split/tasks/main.yml
@@ -0,0 +1,36 @@
+- name: Get control host details
+ setup:
+ delegate_to: localhost
+ register: control_host
+- name: Get managed host details
+ setup:
+ register: managed_host
+- name: Check split state
+ stat:
+ path: "{{ output_dir }}"
+ register: split
+ ignore_errors: yes
+- name: Build non-split status message
+ set_fact:
+ message: "
+ {{ control_host.ansible_facts.ansible_user_id }}@{{ control_host.ansible_facts.ansible_hostname }} on
+ {{ control_host.ansible_facts.ansible_distribution }} {{ control_host.ansible_facts.ansible_distribution_version }}
+ {{ control_host.ansible_facts.ansible_python.executable }} ({{ control_host.ansible_facts.ansible_python_version }})
+ --[ {{ ansible_connection }} ]-->
+ {{ managed_host.ansible_facts.ansible_user_id }} on
+ {{ managed_host.ansible_facts.ansible_python.executable }} ({{ managed_host.ansible_facts.ansible_python_version }})"
+ when: split is success and split.stat.exists
+- name: Build split status message
+ set_fact:
+ message: "
+ {{ control_host.ansible_facts.ansible_user_id }}@{{ control_host.ansible_facts.ansible_hostname }} on
+ {{ control_host.ansible_facts.ansible_distribution }} {{ control_host.ansible_facts.ansible_distribution_version }}
+ {{ control_host.ansible_facts.ansible_python.executable }} ({{ control_host.ansible_facts.ansible_python_version }})
+ --[ {{ ansible_connection }} ]-->
+ {{ managed_host.ansible_facts.ansible_user_id }}@{{ managed_host.ansible_facts.ansible_hostname }} on
+ {{ managed_host.ansible_facts.ansible_distribution }} {{ managed_host.ansible_facts.ansible_distribution_version }}
+ {{ managed_host.ansible_facts.ansible_python.executable }} ({{ managed_host.ansible_facts.ansible_python_version }})"
+ when: split is not success or not split.stat.exists
+- name: Show host details
+ debug:
+ msg: "{{ message | trim }}"
diff --git a/test/integration/targets/stat/aliases b/test/integration/targets/stat/aliases
new file mode 100644
index 0000000..765b70d
--- /dev/null
+++ b/test/integration/targets/stat/aliases
@@ -0,0 +1 @@
+shippable/posix/group2
diff --git a/test/integration/targets/stat/files/foo.txt b/test/integration/targets/stat/files/foo.txt
new file mode 100644
index 0000000..3e96db9
--- /dev/null
+++ b/test/integration/targets/stat/files/foo.txt
@@ -0,0 +1 @@
+templated_var_loaded
diff --git a/test/integration/targets/stat/meta/main.yml b/test/integration/targets/stat/meta/main.yml
new file mode 100644
index 0000000..cb6005d
--- /dev/null
+++ b/test/integration/targets/stat/meta/main.yml
@@ -0,0 +1,3 @@
+dependencies:
+ - prepare_tests
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/stat/tasks/main.yml b/test/integration/targets/stat/tasks/main.yml
new file mode 100644
index 0000000..374cb2f
--- /dev/null
+++ b/test/integration/targets/stat/tasks/main.yml
@@ -0,0 +1,179 @@
+# test code for the stat module
+# (c) 2014, James Tanner <tanner.jc@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: make a new file
+ copy: dest={{remote_tmp_dir}}/foo.txt mode=0644 content="hello world"
+
+- name: check stat of file
+ stat: path={{remote_tmp_dir}}/foo.txt
+ register: stat_result
+
+- debug: var=stat_result
+
+- assert:
+ that:
+ - "'changed' in stat_result"
+ - "stat_result.changed == false"
+ - "'stat' in stat_result"
+ - "'atime' in stat_result.stat"
+ - "'ctime' in stat_result.stat"
+ - "'dev' in stat_result.stat"
+ - "'exists' in stat_result.stat"
+ - "'gid' in stat_result.stat"
+ - "'inode' in stat_result.stat"
+ - "'isblk' in stat_result.stat"
+ - "'ischr' in stat_result.stat"
+ - "'isdir' in stat_result.stat"
+ - "'isfifo' in stat_result.stat"
+ - "'isgid' in stat_result.stat"
+ - "'isreg' in stat_result.stat"
+ - "'issock' in stat_result.stat"
+ - "'isuid' in stat_result.stat"
+ - "'checksum' in stat_result.stat"
+ - "stat_result.stat.checksum == '2aae6c35c94fcfb415dbe95f408b9ce91ee846ed'"
+ - "'mode' in stat_result.stat"
+ - "'mtime' in stat_result.stat"
+ - "'nlink' in stat_result.stat"
+ - "'pw_name' in stat_result.stat"
+ - "'rgrp' in stat_result.stat"
+ - "'roth' in stat_result.stat"
+ - "'rusr' in stat_result.stat"
+ - "'size' in stat_result.stat"
+ - "'uid' in stat_result.stat"
+ - "'wgrp' in stat_result.stat"
+ - "'woth' in stat_result.stat"
+ - "'wusr' in stat_result.stat"
+ - "'xgrp' in stat_result.stat"
+ - "'xoth' in stat_result.stat"
+ - "'xusr' in stat_result.stat"
+
+- name: make a symlink
+ file:
+ src: "{{ remote_tmp_dir }}/foo.txt"
+ path: "{{ remote_tmp_dir }}/foo-link"
+ state: link
+
+- name: check stat of a symlink with follow off
+ stat:
+ path: "{{ remote_tmp_dir }}/foo-link"
+ register: stat_result
+
+- debug: var=stat_result
+
+- assert:
+ that:
+ - "'changed' in stat_result"
+ - "stat_result.changed == false"
+ - "'stat' in stat_result"
+ - "'atime' in stat_result.stat"
+ - "'ctime' in stat_result.stat"
+ - "'dev' in stat_result.stat"
+ - "'exists' in stat_result.stat"
+ - "'gid' in stat_result.stat"
+ - "'inode' in stat_result.stat"
+ - "'isblk' in stat_result.stat"
+ - "'ischr' in stat_result.stat"
+ - "'isdir' in stat_result.stat"
+ - "'isfifo' in stat_result.stat"
+ - "'isgid' in stat_result.stat"
+ - "'isreg' in stat_result.stat"
+ - "'issock' in stat_result.stat"
+ - "'isuid' in stat_result.stat"
+ - "'islnk' in stat_result.stat"
+ - "'mode' in stat_result.stat"
+ - "'mtime' in stat_result.stat"
+ - "'nlink' in stat_result.stat"
+ - "'pw_name' in stat_result.stat"
+ - "'rgrp' in stat_result.stat"
+ - "'roth' in stat_result.stat"
+ - "'rusr' in stat_result.stat"
+ - "'size' in stat_result.stat"
+ - "'uid' in stat_result.stat"
+ - "'wgrp' in stat_result.stat"
+ - "'woth' in stat_result.stat"
+ - "'wusr' in stat_result.stat"
+ - "'xgrp' in stat_result.stat"
+ - "'xoth' in stat_result.stat"
+ - "'xusr' in stat_result.stat"
+
+- name: check stat of a symlink with follow on
+ stat:
+ path: "{{ remote_tmp_dir }}/foo-link"
+ follow: True
+ register: stat_result
+
+- debug: var=stat_result
+
+- assert:
+ that:
+ - "'changed' in stat_result"
+ - "stat_result.changed == false"
+ - "'stat' in stat_result"
+ - "'atime' in stat_result.stat"
+ - "'ctime' in stat_result.stat"
+ - "'dev' in stat_result.stat"
+ - "'exists' in stat_result.stat"
+ - "'gid' in stat_result.stat"
+ - "'inode' in stat_result.stat"
+ - "'isblk' in stat_result.stat"
+ - "'ischr' in stat_result.stat"
+ - "'isdir' in stat_result.stat"
+ - "'isfifo' in stat_result.stat"
+ - "'isgid' in stat_result.stat"
+ - "'isreg' in stat_result.stat"
+ - "'issock' in stat_result.stat"
+ - "'isuid' in stat_result.stat"
+ - "'checksum' in stat_result.stat"
+ - "stat_result.stat.checksum == '2aae6c35c94fcfb415dbe95f408b9ce91ee846ed'"
+ - "'mode' in stat_result.stat"
+ - "'mtime' in stat_result.stat"
+ - "'nlink' in stat_result.stat"
+ - "'pw_name' in stat_result.stat"
+ - "'rgrp' in stat_result.stat"
+ - "'roth' in stat_result.stat"
+ - "'rusr' in stat_result.stat"
+ - "'size' in stat_result.stat"
+ - "'uid' in stat_result.stat"
+ - "'wgrp' in stat_result.stat"
+ - "'woth' in stat_result.stat"
+ - "'wusr' in stat_result.stat"
+ - "'xgrp' in stat_result.stat"
+ - "'xoth' in stat_result.stat"
+ - "'xusr' in stat_result.stat"
+
+- name: make a new file with colon in filename
+ copy:
+ dest: "{{ remote_tmp_dir }}/foo:bar.txt"
+ mode: '0644'
+ content: "hello world"
+
+- name: check stat of a file with colon in name
+ stat:
+ path: "{{ remote_tmp_dir }}/foo:bar.txt"
+ follow: True
+ register: stat_result
+
+- debug:
+ var: stat_result
+
+- assert:
+ that:
+ - "'changed' in stat_result"
+ - "stat_result.changed == false"
+ - "stat_result.stat.mimetype == 'text/plain'"
+ - "stat_result.stat.charset == 'us-ascii'"
diff --git a/test/integration/targets/strategy_free/aliases b/test/integration/targets/strategy_free/aliases
new file mode 100644
index 0000000..70a7b7a
--- /dev/null
+++ b/test/integration/targets/strategy_free/aliases
@@ -0,0 +1 @@
+shippable/posix/group5
diff --git a/test/integration/targets/strategy_free/inventory b/test/integration/targets/strategy_free/inventory
new file mode 100644
index 0000000..39034f1
--- /dev/null
+++ b/test/integration/targets/strategy_free/inventory
@@ -0,0 +1,2 @@
+[local]
+testhost ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
diff --git a/test/integration/targets/strategy_free/last_include_tasks.yml b/test/integration/targets/strategy_free/last_include_tasks.yml
new file mode 100644
index 0000000..6c87242
--- /dev/null
+++ b/test/integration/targets/strategy_free/last_include_tasks.yml
@@ -0,0 +1,2 @@
+- debug:
+ msg: "INCLUDED TASK EXECUTED"
diff --git a/test/integration/targets/strategy_free/runme.sh b/test/integration/targets/strategy_free/runme.sh
new file mode 100755
index 0000000..f5b912c
--- /dev/null
+++ b/test/integration/targets/strategy_free/runme.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+set -eux
+
+export ANSIBLE_STRATEGY=free
+
+set +e
+result="$(ansible-playbook test_last_include_in_always.yml -i inventory "$@" 2>&1)"
+set -e
+grep -q "INCLUDED TASK EXECUTED" <<< "$result"
diff --git a/test/integration/targets/strategy_free/test_last_include_in_always.yml b/test/integration/targets/strategy_free/test_last_include_in_always.yml
new file mode 100644
index 0000000..205f323
--- /dev/null
+++ b/test/integration/targets/strategy_free/test_last_include_in_always.yml
@@ -0,0 +1,9 @@
+- hosts: testhost
+ gather_facts: false
+ strategy: free
+ tasks:
+ - block:
+ - name: EXPECTED FAILURE
+ fail:
+ always:
+ - include_tasks: last_include_tasks.yml
diff --git a/test/integration/targets/strategy_linear/aliases b/test/integration/targets/strategy_linear/aliases
new file mode 100644
index 0000000..70a7b7a
--- /dev/null
+++ b/test/integration/targets/strategy_linear/aliases
@@ -0,0 +1 @@
+shippable/posix/group5
diff --git a/test/integration/targets/strategy_linear/inventory b/test/integration/targets/strategy_linear/inventory
new file mode 100644
index 0000000..698d69d
--- /dev/null
+++ b/test/integration/targets/strategy_linear/inventory
@@ -0,0 +1,3 @@
+[local]
+testhost ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+testhost2 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
diff --git a/test/integration/targets/strategy_linear/roles/role1/tasks/main.yml b/test/integration/targets/strategy_linear/roles/role1/tasks/main.yml
new file mode 100644
index 0000000..51efd43
--- /dev/null
+++ b/test/integration/targets/strategy_linear/roles/role1/tasks/main.yml
@@ -0,0 +1,6 @@
+- name: Include tasks
+ include_tasks: "tasks.yml"
+
+- name: Mark role as finished
+ set_fact:
+ role1_complete: True
diff --git a/test/integration/targets/strategy_linear/roles/role1/tasks/tasks.yml b/test/integration/targets/strategy_linear/roles/role1/tasks/tasks.yml
new file mode 100644
index 0000000..b7a46aa
--- /dev/null
+++ b/test/integration/targets/strategy_linear/roles/role1/tasks/tasks.yml
@@ -0,0 +1,7 @@
+- name: Call role2
+ include_role:
+ name: role2
+
+- name: Call role2 again
+ include_role:
+ name: role2
diff --git a/test/integration/targets/strategy_linear/roles/role2/tasks/main.yml b/test/integration/targets/strategy_linear/roles/role2/tasks/main.yml
new file mode 100644
index 0000000..81e041e
--- /dev/null
+++ b/test/integration/targets/strategy_linear/roles/role2/tasks/main.yml
@@ -0,0 +1,7 @@
+- block:
+ - block:
+ - name: Nested task 1
+ debug: msg="Nested task 1"
+
+ - name: Nested task 2
+ debug: msg="Nested task 2"
diff --git a/test/integration/targets/strategy_linear/runme.sh b/test/integration/targets/strategy_linear/runme.sh
new file mode 100755
index 0000000..cbb6aea
--- /dev/null
+++ b/test/integration/targets/strategy_linear/runme.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook test_include_file_noop.yml -i inventory "$@"
+
+ansible-playbook task_action_templating.yml -i inventory "$@"
diff --git a/test/integration/targets/strategy_linear/task_action_templating.yml b/test/integration/targets/strategy_linear/task_action_templating.yml
new file mode 100644
index 0000000..5f7438f
--- /dev/null
+++ b/test/integration/targets/strategy_linear/task_action_templating.yml
@@ -0,0 +1,26 @@
+- hosts: testhost,testhost2
+ gather_facts: no
+ tasks:
+ - set_fact:
+ module_to_run: 'debug'
+ when: inventory_hostname == 'testhost'
+
+ - set_fact:
+ module_to_run: 'ping'
+ when: inventory_hostname == 'testhost2'
+
+ - action:
+ module: '{{ module_to_run }}'
+ register: out
+
+ - assert:
+ that:
+ - "'msg' in out"
+ - "'ping' not in out"
+ when: inventory_hostname == 'testhost'
+
+ - assert:
+ that:
+ - "'ping' in out"
+ - "'msg' not in out"
+ when: inventory_hostname == 'testhost2'
diff --git a/test/integration/targets/strategy_linear/test_include_file_noop.yml b/test/integration/targets/strategy_linear/test_include_file_noop.yml
new file mode 100644
index 0000000..9dbf83d
--- /dev/null
+++ b/test/integration/targets/strategy_linear/test_include_file_noop.yml
@@ -0,0 +1,16 @@
+- hosts:
+ - testhost
+ - testhost2
+ gather_facts: no
+ vars:
+ secondhost: testhost2
+ tasks:
+ - name: Call the first role only on one host
+ include_role:
+ name: role1
+ when: inventory_hostname is match(secondhost)
+
+ - name: Make sure nothing else runs until role1 finishes
+ assert:
+ that:
+ - "'role1_complete' in hostvars[secondhost]"
diff --git a/test/integration/targets/subversion/aliases b/test/integration/targets/subversion/aliases
new file mode 100644
index 0000000..23ada3c
--- /dev/null
+++ b/test/integration/targets/subversion/aliases
@@ -0,0 +1,7 @@
+setup/always/setup_passlib
+shippable/posix/group2
+skip/osx
+skip/macos
+skip/rhel/9.0b # svn checkout hangs
+destructive
+needs/root
diff --git a/test/integration/targets/subversion/roles/subversion/defaults/main.yml b/test/integration/targets/subversion/roles/subversion/defaults/main.yml
new file mode 100644
index 0000000..e647d59
--- /dev/null
+++ b/test/integration/targets/subversion/roles/subversion/defaults/main.yml
@@ -0,0 +1,10 @@
+---
+apache_port: 11386 # cannot use 80 as httptester overrides this
+subversion_test_dir: /tmp/ansible-svn-test-dir
+subversion_server_dir: /tmp/ansible-svn # cannot use a path in the home dir without userdir or granting exec permission to the apache user
+subversion_repo_name: ansible-test-repo
+subversion_repo_url: http://127.0.0.1:{{ apache_port }}/svn/{{ subversion_repo_name }}
+subversion_repo_auth_url: http://127.0.0.1:{{ apache_port }}/svnauth/{{ subversion_repo_name }}
+subversion_username: subsvn_user'''
+subversion_password: Password123!
+subversion_external_repo_url: https://github.com/ansible/ansible.github.com # GitHub serves SVN
diff --git a/test/integration/targets/subversion/roles/subversion/files/create_repo.sh b/test/integration/targets/subversion/roles/subversion/files/create_repo.sh
new file mode 100644
index 0000000..cc7f407
--- /dev/null
+++ b/test/integration/targets/subversion/roles/subversion/files/create_repo.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+svnadmin create "$1"
+svn mkdir "file://$PWD/$1/trunk" -m "make trunk"
+svn mkdir "file://$PWD/$1/tags" -m "make tags"
+svn mkdir "file://$PWD/$1/branches" -m "make branches"
diff --git a/test/integration/targets/subversion/roles/subversion/tasks/cleanup.yml b/test/integration/targets/subversion/roles/subversion/tasks/cleanup.yml
new file mode 100644
index 0000000..9be43b4
--- /dev/null
+++ b/test/integration/targets/subversion/roles/subversion/tasks/cleanup.yml
@@ -0,0 +1,8 @@
+---
+- name: stop apache after tests
+ shell: "kill -9 $(cat '{{ subversion_server_dir }}/apache.pid')"
+
+- name: remove tmp subversion server dir
+ file:
+ path: '{{ subversion_server_dir }}'
+ state: absent
diff --git a/test/integration/targets/subversion/roles/subversion/tasks/main.yml b/test/integration/targets/subversion/roles/subversion/tasks/main.yml
new file mode 100644
index 0000000..0d6acb8
--- /dev/null
+++ b/test/integration/targets/subversion/roles/subversion/tasks/main.yml
@@ -0,0 +1,20 @@
+---
+- name: setup subversion server
+ import_tasks: setup.yml
+ tags: setup
+
+- name: verify that subversion is installed so this test can continue
+ shell: which svn
+ tags: always
+
+- name: run tests
+ import_tasks: tests.yml
+ tags: tests
+
+- name: run warning
+ import_tasks: warnings.yml
+ tags: warnings
+
+- name: clean up
+ import_tasks: cleanup.yml
+ tags: cleanup
diff --git a/test/integration/targets/subversion/roles/subversion/tasks/setup.yml b/test/integration/targets/subversion/roles/subversion/tasks/setup.yml
new file mode 100644
index 0000000..3cf5af5
--- /dev/null
+++ b/test/integration/targets/subversion/roles/subversion/tasks/setup.yml
@@ -0,0 +1,72 @@
+---
+- name: clean out the checkout dir
+ file:
+ path: '{{ subversion_test_dir }}'
+ state: '{{ item }}'
+ loop:
+ - absent
+ - directory
+
+- name: install SVN pre-reqs
+ package:
+ name: '{{ subversion_packages }}'
+ state: present
+ when: ansible_distribution != 'Alpine'
+
+- name: install SVN pre-reqs - Alpine
+ command: 'apk add -U -u {{ subversion_packages|join(" ") }}'
+ when: ansible_distribution == 'Alpine'
+
+- name: upgrade SVN pre-reqs
+ package:
+ name: '{{ upgrade_packages }}'
+ state: latest
+ when:
+ - upgrade_packages | default([])
+
+- name: create SVN home folder
+ file:
+ path: '{{ subversion_server_dir }}'
+ state: directory
+
+- name: setup selinux when enabled
+ include_tasks: setup_selinux.yml
+ when: ansible_selinux.status == "enabled"
+
+- name: template out configuration file
+ template:
+ src: subversion.conf.j2
+ dest: '{{ subversion_server_dir }}/subversion.conf'
+
+- name: create a test repository
+ script: create_repo.sh {{ subversion_repo_name }}
+ args:
+ chdir: '{{ subversion_server_dir }}'
+ creates: '{{ subversion_server_dir }}/{{ subversion_repo_name }}'
+
+- name: add test user to htpasswd for Subversion site
+ htpasswd:
+ path: '{{ subversion_server_dir }}/svn-auth-users'
+ name: '{{ subversion_username }}'
+ password: '{{ subversion_password }}'
+ state: present
+
+- name: apply ownership for all SVN directories
+ file:
+ path: '{{ subversion_server_dir }}'
+ owner: '{{ apache_user }}'
+ group: '{{ apache_group }}'
+ recurse: True
+
+- name: start test Apache SVN site - non Red Hat
+ command: apachectl -k start -f {{ subversion_server_dir }}/subversion.conf
+ async: 3600 # We kill apache manually in the clean up phase
+ poll: 0
+ when: ansible_os_family not in ['RedHat', 'Alpine']
+
+# On Red Hat based OS', we can't use apachectl to start up own instance, just use the raw httpd
+- name: start test Apache SVN site - Red Hat
+ command: httpd -k start -f {{ subversion_server_dir }}/subversion.conf
+ async: 3600 # We kill apache manually in the clean up phase
+ poll: 0
+ when: ansible_os_family in ['RedHat', 'Alpine']
diff --git a/test/integration/targets/subversion/roles/subversion/tasks/setup_selinux.yml b/test/integration/targets/subversion/roles/subversion/tasks/setup_selinux.yml
new file mode 100644
index 0000000..a9ffa71
--- /dev/null
+++ b/test/integration/targets/subversion/roles/subversion/tasks/setup_selinux.yml
@@ -0,0 +1,11 @@
+- name: set SELinux security context for SVN folder
+ sefcontext:
+ target: '{{ subversion_server_dir }}(/.*)?'
+ setype: '{{ item }}'
+ state: present
+ with_items:
+ - httpd_sys_content_t
+ - httpd_sys_rw_content_t
+
+- name: apply new SELinux context to filesystem
+ command: restorecon -irv {{ subversion_server_dir | quote }}
diff --git a/test/integration/targets/subversion/roles/subversion/tasks/tests.yml b/test/integration/targets/subversion/roles/subversion/tasks/tests.yml
new file mode 100644
index 0000000..b8f85d9
--- /dev/null
+++ b/test/integration/targets/subversion/roles/subversion/tasks/tests.yml
@@ -0,0 +1,145 @@
+# test code for the svn module
+# (c) 2014, Michael DeHaan <michael.dehaan@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# checks out every branch so using a small repo
+
+- name: initial checkout
+ subversion:
+ repo: '{{ subversion_repo_url }}'
+ dest: '{{ subversion_test_dir }}/svn'
+ register: subverted
+
+- name: check if dir was checked out
+ stat:
+ path: '{{ subversion_test_dir }}/svn'
+ register: subverted_result
+
+# FIXME: the before/after logic here should be fixed to make them hashes, see GitHub 6078
+# looks like this: {
+# "after": [
+# "Revision: 9",
+# "URL: https://github.com/jimi-c/test_role"
+# ],
+# "before": null,
+# "changed": true,
+# "item": ""
+# }
+- name: verify information about the initial clone
+ assert:
+ that:
+ - "'after' in subverted"
+ - "subverted.after.1 == 'URL: ' ~ subversion_repo_url"
+ - "not subverted.before"
+ - "subverted.changed"
+ - subverted_result.stat.exists
+
+- name: repeated checkout
+ subversion:
+ repo: '{{ subversion_repo_url }}'
+ dest: '{{ subversion_test_dir }}/svn'
+ register: subverted2
+
+- name: verify on a reclone things are marked unchanged
+ assert:
+ that:
+ - "not subverted2.changed"
+
+- name: check for tags
+ stat: path={{ subversion_test_dir }}/svn/tags
+ register: tags
+
+- name: check for trunk
+ stat: path={{ subversion_test_dir }}/svn/trunk
+ register: trunk
+
+- name: check for branches
+ stat: path={{ subversion_test_dir }}/svn/branches
+ register: branches
+
+- name: assert presence of tags/trunk/branches
+ assert:
+ that:
+ - "tags.stat.isdir"
+ - "trunk.stat.isdir"
+ - "branches.stat.isdir"
+
+- name: remove checked out repo
+ file:
+ path: '{{ subversion_test_dir }}/svn'
+ state: absent
+
+- name: checkout with quotes in username
+ subversion:
+ repo: '{{ subversion_repo_auth_url }}'
+ dest: '{{ subversion_test_dir }}/svn'
+ username: '{{ subversion_username }}'
+ password: '{{ subversion_password }}'
+ register: subverted3
+
+- name: get result of checkout with quotes in username
+ stat:
+ path: '{{ subversion_test_dir }}/svn'
+ register: subverted3_result
+
+- name: assert checkout with quotes in username
+ assert:
+ that:
+ - subverted3 is changed
+ - subverted3_result.stat.exists
+ - subverted3_result.stat.isdir
+
+- name: checkout with export
+ subversion:
+ repo: '{{ subversion_repo_url }}'
+ dest: '{{ subversion_test_dir }}/svn-export'
+ export: True
+ register: subverted4
+
+- name: check for tags
+ stat: path={{ subversion_test_dir }}/svn-export/tags
+ register: export_tags
+
+- name: check for trunk
+ stat: path={{ subversion_test_dir }}/svn-export/trunk
+ register: export_trunk
+
+- name: check for branches
+ stat: path={{ subversion_test_dir }}/svn-export/branches
+ register: export_branches
+
+- name: assert presence of tags/trunk/branches in export
+ assert:
+ that:
+ - "export_tags.stat.isdir"
+ - "export_trunk.stat.isdir"
+ - "export_branches.stat.isdir"
+ - "subverted4.changed"
+
+- name: clone a small external repo with validate_certs=true
+ subversion:
+ repo: "{{ subversion_external_repo_url }}"
+ dest: "{{ subversion_test_dir }}/svn-external1"
+ validate_certs: yes
+
+- name: clone a small external repo with validate_certs=false
+ subversion:
+ repo: "{{ subversion_external_repo_url }}"
+ dest: "{{ subversion_test_dir }}/svn-external2"
+ validate_certs: no
+
+# TBA: test for additional options or URL variants welcome
diff --git a/test/integration/targets/subversion/roles/subversion/tasks/warnings.yml b/test/integration/targets/subversion/roles/subversion/tasks/warnings.yml
new file mode 100644
index 0000000..50ebd44
--- /dev/null
+++ b/test/integration/targets/subversion/roles/subversion/tasks/warnings.yml
@@ -0,0 +1,7 @@
+---
+- name: checkout using a password to test for a warning when using svn lt 1.10.0
+ subversion:
+ repo: '{{ subversion_repo_auth_url }}'
+ dest: '{{ subversion_test_dir }}/svn'
+ username: '{{ subversion_username }}'
+ password: '{{ subversion_password }}'
diff --git a/test/integration/targets/subversion/roles/subversion/templates/subversion.conf.j2 b/test/integration/targets/subversion/roles/subversion/templates/subversion.conf.j2
new file mode 100644
index 0000000..86f4070
--- /dev/null
+++ b/test/integration/targets/subversion/roles/subversion/templates/subversion.conf.j2
@@ -0,0 +1,71 @@
+{% if ansible_os_family == "Debian" %}
+
+{# On Ubuntu 16.04 we can include the default config, other versions require explicit config #}
+{% if ansible_distribution_version == "16.04" %}
+Include /etc/apache2/apache2.conf
+
+{% else %}
+Timeout 300
+KeepAlive On
+MaxKeepAliveRequests 100
+KeepAliveTimeout 5
+User ${APACHE_RUN_USER}
+Group ${APACHE_RUN_GROUP}
+HostnameLookups Off
+LogLevel warn
+LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
+LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
+LogFormat "%h %l %u %t \"%r\" %>s %O" common
+LogFormat "%{Referer}i -> %U" referer
+LogFormat "%{User-agent}i" agent
+
+IncludeOptional mods-enabled/*.load
+IncludeOptional mods-enabled/*.conf
+IncludeOptional conf-enabled/*.conf
+IncludeOptional sites-enabled/*conf
+
+<FilesMatch "^\.ht">
+ Require all denied
+</FilesMatch>
+
+{% endif %}
+
+{% elif ansible_os_family == "FreeBSD" %}
+Include /usr/local/etc/apache24/httpd.conf
+LoadModule dav_module libexec/apache24/mod_dav.so
+LoadModule dav_svn_module libexec/apache24/mod_dav_svn.so
+LoadModule authz_svn_module libexec/apache24/mod_authz_svn.so
+{% elif ansible_os_family == "Suse" %}
+Include /etc/apache2/httpd.conf
+LoadModule dav_module /usr/lib64/apache2/mod_dav.so
+LoadModule dav_svn_module /usr/lib64/apache2/mod_dav_svn.so
+{% elif ansible_os_family == "Alpine" %}
+Include /etc/apache2/httpd.conf
+LoadModule dav_module /usr/lib/apache2/mod_dav.so
+LoadModule dav_svn_module /usr/lib/apache2/mod_dav_svn.so
+{% elif ansible_os_family == "RedHat" %}
+Include /etc/httpd/conf/httpd.conf
+{% endif %}
+
+PidFile {{ subversion_server_dir }}/apache.pid
+Listen 127.0.0.1:{{ apache_port }}
+ErrorLog {{ subversion_server_dir }}/apache2-error.log
+
+<Location /svn>
+ DAV svn
+ SVNParentPath {{ subversion_server_dir }}
+{% if ansible_distribution == "CentOS" and ansible_distribution_version.startswith("6") %}
+ Allow from all
+{% else %}
+ Require all granted
+{% endif %}
+</Location>
+
+<Location /svnauth>
+ DAV svn
+ SVNParentPath {{ subversion_server_dir }}
+ AuthType Basic
+ AuthName "Subversion repositories"
+ AuthUserFile {{ subversion_server_dir }}/svn-auth-users
+ Require valid-user
+</Location>
diff --git a/test/integration/targets/subversion/runme.sh b/test/integration/targets/subversion/runme.sh
new file mode 100755
index 0000000..8a4f0d0
--- /dev/null
+++ b/test/integration/targets/subversion/runme.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+
+set -eux -o pipefail
+
+cleanup() {
+ echo "Cleanup"
+ ansible-playbook runme.yml -i "${INVENTORY_PATH}" "$@" --tags cleanup
+ echo "Done"
+}
+
+trap cleanup INT TERM EXIT
+
+export ANSIBLE_ROLES_PATH=roles/
+
+# Ensure subversion is set up
+ansible-playbook runme.yml -i "${INVENTORY_PATH}" "$@" -v --tags setup
+
+# Test functionality
+ansible-playbook runme.yml -i "${INVENTORY_PATH}" "$@" -v --tags tests
+
+# Test a warning is displayed for versions < 1.10.0 when a password is provided
+ansible-playbook runme.yml -i "${INVENTORY_PATH}" "$@" --tags warnings 2>&1 | tee out.txt
+
+version=$(ANSIBLE_FORCE_COLOR=0 ansible -i "${INVENTORY_PATH}" -m shell -a 'svn --version -q' testhost 2>/dev/null | tail -n 1)
+
+echo "svn --version is '${version}'"
+
+secure=$(python -c "from ansible.module_utils.compat.version import LooseVersion; print(LooseVersion('$version') >= LooseVersion('1.10.0'))")
+
+if [[ "${secure}" = "False" ]] && [[ "$(grep -c 'To securely pass credentials, upgrade svn to version 1.10.0' out.txt)" -eq 1 ]]; then
+ echo "Found the expected warning"
+elif [[ "${secure}" = "False" ]]; then
+ echo "Expected a warning"
+ exit 1
+fi
diff --git a/test/integration/targets/subversion/runme.yml b/test/integration/targets/subversion/runme.yml
new file mode 100644
index 0000000..71c5e4b
--- /dev/null
+++ b/test/integration/targets/subversion/runme.yml
@@ -0,0 +1,15 @@
+---
+- hosts: testhost
+ tasks:
+ - name: load OS specific vars
+ include_vars: '{{ item }}'
+ with_first_found:
+ - files:
+ - '{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml'
+ - '{{ ansible_os_family }}.yml'
+ paths: '../vars'
+ tags: always
+
+ - include_role:
+ name: subversion
+ tags: always
diff --git a/test/integration/targets/subversion/vars/Alpine.yml b/test/integration/targets/subversion/vars/Alpine.yml
new file mode 100644
index 0000000..ce071fd
--- /dev/null
+++ b/test/integration/targets/subversion/vars/Alpine.yml
@@ -0,0 +1,7 @@
+---
+subversion_packages:
+- subversion
+- mod_dav_svn
+- apache2-webdav
+apache_user: apache
+apache_group: apache
diff --git a/test/integration/targets/subversion/vars/Debian.yml b/test/integration/targets/subversion/vars/Debian.yml
new file mode 100644
index 0000000..dfe131b
--- /dev/null
+++ b/test/integration/targets/subversion/vars/Debian.yml
@@ -0,0 +1,6 @@
+---
+subversion_packages:
+- subversion
+- libapache2-mod-svn
+apache_user: www-data
+apache_group: www-data
diff --git a/test/integration/targets/subversion/vars/FreeBSD.yml b/test/integration/targets/subversion/vars/FreeBSD.yml
new file mode 100644
index 0000000..153f523
--- /dev/null
+++ b/test/integration/targets/subversion/vars/FreeBSD.yml
@@ -0,0 +1,7 @@
+---
+subversion_packages:
+- apache24
+- mod_dav_svn
+- subversion
+apache_user: www
+apache_group: www
diff --git a/test/integration/targets/subversion/vars/RedHat.yml b/test/integration/targets/subversion/vars/RedHat.yml
new file mode 100644
index 0000000..3e3f910
--- /dev/null
+++ b/test/integration/targets/subversion/vars/RedHat.yml
@@ -0,0 +1,10 @@
+---
+subversion_packages:
+- mod_dav_svn
+- subversion
+upgrade_packages:
+# prevent sqlite from being out-of-sync with the version subversion was compiled with
+- subversion
+- sqlite
+apache_user: apache
+apache_group: apache
diff --git a/test/integration/targets/subversion/vars/Suse.yml b/test/integration/targets/subversion/vars/Suse.yml
new file mode 100644
index 0000000..eab906e
--- /dev/null
+++ b/test/integration/targets/subversion/vars/Suse.yml
@@ -0,0 +1,6 @@
+---
+subversion_packages:
+- subversion
+- subversion-server
+apache_user: wwwrun
+apache_group: www
diff --git a/test/integration/targets/subversion/vars/Ubuntu-18.yml b/test/integration/targets/subversion/vars/Ubuntu-18.yml
new file mode 100644
index 0000000..dfe131b
--- /dev/null
+++ b/test/integration/targets/subversion/vars/Ubuntu-18.yml
@@ -0,0 +1,6 @@
+---
+subversion_packages:
+- subversion
+- libapache2-mod-svn
+apache_user: www-data
+apache_group: www-data
diff --git a/test/integration/targets/subversion/vars/Ubuntu-20.yml b/test/integration/targets/subversion/vars/Ubuntu-20.yml
new file mode 100644
index 0000000..dfe131b
--- /dev/null
+++ b/test/integration/targets/subversion/vars/Ubuntu-20.yml
@@ -0,0 +1,6 @@
+---
+subversion_packages:
+- subversion
+- libapache2-mod-svn
+apache_user: www-data
+apache_group: www-data
diff --git a/test/integration/targets/systemd/aliases b/test/integration/targets/systemd/aliases
new file mode 100644
index 0000000..a6dafcf
--- /dev/null
+++ b/test/integration/targets/systemd/aliases
@@ -0,0 +1 @@
+shippable/posix/group1
diff --git a/test/integration/targets/systemd/defaults/main.yml b/test/integration/targets/systemd/defaults/main.yml
new file mode 100644
index 0000000..33063b8
--- /dev/null
+++ b/test/integration/targets/systemd/defaults/main.yml
@@ -0,0 +1 @@
+fake_service: nonexisting
diff --git a/test/integration/targets/systemd/handlers/main.yml b/test/integration/targets/systemd/handlers/main.yml
new file mode 100644
index 0000000..57469a0
--- /dev/null
+++ b/test/integration/targets/systemd/handlers/main.yml
@@ -0,0 +1,12 @@
+- name: remove unit file
+ file:
+ path: /etc/systemd/system/sleeper@.service
+ state: absent
+
+- name: remove dummy indirect service
+ file:
+ path: "/etc/systemd/system/{{item}}"
+ state: absent
+ loop:
+ - dummy.service
+ - dummy.socket
diff --git a/test/integration/targets/systemd/meta/main.yml b/test/integration/targets/systemd/meta/main.yml
new file mode 100644
index 0000000..07faa21
--- /dev/null
+++ b/test/integration/targets/systemd/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - prepare_tests
diff --git a/test/integration/targets/systemd/tasks/main.yml b/test/integration/targets/systemd/tasks/main.yml
new file mode 100644
index 0000000..3c585e0
--- /dev/null
+++ b/test/integration/targets/systemd/tasks/main.yml
@@ -0,0 +1,122 @@
+# Test code for the systemd module.
+# (c) 2017, James Tanner <tanner.jc@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+##
+## systemctl
+##
+
+- name: End if this system does not use systemd
+ meta: end_host
+ when: ansible_facts.service_mgr != 'systemd'
+
+- name: Include distribution specific variables
+ include_vars: "{{ lookup('first_found', params) }}"
+ vars:
+ params:
+ files:
+ - "{{ ansible_facts.distribution }}.yml"
+ - "{{ ansible_facts.os_family }}.yml"
+ - default.yml
+ paths:
+ - vars
+
+- name: get a list of running services
+ shell: systemctl | fgrep 'running' | awk '{print $1}' | sed 's/\.service//g' | fgrep -v '.' | egrep ^[a-z]
+ register: running_names
+- debug: var=running_names
+
+- name: check running state
+ systemd:
+ name: "{{ running_names.stdout_lines|random }}"
+ state: started
+ register: systemd_test0
+- debug: var=systemd_test0
+- name: validate results for test0
+ assert:
+ that:
+ - 'systemd_test0.changed is defined'
+ - 'systemd_test0.name is defined'
+ - 'systemd_test0.state is defined'
+ - 'systemd_test0.status is defined'
+ - 'not systemd_test0.changed'
+ - 'systemd_test0.state == "started"'
+
+- name: the module must fail when a service is not found
+ systemd:
+ name: '{{ fake_service }}'
+ state: stopped
+ register: result
+ ignore_errors: yes
+
+- assert:
+ that:
+ - result is failed
+ - 'result is search("Could not find the requested service {{ fake_service }}")'
+
+- name: the module must fail in check_mode as well when a service is not found
+ systemd:
+ name: '{{ fake_service }}'
+ state: stopped
+ register: result
+ check_mode: yes
+ ignore_errors: yes
+
+- assert:
+ that:
+ - result is failed
+ - 'result is search("Could not find the requested service {{ fake_service }}")'
+
+- name: check that the module works even when systemd is offline (eg in chroot)
+ systemd:
+ name: "{{ running_names.stdout_lines|random }}"
+ state: started
+ environment:
+ SYSTEMD_OFFLINE: 1
+
+- name: Disable ssh 1
+ systemd:
+ name: '{{ ssh_service }}'
+ enabled: false
+ register: systemd_disable_ssh_1
+
+- name: Disable ssh 2
+ systemd:
+ name: '{{ ssh_service }}'
+ enabled: false
+ register: systemd_disable_ssh_2
+
+- name: Enable ssh 1
+ systemd:
+ name: '{{ ssh_service }}'
+ enabled: true
+ register: systemd_enable_ssh_1
+
+- name: Enable ssh 2
+ systemd:
+ name: '{{ ssh_service }}'
+ enabled: true
+ register: systemd_enable_ssh_2
+
+- assert:
+ that:
+ - systemd_disable_ssh_2 is not changed
+ - systemd_enable_ssh_1 is changed
+ - systemd_enable_ssh_2 is not changed
+
+- import_tasks: test_unit_template.yml
+- import_tasks: test_indirect_service.yml
diff --git a/test/integration/targets/systemd/tasks/test_indirect_service.yml b/test/integration/targets/systemd/tasks/test_indirect_service.yml
new file mode 100644
index 0000000..fc11343
--- /dev/null
+++ b/test/integration/targets/systemd/tasks/test_indirect_service.yml
@@ -0,0 +1,37 @@
+- name: Copy service file
+ template:
+ src: "{{item}}"
+ dest: "/etc/systemd/system/{{item}}"
+ owner: root
+ group: root
+ loop:
+ - dummy.service
+ - dummy.socket
+ notify: remove dummy indirect service
+
+- name: Ensure dummy indirect service is disabled
+ systemd:
+ name: "{{indirect_service}}"
+ enabled: false
+ register: dummy_disabled
+
+- assert:
+ that:
+ - dummy_disabled is not changed
+
+- name: Enable indirect service 1
+ systemd:
+ name: '{{ indirect_service }}'
+ enabled: true
+ register: systemd_enable_dummy_indirect_1
+
+- name: Enable indirect service 2
+ systemd:
+ name: '{{ indirect_service }}'
+ enabled: true
+ register: systemd_enable_dummy_indirect_2
+
+- assert:
+ that:
+ - systemd_enable_dummy_indirect_1 is changed
+ - systemd_enable_dummy_indirect_2 is not changed \ No newline at end of file
diff --git a/test/integration/targets/systemd/tasks/test_unit_template.yml b/test/integration/targets/systemd/tasks/test_unit_template.yml
new file mode 100644
index 0000000..47cb1c7
--- /dev/null
+++ b/test/integration/targets/systemd/tasks/test_unit_template.yml
@@ -0,0 +1,50 @@
+- name: Copy service file
+ template:
+ src: sleeper@.service
+ dest: /etc/systemd/system/sleeper@.service
+ owner: root
+ group: root
+ mode: '0644'
+ notify: remove unit file
+
+- name: Reload systemd
+ systemd:
+ daemon_reload: yes
+
+- name: Start and enable service using unit template
+ systemd:
+ name: sleeper@100.service
+ state: started
+ enabled: yes
+ register: template_test_1
+
+- name: Start and enable service using unit template again
+ systemd:
+ name: sleeper@100.service
+ state: started
+ enabled: yes
+ register: template_test_2
+
+- name: Stop and disable service using unit template
+ systemd:
+ name: sleeper@100.service
+ state: stopped
+ enabled: no
+ register: template_test_3
+
+- name: Stop and disable service using unit template again
+ systemd:
+ name: sleeper@100.service
+ state: stopped
+ enabled: no
+ register: template_test_4
+
+- name:
+ assert:
+ that:
+ - template_test_1 is changed
+ - template_test_1 is success
+ - template_test_2 is not changed
+ - template_test_2 is success
+ - template_test_3 is changed
+ - template_test_4 is not changed
diff --git a/test/integration/targets/systemd/templates/dummy.service b/test/integration/targets/systemd/templates/dummy.service
new file mode 100644
index 0000000..f38dce1
--- /dev/null
+++ b/test/integration/targets/systemd/templates/dummy.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=Dummy Server
+Requires=dummy.socket
+Documentation=dummy
+
+[Service]
+ExecStart=/bin/yes
+StandardInput=socket
+
+[Install]
+Also=dummy.socket
diff --git a/test/integration/targets/systemd/templates/dummy.socket b/test/integration/targets/systemd/templates/dummy.socket
new file mode 100644
index 0000000..f23bf9b
--- /dev/null
+++ b/test/integration/targets/systemd/templates/dummy.socket
@@ -0,0 +1,8 @@
+[Unit]
+Description=Dummy Server Activation Socket
+
+[Socket]
+ListenDatagram=69
+
+[Install]
+WantedBy=sockets.target \ No newline at end of file
diff --git a/test/integration/targets/systemd/templates/sleeper@.service b/test/integration/targets/systemd/templates/sleeper@.service
new file mode 100644
index 0000000..8b47982
--- /dev/null
+++ b/test/integration/targets/systemd/templates/sleeper@.service
@@ -0,0 +1,8 @@
+[Unit]
+Description=Basic service to use as a template
+
+[Service]
+ExecStart={{ sleep_bin_path }} %i
+
+[Install]
+WantedBy=multi-user.target
diff --git a/test/integration/targets/systemd/vars/Debian.yml b/test/integration/targets/systemd/vars/Debian.yml
new file mode 100644
index 0000000..613410f
--- /dev/null
+++ b/test/integration/targets/systemd/vars/Debian.yml
@@ -0,0 +1,3 @@
+ssh_service: ssh
+sleep_bin_path: /bin/sleep
+indirect_service: dummy \ No newline at end of file
diff --git a/test/integration/targets/systemd/vars/default.yml b/test/integration/targets/systemd/vars/default.yml
new file mode 100644
index 0000000..0bf1f89
--- /dev/null
+++ b/test/integration/targets/systemd/vars/default.yml
@@ -0,0 +1,3 @@
+ssh_service: sshd
+indirect_service: dummy
+sleep_bin_path: /usr/bin/sleep
diff --git a/test/integration/targets/tags/aliases b/test/integration/targets/tags/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/tags/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/tags/ansible_run_tags.yml b/test/integration/targets/tags/ansible_run_tags.yml
new file mode 100644
index 0000000..0e965ad
--- /dev/null
+++ b/test/integration/targets/tags/ansible_run_tags.yml
@@ -0,0 +1,49 @@
+---
+- name: verify ansible_run_tags work as expected
+ hosts: testhost
+ gather_facts: False
+ tasks:
+ - debug:
+ var: ansible_run_tags
+ tags:
+ - always
+
+ - debug:
+ var: expect
+ tags:
+ - always
+
+ - assert:
+ that:
+ - ansible_run_tags == ['all']
+ when: expect == 'all'
+ tags:
+ - always
+
+ - assert:
+ that:
+ - ansible_run_tags|sort == ['tag1', 'tag3']
+ when: expect == 'list'
+ tags:
+ - always
+
+ - assert:
+ that:
+ - ansible_run_tags == ['untagged']
+ when: expect == 'untagged'
+ tags:
+ - always
+
+ - assert:
+ that:
+ - ansible_run_tags|sort == ['tag3', 'untagged']
+ when: expect == 'untagged_list'
+ tags:
+ - always
+
+ - assert:
+ that:
+ - ansible_run_tags == ['tagged']
+ when: expect == 'tagged'
+ tags:
+ - always
diff --git a/test/integration/targets/tags/runme.sh b/test/integration/targets/tags/runme.sh
new file mode 100755
index 0000000..9da0b30
--- /dev/null
+++ b/test/integration/targets/tags/runme.sh
@@ -0,0 +1,75 @@
+#!/usr/bin/env bash
+
+set -eux -o pipefail
+
+# Run these using en_US.UTF-8 because list-tasks is a user output function and so it tailors its output to the
+# user's locale. For unicode tags, this means replacing non-ascii chars with "?"
+
+COMMAND=(ansible-playbook -i ../../inventory test_tags.yml -v --list-tasks)
+
+export LC_ALL=en_US.UTF-8
+
+# Run everything by default
+[ "$("${COMMAND[@]}" | grep -F Task_with | xargs)" = \
+"Task_with_tag TAGS: [tag] Task_with_always_tag TAGS: [always] Task_with_unicode_tag TAGS: [ãらã¨ã¿] Task_with_list_of_tags TAGS: [café, press] Task_without_tag TAGS: [] Task_with_csv_tags TAGS: [tag1, tag2] Task_with_templated_tags TAGS: [tag3] Task_with_meta_tags TAGS: [meta_tag]" ]
+
+# Run the exact tags, and always
+[ "$("${COMMAND[@]}" --tags tag | grep -F Task_with | xargs)" = \
+"Task_with_tag TAGS: [tag] Task_with_always_tag TAGS: [always]" ]
+
+# Skip one tag
+[ "$("${COMMAND[@]}" --skip-tags tag | grep -F Task_with | xargs)" = \
+"Task_with_always_tag TAGS: [always] Task_with_unicode_tag TAGS: [ãらã¨ã¿] Task_with_list_of_tags TAGS: [café, press] Task_without_tag TAGS: [] Task_with_csv_tags TAGS: [tag1, tag2] Task_with_templated_tags TAGS: [tag3] Task_with_meta_tags TAGS: [meta_tag]" ]
+
+# Skip a unicode tag
+[ "$("${COMMAND[@]}" --skip-tags 'ãらã¨ã¿' | grep -F Task_with | xargs)" = \
+"Task_with_tag TAGS: [tag] Task_with_always_tag TAGS: [always] Task_with_list_of_tags TAGS: [café, press] Task_without_tag TAGS: [] Task_with_csv_tags TAGS: [tag1, tag2] Task_with_templated_tags TAGS: [tag3] Task_with_meta_tags TAGS: [meta_tag]" ]
+
+# Skip a meta task tag
+[ "$("${COMMAND[@]}" --skip-tags meta_tag | grep -F Task_with | xargs)" = \
+"Task_with_tag TAGS: [tag] Task_with_always_tag TAGS: [always] Task_with_unicode_tag TAGS: [ãらã¨ã¿] Task_with_list_of_tags TAGS: [café, press] Task_without_tag TAGS: [] Task_with_csv_tags TAGS: [tag1, tag2] Task_with_templated_tags TAGS: [tag3]" ]
+
+# Run just a unicode tag and always
+[ "$("${COMMAND[@]}" --tags 'ãらã¨ã¿' | grep -F Task_with | xargs)" = \
+"Task_with_always_tag TAGS: [always] Task_with_unicode_tag TAGS: [ãらã¨ã¿]" ]
+
+# Run a tag from a list of tags and always
+[ "$("${COMMAND[@]}" --tags café | grep -F Task_with | xargs)" = \
+"Task_with_always_tag TAGS: [always] Task_with_list_of_tags TAGS: [café, press]" ]
+
+# Run tag with never
+[ "$("${COMMAND[@]}" --tags donever | grep -F Task_with | xargs)" = \
+"Task_with_always_tag TAGS: [always] Task_with_never_tag TAGS: [donever, never]" ]
+
+# Run csv tags
+[ "$("${COMMAND[@]}" --tags tag1 | grep -F Task_with | xargs)" = \
+"Task_with_always_tag TAGS: [always] Task_with_csv_tags TAGS: [tag1, tag2]" ]
+
+# Run templated tags
+[ "$("${COMMAND[@]}" --tags tag3 | grep -F Task_with | xargs)" = \
+"Task_with_always_tag TAGS: [always] Task_with_templated_tags TAGS: [tag3]" ]
+
+# Run meta tags
+[ "$("${COMMAND[@]}" --tags meta_tag | grep -F Task_with | xargs)" = \
+"Task_with_always_tag TAGS: [always] Task_with_meta_tags TAGS: [meta_tag]" ]
+
+# Run tagged
+[ "$("${COMMAND[@]}" --tags tagged | grep -F Task_with | xargs)" = \
+"Task_with_tag TAGS: [tag] Task_with_always_tag TAGS: [always] Task_with_unicode_tag TAGS: [ãらã¨ã¿] Task_with_list_of_tags TAGS: [café, press] Task_with_csv_tags TAGS: [tag1, tag2] Task_with_templated_tags TAGS: [tag3] Task_with_meta_tags TAGS: [meta_tag]" ]
+
+# Run untagged
+[ "$("${COMMAND[@]}" --tags untagged | grep -F Task_with | xargs)" = \
+"Task_with_always_tag TAGS: [always] Task_without_tag TAGS: []" ]
+
+# Skip 'always'
+[ "$("${COMMAND[@]}" --tags untagged --skip-tags always | grep -F Task_with | xargs)" = \
+"Task_without_tag TAGS: []" ]
+
+# Test ansible_run_tags
+ansible-playbook -i ../../inventory ansible_run_tags.yml -e expect=all "$@"
+ansible-playbook -i ../../inventory ansible_run_tags.yml -e expect=all --tags all "$@"
+ansible-playbook -i ../../inventory ansible_run_tags.yml -e expect=list --tags tag1,tag3 "$@"
+ansible-playbook -i ../../inventory ansible_run_tags.yml -e expect=list --tags tag1 --tags tag3 "$@"
+ansible-playbook -i ../../inventory ansible_run_tags.yml -e expect=untagged --tags untagged "$@"
+ansible-playbook -i ../../inventory ansible_run_tags.yml -e expect=untagged_list --tags untagged,tag3 "$@"
+ansible-playbook -i ../../inventory ansible_run_tags.yml -e expect=tagged --tags tagged "$@"
diff --git a/test/integration/targets/tags/test_tags.yml b/test/integration/targets/tags/test_tags.yml
new file mode 100644
index 0000000..f0f9a72
--- /dev/null
+++ b/test/integration/targets/tags/test_tags.yml
@@ -0,0 +1,36 @@
+---
+- name: verify tags work as expected
+ hosts: testhost
+ gather_facts: False
+ vars:
+ the_tags:
+ - tag3
+ tasks:
+ - name: Task_with_tag
+ debug: msg=
+ tags: tag
+ - name: Task_with_always_tag
+ debug: msg=
+ tags: always
+ - name: Task_with_unicode_tag
+ debug: msg=
+ tags: ãらã¨ã¿
+ - name: Task_with_list_of_tags
+ debug: msg=
+ tags:
+ - café
+ - press
+ - name: Task_without_tag
+ debug: msg=
+ - name: Task_with_never_tag
+ debug: msg=NEVER
+ tags: ['never', 'donever']
+ - name: Task_with_csv_tags
+ debug: msg=csv
+ tags: tag1,tag2
+ - name: Task_with_templated_tags
+ debug: msg=templated
+ tags: "{{ the_tags }}"
+ - name: Task_with_meta_tags
+ meta: reset_connection
+ tags: meta_tag
diff --git a/test/integration/targets/task_ordering/aliases b/test/integration/targets/task_ordering/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/task_ordering/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/task_ordering/meta/main.yml b/test/integration/targets/task_ordering/meta/main.yml
new file mode 100644
index 0000000..1810d4b
--- /dev/null
+++ b/test/integration/targets/task_ordering/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/task_ordering/tasks/main.yml b/test/integration/targets/task_ordering/tasks/main.yml
new file mode 100644
index 0000000..a666006
--- /dev/null
+++ b/test/integration/targets/task_ordering/tasks/main.yml
@@ -0,0 +1,15 @@
+- set_fact:
+ temppath: "{{ remote_tmp_dir }}/output.txt"
+
+- include_tasks: taskorder-include.yml
+ with_items:
+ - 1
+ - 2
+ - 3
+
+- slurp:
+ src: "{{ temppath }}"
+ register: tempout
+
+- assert:
+ that: tempout.content | b64decode == "one.1.two.1.three.1.four.1.one.2.two.2.three.2.four.2.one.3.two.3.three.3.four.3."
diff --git a/test/integration/targets/task_ordering/tasks/taskorder-include.yml b/test/integration/targets/task_ordering/tasks/taskorder-include.yml
new file mode 100644
index 0000000..228e897
--- /dev/null
+++ b/test/integration/targets/task_ordering/tasks/taskorder-include.yml
@@ -0,0 +1,10 @@
+# This test ensures that included tasks are run in order.
+# There have been regressions where included tasks and
+# nested blocks ran out of order...
+
+- shell: printf one.{{ item }}. >> {{ temppath }}
+- block:
+ - shell: printf two.{{ item }}. >> {{ temppath }}
+ - block:
+ - shell: printf three.{{ item }}. >> {{ temppath }}
+- shell: printf four.{{ item }}. >> {{ temppath }}
diff --git a/test/integration/targets/tasks/aliases b/test/integration/targets/tasks/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/tasks/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/tasks/playbook.yml b/test/integration/targets/tasks/playbook.yml
new file mode 100644
index 0000000..80d9f8b
--- /dev/null
+++ b/test/integration/targets/tasks/playbook.yml
@@ -0,0 +1,19 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ # make sure tasks with an undefined variable in the name are gracefully handled
+ - name: "Task name with undefined variable: {{ not_defined }}"
+ debug:
+ msg: Hello
+
+ - name: ensure malformed raw_params on arbitrary actions are not ignored
+ debug:
+ garbage {{"with a template"}}
+ ignore_errors: true
+ register: bad_templated_raw_param
+
+ - assert:
+ that:
+ - bad_templated_raw_param is failed
+ - |
+ "invalid or malformed argument: 'garbage with a template'" in bad_templated_raw_param.msg
diff --git a/test/integration/targets/tasks/runme.sh b/test/integration/targets/tasks/runme.sh
new file mode 100755
index 0000000..594447b
--- /dev/null
+++ b/test/integration/targets/tasks/runme.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+ansible-playbook playbook.yml "$@"
diff --git a/test/integration/targets/tempfile/aliases b/test/integration/targets/tempfile/aliases
new file mode 100644
index 0000000..a6dafcf
--- /dev/null
+++ b/test/integration/targets/tempfile/aliases
@@ -0,0 +1 @@
+shippable/posix/group1
diff --git a/test/integration/targets/tempfile/meta/main.yml b/test/integration/targets/tempfile/meta/main.yml
new file mode 100644
index 0000000..1810d4b
--- /dev/null
+++ b/test/integration/targets/tempfile/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/tempfile/tasks/main.yml b/test/integration/targets/tempfile/tasks/main.yml
new file mode 100644
index 0000000..2d783e6
--- /dev/null
+++ b/test/integration/targets/tempfile/tasks/main.yml
@@ -0,0 +1,63 @@
+- name: Create a temporary file with defaults
+ tempfile:
+ register: temp_file_default
+
+- name: Create a temporary directory with defaults
+ tempfile:
+ state: directory
+ register: temp_dir_default
+
+- name: Create a temporary file with optional parameters
+ tempfile:
+ path: "{{ remote_tmp_dir }}"
+ prefix: hello.
+ suffix: .goodbye
+ register: temp_file_options
+
+- name: Create a temporary directory with optional parameters
+ tempfile:
+ state: directory
+ path: "{{ remote_tmp_dir }}"
+ prefix: hello.
+ suffix: .goodbye
+ register: temp_dir_options
+
+- name: Create a temporary file in a non-existent directory
+ tempfile:
+ path: "{{ remote_tmp_dir }}/does_not_exist"
+ register: temp_file_non_existent_path
+ ignore_errors: yes
+
+- name: Create a temporary directory in a non-existent directory
+ tempfile:
+ state: directory
+ path: "{{ remote_tmp_dir }}/does_not_exist"
+ register: temp_dir_non_existent_path
+ ignore_errors: yes
+
+- name: Check results
+ assert:
+ that:
+ - temp_file_default is changed
+ - temp_file_default.state == 'file'
+ - temp_file_default.path | basename | split('.') | first == 'ansible'
+
+ - temp_dir_default is changed
+ - temp_dir_default.state == 'directory'
+ - temp_dir_default.path | basename | split('.') | first == 'ansible'
+
+ - temp_file_options is changed
+ - temp_file_options.state == 'file'
+ - temp_file_options.path.startswith(remote_tmp_dir)
+ - temp_file_options.path | basename | split('.') | first == 'hello'
+ - temp_file_options.path | basename | split('.') | last == 'goodbye'
+
+ - temp_dir_options is changed
+ - temp_dir_options.state == 'directory'
+ - temp_dir_options.path.startswith(remote_tmp_dir)
+ - temp_dir_options.path | basename | split('.') | first == 'hello'
+ - temp_dir_options.path | basename | split('.') | last == 'goodbye'
+
+ - temp_file_non_existent_path is failed
+
+ - temp_dir_non_existent_path is failed
diff --git a/test/integration/targets/template/6653.yml b/test/integration/targets/template/6653.yml
new file mode 100644
index 0000000..970478f
--- /dev/null
+++ b/test/integration/targets/template/6653.yml
@@ -0,0 +1,10 @@
+- hosts: localhost
+ gather_facts: no
+ vars:
+ mylist:
+ - alpha
+ - bravo
+ tasks:
+ - name: Should not fail on undefined variable
+ set_fact:
+ template_result: "{{ lookup('template', '6653.j2') }}"
diff --git a/test/integration/targets/template/72262.yml b/test/integration/targets/template/72262.yml
new file mode 100644
index 0000000..33c610d
--- /dev/null
+++ b/test/integration/targets/template/72262.yml
@@ -0,0 +1,6 @@
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - name: Should not fail on undefined variable
+ set_fact:
+ template_result: "{{ lookup('template', '72262.j2') }}"
diff --git a/test/integration/targets/template/72615.yml b/test/integration/targets/template/72615.yml
new file mode 100644
index 0000000..153cfd6
--- /dev/null
+++ b/test/integration/targets/template/72615.yml
@@ -0,0 +1,18 @@
+- hosts: localhost
+ gather_facts: no
+ vars:
+ foo: "top-level-foo"
+ tasks:
+ - set_fact:
+ template_result: "{{ lookup('template', '72615.j2') }}"
+
+ - assert:
+ that:
+ - "'template-level-bar' in template_result"
+ - "'template-nested-level-bar' in template_result"
+
+ - assert:
+ that:
+ - "'top-level-foo' not in template_result"
+ - "'template-level-foo' in template_result"
+ - "'template-nested-level-foo' in template_result"
diff --git a/test/integration/targets/template/aliases b/test/integration/targets/template/aliases
new file mode 100644
index 0000000..4e09af1
--- /dev/null
+++ b/test/integration/targets/template/aliases
@@ -0,0 +1,3 @@
+needs/root
+shippable/posix/group4
+context/controller # this "module" is actually an action that runs on the controller
diff --git a/test/integration/targets/template/ansible_managed.cfg b/test/integration/targets/template/ansible_managed.cfg
new file mode 100644
index 0000000..3626429
--- /dev/null
+++ b/test/integration/targets/template/ansible_managed.cfg
@@ -0,0 +1,2 @@
+[defaults]
+ansible_managed=ansible_managed = Ansible managed: {file} modified on %Y-%m-%d %H:%M:%S by {uid} on {host}
diff --git a/test/integration/targets/template/ansible_managed.yml b/test/integration/targets/template/ansible_managed.yml
new file mode 100644
index 0000000..2bd7c2c
--- /dev/null
+++ b/test/integration/targets/template/ansible_managed.yml
@@ -0,0 +1,14 @@
+---
+- hosts: testhost
+ gather_facts: False
+ tasks:
+ - set_fact:
+ output_dir: "{{ lookup('env', 'OUTPUT_DIR') }}"
+ - file:
+ path: '{{ output_dir }}/café.txt'
+ state: 'absent'
+ # Smoketest that ansible_managed with non-ascii chars works:
+ # https://github.com/ansible/ansible/issues/27262
+ - template:
+ src: 'templates/café.j2'
+ dest: '{{ output_dir }}/café.txt'
diff --git a/test/integration/targets/template/badnull1.cfg b/test/integration/targets/template/badnull1.cfg
new file mode 100644
index 0000000..e1d0688
--- /dev/null
+++ b/test/integration/targets/template/badnull1.cfg
@@ -0,0 +1,2 @@
+[defaults]
+null_representation = null
diff --git a/test/integration/targets/template/badnull2.cfg b/test/integration/targets/template/badnull2.cfg
new file mode 100644
index 0000000..058ca48
--- /dev/null
+++ b/test/integration/targets/template/badnull2.cfg
@@ -0,0 +1,2 @@
+[defaults]
+null_representation = ''
diff --git a/test/integration/targets/template/badnull3.cfg b/test/integration/targets/template/badnull3.cfg
new file mode 100644
index 0000000..3c743fb
--- /dev/null
+++ b/test/integration/targets/template/badnull3.cfg
@@ -0,0 +1,2 @@
+[defaults]
+null_representation = none
diff --git a/test/integration/targets/template/corner_cases.yml b/test/integration/targets/template/corner_cases.yml
new file mode 100644
index 0000000..9d41ed9
--- /dev/null
+++ b/test/integration/targets/template/corner_cases.yml
@@ -0,0 +1,55 @@
+- name: test tempating corner cases
+ hosts: localhost
+ gather_facts: false
+ vars:
+ empty_list: []
+ dont: I SHOULD NOT BE TEMPLATED
+ other: I WORK
+ tasks:
+ - name: 'ensure we are not interpolating data from outside of j2 delmiters'
+ assert:
+ that:
+ - '"I SHOULD NOT BE TEMPLATED" not in adjacent'
+ - globals1 == "[[], globals()]"
+ - globals2 == "[[], globals]"
+ - left_hand == '[1] + [2]'
+ - left_hand_2 == '[1 + 2 * 3 / 4] + [-2.5, 2.5, 3.5]'
+ vars:
+ adjacent: "{{ empty_list }} + [dont]"
+ globals1: "[{{ empty_list }}, globals()]"
+ globals2: "[{{ empty_list }}, globals]"
+ left_hand: '[1] + {{ [2] }}'
+ left_hand_2: '[1 + 2 * 3 / 4] + {{ [-2.5, +2.5, 1 + 2.5] }}'
+
+ - name: 'ensure we can add lists'
+ assert:
+ that:
+ - (empty_list + [other]) == [other]
+ - (empty_list + [other, other]) == [other, other]
+ - (dont_exist|default([]) + [other]) == [other]
+ - ([other] + [empty_list, other]) == [other, [], other]
+
+ - name: 'ensure comments go away and we still dont interpolate in string'
+ assert:
+ that:
+ - 'comm1 == " + [dont]"'
+ - 'comm2 == " #} + [dont]"'
+ vars:
+ comm1: '{# {{nothing}} {# #} + [dont]'
+ comm2: "{# {{nothing}} {# #} #} + [dont]"
+
+ - name: test additions with facts, set them up
+ set_fact:
+ inames: []
+ iname: "{{ prefix ~ '-options' }}"
+ iname_1: "{{ prefix ~ '-options-1' }}"
+ vars:
+ prefix: 'bo'
+
+ - name: add the facts
+ set_fact:
+ inames: '{{ inames + [iname, iname_1] }}'
+
+ - assert:
+ that:
+ - inames == ['bo-options', 'bo-options-1']
diff --git a/test/integration/targets/template/custom_tasks/tasks/main.yml b/test/integration/targets/template/custom_tasks/tasks/main.yml
new file mode 100644
index 0000000..182f7cc
--- /dev/null
+++ b/test/integration/targets/template/custom_tasks/tasks/main.yml
@@ -0,0 +1,15 @@
+---
+- set_fact:
+ output_dir: "{{ lookup('env', 'OUTPUT_DIR') }}"
+
+- template:
+ src: test
+ dest: "{{ output_dir }}/templated_test"
+ register: custom_template_result
+
+- debug:
+ msg: "{{ custom_template_result }}"
+
+- assert:
+ that:
+ - custom_template_result.changed
diff --git a/test/integration/targets/template/custom_tasks/templates/test b/test/integration/targets/template/custom_tasks/templates/test
new file mode 100644
index 0000000..d033f12
--- /dev/null
+++ b/test/integration/targets/template/custom_tasks/templates/test
@@ -0,0 +1 @@
+Sample Text
diff --git a/test/integration/targets/template/custom_template.yml b/test/integration/targets/template/custom_template.yml
new file mode 100644
index 0000000..e5c7aac
--- /dev/null
+++ b/test/integration/targets/template/custom_template.yml
@@ -0,0 +1,4 @@
+- hosts: testhost
+ gather_facts: yes
+ roles:
+ - { role: custom_tasks }
diff --git a/test/integration/targets/template/files/custom_comment_string.expected b/test/integration/targets/template/files/custom_comment_string.expected
new file mode 100644
index 0000000..f3a08f7
--- /dev/null
+++ b/test/integration/targets/template/files/custom_comment_string.expected
@@ -0,0 +1,2 @@
+Before
+After
diff --git a/test/integration/targets/template/files/encoding_1252_utf-8.expected b/test/integration/targets/template/files/encoding_1252_utf-8.expected
new file mode 100644
index 0000000..0d3cc35
--- /dev/null
+++ b/test/integration/targets/template/files/encoding_1252_utf-8.expected
@@ -0,0 +1 @@
+windows-1252 Special Characters: €‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“â€â€¢â€“—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÃÂÃÄÅÆÇÈÉÊËÌÃÃŽÃÃÑÒÓÔÕÖ×ØÙÚÛÜÃÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ
diff --git a/test/integration/targets/template/files/encoding_1252_windows-1252.expected b/test/integration/targets/template/files/encoding_1252_windows-1252.expected
new file mode 100644
index 0000000..7fb94a7
--- /dev/null
+++ b/test/integration/targets/template/files/encoding_1252_windows-1252.expected
@@ -0,0 +1 @@
+windows-1252 Special Characters: €‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ
diff --git a/test/integration/targets/template/files/foo-py26.txt b/test/integration/targets/template/files/foo-py26.txt
new file mode 100644
index 0000000..76b0bb5
--- /dev/null
+++ b/test/integration/targets/template/files/foo-py26.txt
@@ -0,0 +1,9 @@
+templated_var_loaded
+
+{
+ "bool": true,
+ "multi_part": "1Foo",
+ "null_type": null,
+ "number": 5,
+ "string_num": "5"
+}
diff --git a/test/integration/targets/template/files/foo.dos.txt b/test/integration/targets/template/files/foo.dos.txt
new file mode 100644
index 0000000..b716eca
--- /dev/null
+++ b/test/integration/targets/template/files/foo.dos.txt
@@ -0,0 +1,3 @@
+BEGIN
+templated_var_loaded
+END
diff --git a/test/integration/targets/template/files/foo.txt b/test/integration/targets/template/files/foo.txt
new file mode 100644
index 0000000..58af3be
--- /dev/null
+++ b/test/integration/targets/template/files/foo.txt
@@ -0,0 +1,9 @@
+templated_var_loaded
+
+{
+ "bool": true,
+ "multi_part": "1Foo",
+ "null_type": null,
+ "number": 5,
+ "string_num": "5"
+}
diff --git a/test/integration/targets/template/files/foo.unix.txt b/test/integration/targets/template/files/foo.unix.txt
new file mode 100644
index 0000000..d33849f
--- /dev/null
+++ b/test/integration/targets/template/files/foo.unix.txt
@@ -0,0 +1,3 @@
+BEGIN
+templated_var_loaded
+END
diff --git a/test/integration/targets/template/files/import_as.expected b/test/integration/targets/template/files/import_as.expected
new file mode 100644
index 0000000..fc6ea02
--- /dev/null
+++ b/test/integration/targets/template/files/import_as.expected
@@ -0,0 +1,3 @@
+hello world import as
+WIBBLE
+Goodbye
diff --git a/test/integration/targets/template/files/import_as_with_context.expected b/test/integration/targets/template/files/import_as_with_context.expected
new file mode 100644
index 0000000..7099a47
--- /dev/null
+++ b/test/integration/targets/template/files/import_as_with_context.expected
@@ -0,0 +1,2 @@
+hello world as qux with context
+WIBBLE
diff --git a/test/integration/targets/template/files/import_with_context.expected b/test/integration/targets/template/files/import_with_context.expected
new file mode 100644
index 0000000..5323655
--- /dev/null
+++ b/test/integration/targets/template/files/import_with_context.expected
@@ -0,0 +1,3 @@
+hello world with context
+WIBBLE
+Goodbye
diff --git a/test/integration/targets/template/files/lstrip_blocks_false.expected b/test/integration/targets/template/files/lstrip_blocks_false.expected
new file mode 100644
index 0000000..1260001
--- /dev/null
+++ b/test/integration/targets/template/files/lstrip_blocks_false.expected
@@ -0,0 +1,4 @@
+ hello world
+ hello world
+ hello world
+
diff --git a/test/integration/targets/template/files/lstrip_blocks_true.expected b/test/integration/targets/template/files/lstrip_blocks_true.expected
new file mode 100644
index 0000000..1b11f8b
--- /dev/null
+++ b/test/integration/targets/template/files/lstrip_blocks_true.expected
@@ -0,0 +1,3 @@
+hello world
+hello world
+hello world
diff --git a/test/integration/targets/template/files/override_colon_value.expected b/test/integration/targets/template/files/override_colon_value.expected
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/test/integration/targets/template/files/override_colon_value.expected
@@ -0,0 +1 @@
+foo
diff --git a/test/integration/targets/template/files/string_type_filters.expected b/test/integration/targets/template/files/string_type_filters.expected
new file mode 100644
index 0000000..989c356
--- /dev/null
+++ b/test/integration/targets/template/files/string_type_filters.expected
@@ -0,0 +1,4 @@
+{
+ "foo": "bar",
+ "foobar": 1
+}
diff --git a/test/integration/targets/template/files/trim_blocks_false.expected b/test/integration/targets/template/files/trim_blocks_false.expected
new file mode 100644
index 0000000..283cefc
--- /dev/null
+++ b/test/integration/targets/template/files/trim_blocks_false.expected
@@ -0,0 +1,4 @@
+
+Hello world
+
+Goodbye
diff --git a/test/integration/targets/template/files/trim_blocks_true.expected b/test/integration/targets/template/files/trim_blocks_true.expected
new file mode 100644
index 0000000..03acd5d
--- /dev/null
+++ b/test/integration/targets/template/files/trim_blocks_true.expected
@@ -0,0 +1,2 @@
+Hello world
+Goodbye
diff --git a/test/integration/targets/template/filter_plugins.yml b/test/integration/targets/template/filter_plugins.yml
new file mode 100644
index 0000000..c3e97a5
--- /dev/null
+++ b/test/integration/targets/template/filter_plugins.yml
@@ -0,0 +1,9 @@
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - debug:
+ msg: "force templating in delegate_to before we hit the second one with a filter"
+ delegate_to: "{{ 'localhost' }}"
+
+ - include_role:
+ name: role_filter
diff --git a/test/integration/targets/template/in_template_overrides.j2 b/test/integration/targets/template/in_template_overrides.j2
new file mode 100644
index 0000000..2247607
--- /dev/null
+++ b/test/integration/targets/template/in_template_overrides.j2
@@ -0,0 +1,5 @@
+#jinja2:variable_start_string:'<<' , variable_end_string:'>>'
+var_a: << var_a >>
+var_b: << var_b >>
+var_c: << var_c >>
+var_d: << var_d >>
diff --git a/test/integration/targets/template/in_template_overrides.yml b/test/integration/targets/template/in_template_overrides.yml
new file mode 100644
index 0000000..3c2d4d9
--- /dev/null
+++ b/test/integration/targets/template/in_template_overrides.yml
@@ -0,0 +1,28 @@
+- hosts: localhost
+ gather_facts: false
+ vars:
+ var_a: "value"
+ var_b: "{{ var_a }}"
+ var_c: "<< var_a >>"
+ tasks:
+ - set_fact:
+ var_d: "{{ var_a }}"
+
+ - block:
+ - template:
+ src: in_template_overrides.j2
+ dest: out.txt
+
+ - command: cat out.txt
+ register: out
+
+ - assert:
+ that:
+ - "'var_a: value' in out.stdout"
+ - "'var_b: value' in out.stdout"
+ - "'var_c: << var_a >>' in out.stdout"
+ - "'var_d: value' in out.stdout"
+ always:
+ - file:
+ path: out.txt
+ state: absent
diff --git a/test/integration/targets/template/lazy_eval.yml b/test/integration/targets/template/lazy_eval.yml
new file mode 100644
index 0000000..856b710
--- /dev/null
+++ b/test/integration/targets/template/lazy_eval.yml
@@ -0,0 +1,24 @@
+- hosts: testhost
+ gather_facts: false
+ vars:
+ deep_undefined: "{{ nested_undefined_variable }}"
+ tasks:
+ - name: These do not throw an error, deep_undefined is just evaluated to undefined, since 2.14
+ assert:
+ that:
+ - lazy_eval or deep_undefined
+ - deep_undefined is undefined
+ - deep_undefined|default('defaulted') == 'defaulted'
+ vars:
+ lazy_eval: true
+
+ - name: EXPECTED FAILURE actually using deep_undefined fails
+ debug:
+ msg: "{{ deep_undefined }}"
+ ignore_errors: true
+ register: res
+
+ - assert:
+ that:
+ - res.failed
+ - res.msg is contains("'nested_undefined_variable' is undefined")
diff --git a/test/integration/targets/template/meta/main.yml b/test/integration/targets/template/meta/main.yml
new file mode 100644
index 0000000..06d4fd2
--- /dev/null
+++ b/test/integration/targets/template/meta/main.yml
@@ -0,0 +1,3 @@
+dependencies:
+ - prepare_tests
+ - setup_nobody
diff --git a/test/integration/targets/template/role_filter/filter_plugins/myplugin.py b/test/integration/targets/template/role_filter/filter_plugins/myplugin.py
new file mode 100644
index 0000000..b0a8889
--- /dev/null
+++ b/test/integration/targets/template/role_filter/filter_plugins/myplugin.py
@@ -0,0 +1,12 @@
+#!/usr/bin/env python
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+class FilterModule(object):
+ def filters(self):
+ return {'parse_ip': self.parse_ip}
+
+ def parse_ip(self, ip):
+ return ip
diff --git a/test/integration/targets/template/role_filter/tasks/main.yml b/test/integration/targets/template/role_filter/tasks/main.yml
new file mode 100644
index 0000000..7d962a2
--- /dev/null
+++ b/test/integration/targets/template/role_filter/tasks/main.yml
@@ -0,0 +1,3 @@
+- name: test
+ command: echo hello
+ delegate_to: "{{ '127.0.0.1' | parse_ip }}"
diff --git a/test/integration/targets/template/runme.sh b/test/integration/targets/template/runme.sh
new file mode 100755
index 0000000..30163af
--- /dev/null
+++ b/test/integration/targets/template/runme.sh
@@ -0,0 +1,54 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ANSIBLE_ROLES_PATH=../ ansible-playbook template.yml -i ../../inventory -v "$@"
+
+# Test for https://github.com/ansible/ansible/pull/35571
+ansible testhost -i testhost, -m debug -a 'msg={{ hostvars["localhost"] }}' -e "vars1={{ undef() }}" -e "vars2={{ vars1 }}"
+
+# Test for https://github.com/ansible/ansible/issues/27262
+ansible-playbook ansible_managed.yml -c ansible_managed.cfg -i ../../inventory -v "$@"
+
+# Test for #42585
+ANSIBLE_ROLES_PATH=../ ansible-playbook custom_template.yml -i ../../inventory -v "$@"
+
+
+# Test for several corner cases #57188
+ansible-playbook corner_cases.yml -v "$@"
+
+# Test for #57351
+ansible-playbook filter_plugins.yml -v "$@"
+
+# https://github.com/ansible/ansible/issues/68699
+ansible-playbook unused_vars_include.yml -v "$@"
+
+# https://github.com/ansible/ansible/issues/55152
+ansible-playbook undefined_var_info.yml -v "$@"
+
+# https://github.com/ansible/ansible/issues/72615
+ansible-playbook 72615.yml -v "$@"
+
+# https://github.com/ansible/ansible/issues/6653
+ansible-playbook 6653.yml -v "$@"
+
+# https://github.com/ansible/ansible/issues/72262
+ansible-playbook 72262.yml -v "$@"
+
+# ensure unsafe is preserved, even with extra newlines
+ansible-playbook unsafe.yml -v "$@"
+
+# ensure Jinja2 overrides from a template are used
+ansible-playbook in_template_overrides.yml -v "$@"
+
+ansible-playbook lazy_eval.yml -i ../../inventory -v "$@"
+
+ansible-playbook undefined_in_import.yml -i ../../inventory -v "$@"
+
+# ensure diff null configs work #76493
+for badcfg in "badnull1" "badnull2" "badnull3"
+do
+ [ -f "./${badcfg}.cfg" ]
+ ANSIBLE_CONFIG="./${badcfg}.cfg" ansible-config dump --only-changed
+done
+
diff --git a/test/integration/targets/template/tasks/backup_test.yml b/test/integration/targets/template/tasks/backup_test.yml
new file mode 100644
index 0000000..eb4eff1
--- /dev/null
+++ b/test/integration/targets/template/tasks/backup_test.yml
@@ -0,0 +1,60 @@
+# https://github.com/ansible/ansible/issues/24408
+
+- set_fact:
+ t_username: templateuser1
+ t_groupname: templateuser1
+
+- name: create the test group
+ group:
+ name: "{{ t_groupname }}"
+
+- name: create the test user
+ user:
+ name: "{{ t_username }}"
+ group: "{{ t_groupname }}"
+ createhome: no
+
+- name: set the dest file
+ set_fact:
+ t_dest: "{{ output_dir + '/tfile_dest.txt' }}"
+
+- name: create the old file
+ file:
+ path: "{{ t_dest }}"
+ state: touch
+ mode: 0777
+ owner: "{{ t_username }}"
+ group: "{{ t_groupname }}"
+
+- name: failsafe attr change incase underlying system does not support it
+ shell: chattr =j "{{ t_dest }}"
+ ignore_errors: True
+
+- name: run the template
+ template:
+ src: foo.j2
+ dest: "{{ t_dest }}"
+ backup: True
+ register: t_backup_res
+
+- name: check the data for the backup
+ stat:
+ path: "{{ t_backup_res.backup_file }}"
+ register: t_backup_stats
+
+- name: validate result of preserved backup
+ assert:
+ that:
+ - 't_backup_stats.stat.mode == "0777"'
+ - 't_backup_stats.stat.pw_name == t_username'
+ - 't_backup_stats.stat.gr_name == t_groupname'
+
+- name: cleanup the user
+ user:
+ name: "{{ t_username }}"
+ state: absent
+
+- name: cleanup the group
+ user:
+ name: "{{ t_groupname }}"
+ state: absent
diff --git a/test/integration/targets/template/tasks/main.yml b/test/integration/targets/template/tasks/main.yml
new file mode 100644
index 0000000..c0d2e11
--- /dev/null
+++ b/test/integration/targets/template/tasks/main.yml
@@ -0,0 +1,811 @@
+# test code for the template module
+# (c) 2014, Michael DeHaan <michael.dehaan@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- set_fact:
+ output_dir: "{{ lookup('env', 'OUTPUT_DIR') }}"
+
+- name: show python interpreter
+ debug:
+ msg: "{{ ansible_python['executable'] }}"
+
+- name: show jinja2 version
+ debug:
+ msg: "{{ lookup('pipe', '{{ ansible_python[\"executable\"] }} -c \"import jinja2; print(jinja2.__version__)\"') }}"
+
+- name: get default group
+ shell: id -gn
+ register: group
+
+- name: fill in a basic template
+ template: src=foo.j2 dest={{output_dir}}/foo.templated mode=0644
+ register: template_result
+
+- assert:
+ that:
+ - "'changed' in template_result"
+ - "'dest' in template_result"
+ - "'group' in template_result"
+ - "'gid' in template_result"
+ - "'md5sum' in template_result"
+ - "'checksum' in template_result"
+ - "'owner' in template_result"
+ - "'size' in template_result"
+ - "'src' in template_result"
+ - "'state' in template_result"
+ - "'uid' in template_result"
+
+- name: verify that the file was marked as changed
+ assert:
+ that:
+ - "template_result.changed == true"
+
+# Basic template with non-ascii names
+- name: Check that non-ascii source and dest work
+ template:
+ src: 'café.j2'
+ dest: '{{ output_dir }}/café.txt'
+ register: template_results
+
+- name: Check that the resulting file exists
+ stat:
+ path: '{{ output_dir }}/café.txt'
+ register: stat_results
+
+- name: Check that template created the right file
+ assert:
+ that:
+ - 'template_results is changed'
+ - 'stat_results.stat["exists"]'
+
+# test for import with context on jinja-2.9 See https://github.com/ansible/ansible/issues/20494
+- name: fill in a template using import with context ala issue 20494
+ template: src=import_with_context.j2 dest={{output_dir}}/import_with_context.templated mode=0644
+ register: template_result
+
+- name: copy known good import_with_context.expected into place
+ copy: src=import_with_context.expected dest={{output_dir}}/import_with_context.expected
+
+- name: compare templated file to known good import_with_context
+ shell: diff -uw {{output_dir}}/import_with_context.templated {{output_dir}}/import_with_context.expected
+ register: diff_result
+
+- name: verify templated import_with_context matches known good
+ assert:
+ that:
+ - 'diff_result.stdout == ""'
+ - "diff_result.rc == 0"
+
+# test for nested include https://github.com/ansible/ansible/issues/34886
+- name: test if parent variables are defined in nested include
+ template: src=for_loop.j2 dest={{output_dir}}/for_loop.templated mode=0644
+
+- name: save templated output
+ shell: "cat {{output_dir}}/for_loop.templated"
+ register: for_loop_out
+- debug: var=for_loop_out
+- name: verify variables got templated
+ assert:
+ that:
+ - '"foo" in for_loop_out.stdout'
+ - '"bar" in for_loop_out.stdout'
+ - '"bam" in for_loop_out.stdout'
+
+# test for 'import as' on jinja-2.9 See https://github.com/ansible/ansible/issues/20494
+- name: fill in a template using import as ala fails2 case in issue 20494
+ template: src=import_as.j2 dest={{output_dir}}/import_as.templated mode=0644
+ register: import_as_template_result
+
+- name: copy known good import_as.expected into place
+ copy: src=import_as.expected dest={{output_dir}}/import_as.expected
+
+- name: compare templated file to known good import_as
+ shell: diff -uw {{output_dir}}/import_as.templated {{output_dir}}/import_as.expected
+ register: import_as_diff_result
+
+- name: verify templated import_as matches known good
+ assert:
+ that:
+ - 'import_as_diff_result.stdout == ""'
+ - "import_as_diff_result.rc == 0"
+
+# test for 'import as with context' on jinja-2.9 See https://github.com/ansible/ansible/issues/20494
+- name: fill in a template using import as with context ala fails2 case in issue 20494
+ template: src=import_as_with_context.j2 dest={{output_dir}}/import_as_with_context.templated mode=0644
+ register: import_as_with_context_template_result
+
+- name: copy known good import_as_with_context.expected into place
+ copy: src=import_as_with_context.expected dest={{output_dir}}/import_as_with_context.expected
+
+- name: compare templated file to known good import_as_with_context
+ shell: diff -uw {{output_dir}}/import_as_with_context.templated {{output_dir}}/import_as_with_context.expected
+ register: import_as_with_context_diff_result
+
+- name: verify templated import_as_with_context matches known good
+ assert:
+ that:
+ - 'import_as_with_context_diff_result.stdout == ""'
+ - "import_as_with_context_diff_result.rc == 0"
+
+# VERIFY comment_start_string and comment_end_string
+
+- name: Render a template with "comment_start_string" set to [#
+ template:
+ src: custom_comment_string.j2
+ dest: "{{output_dir}}/custom_comment_string.templated"
+ comment_start_string: "[#"
+ comment_end_string: "#]"
+ register: custom_comment_string_result
+
+- name: Get checksum of known good custom_comment_string.expected
+ stat:
+ path: "{{role_path}}/files/custom_comment_string.expected"
+ register: custom_comment_string_good
+
+- name: Verify templated custom_comment_string matches known good using checksum
+ assert:
+ that:
+ - "custom_comment_string_result.checksum == custom_comment_string_good.stat.checksum"
+
+# VERIFY trim_blocks
+
+- name: Render a template with "trim_blocks" set to False
+ template:
+ src: trim_blocks.j2
+ dest: "{{output_dir}}/trim_blocks_false.templated"
+ trim_blocks: False
+ register: trim_blocks_false_result
+
+- name: Get checksum of known good trim_blocks_false.expected
+ stat:
+ path: "{{role_path}}/files/trim_blocks_false.expected"
+ register: trim_blocks_false_good
+
+- name: Verify templated trim_blocks_false matches known good using checksum
+ assert:
+ that:
+ - "trim_blocks_false_result.checksum == trim_blocks_false_good.stat.checksum"
+
+- name: Render a template with "trim_blocks" set to True
+ template:
+ src: trim_blocks.j2
+ dest: "{{output_dir}}/trim_blocks_true.templated"
+ trim_blocks: True
+ register: trim_blocks_true_result
+
+- name: Get checksum of known good trim_blocks_true.expected
+ stat:
+ path: "{{role_path}}/files/trim_blocks_true.expected"
+ register: trim_blocks_true_good
+
+- name: Verify templated trim_blocks_true matches known good using checksum
+ assert:
+ that:
+ - "trim_blocks_true_result.checksum == trim_blocks_true_good.stat.checksum"
+
+# VERIFY lstrip_blocks
+
+- name: Render a template with "lstrip_blocks" set to False
+ template:
+ src: lstrip_blocks.j2
+ dest: "{{output_dir}}/lstrip_blocks_false.templated"
+ lstrip_blocks: False
+ register: lstrip_blocks_false_result
+
+- name: Get checksum of known good lstrip_blocks_false.expected
+ stat:
+ path: "{{role_path}}/files/lstrip_blocks_false.expected"
+ register: lstrip_blocks_false_good
+
+- name: Verify templated lstrip_blocks_false matches known good using checksum
+ assert:
+ that:
+ - "lstrip_blocks_false_result.checksum == lstrip_blocks_false_good.stat.checksum"
+
+- name: Render a template with "lstrip_blocks" set to True
+ template:
+ src: lstrip_blocks.j2
+ dest: "{{output_dir}}/lstrip_blocks_true.templated"
+ lstrip_blocks: True
+ register: lstrip_blocks_true_result
+ ignore_errors: True
+
+- name: Get checksum of known good lstrip_blocks_true.expected
+ stat:
+ path: "{{role_path}}/files/lstrip_blocks_true.expected"
+ register: lstrip_blocks_true_good
+
+- name: Verify templated lstrip_blocks_true matches known good using checksum
+ assert:
+ that:
+ - "lstrip_blocks_true_result.checksum == lstrip_blocks_true_good.stat.checksum"
+
+# VERIFY CONTENTS
+
+- name: copy known good into place
+ copy: src=foo.txt dest={{output_dir}}/foo.txt
+
+- name: compare templated file to known good
+ shell: diff -uw {{output_dir}}/foo.templated {{output_dir}}/foo.txt
+ register: diff_result
+
+- name: verify templated file matches known good
+ assert:
+ that:
+ - 'diff_result.stdout == ""'
+ - "diff_result.rc == 0"
+
+# VERIFY MODE
+
+- name: set file mode
+ file: path={{output_dir}}/foo.templated mode=0644
+ register: file_result
+
+- name: ensure file mode did not change
+ assert:
+ that:
+ - "file_result.changed != True"
+
+# VERIFY dest as a directory does not break file attributes
+# Note: expanduser is needed to go down the particular codepath that was broken before
+- name: setup directory for test
+ file: state=directory dest={{output_dir | expanduser}}/template-dir mode=0755 owner=nobody group={{ group.stdout }}
+
+- name: set file mode when the destination is a directory
+ template: src=foo.j2 dest={{output_dir | expanduser}}/template-dir/ mode=0600 owner=root group={{ group.stdout }}
+
+- name: set file mode when the destination is a directory
+ template: src=foo.j2 dest={{output_dir | expanduser}}/template-dir/ mode=0600 owner=root group={{ group.stdout }}
+ register: file_result
+
+- name: check that the file has the correct attributes
+ stat: path={{output_dir | expanduser}}/template-dir/foo.j2
+ register: file_attrs
+
+- assert:
+ that:
+ - "file_attrs.stat.uid == 0"
+ - "file_attrs.stat.pw_name == 'root'"
+ - "file_attrs.stat.mode == '0600'"
+
+- name: check that the containing directory did not change attributes
+ stat: path={{output_dir | expanduser}}/template-dir/
+ register: dir_attrs
+
+- assert:
+ that:
+ - "dir_attrs.stat.uid != 0"
+ - "dir_attrs.stat.pw_name == 'nobody'"
+ - "dir_attrs.stat.mode == '0755'"
+
+- name: Check that template to a directory where the directory does not end with a / is allowed
+ template: src=foo.j2 dest={{output_dir | expanduser}}/template-dir mode=0600 owner=root group={{ group.stdout }}
+
+- name: make a symlink to the templated file
+ file:
+ path: '{{ output_dir }}/foo.symlink'
+ src: '{{ output_dir }}/foo.templated'
+ state: link
+
+- name: check that templating the symlink results in the file being templated
+ template:
+ src: foo.j2
+ dest: '{{output_dir}}/foo.symlink'
+ mode: 0600
+ follow: True
+ register: template_result
+
+- assert:
+ that:
+ - "template_result.changed == True"
+
+- name: check that the file has the correct attributes
+ stat: path={{output_dir | expanduser}}/template-dir/foo.j2
+ register: file_attrs
+
+- assert:
+ that:
+ - "file_attrs.stat.mode == '0600'"
+
+- name: check that templating the symlink again makes no changes
+ template:
+ src: foo.j2
+ dest: '{{output_dir}}/foo.symlink'
+ mode: 0600
+ follow: True
+ register: template_result
+
+- assert:
+ that:
+ - "template_result.changed == False"
+
+# Test strange filenames
+
+- name: Create a temp dir for filename tests
+ file:
+ state: directory
+ dest: '{{ output_dir }}/filename-tests'
+
+- name: create a file with an unusual filename
+ template:
+ src: foo.j2
+ dest: "{{ output_dir }}/filename-tests/foo t'e~m\\plated"
+ register: template_result
+
+- assert:
+ that:
+ - "template_result.changed == True"
+
+- name: check that the unusual filename was created
+ command: "ls {{ output_dir }}/filename-tests/"
+ register: unusual_results
+
+- assert:
+ that:
+ - "\"foo t'e~m\\plated\" in unusual_results.stdout_lines"
+ - "{{unusual_results.stdout_lines| length}} == 1"
+
+- name: check that the unusual filename can be checked for changes
+ template:
+ src: foo.j2
+ dest: "{{ output_dir }}/filename-tests/foo t'e~m\\plated"
+ register: template_result
+
+- assert:
+ that:
+ - "template_result.changed == False"
+
+
+# check_mode
+
+- name: fill in a basic template in check mode
+ template: src=short.j2 dest={{output_dir}}/short.templated
+ register: template_result
+ check_mode: True
+
+- name: check file exists
+ stat: path={{output_dir}}/short.templated
+ register: templated
+
+- name: verify that the file was marked as changed in check mode but was not created
+ assert:
+ that:
+ - "not templated.stat.exists"
+ - "template_result is changed"
+
+- name: fill in a basic template
+ template: src=short.j2 dest={{output_dir}}/short.templated
+
+- name: fill in a basic template in check mode
+ template: src=short.j2 dest={{output_dir}}/short.templated
+ register: template_result
+ check_mode: True
+
+- name: verify that the file was marked as not changes in check mode
+ assert:
+ that:
+ - "template_result is not changed"
+ - "'templated_var_loaded' in lookup('file', output_dir + '/short.templated')"
+
+- name: change var for the template
+ set_fact:
+ templated_var: "changed"
+
+- name: fill in a basic template with changed var in check mode
+ template: src=short.j2 dest={{output_dir}}/short.templated
+ register: template_result
+ check_mode: True
+
+- name: verify that the file was marked as changed in check mode but the content was not changed
+ assert:
+ that:
+ - "'templated_var_loaded' in lookup('file', output_dir + '/short.templated')"
+ - "template_result is changed"
+
+# Create a template using a child template, to ensure that variables
+# are passed properly from the parent to subtemplate context (issue #20063)
+
+- name: test parent and subtemplate creation of context
+ template: src=parent.j2 dest={{output_dir}}/parent_and_subtemplate.templated
+ register: template_result
+
+- stat: path={{output_dir}}/parent_and_subtemplate.templated
+
+- name: verify that the parent and subtemplate creation worked
+ assert:
+ that:
+ - "template_result is changed"
+
+#
+# template module can overwrite a file that's been hard linked
+# https://github.com/ansible/ansible/issues/10834
+#
+
+- name: ensure test dir is absent
+ file:
+ path: '{{ output_dir | expanduser }}/hlink_dir'
+ state: absent
+
+- name: create test dir
+ file:
+ path: '{{ output_dir | expanduser }}/hlink_dir'
+ state: directory
+
+- name: template out test file to system 1
+ template:
+ src: foo.j2
+ dest: '{{ output_dir | expanduser }}/hlink_dir/test_file'
+
+- name: make hard link
+ file:
+ src: '{{ output_dir | expanduser }}/hlink_dir/test_file'
+ dest: '{{ output_dir | expanduser }}/hlink_dir/test_file_hlink'
+ state: hard
+
+- name: template out test file to system 2
+ template:
+ src: foo.j2
+ dest: '{{ output_dir | expanduser }}/hlink_dir/test_file'
+ register: hlink_result
+
+- name: check that the files are still hardlinked
+ stat:
+ path: '{{ output_dir | expanduser }}/hlink_dir/test_file'
+ register: orig_file
+
+- name: check that the files are still hardlinked
+ stat:
+ path: '{{ output_dir | expanduser }}/hlink_dir/test_file_hlink'
+ register: hlink_file
+
+# We've done nothing at this point to update the content of the file so it should still be hardlinked
+- assert:
+ that:
+ - "hlink_result.changed == False"
+ - "orig_file.stat.inode == hlink_file.stat.inode"
+
+- name: change var for the template
+ set_fact:
+ templated_var: "templated_var_loaded"
+
+# UNIX TEMPLATE
+- name: fill in a basic template (Unix)
+ template:
+ src: foo2.j2
+ dest: '{{ output_dir }}/foo.unix.templated'
+ register: template_result
+
+- name: verify that the file was marked as changed (Unix)
+ assert:
+ that:
+ - 'template_result is changed'
+
+- name: fill in a basic template again (Unix)
+ template:
+ src: foo2.j2
+ dest: '{{ output_dir }}/foo.unix.templated'
+ register: template_result2
+
+- name: verify that the template was not changed (Unix)
+ assert:
+ that:
+ - 'template_result2 is not changed'
+
+# VERIFY UNIX CONTENTS
+- name: copy known good into place (Unix)
+ copy:
+ src: foo.unix.txt
+ dest: '{{ output_dir }}/foo.unix.txt'
+
+- name: Dump templated file (Unix)
+ command: hexdump -C {{ output_dir }}/foo.unix.templated
+
+- name: Dump expected file (Unix)
+ command: hexdump -C {{ output_dir }}/foo.unix.txt
+
+- name: compare templated file to known good (Unix)
+ command: diff -u {{ output_dir }}/foo.unix.templated {{ output_dir }}/foo.unix.txt
+ register: diff_result
+
+- name: verify templated file matches known good (Unix)
+ assert:
+ that:
+ - 'diff_result.stdout == ""'
+ - "diff_result.rc == 0"
+
+# DOS TEMPLATE
+- name: fill in a basic template (DOS)
+ template:
+ src: foo2.j2
+ dest: '{{ output_dir }}/foo.dos.templated'
+ newline_sequence: '\r\n'
+ register: template_result
+
+- name: verify that the file was marked as changed (DOS)
+ assert:
+ that:
+ - 'template_result is changed'
+
+- name: fill in a basic template again (DOS)
+ template:
+ src: foo2.j2
+ dest: '{{ output_dir }}/foo.dos.templated'
+ newline_sequence: '\r\n'
+ register: template_result2
+
+- name: verify that the template was not changed (DOS)
+ assert:
+ that:
+ - 'template_result2 is not changed'
+
+# VERIFY DOS CONTENTS
+- name: copy known good into place (DOS)
+ copy:
+ src: foo.dos.txt
+ dest: '{{ output_dir }}/foo.dos.txt'
+
+- name: Dump templated file (DOS)
+ command: hexdump -C {{ output_dir }}/foo.dos.templated
+
+- name: Dump expected file (DOS)
+ command: hexdump -C {{ output_dir }}/foo.dos.txt
+
+- name: compare templated file to known good (DOS)
+ command: diff -u {{ output_dir }}/foo.dos.templated {{ output_dir }}/foo.dos.txt
+ register: diff_result
+
+- name: verify templated file matches known good (DOS)
+ assert:
+ that:
+ - 'diff_result.stdout == ""'
+ - "diff_result.rc == 0"
+
+# VERIFY DOS CONTENTS
+- name: copy known good into place (Unix)
+ copy:
+ src: foo.unix.txt
+ dest: '{{ output_dir }}/foo.unix.txt'
+
+- name: Dump templated file (Unix)
+ command: hexdump -C {{ output_dir }}/foo.unix.templated
+
+- name: Dump expected file (Unix)
+ command: hexdump -C {{ output_dir }}/foo.unix.txt
+
+- name: compare templated file to known good (Unix)
+ command: diff -u {{ output_dir }}/foo.unix.templated {{ output_dir }}/foo.unix.txt
+ register: diff_result
+
+- name: verify templated file matches known good (Unix)
+ assert:
+ that:
+ - 'diff_result.stdout == ""'
+ - "diff_result.rc == 0"
+
+# Check that mode=preserve works with template
+- name: Create a template which has strange permissions
+ copy:
+ content: !unsafe '{{ ansible_managed }}\n'
+ dest: '{{ output_dir }}/foo-template.j2'
+ mode: 0547
+ delegate_to: localhost
+
+- name: Use template with mode=preserve
+ template:
+ src: '{{ output_dir }}/foo-template.j2'
+ dest: '{{ output_dir }}/foo-templated.txt'
+ mode: 'preserve'
+ register: template_results
+
+- name: Get permissions from the templated file
+ stat:
+ path: '{{ output_dir }}/foo-templated.txt'
+ register: stat_results
+
+- name: Check that the resulting file has the correct permissions
+ assert:
+ that:
+ - 'template_results is changed'
+ - 'template_results.mode == "0547"'
+ - 'stat_results.stat["mode"] == "0547"'
+
+# Test output_encoding
+- name: Prepare the list of encodings we want to check, including empty string for defaults
+ set_fact:
+ template_encoding_1252_encodings: ['', 'utf-8', 'windows-1252']
+
+- name: Copy known good encoding_1252_*.expected into place
+ copy:
+ src: 'encoding_1252_{{ item | default("utf-8", true) }}.expected'
+ dest: '{{ output_dir }}/encoding_1252_{{ item }}.expected'
+ loop: '{{ template_encoding_1252_encodings }}'
+
+- name: Generate the encoding_1252_* files from templates using various encoding combinations
+ template:
+ src: 'encoding_1252.j2'
+ dest: '{{ output_dir }}/encoding_1252_{{ item }}.txt'
+ output_encoding: '{{ item }}'
+ loop: '{{ template_encoding_1252_encodings }}'
+
+- name: Compare the encoding_1252_* templated files to known good
+ command: diff -u {{ output_dir }}/encoding_1252_{{ item }}.expected {{ output_dir }}/encoding_1252_{{ item }}.txt
+ register: encoding_1252_diff_result
+ loop: '{{ template_encoding_1252_encodings }}'
+
+- name: Check that nested undefined values return Undefined
+ vars:
+ dict_var:
+ bar: {}
+ list_var:
+ - foo: {}
+ assert:
+ that:
+ - dict_var is defined
+ - dict_var.bar is defined
+ - dict_var.bar.baz is not defined
+ - dict_var.bar.baz | default('DEFAULT') == 'DEFAULT'
+ - dict_var.bar.baz.abc is not defined
+ - dict_var.bar.baz.abc | default('DEFAULT') == 'DEFAULT'
+ - dict_var.baz is not defined
+ - dict_var.baz.abc is not defined
+ - dict_var.baz.abc | default('DEFAULT') == 'DEFAULT'
+ - list_var.0 is defined
+ - list_var.1 is not defined
+ - list_var.0.foo is defined
+ - list_var.0.foo.bar is not defined
+ - list_var.0.foo.bar | default('DEFAULT') == 'DEFAULT'
+ - list_var.1.foo is not defined
+ - list_var.1.foo | default('DEFAULT') == 'DEFAULT'
+ - dict_var is defined
+ - dict_var['bar'] is defined
+ - dict_var['bar']['baz'] is not defined
+ - dict_var['bar']['baz'] | default('DEFAULT') == 'DEFAULT'
+ - dict_var['bar']['baz']['abc'] is not defined
+ - dict_var['bar']['baz']['abc'] | default('DEFAULT') == 'DEFAULT'
+ - dict_var['baz'] is not defined
+ - dict_var['baz']['abc'] is not defined
+ - dict_var['baz']['abc'] | default('DEFAULT') == 'DEFAULT'
+ - list_var[0] is defined
+ - list_var[1] is not defined
+ - list_var[0]['foo'] is defined
+ - list_var[0]['foo']['bar'] is not defined
+ - list_var[0]['foo']['bar'] | default('DEFAULT') == 'DEFAULT'
+ - list_var[1]['foo'] is not defined
+ - list_var[1]['foo'] | default('DEFAULT') == 'DEFAULT'
+ - dict_var['bar'].baz is not defined
+ - dict_var['bar'].baz | default('DEFAULT') == 'DEFAULT'
+
+- template:
+ src: template_destpath_test.j2
+ dest: "{{ output_dir }}/template_destpath.templated"
+
+- copy:
+ content: "{{ output_dir}}/template_destpath.templated\n"
+ dest: "{{ output_dir }}/template_destpath.expected"
+
+- name: compare templated file to known good template_destpath
+ shell: diff -uw {{output_dir}}/template_destpath.templated {{output_dir}}/template_destpath.expected
+ register: diff_result
+
+- name: verify templated template_destpath matches known good
+ assert:
+ that:
+ - 'diff_result.stdout == ""'
+ - "diff_result.rc == 0"
+
+- debug:
+ msg: "{{ 'x' in y }}"
+ ignore_errors: yes
+ register: error
+
+- name: check that proper error message is emitted when in operator is used
+ assert:
+ that: "\"'y' is undefined\" in error.msg"
+
+- template:
+ src: template_import_macro_globals.j2
+ dest: "{{ output_dir }}/template_import_macro_globals.templated"
+
+- command: "cat {{ output_dir }}/template_import_macro_globals.templated"
+ register: out
+
+- assert:
+ that:
+ - out.stdout == "bar=lookedup_bar"
+
+# aliases file requires root for template tests so this should be safe
+- import_tasks: backup_test.yml
+
+- name: test STRING_TYPE_FILTERS
+ copy:
+ content: "{{ a_dict | to_nice_json(indent=(indent_value|int))}}\n"
+ dest: "{{ output_dir }}/string_type_filters.templated"
+ vars:
+ a_dict:
+ foo: bar
+ foobar: 1
+ indent_value: 2
+
+- name: copy known good string_type_filters.expected into place
+ copy:
+ src: string_type_filters.expected
+ dest: "{{ output_dir }}/string_type_filters.expected"
+
+- command: "diff {{ output_dir }}/string_type_filters.templated {{ output_dir}}/string_type_filters.expected"
+ register: out
+
+- assert:
+ that:
+ - out.rc == 0
+
+- template:
+ src: empty_template.j2
+ dest: "{{ output_dir }}/empty_template.templated"
+
+- assert:
+ that:
+ - test
+ vars:
+ test: "{{ lookup('file', '{{ output_dir }}/empty_template.templated')|length == 0 }}"
+
+- name: test jinja2 override without colon throws proper error
+ block:
+ - template:
+ src: override_separator.j2
+ dest: "{{ output_dir }}/override_separator.templated"
+ - assert:
+ that:
+ - False
+ rescue:
+ - assert:
+ that:
+ - "'failed to parse jinja2 override' in ansible_failed_result.msg"
+
+- name: test jinja2 override with colon in value
+ template:
+ src: override_colon_value.j2
+ dest: "{{ output_dir }}/override_colon_value.templated"
+ ignore_errors: yes
+ register: override_colon_value_task
+
+- copy:
+ src: override_colon_value.expected
+ dest: "{{output_dir}}/override_colon_value.expected"
+
+- command: "diff {{ output_dir }}/override_colon_value.templated {{ output_dir}}/override_colon_value.expected"
+ register: override_colon_value_diff
+
+- assert:
+ that:
+ - override_colon_value_task is success
+ - override_colon_value_diff.rc == 0
+
+- assert:
+ that:
+ - data_not_converted | type_debug == 'NativeJinjaUnsafeText'
+ - data_converted | type_debug == 'dict'
+ vars:
+ data_not_converted: "{{ lookup('template', 'json_macro.j2', convert_data=False) }}"
+ data_converted: "{{ lookup('template', 'json_macro.j2') }}"
+
+- name: Test convert_data is correctly set to True for nested vars evaluation
+ debug:
+ msg: "{{ lookup('template', 'indirect_dict.j2', convert_data=False) }}"
+ vars:
+ d:
+ foo: bar
+ v: "{{ d }}"
diff --git a/test/integration/targets/template/template.yml b/test/integration/targets/template/template.yml
new file mode 100644
index 0000000..d33293b
--- /dev/null
+++ b/test/integration/targets/template/template.yml
@@ -0,0 +1,4 @@
+- hosts: testhost
+ gather_facts: yes
+ roles:
+ - { role: template }
diff --git a/test/integration/targets/template/templates/6653-include.j2 b/test/integration/targets/template/templates/6653-include.j2
new file mode 100644
index 0000000..26443b1
--- /dev/null
+++ b/test/integration/targets/template/templates/6653-include.j2
@@ -0,0 +1 @@
+{{ x }}
diff --git a/test/integration/targets/template/templates/6653.j2 b/test/integration/targets/template/templates/6653.j2
new file mode 100644
index 0000000..8026a79
--- /dev/null
+++ b/test/integration/targets/template/templates/6653.j2
@@ -0,0 +1,4 @@
+{% for x in mylist %}
+{{ x }}
+{% include '6653-include.j2' with context %}
+{% endfor %}
diff --git a/test/integration/targets/template/templates/72262-included.j2 b/test/integration/targets/template/templates/72262-included.j2
new file mode 100644
index 0000000..35700cb
--- /dev/null
+++ b/test/integration/targets/template/templates/72262-included.j2
@@ -0,0 +1 @@
+{{ vars.test }}
diff --git a/test/integration/targets/template/templates/72262-vars.j2 b/test/integration/targets/template/templates/72262-vars.j2
new file mode 100644
index 0000000..6ef9220
--- /dev/null
+++ b/test/integration/targets/template/templates/72262-vars.j2
@@ -0,0 +1 @@
+{% set test = "I'm test variable" %}
diff --git a/test/integration/targets/template/templates/72262.j2 b/test/integration/targets/template/templates/72262.j2
new file mode 100644
index 0000000..b72be0d
--- /dev/null
+++ b/test/integration/targets/template/templates/72262.j2
@@ -0,0 +1,3 @@
+{% import '72262-vars.j2' as vars with context %}
+{% macro included() %}{% include '72262-included.j2' %}{% endmacro %}
+{{ included()|indent }}
diff --git a/test/integration/targets/template/templates/72615-macro-nested.j2 b/test/integration/targets/template/templates/72615-macro-nested.j2
new file mode 100644
index 0000000..c47a499
--- /dev/null
+++ b/test/integration/targets/template/templates/72615-macro-nested.j2
@@ -0,0 +1,4 @@
+{% macro print_context_vars_nested(value) %}
+foo: {{ foo }}
+bar: {{ value }}
+{% endmacro %}
diff --git a/test/integration/targets/template/templates/72615-macro.j2 b/test/integration/targets/template/templates/72615-macro.j2
new file mode 100644
index 0000000..328c271
--- /dev/null
+++ b/test/integration/targets/template/templates/72615-macro.j2
@@ -0,0 +1,8 @@
+{% macro print_context_vars(value) %}
+{{ foo }}
+{{ value }}
+{% set foo = "template-nested-level-foo" %}
+{% set bar = "template-nested-level-bar" %}
+{% from '72615-macro-nested.j2' import print_context_vars_nested with context %}
+{{ print_context_vars_nested(bar) }}
+{% endmacro %}
diff --git a/test/integration/targets/template/templates/72615.j2 b/test/integration/targets/template/templates/72615.j2
new file mode 100644
index 0000000..b79f88e
--- /dev/null
+++ b/test/integration/targets/template/templates/72615.j2
@@ -0,0 +1,4 @@
+{% set foo = "template-level-foo" %}
+{% set bar = "template-level-bar" %}
+{% from '72615-macro.j2' import print_context_vars with context %}
+{{ print_context_vars(bar) }}
diff --git a/test/integration/targets/template/templates/bar b/test/integration/targets/template/templates/bar
new file mode 100644
index 0000000..2b60207
--- /dev/null
+++ b/test/integration/targets/template/templates/bar
@@ -0,0 +1 @@
+Goodbye
diff --git a/test/integration/targets/template/templates/café.j2 b/test/integration/targets/template/templates/café.j2
new file mode 100644
index 0000000..ef7e08e
--- /dev/null
+++ b/test/integration/targets/template/templates/café.j2
@@ -0,0 +1 @@
+{{ ansible_managed }}
diff --git a/test/integration/targets/template/templates/custom_comment_string.j2 b/test/integration/targets/template/templates/custom_comment_string.j2
new file mode 100644
index 0000000..db0af48
--- /dev/null
+++ b/test/integration/targets/template/templates/custom_comment_string.j2
@@ -0,0 +1,3 @@
+Before
+[# Test comment_start_string #]
+After
diff --git a/test/integration/targets/template/templates/empty_template.j2 b/test/integration/targets/template/templates/empty_template.j2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/template/templates/empty_template.j2
diff --git a/test/integration/targets/template/templates/encoding_1252.j2 b/test/integration/targets/template/templates/encoding_1252.j2
new file mode 100644
index 0000000..0d3cc35
--- /dev/null
+++ b/test/integration/targets/template/templates/encoding_1252.j2
@@ -0,0 +1 @@
+windows-1252 Special Characters: €‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“â€â€¢â€“—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÃÂÃÄÅÆÇÈÉÊËÌÃÃŽÃÃÑÒÓÔÕÖ×ØÙÚÛÜÃÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ
diff --git a/test/integration/targets/template/templates/foo.j2 b/test/integration/targets/template/templates/foo.j2
new file mode 100644
index 0000000..22187f9
--- /dev/null
+++ b/test/integration/targets/template/templates/foo.j2
@@ -0,0 +1,3 @@
+{{ templated_var }}
+
+{{ templated_dict | to_nice_json }}
diff --git a/test/integration/targets/template/templates/foo2.j2 b/test/integration/targets/template/templates/foo2.j2
new file mode 100644
index 0000000..e6e3485
--- /dev/null
+++ b/test/integration/targets/template/templates/foo2.j2
@@ -0,0 +1,3 @@
+BEGIN
+{{ templated_var }}
+END
diff --git a/test/integration/targets/template/templates/foo3.j2 b/test/integration/targets/template/templates/foo3.j2
new file mode 100644
index 0000000..710d55a
--- /dev/null
+++ b/test/integration/targets/template/templates/foo3.j2
@@ -0,0 +1,3 @@
+BEGIN
+[% templated_var %]
+END
diff --git a/test/integration/targets/template/templates/for_loop.j2 b/test/integration/targets/template/templates/for_loop.j2
new file mode 100644
index 0000000..49fa412
--- /dev/null
+++ b/test/integration/targets/template/templates/for_loop.j2
@@ -0,0 +1,4 @@
+{% for par_var in parent_vars %}
+{% include 'for_loop_include.j2' %}
+
+{% endfor %}
diff --git a/test/integration/targets/template/templates/for_loop_include.j2 b/test/integration/targets/template/templates/for_loop_include.j2
new file mode 100644
index 0000000..b1a0ad7
--- /dev/null
+++ b/test/integration/targets/template/templates/for_loop_include.j2
@@ -0,0 +1,3 @@
+{% if par_var is defined %}
+{% include 'for_loop_include_nested.j2' %}
+{% endif %}
diff --git a/test/integration/targets/template/templates/for_loop_include_nested.j2 b/test/integration/targets/template/templates/for_loop_include_nested.j2
new file mode 100644
index 0000000..368bce4
--- /dev/null
+++ b/test/integration/targets/template/templates/for_loop_include_nested.j2
@@ -0,0 +1 @@
+{{ par_var }}
diff --git a/test/integration/targets/template/templates/import_as.j2 b/test/integration/targets/template/templates/import_as.j2
new file mode 100644
index 0000000..b06f1be
--- /dev/null
+++ b/test/integration/targets/template/templates/import_as.j2
@@ -0,0 +1,4 @@
+{% import 'qux' as qux %}
+hello world import as
+{{ qux.wibble }}
+{% include 'bar' %}
diff --git a/test/integration/targets/template/templates/import_as_with_context.j2 b/test/integration/targets/template/templates/import_as_with_context.j2
new file mode 100644
index 0000000..3dd806a
--- /dev/null
+++ b/test/integration/targets/template/templates/import_as_with_context.j2
@@ -0,0 +1,3 @@
+{% import 'qux' as qux with context %}
+hello world as qux with context
+{{ qux.wibble }}
diff --git a/test/integration/targets/template/templates/import_with_context.j2 b/test/integration/targets/template/templates/import_with_context.j2
new file mode 100644
index 0000000..104e68b
--- /dev/null
+++ b/test/integration/targets/template/templates/import_with_context.j2
@@ -0,0 +1,4 @@
+{% import 'qux' as qux with context %}
+hello world with context
+{{ qux.wibble }}
+{% include 'bar' %}
diff --git a/test/integration/targets/template/templates/indirect_dict.j2 b/test/integration/targets/template/templates/indirect_dict.j2
new file mode 100644
index 0000000..3124371
--- /dev/null
+++ b/test/integration/targets/template/templates/indirect_dict.j2
@@ -0,0 +1 @@
+{{ v.foo }}
diff --git a/test/integration/targets/template/templates/json_macro.j2 b/test/integration/targets/template/templates/json_macro.j2
new file mode 100644
index 0000000..080f164
--- /dev/null
+++ b/test/integration/targets/template/templates/json_macro.j2
@@ -0,0 +1,2 @@
+{% macro m() %}{{ {"foo":"bar"} }}{% endmacro %}
+{{ m() }}
diff --git a/test/integration/targets/template/templates/lstrip_blocks.j2 b/test/integration/targets/template/templates/lstrip_blocks.j2
new file mode 100644
index 0000000..d572da6
--- /dev/null
+++ b/test/integration/targets/template/templates/lstrip_blocks.j2
@@ -0,0 +1,8 @@
+{% set hello_world="hello world" %}
+{% for i in [1, 2, 3] %}
+ {% if loop.first %}
+{{hello_world}}
+ {% else %}
+{{hello_world}}
+ {% endif %}
+{% endfor %}
diff --git a/test/integration/targets/template/templates/macro_using_globals.j2 b/test/integration/targets/template/templates/macro_using_globals.j2
new file mode 100644
index 0000000..d8d0626
--- /dev/null
+++ b/test/integration/targets/template/templates/macro_using_globals.j2
@@ -0,0 +1,3 @@
+{% macro foo(bar) -%}
+{{ bar }}={{ lookup('lines', 'echo lookedup_bar') }}
+{%- endmacro %}
diff --git a/test/integration/targets/template/templates/override_colon_value.j2 b/test/integration/targets/template/templates/override_colon_value.j2
new file mode 100644
index 0000000..2ca9bb8
--- /dev/null
+++ b/test/integration/targets/template/templates/override_colon_value.j2
@@ -0,0 +1,4 @@
+#jinja2: line_statement_prefix:":"
+: if true
+foo
+: endif
diff --git a/test/integration/targets/template/templates/override_separator.j2 b/test/integration/targets/template/templates/override_separator.j2
new file mode 100644
index 0000000..7589cc3
--- /dev/null
+++ b/test/integration/targets/template/templates/override_separator.j2
@@ -0,0 +1 @@
+#jinja2: lstrip_blocks=True
diff --git a/test/integration/targets/template/templates/parent.j2 b/test/integration/targets/template/templates/parent.j2
new file mode 100644
index 0000000..99a8e4c
--- /dev/null
+++ b/test/integration/targets/template/templates/parent.j2
@@ -0,0 +1,3 @@
+{% for parent_item in parent_vars %}
+{% include "subtemplate.j2" %}
+{% endfor %}
diff --git a/test/integration/targets/template/templates/qux b/test/integration/targets/template/templates/qux
new file mode 100644
index 0000000..d8cd22e
--- /dev/null
+++ b/test/integration/targets/template/templates/qux
@@ -0,0 +1 @@
+{% set wibble = "WIBBLE" %}
diff --git a/test/integration/targets/template/templates/short.j2 b/test/integration/targets/template/templates/short.j2
new file mode 100644
index 0000000..55aab8f
--- /dev/null
+++ b/test/integration/targets/template/templates/short.j2
@@ -0,0 +1 @@
+{{ templated_var }}
diff --git a/test/integration/targets/template/templates/subtemplate.j2 b/test/integration/targets/template/templates/subtemplate.j2
new file mode 100644
index 0000000..f359bf2
--- /dev/null
+++ b/test/integration/targets/template/templates/subtemplate.j2
@@ -0,0 +1,2 @@
+{{ parent_item }}
+
diff --git a/test/integration/targets/template/templates/template_destpath_test.j2 b/test/integration/targets/template/templates/template_destpath_test.j2
new file mode 100644
index 0000000..1d21d8c
--- /dev/null
+++ b/test/integration/targets/template/templates/template_destpath_test.j2
@@ -0,0 +1 @@
+{{ template_destpath }}
diff --git a/test/integration/targets/template/templates/template_import_macro_globals.j2 b/test/integration/targets/template/templates/template_import_macro_globals.j2
new file mode 100644
index 0000000..9b9a9c6
--- /dev/null
+++ b/test/integration/targets/template/templates/template_import_macro_globals.j2
@@ -0,0 +1,2 @@
+{% from 'macro_using_globals.j2' import foo %}
+{{ foo('bar') }}
diff --git a/test/integration/targets/template/templates/trim_blocks.j2 b/test/integration/targets/template/templates/trim_blocks.j2
new file mode 100644
index 0000000..824a0a0
--- /dev/null
+++ b/test/integration/targets/template/templates/trim_blocks.j2
@@ -0,0 +1,4 @@
+{% if True %}
+Hello world
+{% endif %}
+Goodbye
diff --git a/test/integration/targets/template/templates/unused_vars_include.j2 b/test/integration/targets/template/templates/unused_vars_include.j2
new file mode 100644
index 0000000..457cbbc
--- /dev/null
+++ b/test/integration/targets/template/templates/unused_vars_include.j2
@@ -0,0 +1 @@
+{{ var_set_in_template }}
diff --git a/test/integration/targets/template/templates/unused_vars_template.j2 b/test/integration/targets/template/templates/unused_vars_template.j2
new file mode 100644
index 0000000..28afc90
--- /dev/null
+++ b/test/integration/targets/template/templates/unused_vars_template.j2
@@ -0,0 +1,2 @@
+{% set var_set_in_template=test_var %}
+{% include "unused_vars_include.j2" %}
diff --git a/test/integration/targets/template/undefined_in_import-import.j2 b/test/integration/targets/template/undefined_in_import-import.j2
new file mode 100644
index 0000000..fbb97b0
--- /dev/null
+++ b/test/integration/targets/template/undefined_in_import-import.j2
@@ -0,0 +1 @@
+{{ undefined_variable }}
diff --git a/test/integration/targets/template/undefined_in_import.j2 b/test/integration/targets/template/undefined_in_import.j2
new file mode 100644
index 0000000..619e4f7
--- /dev/null
+++ b/test/integration/targets/template/undefined_in_import.j2
@@ -0,0 +1 @@
+{% import 'undefined_in_import-import.j2' as t %}
diff --git a/test/integration/targets/template/undefined_in_import.yml b/test/integration/targets/template/undefined_in_import.yml
new file mode 100644
index 0000000..62f60d6
--- /dev/null
+++ b/test/integration/targets/template/undefined_in_import.yml
@@ -0,0 +1,11 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - debug:
+ msg: "{{ lookup('template', 'undefined_in_import.j2') }}"
+ ignore_errors: true
+ register: res
+
+ - assert:
+ that:
+ - "\"'undefined_variable' is undefined\" in res.msg"
diff --git a/test/integration/targets/template/undefined_var_info.yml b/test/integration/targets/template/undefined_var_info.yml
new file mode 100644
index 0000000..b96a58d
--- /dev/null
+++ b/test/integration/targets/template/undefined_var_info.yml
@@ -0,0 +1,15 @@
+- hosts: localhost
+ gather_facts: no
+ vars:
+ foo: []
+ bar: "{{ foo[0] }}"
+ tasks:
+ - debug:
+ msg: "{{ bar }}"
+ register: result
+ ignore_errors: yes
+
+ - assert:
+ that:
+ - '"foo[0]" in result.msg'
+ - '"object has no element 0" in result.msg'
diff --git a/test/integration/targets/template/unsafe.yml b/test/integration/targets/template/unsafe.yml
new file mode 100644
index 0000000..bef9a4b
--- /dev/null
+++ b/test/integration/targets/template/unsafe.yml
@@ -0,0 +1,64 @@
+- hosts: localhost
+ gather_facts: false
+ vars:
+ nottemplated: this should not be seen
+ imunsafe: !unsafe '{{ nottemplated }}'
+ tasks:
+
+ - set_fact:
+ this_was_unsafe: >
+ {{ imunsafe }}
+
+ - set_fact:
+ this_always_safe: '{{ imunsafe }}'
+
+ - name: ensure nothing was templated
+ assert:
+ that:
+ - this_always_safe == imunsafe
+ - imunsafe == this_was_unsafe.strip()
+
+
+- hosts: localhost
+ gather_facts: false
+ vars:
+ output_dir: "{{ lookup('env', 'OUTPUT_DIR') }}"
+ tasks:
+ - set_fact:
+ unsafe_foo: "{{ lookup('list', var0) }}"
+ vars:
+ var0: "{{ var1 }}"
+ var1:
+ - unsafe
+
+ - assert:
+ that:
+ - "{{ unsafe_foo[0] | type_debug == 'AnsibleUnsafeText' }}"
+
+ - block:
+ - copy:
+ dest: "{{ file_name }}"
+ content: !unsafe "{{ i_should_not_be_templated }}"
+
+ - set_fact:
+ file_content: "{{ lookup('file', file_name) }}"
+
+ - assert:
+ that:
+ - not file_content is contains('unsafe')
+
+ - set_fact:
+ file_content: "{{ lookup('file', file_name_tmpl) }}"
+ vars:
+ file_name_tmpl: "{{ file_name }}"
+
+ - assert:
+ that:
+ - not file_content is contains('unsafe')
+ vars:
+ file_name: "{{ output_dir }}/unsafe_file"
+ i_should_not_be_templated: unsafe
+ always:
+ - file:
+ dest: "{{ file_name }}"
+ state: absent
diff --git a/test/integration/targets/template/unused_vars_include.yml b/test/integration/targets/template/unused_vars_include.yml
new file mode 100644
index 0000000..ff31b70
--- /dev/null
+++ b/test/integration/targets/template/unused_vars_include.yml
@@ -0,0 +1,8 @@
+- hosts: localhost
+ gather_facts: no
+ vars:
+ test_var: foo
+ unused_var: "{{ undefined_var }}"
+ tasks:
+ - debug:
+ msg: "{{ lookup('template', 'unused_vars_template.j2') }}"
diff --git a/test/integration/targets/template/vars/main.yml b/test/integration/targets/template/vars/main.yml
new file mode 100644
index 0000000..9d45cf2
--- /dev/null
+++ b/test/integration/targets/template/vars/main.yml
@@ -0,0 +1,20 @@
+templated_var: templated_var_loaded
+
+number_var: 5
+string_num: "5"
+bool_var: true
+part_1: 1
+part_2: "Foo"
+null_type: !!null
+
+templated_dict:
+ number: "{{ number_var }}"
+ string_num: "{{ string_num }}"
+ null_type: "{{ null_type }}"
+ bool: "{{ bool_var }}"
+ multi_part: "{{ part_1 }}{{ part_2 }}"
+
+parent_vars:
+- foo
+- bar
+- bam
diff --git a/test/integration/targets/template_jinja2_non_native/46169.yml b/test/integration/targets/template_jinja2_non_native/46169.yml
new file mode 100644
index 0000000..4dc3dc0
--- /dev/null
+++ b/test/integration/targets/template_jinja2_non_native/46169.yml
@@ -0,0 +1,31 @@
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - set_fact:
+ output_dir: "{{ lookup('env', 'OUTPUT_DIR') }}"
+
+ - template:
+ src: templates/46169.json.j2
+ dest: "{{ output_dir }}/result.json"
+
+ - command: "diff templates/46169.json.j2 {{ output_dir }}/result.json"
+ register: diff_result
+
+ - assert:
+ that:
+ - diff_result.stdout == ""
+
+ - block:
+ - set_fact:
+ non_native_lookup: "{{ lookup('template', 'templates/46169.json.j2') }}"
+
+ - assert:
+ that:
+ - non_native_lookup | type_debug == 'NativeJinjaUnsafeText'
+
+ - set_fact:
+ native_lookup: "{{ lookup('template', 'templates/46169.json.j2', jinja2_native=true) }}"
+
+ - assert:
+ that:
+ - native_lookup | type_debug == 'dict'
diff --git a/test/integration/targets/template_jinja2_non_native/aliases b/test/integration/targets/template_jinja2_non_native/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/template_jinja2_non_native/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/template_jinja2_non_native/runme.sh b/test/integration/targets/template_jinja2_non_native/runme.sh
new file mode 100755
index 0000000..fe9d495
--- /dev/null
+++ b/test/integration/targets/template_jinja2_non_native/runme.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+set -eux
+
+export ANSIBLE_JINJA2_NATIVE=1
+ansible-playbook 46169.yml -v "$@"
+unset ANSIBLE_JINJA2_NATIVE
diff --git a/test/integration/targets/template_jinja2_non_native/templates/46169.json.j2 b/test/integration/targets/template_jinja2_non_native/templates/46169.json.j2
new file mode 100644
index 0000000..a4fc3f6
--- /dev/null
+++ b/test/integration/targets/template_jinja2_non_native/templates/46169.json.j2
@@ -0,0 +1,3 @@
+{
+ "key": "bar"
+}
diff --git a/test/integration/targets/templating/aliases b/test/integration/targets/templating/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/templating/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/templating/tasks/main.yml b/test/integration/targets/templating/tasks/main.yml
new file mode 100644
index 0000000..312e171
--- /dev/null
+++ b/test/integration/targets/templating/tasks/main.yml
@@ -0,0 +1,35 @@
+- command: echo {% raw %}{{ foo }}{% endraw %}
+ register: result
+
+- assert:
+ that:
+ - result.stdout_lines|first == expected
+ vars:
+ expected: !unsafe '{{ foo }}'
+
+- name: Assert that templating can convert JSON null, true, and false to Python
+ assert:
+ that:
+ - foo.null is none
+ - foo.true is true
+ - foo.false is false
+ vars:
+ # Kind of hack to just send a JSON string through jinja, by templating out nothing
+ foo: '{{ "" }}{"null": null, "true": true, "false": false}'
+
+- name: Make sure that test with name that isn't a valid Ansible plugin name does not result in a crash (1/2)
+ set_fact:
+ foo: '{{ [{"failed": false}] | selectattr("failed", "==", true) }}'
+
+- name: Make sure that test with name that isn't a valid Ansible plugin name does not result in a crash (2/2)
+ template:
+ src: invalid_test_name.j2
+ dest: /tmp/foo
+ ignore_errors: true
+ register: result
+
+- assert:
+ that:
+ - result is failed
+ - >-
+ "TemplateSyntaxError: Could not load \"asdf \": 'invalid plugin name: ansible.builtin.asdf '" in result.msg
diff --git a/test/integration/targets/templating/templates/invalid_test_name.j2 b/test/integration/targets/templating/templates/invalid_test_name.j2
new file mode 100644
index 0000000..98b836f
--- /dev/null
+++ b/test/integration/targets/templating/templates/invalid_test_name.j2
@@ -0,0 +1 @@
+{{ [{"failed": false}] | selectattr("failed", "asdf ", true) }}
diff --git a/test/integration/targets/templating_lookups/aliases b/test/integration/targets/templating_lookups/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/templating_lookups/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/templating_lookups/runme.sh b/test/integration/targets/templating_lookups/runme.sh
new file mode 100755
index 0000000..60b3923
--- /dev/null
+++ b/test/integration/targets/templating_lookups/runme.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ANSIBLE_LOOKUP_PLUGINS=. ANSIBLE_ROLES_PATH=./ UNICODE_VAR=café ansible-playbook runme.yml "$@"
+
+ansible-playbook template_lookup_vaulted/playbook.yml --vault-password-file template_lookup_vaulted/test_vault_pass "$@"
+
+ansible-playbook template_deepcopy/playbook.yml -i template_deepcopy/hosts "$@"
diff --git a/test/integration/targets/templating_lookups/runme.yml b/test/integration/targets/templating_lookups/runme.yml
new file mode 100644
index 0000000..a27337b
--- /dev/null
+++ b/test/integration/targets/templating_lookups/runme.yml
@@ -0,0 +1,4 @@
+- hosts: localhost
+ gather_facts: no
+ roles:
+ - { role: template_lookups }
diff --git a/test/integration/targets/templating_lookups/template_deepcopy/hosts b/test/integration/targets/templating_lookups/template_deepcopy/hosts
new file mode 100644
index 0000000..ecd3b96
--- /dev/null
+++ b/test/integration/targets/templating_lookups/template_deepcopy/hosts
@@ -0,0 +1 @@
+h1 ansible_connection=local host_var=foo
diff --git a/test/integration/targets/templating_lookups/template_deepcopy/playbook.yml b/test/integration/targets/templating_lookups/template_deepcopy/playbook.yml
new file mode 100644
index 0000000..da55c16
--- /dev/null
+++ b/test/integration/targets/templating_lookups/template_deepcopy/playbook.yml
@@ -0,0 +1,10 @@
+- hosts: h1
+ gather_facts: no
+ tasks:
+ - set_fact:
+ templated_foo: "{{ lookup('template', 'template.in') }}"
+
+ - name: Test that the hostvar was templated correctly
+ assert:
+ that:
+ - templated_foo == "foo\n"
diff --git a/test/integration/targets/templating_lookups/template_deepcopy/template.in b/test/integration/targets/templating_lookups/template_deepcopy/template.in
new file mode 100644
index 0000000..77de0ad
--- /dev/null
+++ b/test/integration/targets/templating_lookups/template_deepcopy/template.in
@@ -0,0 +1 @@
+{{hostvars['h1'].host_var}}
diff --git a/test/integration/targets/templating_lookups/template_lookup_vaulted/playbook.yml b/test/integration/targets/templating_lookups/template_lookup_vaulted/playbook.yml
new file mode 100644
index 0000000..23f32e8
--- /dev/null
+++ b/test/integration/targets/templating_lookups/template_lookup_vaulted/playbook.yml
@@ -0,0 +1,13 @@
+# https://github.com/ansible/ansible/issues/34209
+- hosts: localhost
+ gather_facts: no
+ vars:
+ hello_world: Hello World
+ tasks:
+ - name: Test that template lookup can handle vaulted templates
+ set_fact:
+ vaulted_hello_world: "{{ lookup('template', 'vaulted_hello.j2') }}"
+
+ - assert:
+ that:
+ - "vaulted_hello_world|trim == 'Unvaulted Hello World!'"
diff --git a/test/integration/targets/templating_lookups/template_lookup_vaulted/templates/vaulted_hello.j2 b/test/integration/targets/templating_lookups/template_lookup_vaulted/templates/vaulted_hello.j2
new file mode 100644
index 0000000..a6e98bd
--- /dev/null
+++ b/test/integration/targets/templating_lookups/template_lookup_vaulted/templates/vaulted_hello.j2
@@ -0,0 +1,6 @@
+$ANSIBLE_VAULT;1.1;AES256
+33623433323331343363343830343365376233386637366264646634663632343963396664393463
+3734626234626639323061643863613164643365363063310a663336663762356135396430353435
+39303930613231336135623761363130653235666433383965306235653963343166633233323638
+6635303662333734300a623063393761376531636535383164333632613839663237336463616436
+62643437623538633335366435346532636666616139386332323034336530356131
diff --git a/test/integration/targets/templating_lookups/template_lookup_vaulted/test_vault_pass b/test/integration/targets/templating_lookups/template_lookup_vaulted/test_vault_pass
new file mode 100644
index 0000000..9daeafb
--- /dev/null
+++ b/test/integration/targets/templating_lookups/template_lookup_vaulted/test_vault_pass
@@ -0,0 +1 @@
+test
diff --git a/test/integration/targets/templating_lookups/template_lookups/mock_lookup_plugins/77788.py b/test/integration/targets/templating_lookups/template_lookups/mock_lookup_plugins/77788.py
new file mode 100644
index 0000000..436ceaf
--- /dev/null
+++ b/test/integration/targets/templating_lookups/template_lookups/mock_lookup_plugins/77788.py
@@ -0,0 +1,6 @@
+from ansible.plugins.lookup import LookupBase
+
+
+class LookupModule(LookupBase):
+ def run(self, terms, variables, **kwargs):
+ return {'one': 1, 'two': 2}
diff --git a/test/integration/targets/templating_lookups/template_lookups/tasks/errors.yml b/test/integration/targets/templating_lookups/template_lookups/tasks/errors.yml
new file mode 100644
index 0000000..da57631
--- /dev/null
+++ b/test/integration/targets/templating_lookups/template_lookups/tasks/errors.yml
@@ -0,0 +1,31 @@
+- name: Task that fails due to templating error for plugin option
+ debug: msg="{{ 5 / 0 | int }}"
+ ignore_errors: true
+ register: result
+
+- assert:
+ that:
+ - result.failed
+ - result.exception
+
+- name: Loop that fails due to templating error in first entry and ignores errors
+ debug: msg="{{ 5 / item }}"
+ ignore_errors: true
+ register: result
+ loop: [0, 0, 1]
+
+- debug: var=result
+
+- assert:
+ that:
+ - result.results[0].failed
+ - result.results[0].exception
+ - result.results[0].item == 0
+
+ - result.results[1].failed
+ - result.results[1].exception
+ - result.results[1].item == 0
+
+ - not result.results[2].failed
+ - result.results[2].exception is undefined
+ - result.results[2].item == 1
diff --git a/test/integration/targets/templating_lookups/template_lookups/tasks/main.yml b/test/integration/targets/templating_lookups/template_lookups/tasks/main.yml
new file mode 100644
index 0000000..430ac91
--- /dev/null
+++ b/test/integration/targets/templating_lookups/template_lookups/tasks/main.yml
@@ -0,0 +1,101 @@
+# UNICODE
+
+# https://github.com/ansible/ansible/issues/65297
+- name: get UNICODE_VAR environment var value
+ shell: "echo $UNICODE_VAR"
+ register: unicode_var_value
+
+- name: verify the UNICODE_VAR is defined
+ assert:
+ that:
+ - "unicode_var_value.stdout"
+
+- name: use env lookup to get UNICODE_VAR value
+ set_fact:
+ test_unicode_val: "{{ lookup('env', 'UNICODE_VAR') }}"
+
+- debug: var=unicode_var_value
+- debug: var=test_unicode_val
+
+- name: compare unicode values
+ assert:
+ that:
+ - "test_unicode_val == unicode_var_value.stdout"
+
+# LOOKUP TEMPLATING
+
+- name: use bare interpolation
+ debug: msg="got {{item}}"
+ with_items: "{{things1}}"
+ register: bare_var
+
+- name: verify that list was interpolated
+ assert:
+ that:
+ - "bare_var.results[0].item == 1"
+ - "bare_var.results[1].item == 2"
+
+- name: use list with bare strings in it
+ debug: msg={{item}}
+ with_items:
+ - things2
+ - things1
+
+- name: use list with undefined var in it
+ debug: msg={{item}}
+ with_items: "{{things2}}"
+ ignore_errors: True
+
+# BUG #10073 nested template handling
+
+- name: set variable that clashes
+ set_fact:
+ PATH: foobar
+
+- name: get PATH environment var value
+ set_fact:
+ known_var_value: "{{ lookup('pipe', 'echo $PATH') }}"
+
+- name: do the lookup for env PATH
+ set_fact:
+ test_val: "{{ lookup('env', 'PATH') }}"
+
+- debug: var=test_val
+
+- name: compare values
+ assert:
+ that:
+ - "test_val != ''"
+ - "test_val == known_var_value"
+
+- name: set with_dict
+ shell: echo "{{ item.key + '=' + item.value }}"
+ with_dict: "{{ mydict }}"
+
+# BUG #34144 bad template caching
+
+- name: generate two random passwords
+ set_fact:
+ password1: "{{ lookup('password', '/dev/null length=20') }}"
+ password2: "{{ lookup('password', '/dev/null length=20') }}"
+ # If the passwords are generated randomly, the chance that they
+ # coincide is neglectable (< 1e-18 assuming 120 bits of randomness
+ # per password).
+
+- name: make sure passwords are not the same
+ assert:
+ that:
+ - password1 != password2
+
+# 77788 - KeyError when wantlist=False with dict returned
+- name: Test that dicts can be parsed with wantlist false
+ set_fact:
+ dict_wantlist_true: "{{ lookup('77788', wantlist=True) }}"
+ dict_wantlist_false: "{{ lookup('77788', wantlist=False) }}"
+
+- assert:
+ that:
+ - dict_wantlist_true is mapping
+ - dict_wantlist_false is string
+
+- include_tasks: ./errors.yml
diff --git a/test/integration/targets/templating_lookups/template_lookups/vars/main.yml b/test/integration/targets/templating_lookups/template_lookups/vars/main.yml
new file mode 100644
index 0000000..4c44b1c
--- /dev/null
+++ b/test/integration/targets/templating_lookups/template_lookups/vars/main.yml
@@ -0,0 +1,9 @@
+mydict:
+ mykey1: myval1
+ mykey2: myval2
+things1:
+ - 1
+ - 2
+things2:
+ - "{{ foo }}"
+ - "{{ foob | default('') }}"
diff --git a/test/integration/targets/templating_settings/aliases b/test/integration/targets/templating_settings/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/templating_settings/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/templating_settings/dont_warn_register.yml b/test/integration/targets/templating_settings/dont_warn_register.yml
new file mode 100644
index 0000000..277ce78
--- /dev/null
+++ b/test/integration/targets/templating_settings/dont_warn_register.yml
@@ -0,0 +1,6 @@
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - name: template in register warns, but no template should not
+ debug: msg=unimportant
+ register: thisshouldnotwarn
diff --git a/test/integration/targets/templating_settings/runme.sh b/test/integration/targets/templating_settings/runme.sh
new file mode 100755
index 0000000..2fb202c
--- /dev/null
+++ b/test/integration/targets/templating_settings/runme.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook test_templating_settings.yml -i ../../inventory -v "$@"
+[ "$(ansible-playbook dont_warn_register.yml -i ../../inventory -v "$@" 2>&1| grep -c 'is not templatable, but we found')" == "0" ]
diff --git a/test/integration/targets/templating_settings/test_templating_settings.yml b/test/integration/targets/templating_settings/test_templating_settings.yml
new file mode 100644
index 0000000..0c024df
--- /dev/null
+++ b/test/integration/targets/templating_settings/test_templating_settings.yml
@@ -0,0 +1,14 @@
+---
+- name: 'Test templating in name'
+ hosts: testhost
+ vars:
+ a_list:
+ - 'part'
+ - 'of a'
+ - 'name'
+
+ tasks:
+ # Note: this only tests that we do not traceback. It doesn't test that the
+ # name goes through templating correctly
+ - name: 'Task: {{ a_list | to_json }}'
+ debug: msg='{{ a_list | to_json }}'
diff --git a/test/integration/targets/test_core/aliases b/test/integration/targets/test_core/aliases
new file mode 100644
index 0000000..3005e4b
--- /dev/null
+++ b/test/integration/targets/test_core/aliases
@@ -0,0 +1 @@
+shippable/posix/group4
diff --git a/test/integration/targets/test_core/inventory b/test/integration/targets/test_core/inventory
new file mode 100644
index 0000000..0fdd8ae
--- /dev/null
+++ b/test/integration/targets/test_core/inventory
@@ -0,0 +1 @@
+unreachable ansible_connection=ssh ansible_host=127.0.0.1 ansible_port=1011 # IANA Reserved port
diff --git a/test/integration/targets/test_core/runme.sh b/test/integration/targets/test_core/runme.sh
new file mode 100755
index 0000000..5daa5fe
--- /dev/null
+++ b/test/integration/targets/test_core/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ANSIBLE_ROLES_PATH=../ ansible-playbook --vault-password-file vault-password runme.yml -i inventory "${@}"
diff --git a/test/integration/targets/test_core/runme.yml b/test/integration/targets/test_core/runme.yml
new file mode 100644
index 0000000..20a9467
--- /dev/null
+++ b/test/integration/targets/test_core/runme.yml
@@ -0,0 +1,4 @@
+- hosts: localhost
+ gather_facts: no
+ roles:
+ - test_core
diff --git a/test/integration/targets/test_core/tasks/main.yml b/test/integration/targets/test_core/tasks/main.yml
new file mode 100644
index 0000000..8c2decb
--- /dev/null
+++ b/test/integration/targets/test_core/tasks/main.yml
@@ -0,0 +1,350 @@
+- name: Failure
+ set_fact:
+ hello: world
+ failed_when: true
+ ignore_errors: yes
+ register: intentional_failure
+
+- name: Success
+ set_fact:
+ hello: world
+ register: intentional_success
+
+- name: Try failure test on non-dictionary
+ set_fact:
+ hello: "{{ 'nope' is failure }}"
+ ignore_errors: yes
+ register: misuse_of_failure
+
+- name: Assert failure tests work
+ assert:
+ that:
+ - intentional_failure is failed # old name
+ - intentional_failure is failure
+ - intentional_success is not failure
+ - misuse_of_failure is failed
+
+- name: Assert successful tests work
+ assert:
+ that:
+ - intentional_success is succeeded # old name
+ - intentional_success is success # old name
+ - intentional_success is successful
+ - intentional_failure is not successful
+
+- name: Try reachable host
+ command: id
+ register: reachable_host
+
+- name: Try unreachable host
+ command: id
+ delegate_to: unreachable
+ ignore_unreachable: yes
+ ignore_errors: yes
+ register: unreachable_host
+
+- name: Try reachable test on non-dictionary
+ set_fact:
+ hello: "{{ 'nope' is reachable }}"
+ ignore_errors: yes
+ register: misuse_of_reachable
+
+- name: Assert reachable tests work
+ assert:
+ that:
+ - misuse_of_reachable is failed
+ - reachable_host is reachable
+ - unreachable_host is not reachable
+
+- name: Try unreachable test on non-dictionary
+ set_fact:
+ hello: "{{ 'nope' is unreachable }}"
+ ignore_errors: yes
+ register: misuse_of_unreachable
+
+- name: Assert unreachable tests work
+ assert:
+ that:
+ - misuse_of_unreachable is failed
+ - reachable_host is not unreachable
+ - unreachable_host is unreachable
+
+- name: Make changes
+ file:
+ path: dir_for_changed
+ state: directory
+ register: directory_created
+
+- name: Make no changes
+ file:
+ path: dir_for_changed
+ state: directory
+ register: directory_unchanged
+
+- name: Try changed test on non-dictionary
+ set_fact:
+ hello: "{{ 'nope' is changed }}"
+ ignore_errors: yes
+ register: misuse_of_changed
+
+# providing artificial task results since there are no modules in ansible-core that provide a 'results' list instead of 'changed'
+- name: Prepare artificial task results
+ set_fact:
+ results_all_changed:
+ results:
+ - changed: true
+ - changed: true
+ results_some_changed:
+ results:
+ - changed: true
+ - changed: false
+ results_none_changed:
+ results:
+ - changed: false
+ - changed: false
+ results_missing_changed: {}
+
+- name: Assert changed tests work
+ assert:
+ that:
+ - directory_created is changed
+ - directory_unchanged is not changed
+ - misuse_of_changed is failed
+ - results_all_changed is changed
+ - results_some_changed is changed
+ - results_none_changed is not changed
+ - results_missing_changed is not changed
+
+- name: Skip me
+ set_fact:
+ hello: world
+ when: false
+ register: skipped_task
+
+- name: Don't skip me
+ set_fact:
+ hello: world
+ register: executed_task
+
+- name: Try skipped test on non-dictionary
+ set_fact:
+ hello: "{{ 'nope' is skipped }}"
+ ignore_errors: yes
+ register: misuse_of_skipped
+
+- name: Assert skipped tests work
+ assert:
+ that:
+ - skipped_task is skipped
+ - executed_task is not skipped
+ - misuse_of_skipped is failure
+
+- name: Not an async task
+ set_fact:
+ hello: world
+ register: non_async_task
+
+- name: Complete an async task
+ command: id
+ async: 10
+ poll: 1
+ register: async_completed
+
+- name: Start an async task without waiting for completion
+ shell: sleep 3
+ async: 10
+ poll: 0
+ register: async_incomplete
+
+- name: Try finished test on non-dictionary
+ set_fact:
+ hello: "{{ 'nope' is finished }}"
+ ignore_errors: yes
+ register: misuse_of_finished
+
+- name: Assert finished tests work (warning expected)
+ assert:
+ that:
+ - non_async_task is finished
+ - misuse_of_finished is failed
+ - async_completed is finished
+ - async_incomplete is not finished
+
+- name: Try started test on non-dictionary
+ set_fact:
+ hello: "{{ 'nope' is started }}"
+ ignore_errors: yes
+ register: misuse_of_started
+
+- name: Assert started tests work (warning expected)
+ assert:
+ that:
+ - non_async_task is started
+ - misuse_of_started is failed
+ - async_completed is started
+ - async_incomplete is started
+
+- name: Assert match tests work
+ assert:
+ that:
+ - "'hello' is match('h.ll.')"
+ - "'hello' is not match('.ll.')"
+
+- name: Assert search tests work
+ assert:
+ that:
+ - "'hello' is search('.l')"
+ - "'hello' is not search('nope')"
+
+- name: Assert regex tests work
+ assert:
+ that:
+ - "'hello' is regex('.l')"
+ - "'hello' is regex('.L', ignorecase=true)"
+ - "'hello\nAnsible' is regex('^Ansible', multiline=true)"
+ - "'hello' is not regex('.L')"
+ - "'hello\nAnsible' is not regex('^Ansible')"
+
+- name: Try version tests with bad operator
+ set_fact:
+ result: "{{ '1.0' is version('1.0', 'equals') }}"
+ ignore_errors: yes
+ register: version_bad_operator
+
+- name: Try version tests with bad value
+ set_fact:
+ result: "{{ '1.0' is version('nope', '==', true) }}"
+ ignore_errors: yes
+ register: version_bad_value
+
+- name: Try version with both strict and version_type
+ debug:
+ msg: "{{ '1.0' is version('1.0', strict=False, version_type='loose') }}"
+ ignore_errors: yes
+ register: version_strict_version_type
+
+- name: Try version with bad version_type
+ debug:
+ msg: "{{ '1.0' is version('1.0', version_type='boom') }}"
+ ignore_errors: yes
+ register: version_bad_version_type
+
+- name: Try version with bad semver
+ debug:
+ msg: "{{ 'nope' is version('nopenope', version_type='semver') }}"
+ ignore_errors: yes
+ register: version_bad_semver
+
+- name: Try version with empty input value
+ debug:
+ msg: "{{ '' is version('1.0', '>') }}"
+ ignore_errors: yes
+ register: version_empty_input
+
+- name: Try version with empty comparison value
+ debug:
+ msg: "{{ '1.0' is version('', '>') }}"
+ ignore_errors: yes
+ register: version_empty_comparison
+
+- name: Try version with empty input and comparison values
+ debug:
+ msg: "{{ '' is version('', '>') }}"
+ ignore_errors: yes
+ register: version_empty_both
+
+- name: Assert version tests work
+ assert:
+ that:
+ - "'1.0' is version_compare('1.0', '==')" # old name
+ - "'1.0' is version('1.0', '==')"
+ - "'1.0' is version('2.0', '!=')"
+ - "'1.0' is version('2.0', '<')"
+ - "'2.0' is version('1.0', '>')"
+ - "'1.0' is version('1.0', '<=')"
+ - "'1.0' is version('1.0', '>=')"
+ - "'1.0' is version_compare('1.0', '==', true)" # old name
+ - "'1.0' is version('1.0', '==', true)"
+ - "'1.0' is version('2.0', '!=', true)"
+ - "'1.0' is version('2.0', '<', true)"
+ - "'2.0' is version('1.0', '>', true)"
+ - "'1.0' is version('1.0', '<=', true)"
+ - "'1.0' is version('1.0', '>=', true)"
+ - "'1.2.3' is version('2.0.0', 'lt', version_type='semver')"
+ - "'2.14.0rc1' is version('2.14.0', 'lt', version_type='pep440')"
+ - version_bad_operator is failed
+ - version_bad_value is failed
+ - version_strict_version_type is failed
+ - version_bad_version_type is failed
+ - version_bad_semver is failed
+ - version_empty_input is failed
+ - version_empty_input is search('version value cannot be empty')
+ - version_empty_comparison is failed
+ - version_empty_comparison is search('to compare against cannot be empty')
+ - version_empty_both is failed
+ - version_empty_both is search('version value cannot be empty')
+
+- name: Assert any tests work
+ assert:
+ that:
+ - "[true, false] is any"
+ - "[false] is not any"
+
+- name: Assert all tests work
+ assert:
+ that:
+ - "[true] is all"
+ - "[true, false] is not all"
+
+- name: Assert truthy tests work
+ assert:
+ that:
+ - '"string" is truthy'
+ - '"" is not truthy'
+ - True is truthy
+ - False is not truthy
+ - true is truthy
+ - false is not truthy
+ - 1 is truthy
+ - 0 is not truthy
+ - '[""] is truthy'
+ - '[] is not truthy'
+ - '"on" is truthy(convert_bool=True)'
+ - '"off" is not truthy(convert_bool=True)'
+ - '"fred" is truthy(convert_bool=True)'
+ - '{} is not truthy'
+ - '{"key": "value"} is truthy'
+
+- name: Assert falsy tests work
+ assert:
+ that:
+ - '"string" is not falsy'
+ - '"" is falsy'
+ - True is not falsy
+ - False is falsy
+ - true is not falsy
+ - false is falsy
+ - 1 is not falsy
+ - 0 is falsy
+ - '[""] is not falsy'
+ - '[] is falsy'
+ - '"on" is not falsy(convert_bool=True)'
+ - '"off" is falsy(convert_bool=True)'
+ - '{} is falsy'
+ - '{"key": "value"} is not falsy'
+
+- name: Create vaulted variable for vault_encrypted test
+ set_fact:
+ vaulted_value: !vault |
+ $ANSIBLE_VAULT;1.1;AES256
+ 35323961353038346165643738646465376139363061353835303739663538343266303232326635
+ 3365353662646236356665323135633630656238316530640a663362363763633436373439663031
+ 33663433383037396438656464636433653837376361313638366362333037323961316364363363
+ 3835616438623261650a636164376534376661393134326662326362323131373964313961623365
+ 3833
+
+- name: Assert vault_encrypted tests work
+ assert:
+ that:
+ - vaulted_value is vault_encrypted
+ - inventory_hostname is not vault_encrypted
diff --git a/test/integration/targets/test_core/vault-password b/test/integration/targets/test_core/vault-password
new file mode 100644
index 0000000..9697392
--- /dev/null
+++ b/test/integration/targets/test_core/vault-password
@@ -0,0 +1 @@
+test-vault-password
diff --git a/test/integration/targets/test_files/aliases b/test/integration/targets/test_files/aliases
new file mode 100644
index 0000000..3005e4b
--- /dev/null
+++ b/test/integration/targets/test_files/aliases
@@ -0,0 +1 @@
+shippable/posix/group4
diff --git a/test/integration/targets/test_files/tasks/main.yml b/test/integration/targets/test_files/tasks/main.yml
new file mode 100644
index 0000000..0d51fc9
--- /dev/null
+++ b/test/integration/targets/test_files/tasks/main.yml
@@ -0,0 +1,60 @@
+- name: Create a broken symbolic link
+ file:
+ src: does_not_exist
+ dest: link_to_nonexistent_file
+ state: link
+ force: yes
+ follow: no
+
+- name: Assert directory tests work
+ assert:
+ that:
+ - "'.' is is_dir" # old name
+ - "'.' is directory"
+ - "'does_not_exist' is not directory"
+
+- name: Assert file tests work
+ assert:
+ that:
+ - "(role_path + '/aliases') is is_file" # old name
+ - "(role_path + '/aliases') is file"
+ - "'does_not_exist' is not file"
+
+- name: Assert link tests work
+ assert:
+ that:
+ - "'link_to_nonexistent_file' is link"
+ - "'.' is not link"
+
+- name: Assert exists tests work
+ assert:
+ that:
+ - "(role_path + '/aliases') is exists"
+ - "'link_to_nonexistent_file' is not exists"
+
+- name: Assert link_exists tests work
+ assert:
+ that:
+ - "'link_to_nonexistent_file' is link_exists"
+ - "'does_not_exist' is not link_exists"
+
+- name: Assert abs tests work
+ assert:
+ that:
+ - "'/' is is_abs" # old name
+ - "'/' is abs"
+ - "'../' is not abs"
+
+- name: Assert same_file tests work
+ assert:
+ that:
+ - "'/' is is_same_file('/')" # old name
+ - "'/' is same_file('/')"
+ - "'/' is not same_file(role_path + '/aliases')"
+
+- name: Assert mount tests work
+ assert:
+ that:
+ - "'/' is is_mount" # old name
+ - "'/' is mount"
+ - "'/does_not_exist' is not mount"
diff --git a/test/integration/targets/test_mathstuff/aliases b/test/integration/targets/test_mathstuff/aliases
new file mode 100644
index 0000000..3005e4b
--- /dev/null
+++ b/test/integration/targets/test_mathstuff/aliases
@@ -0,0 +1 @@
+shippable/posix/group4
diff --git a/test/integration/targets/test_mathstuff/tasks/main.yml b/test/integration/targets/test_mathstuff/tasks/main.yml
new file mode 100644
index 0000000..b5109ce
--- /dev/null
+++ b/test/integration/targets/test_mathstuff/tasks/main.yml
@@ -0,0 +1,27 @@
+- name: Assert subset tests work
+ assert:
+ that:
+ - "[1] is issubset([1, 2])" # old name
+ - "[1] is subset([1, 2])"
+ - "[1] is not subset([2])"
+
+- name: Assert superset tests work
+ assert:
+ that:
+ - "[1, 2] is issuperset([1])" # old name
+ - "[1, 2] is superset([1])"
+ - "[2] is not superset([1])"
+
+- name: Assert contains tests work
+ assert:
+ that:
+ - "[1] is contains(1)"
+ - "[1] is not contains(2)"
+
+- name: Assert nan tests work
+ assert:
+ that:
+ - "'bad' is not nan"
+ - "1.1 | float is not nan"
+ - "'nan' | float is isnan" # old name
+ - "'nan' | float is nan"
diff --git a/test/integration/targets/test_uri/aliases b/test/integration/targets/test_uri/aliases
new file mode 100644
index 0000000..70a7b7a
--- /dev/null
+++ b/test/integration/targets/test_uri/aliases
@@ -0,0 +1 @@
+shippable/posix/group5
diff --git a/test/integration/targets/test_uri/tasks/main.yml b/test/integration/targets/test_uri/tasks/main.yml
new file mode 100644
index 0000000..21a47a9
--- /dev/null
+++ b/test/integration/targets/test_uri/tasks/main.yml
@@ -0,0 +1,43 @@
+- name: urX tests
+ vars:
+ special: ['http', 'https', 'ftp', 'ftps', 'ws', 'wss', 'file']
+ block:
+ - name: Assert uri tests
+ assert:
+ that:
+ - "'http://searchengine.tld' is uri" # simple case, urls are uris but not all uris are urls
+ - "'ftp://searchengine.tld' is uri" # other scheme
+ - "'file://etc/hosts' is uri"
+ - "'mailto://me@example.com' is uri"
+ - "'sftp://me@example.com' is uri"
+ - "'asldkfjhalsidfthjo' is uri" # junk can look like uri (either implied scheme or empty path)
+ - "'asldkfjhalsidfthjo' is not uri(special)" # validate against the schemes i know i need
+ - "'http://admin:secret@example.com' is uri"
+ - "'ftps://admin:secret@example.com' is uri"
+ - "'admin:secret@example.com' is uri" # scheme is implied
+ - "'http://admin:secret@example.com/myfile?parm=1&param=2' is uri"
+ - "'urn:isbn:9780307476463' is uri" # book ref
+
+ - name: Assert url tests
+ assert:
+ that:
+ - "'http://searchengine.tld' is url" # simple case
+ - "'htp://searchengine.tld' is not url(special)" # bad scheme for explicit expectations
+ - "'htp://searchengine.tld' is url" # bad scheme, but valid if no explicit list
+ - "'ftp://searchengine.tld' is url"
+ - "'ftp://searchengine.tld' is url"
+ - "'ftp:// searchengine.tld' is url"
+ - "'file://etc/hosts' is url"
+ - "'mailto://me@example.com' is url"
+ - "'asldkfjhalsidfthjo' is not url" # junk
+ - "'http://admin:secret@example.com' is url"
+ - "'ftps://admin:secret@example.com' is url"
+ - "'admin:secret@example.com' is not url"
+ - "'http://admin:secret@example.com/myfile?parm=1&param=2' is url"
+ - "'urn:isbn:9780307476463' is not url" # book ref
+ - name: assert urn
+ assert:
+ that:
+ - "'urn:isbn:9780307476463' is urn" # book ref
+ - "'ftps://admin:secret@example.com' is not urn"
+ - "'admin:secret@example.com' is not urn"
diff --git a/test/integration/targets/throttle/aliases b/test/integration/targets/throttle/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/throttle/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/throttle/group_vars/all.yml b/test/integration/targets/throttle/group_vars/all.yml
new file mode 100644
index 0000000..b04b2aa
--- /dev/null
+++ b/test/integration/targets/throttle/group_vars/all.yml
@@ -0,0 +1,4 @@
+---
+throttledir: '{{ base_throttledir }}/{{ subdir }}'
+base_throttledir: "{{ lookup('env', 'OUTPUT_DIR') }}/throttle.dir"
+subdir: "{{ test_id if lookup('env', 'SELECTED_STRATEGY') in ['free', 'host_pinned'] else '' }}"
diff --git a/test/integration/targets/throttle/inventory b/test/integration/targets/throttle/inventory
new file mode 100644
index 0000000..9f062d9
--- /dev/null
+++ b/test/integration/targets/throttle/inventory
@@ -0,0 +1,6 @@
+[localhosts]
+testhost[00:11]
+
+[localhosts:vars]
+ansible_connection=local
+ansible_python_interpreter="{{ ansible_playbook_python }}"
diff --git a/test/integration/targets/throttle/runme.sh b/test/integration/targets/throttle/runme.sh
new file mode 100755
index 0000000..0db5098
--- /dev/null
+++ b/test/integration/targets/throttle/runme.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# https://github.com/ansible/ansible/pull/42528
+SELECTED_STRATEGY='linear' ansible-playbook test_throttle.yml -vv -i inventory --forks 12 "$@"
+SELECTED_STRATEGY='free' ansible-playbook test_throttle.yml -vv -i inventory --forks 12 "$@"
diff --git a/test/integration/targets/throttle/test_throttle.py b/test/integration/targets/throttle/test_throttle.py
new file mode 100755
index 0000000..1a5bdd3
--- /dev/null
+++ b/test/integration/targets/throttle/test_throttle.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+import sys
+import time
+
+# read the args from sys.argv
+throttledir, inventory_hostname, max_throttle = sys.argv[1:]
+# format/create additional vars
+max_throttle = int(max_throttle)
+throttledir = os.path.expanduser(throttledir)
+throttlefile = os.path.join(throttledir, inventory_hostname)
+try:
+ # create the file
+ with open(throttlefile, 'a'):
+ os.utime(throttlefile, None)
+ # count the number of files in the dir
+ throttlelist = os.listdir(throttledir)
+ print("tasks: %d/%d" % (len(throttlelist), max_throttle))
+ # if we have too many files, fail
+ if len(throttlelist) > max_throttle:
+ print(throttlelist)
+ raise ValueError("Too many concurrent tasks: %d/%d" % (len(throttlelist), max_throttle))
+ time.sleep(1.5)
+finally:
+ # remove the file, then wait to make sure it's gone
+ os.unlink(throttlefile)
+ while True:
+ if not os.path.exists(throttlefile):
+ break
+ time.sleep(0.1)
diff --git a/test/integration/targets/throttle/test_throttle.yml b/test/integration/targets/throttle/test_throttle.yml
new file mode 100644
index 0000000..8990ea2
--- /dev/null
+++ b/test/integration/targets/throttle/test_throttle.yml
@@ -0,0 +1,84 @@
+---
+- hosts: localhosts
+ gather_facts: false
+ strategy: linear
+ run_once: yes
+ tasks:
+ - name: Clean base throttledir '{{ base_throttledir }}'
+ file:
+ state: absent
+ path: '{{ base_throttledir }}'
+ ignore_errors: yes
+
+ - name: Create throttledir '{{ throttledir }}'
+ file:
+ state: directory
+ path: '{{ throttledir }}'
+ loop: "{{ range(1, test_count|int)|list }}"
+ loop_control:
+ loop_var: test_id
+ vars:
+ test_count: "{{ 9 if lookup('env', 'SELECTED_STRATEGY') in ['free', 'host_pinned'] else 2 }}"
+
+- hosts: localhosts
+ gather_facts: false
+ strategy: "{{ lookup('env', 'SELECTED_STRATEGY') }}"
+ tasks:
+ - block:
+ - name: "Test 1 (max throttle: 3)"
+ script: "test_throttle.py {{throttledir}} {{inventory_hostname}} 3"
+ vars:
+ test_id: 1
+ throttle: 3
+ - block:
+ - name: "Test 2 (max throttle: 5)"
+ script: "test_throttle.py {{throttledir}} {{inventory_hostname}} 5"
+ throttle: 5
+ vars:
+ test_id: 2
+ - block:
+ - name: "Test 3 (max throttle: 8)"
+ script: "test_throttle.py {{throttledir}} {{inventory_hostname}} 8"
+ throttle: 8
+ throttle: 6
+ vars:
+ test_id: 3
+ - block:
+ - block:
+ - name: "Test 4 (max throttle: 8)"
+ script: "test_throttle.py {{throttledir}} {{inventory_hostname}} 8"
+ throttle: 8
+ vars:
+ test_id: 4
+ throttle: 6
+ throttle: 12
+ throttle: 15
+ - block:
+ - name: "Teat 5 (max throttle: 3)"
+ script: "test_throttle.py {{throttledir}} {{inventory_hostname}} 3"
+ vars:
+ test_id: 5
+ throttle: 3
+ - block:
+ - name: "Test 6 (max throttle: 5)"
+ script: "test_throttle.py {{throttledir}} {{inventory_hostname}} 5"
+ throttle: 5
+ vars:
+ test_id: 6
+ - block:
+ - name: "Test 7 (max throttle: 6)"
+ script: "test_throttle.py {{throttledir}} {{inventory_hostname}} 6"
+ throttle: 6
+ vars:
+ test_id: 7
+ throttle: 3
+ - block:
+ - block:
+ - name: "Test 8 (max throttle: 8)"
+ script: "test_throttle.py {{throttledir}} {{inventory_hostname}} 8"
+ throttle: 8
+ vars:
+ test_id: 8
+ throttle: 6
+ throttle: 4
+ throttle: 2
diff --git a/test/integration/targets/unarchive/aliases b/test/integration/targets/unarchive/aliases
new file mode 100644
index 0000000..961b205
--- /dev/null
+++ b/test/integration/targets/unarchive/aliases
@@ -0,0 +1,3 @@
+needs/root
+shippable/posix/group2
+destructive
diff --git a/test/integration/targets/unarchive/files/foo.txt b/test/integration/targets/unarchive/files/foo.txt
new file mode 100644
index 0000000..7c6ded1
--- /dev/null
+++ b/test/integration/targets/unarchive/files/foo.txt
@@ -0,0 +1 @@
+foo.txt
diff --git a/test/integration/targets/unarchive/files/test-unarchive-nonascii-ãらã¨ã¿.tar.gz b/test/integration/targets/unarchive/files/test-unarchive-nonascii-ãらã¨ã¿.tar.gz
new file mode 100644
index 0000000..4882b92
--- /dev/null
+++ b/test/integration/targets/unarchive/files/test-unarchive-nonascii-ãらã¨ã¿.tar.gz
Binary files differ
diff --git a/test/integration/targets/unarchive/handlers/main.yml b/test/integration/targets/unarchive/handlers/main.yml
new file mode 100644
index 0000000..cb8b671
--- /dev/null
+++ b/test/integration/targets/unarchive/handlers/main.yml
@@ -0,0 +1,3 @@
+- name: restore packages
+ package:
+ name: "{{ unarchive_packages }}"
diff --git a/test/integration/targets/unarchive/meta/main.yml b/test/integration/targets/unarchive/meta/main.yml
new file mode 100644
index 0000000..ae54a4e
--- /dev/null
+++ b/test/integration/targets/unarchive/meta/main.yml
@@ -0,0 +1,4 @@
+dependencies:
+ - setup_remote_tmp_dir
+ - setup_gnutar
+ - setup_test_user
diff --git a/test/integration/targets/unarchive/tasks/main.yml b/test/integration/targets/unarchive/tasks/main.yml
new file mode 100644
index 0000000..148e583
--- /dev/null
+++ b/test/integration/targets/unarchive/tasks/main.yml
@@ -0,0 +1,22 @@
+- import_tasks: prepare_tests.yml
+- import_tasks: test_missing_binaries.yml
+- import_tasks: test_tar.yml
+- import_tasks: test_tar_gz.yml
+- import_tasks: test_tar_gz_creates.yml
+- import_tasks: test_tar_gz_owner_group.yml
+- import_tasks: test_tar_gz_keep_newer.yml
+- import_tasks: test_tar_zst.yml
+- import_tasks: test_zip.yml
+- import_tasks: test_exclude.yml
+- import_tasks: test_include.yml
+- import_tasks: test_parent_not_writeable.yml
+- import_tasks: test_mode.yml
+- import_tasks: test_quotable_characters.yml
+- import_tasks: test_non_ascii_filename.yml
+- import_tasks: test_missing_files.yml
+- import_tasks: test_symlink.yml
+- import_tasks: test_download.yml
+- import_tasks: test_unprivileged_user.yml
+- import_tasks: test_different_language_var.yml
+- import_tasks: test_invalid_options.yml
+- import_tasks: test_ownership_top_folder.yml
diff --git a/test/integration/targets/unarchive/tasks/prepare_tests.yml b/test/integration/targets/unarchive/tasks/prepare_tests.yml
new file mode 100644
index 0000000..98a8ba1
--- /dev/null
+++ b/test/integration/targets/unarchive/tasks/prepare_tests.yml
@@ -0,0 +1,115 @@
+- name: Include system specific variables
+ include_vars: "{{ ansible_facts.system }}.yml"
+
+# Need unzip for unarchive module, and zip for archive creation.
+- name: Ensure required binaries are present
+ package:
+ name: "{{ unarchive_packages }}"
+ when: ansible_pkg_mgr in ('yum', 'dnf', 'apt', 'pkgng')
+
+- name: prep our file
+ copy:
+ src: foo.txt
+ dest: "{{remote_tmp_dir}}/foo-unarchive.txt"
+ mode: preserve
+
+- name: prep a tar file
+ shell: tar cvf test-unarchive.tar foo-unarchive.txt chdir={{remote_tmp_dir}}
+
+- name: prep a tar.gz file
+ shell: tar czvf test-unarchive.tar.gz foo-unarchive.txt chdir={{remote_tmp_dir}}
+
+- name: see if we have the zstd executable
+ ignore_errors: true
+ shell: zstd --version
+ register: zstd_available
+
+- when: zstd_available.rc == 0
+ block:
+ - name: find gnu tar
+ shell: |
+ #!/bin/sh
+ which gtar 2>/dev/null
+ if test $? -ne 0; then
+ if test -z "`tar --version | grep bsdtar`"; then
+ which tar
+ fi
+ fi
+ register: gnu_tar
+
+ - name: prep a tar.zst file
+ shell: "{{ gnu_tar.stdout }} --use-compress-program=zstd -cvf test-unarchive.tar.zst foo-unarchive.txt chdir={{remote_tmp_dir}}"
+ when: gnu_tar.stdout != ""
+
+- name: prep a chmodded file for zip
+ copy:
+ src: foo.txt
+ dest: '{{remote_tmp_dir}}/foo-unarchive-777.txt'
+ mode: '0777'
+
+- name: prep a windows permission file for our zip
+ copy:
+ src: foo.txt
+ dest: '{{remote_tmp_dir}}/FOO-UNAR.TXT'
+ mode: preserve
+
+# This gets around an unzip timestamp bug in some distributions
+# Recent unzip on Ubuntu and BSD will randomly round some timestamps up.
+# But that doesn't seem to happen when the timestamp has an even second.
+- name: Bug work around
+ command: touch -t "201705111530.00" {{remote_tmp_dir}}/foo-unarchive.txt {{remote_tmp_dir}}/foo-unarchive-777.txt {{remote_tmp_dir}}/FOO-UNAR.TXT
+# See Ubuntu bug 1691636: https://bugs.launchpad.net/ubuntu/+source/unzip/+bug/1691636
+# When these are fixed, this code should be removed.
+
+- name: prep a zip file
+ shell: zip test-unarchive.zip foo-unarchive.txt foo-unarchive-777.txt chdir={{remote_tmp_dir}}
+
+- name: Prepare - Create test dirs
+ file:
+ path: "{{remote_tmp_dir}}/{{item}}"
+ state: directory
+ with_items:
+ - created/include
+ - created/exclude
+ - created/other
+
+- name: Prepare - Create test files
+ file:
+ path: "{{remote_tmp_dir}}/created/{{item}}"
+ state: touch
+ with_items:
+ - include/include-1.txt
+ - include/include-2.txt
+ - include/include-3.txt
+ - exclude/exclude-1.txt
+ - exclude/exclude-2.txt
+ - exclude/exclude-3.txt
+ - other/include-1.ext
+ - other/include-2.ext
+ - other/exclude-1.ext
+ - other/exclude-2.ext
+ - other/other-1.ext
+ - other/other-2.ext
+
+- name: Prepare - zip file
+ shell: zip -r {{remote_tmp_dir}}/unarchive-00.zip * chdir={{remote_tmp_dir}}/created/
+
+- name: Prepare - tar file
+ shell: tar czvf {{remote_tmp_dir}}/unarchive-00.tar * chdir={{remote_tmp_dir}}/created/
+
+- name: add a file with Windows permissions to zip file
+ shell: zip -k test-unarchive.zip FOO-UNAR.TXT chdir={{remote_tmp_dir}}
+
+- name: prep a subdirectory
+ file:
+ path: '{{remote_tmp_dir}}/unarchive-dir'
+ state: directory
+
+- name: prep our file
+ copy:
+ src: foo.txt
+ dest: '{{remote_tmp_dir}}/unarchive-dir/foo-unarchive.txt'
+ mode: preserve
+
+- name: prep a tar.gz file with directory
+ shell: tar czvf test-unarchive-dir.tar.gz unarchive-dir chdir={{remote_tmp_dir}}
diff --git a/test/integration/targets/unarchive/tasks/test_different_language_var.yml b/test/integration/targets/unarchive/tasks/test_different_language_var.yml
new file mode 100644
index 0000000..9eec658
--- /dev/null
+++ b/test/integration/targets/unarchive/tasks/test_different_language_var.yml
@@ -0,0 +1,41 @@
+- name: test non-ascii with different LANGUAGE
+ when: ansible_os_family == 'Debian'
+ block:
+ - name: install fr language pack
+ apt:
+ name: language-pack-fr
+ state: present
+
+ - name: create our unarchive destination
+ file:
+ path: "{{ remote_tmp_dir }}/test-unarchive-nonascii-ãらã¨ã¿-tar-gz"
+ state: directory
+
+ - name: test that unarchive works with an archive that contains non-ascii filenames
+ unarchive:
+ # Both the filename of the tarball and the filename inside the tarball have
+ # nonascii chars
+ src: "test-unarchive-nonascii-ãらã¨ã¿.tar.gz"
+ dest: "{{ remote_tmp_dir }}/test-unarchive-nonascii-ãらã¨ã¿-tar-gz"
+ mode: "u+rwX,go+rX"
+ remote_src: no
+ register: nonascii_result0
+
+ - name: Check that file is really there
+ stat:
+ path: "{{ remote_tmp_dir }}/test-unarchive-nonascii-ãらã¨ã¿-tar-gz/storage/aÌ€âæçeÌeÌ€ïiÌ‚oÌ‚Å“(copy)!@#$%^&-().jpg"
+ register: nonascii_stat0
+
+ - name: Assert that nonascii tests succeeded
+ assert:
+ that:
+ - "nonascii_result0.changed == true"
+ - "nonascii_stat0.stat.exists == true"
+
+ - name: remove nonascii test
+ file:
+ path: "{{ remote_tmp_dir }}/test-unarchive-nonascii-ãらã¨ã¿-tar-gz"
+ state: absent
+
+ environment:
+ LANGUAGE: fr_FR:fr
diff --git a/test/integration/targets/unarchive/tasks/test_download.yml b/test/integration/targets/unarchive/tasks/test_download.yml
new file mode 100644
index 0000000..241f11b
--- /dev/null
+++ b/test/integration/targets/unarchive/tasks/test_download.yml
@@ -0,0 +1,44 @@
+# Test downloading a file before unarchiving it
+- name: create our unarchive destination
+ file:
+ path: '{{remote_tmp_dir}}/test-unarchive-tar-gz'
+ state: directory
+
+- name: Test TLS download
+ block:
+ - name: Install packages to make TLS connections work on CentOS 6
+ pip:
+ name:
+ - urllib3==1.10.2
+ - ndg_httpsclient==0.4.4
+ - pyOpenSSL==16.2.0
+ state: present
+ when:
+ - ansible_facts.distribution == 'CentOS'
+ - not ansible_facts.python.has_sslcontext
+ - name: unarchive a tar from an URL
+ unarchive:
+ src: "https://releases.ansible.com/ansible/ansible-latest.tar.gz"
+ dest: "{{ remote_tmp_dir }}/test-unarchive-tar-gz"
+ mode: "0700"
+ remote_src: yes
+ register: unarchive13
+ - name: Test that unarchive succeeded
+ assert:
+ that:
+ - "unarchive13.changed == true"
+ always:
+ - name: Uninstall CentOS 6 TLS connections packages
+ pip:
+ name:
+ - urllib3
+ - ndg_httpsclient
+ - pyOpenSSL
+ state: absent
+ when:
+ - ansible_facts.distribution == 'CentOS'
+ - not ansible_facts.python.has_sslcontext
+ - name: remove our tar.gz unarchive destination
+ file:
+ path: '{{ remote_tmp_dir }}/test-unarchive-tar-gz'
+ state: absent
diff --git a/test/integration/targets/unarchive/tasks/test_exclude.yml b/test/integration/targets/unarchive/tasks/test_exclude.yml
new file mode 100644
index 0000000..8d3183c
--- /dev/null
+++ b/test/integration/targets/unarchive/tasks/test_exclude.yml
@@ -0,0 +1,54 @@
+- name: "Create {{ remote_tmp_dir }}/exclude directory"
+ file:
+ state: directory
+ path: "{{ remote_tmp_dir }}/exclude-{{item}}"
+ with_items:
+ - zip
+ - tar
+
+- name: Unpack archive file excluding regular and glob files.
+ unarchive:
+ src: "{{ remote_tmp_dir }}/unarchive-00.{{item}}"
+ dest: "{{ remote_tmp_dir }}/exclude-{{item}}"
+ remote_src: yes
+ list_files: yes
+ exclude:
+ - "exclude/exclude-*.txt"
+ - "other/exclude-1.ext"
+ register: result_of_unarchive
+ with_items:
+ - zip
+ - tar
+
+- name: Make sure unarchive module reported back extracted files
+ assert:
+ that:
+ - "'include/include-1.txt' in item.files"
+ - "'include/include-2.txt' in item.files"
+ - "'include/include-3.txt' in item.files"
+ - "'other/include-1.ext' in item.files"
+ - "'other/include-2.ext' in item.files"
+ - "'other/exclude-2.ext' in item.files"
+ - "'other/other-1.ext' in item.files"
+ - "'other/other-2.ext' in item.files"
+ loop: "{{ result_of_unarchive.results }}"
+
+- name: verify that the file was unarchived
+ shell: find {{ remote_tmp_dir }}/exclude-{{item}} chdir={{ remote_tmp_dir }}
+ register: unarchive00
+ with_items:
+ - zip
+ - tar
+
+- name: verify that archive extraction excluded the files
+ assert:
+ that:
+ - "'exclude/exclude-1.txt' not in item.stdout"
+ - "'other/exclude-1.ext' not in item.stdout"
+ with_items:
+ - "{{ unarchive00.results }}"
+
+- name: remove our zip unarchive destination
+ file:
+ path: '{{remote_tmp_dir}}/test-unarchive-zip'
+ state: absent
diff --git a/test/integration/targets/unarchive/tasks/test_include.yml b/test/integration/targets/unarchive/tasks/test_include.yml
new file mode 100644
index 0000000..ea3a01c
--- /dev/null
+++ b/test/integration/targets/unarchive/tasks/test_include.yml
@@ -0,0 +1,81 @@
+- name: Create a tar file with multiple files
+ shell: tar cvf test-unarchive-multi.tar foo-unarchive-777.txt foo-unarchive.txt
+ args:
+ chdir: "{{ remote_tmp_dir }}"
+
+- name: Create include test directories
+ file:
+ state: directory
+ path: "{{ remote_tmp_dir }}/{{ item }}"
+ loop:
+ - include-zip
+ - include-tar
+
+- name: Unpack zip file include one file
+ unarchive:
+ src: "{{ remote_tmp_dir }}/test-unarchive.zip"
+ dest: "{{ remote_tmp_dir }}/include-zip"
+ remote_src: yes
+ include:
+ - FOO-UNAR.TXT
+
+- name: Verify that single file was unarchived
+ find:
+ paths: "{{ remote_tmp_dir }}/include-zip"
+ register: unarchive_dir02
+
+- name: Verify that zip extraction included only one file
+ assert:
+ that:
+ - file_names == ['FOO-UNAR.TXT']
+ vars:
+ file_names: "{{ unarchive_dir02.files | map(attribute='path') | map('basename') }}"
+
+- name: Unpack tar file include one file
+ unarchive:
+ src: "{{ remote_tmp_dir }}/test-unarchive-multi.tar"
+ dest: "{{ remote_tmp_dir }}/include-tar"
+ remote_src: yes
+ include:
+ - foo-unarchive-777.txt
+
+- name: verify that single file was unarchived from tar
+ find:
+ paths: "{{ remote_tmp_dir }}/include-tar"
+ register: unarchive_dir03
+
+- name: Verify that tar extraction included only one file
+ assert:
+ that:
+ - file_names == ['foo-unarchive-777.txt']
+ vars:
+ file_names: "{{ unarchive_dir03.files | map(attribute='path') | map('basename') }}"
+ when:
+ - "ansible_facts.os_family == 'RedHat'"
+ - ansible_facts.distribution_major_version is version('7', '>=')
+
+- name: Check mutually exclusive parameters
+ unarchive:
+ src: "{{ remote_tmp_dir }}/test-unarchive-multi.tar"
+ dest: "{{ remote_tmp_dir }}/include-tar"
+ remote_src: yes
+ include:
+ - foo-unarchive-777.txt
+ exclude:
+ - foo
+ ignore_errors: yes
+ register: unarchive_mutually_exclusive_check
+
+- name: Check mutually exclusive parameters
+ assert:
+ that:
+ - unarchive_mutually_exclusive_check is failed
+ - "'mutually exclusive' in unarchive_mutually_exclusive_check.msg"
+
+- name: "Remove include feature tests directory"
+ file:
+ state: absent
+ path: "{{ remote_tmp_dir }}/{{ item }}"
+ loop:
+ - 'include-zip'
+ - 'include-tar'
diff --git a/test/integration/targets/unarchive/tasks/test_invalid_options.yml b/test/integration/targets/unarchive/tasks/test_invalid_options.yml
new file mode 100644
index 0000000..68a0621
--- /dev/null
+++ b/test/integration/targets/unarchive/tasks/test_invalid_options.yml
@@ -0,0 +1,27 @@
+- name: create our tar unarchive destination
+ file:
+ path: '{{remote_tmp_dir}}/test-unarchive-tar'
+ state: directory
+
+- name: unarchive a tar file with an invalid option
+ unarchive:
+ src: '{{remote_tmp_dir}}/test-unarchive.tar'
+ dest: '{{remote_tmp_dir}}/test-unarchive-tar'
+ remote_src: yes
+ extra_opts:
+ - "--invalid-éxtra-optら"
+ ignore_errors: yes
+ register: unarchive
+
+- name: verify that the invalid option is in the error message
+ assert:
+ that:
+ - "unarchive is failed"
+ - "unarchive['msg'] is search(msg)"
+ vars:
+ msg: "Unable to list files in the archive: /.*/(tar|gtar): unrecognized option '--invalid-éxtra-optら'"
+
+- name: remove our tar unarchive destination
+ file:
+ path: '{{remote_tmp_dir}}/test-unarchive-tar'
+ state: absent
diff --git a/test/integration/targets/unarchive/tasks/test_missing_binaries.yml b/test/integration/targets/unarchive/tasks/test_missing_binaries.yml
new file mode 100644
index 0000000..58d38f4
--- /dev/null
+++ b/test/integration/targets/unarchive/tasks/test_missing_binaries.yml
@@ -0,0 +1,87 @@
+- name: Test missing binaries
+ when: ansible_pkg_mgr in ('yum', 'dnf', 'apt', 'pkgng')
+ block:
+ - name: Remove zip binaries
+ package:
+ state: absent
+ name:
+ - zip
+ - unzip
+ notify: restore packages
+
+ - name: create unarchive destinations
+ file:
+ path: '{{ remote_tmp_dir }}/test-unarchive-{{ item }}'
+ state: directory
+ loop:
+ - zip
+ - tar
+
+ # With the zip binaries absent and tar still present, this task should work
+ - name: unarchive a tar file
+ unarchive:
+ src: '{{remote_tmp_dir}}/test-unarchive.tar'
+ dest: '{{remote_tmp_dir}}/test-unarchive-tar'
+ remote_src: yes
+ register: tar
+
+ - name: unarchive a zip file
+ unarchive:
+ src: '{{remote_tmp_dir}}/test-unarchive.zip'
+ dest: '{{remote_tmp_dir}}/test-unarchive-zip'
+ list_files: True
+ remote_src: yes
+ register: zip_fail
+ ignore_errors: yes
+ # FreeBSD does not have zipinfo, but does have a bootstrapped unzip in /usr/bin
+ # which alone is sufficient to run unarchive.
+ # Exclude /usr/bin from the PATH to test having no binary available.
+ environment:
+ PATH: "{{ ENV_PATH }}"
+ vars:
+ ENV_PATH: "{{ lookup('env', 'PATH') | regex_replace(re, '') }}"
+ re: "[^A-Za-z](\/usr\/bin:?)"
+
+ - name: Ensure tasks worked as expected
+ assert:
+ that:
+ - tar is success
+ - zip_fail is failed
+ - zip_fail.msg is search('Unable to find required')
+
+ - name: unarchive a zip file using unzip without zipinfo
+ unarchive:
+ src: '{{remote_tmp_dir}}/test-unarchive.zip'
+ dest: '{{remote_tmp_dir}}/test-unarchive-zip'
+ list_files: True
+ remote_src: yes
+ register: zip_success
+ # FreeBSD does not have zipinfo, but does have a bootstrapped unzip in /usr/bin
+ # which alone is sufficient to run unarchive.
+ when: ansible_pkg_mgr == 'pkgng'
+
+ - assert:
+ that:
+ - zip_success is success
+ - zip_success.changed
+ # Verify that file list is generated
+ - "'files' in zip_success"
+ - "{{zip_success['files']| length}} == 3"
+ - "'foo-unarchive.txt' in zip_success['files']"
+ - "'foo-unarchive-777.txt' in zip_success['files']"
+ - "'FOO-UNAR.TXT' in zip_success['files']"
+ when: ansible_pkg_mgr == 'pkgng'
+
+ - name: Remove unarchive destinations
+ file:
+ path: '{{ remote_tmp_dir }}/test-unarchive-{{ item }}'
+ state: absent
+ loop:
+ - zip
+ - tar
+
+ - name: Reinsntall zip binaries
+ package:
+ name:
+ - zip
+ - unzip
diff --git a/test/integration/targets/unarchive/tasks/test_missing_files.yml b/test/integration/targets/unarchive/tasks/test_missing_files.yml
new file mode 100644
index 0000000..4f57e18
--- /dev/null
+++ b/test/integration/targets/unarchive/tasks/test_missing_files.yml
@@ -0,0 +1,47 @@
+# Test that unarchiving is performed if files are missing
+# https://github.com/ansible/ansible-modules-core/issues/1064
+- name: create our unarchive destination
+ file:
+ path: '{{remote_tmp_dir}}/test-unarchive-tar-gz'
+ state: directory
+
+- name: unarchive a tar that has directories
+ unarchive:
+ src: "{{ remote_tmp_dir }}/test-unarchive-dir.tar.gz"
+ dest: "{{ remote_tmp_dir }}/test-unarchive-tar-gz"
+ mode: "0700"
+ remote_src: yes
+ register: unarchive10
+
+- name: Test that unarchive succeeded
+ assert:
+ that:
+ - "unarchive10.changed == true"
+
+- name: Change the mode of the toplevel dir
+ file:
+ path: "{{ remote_tmp_dir }}/test-unarchive-tar-gz/unarchive-dir"
+ mode: "0701"
+
+- name: Remove a file from the extraction point
+ file:
+ path: "{{ remote_tmp_dir }}/test-unarchive-tar-gz/unarchive-dir/foo-unarchive.txt"
+ state: absent
+
+- name: unarchive a tar that has directories
+ unarchive:
+ src: "{{ remote_tmp_dir }}/test-unarchive-dir.tar.gz"
+ dest: "{{ remote_tmp_dir }}/test-unarchive-tar-gz"
+ mode: "0700"
+ remote_src: yes
+ register: unarchive10_1
+
+- name: Test that unarchive succeeded
+ assert:
+ that:
+ - "unarchive10_1.changed == true"
+
+- name: remove our tar.gz unarchive destination
+ file:
+ path: '{{ remote_tmp_dir }}/test-unarchive-tar-gz'
+ state: absent
diff --git a/test/integration/targets/unarchive/tasks/test_mode.yml b/test/integration/targets/unarchive/tasks/test_mode.yml
new file mode 100644
index 0000000..c69e3bd
--- /dev/null
+++ b/test/integration/targets/unarchive/tasks/test_mode.yml
@@ -0,0 +1,151 @@
+- name: create our unarchive destination
+ file:
+ path: '{{remote_tmp_dir}}/test-unarchive-tar-gz'
+ state: directory
+
+- name: unarchive and set mode to 0600, directories 0700
+ unarchive:
+ src: "{{ remote_tmp_dir }}/test-unarchive.tar.gz"
+ dest: "{{ remote_tmp_dir }}/test-unarchive-tar-gz"
+ remote_src: yes
+ mode: "u+rwX,g-rwx,o-rwx"
+ list_files: True
+ register: unarchive06
+
+- name: Test that the file modes were changed
+ stat:
+ path: "{{ remote_tmp_dir }}/test-unarchive-tar-gz/foo-unarchive.txt"
+ register: unarchive06_stat
+
+- name: Test that the file modes were changed
+ assert:
+ that:
+ - "unarchive06.changed == true"
+ - "unarchive06_stat.stat.mode == '0600'"
+ # Verify that file list is generated
+ - "'files' in unarchive06"
+ - "{{unarchive06['files']| length}} == 1"
+ - "'foo-unarchive.txt' in unarchive06['files']"
+
+- name: remove our tar.gz unarchive destination
+ file:
+ path: '{{ remote_tmp_dir }}/test-unarchive-tar-gz'
+ state: absent
+
+- name: create our unarchive destination
+ file:
+ path: '{{remote_tmp_dir}}/test-unarchive-tar-gz'
+ state: directory
+
+- name: unarchive over existing extraction and set mode to 0644
+ unarchive:
+ src: "{{ remote_tmp_dir }}/test-unarchive.tar.gz"
+ dest: "{{ remote_tmp_dir }}/test-unarchive-tar-gz"
+ remote_src: yes
+ mode: "u+rwX,g-wx,o-wx,g+r,o+r"
+ register: unarchive06_2
+
+- name: Test that the file modes were changed
+ stat:
+ path: "{{ remote_tmp_dir }}/test-unarchive-tar-gz/foo-unarchive.txt"
+ register: unarchive06_2_stat
+
+- debug:
+ var: unarchive06_2_stat.stat.mode
+
+- name: Test that the files were changed
+ assert:
+ that:
+ - "unarchive06_2.changed == true"
+ - "unarchive06_2_stat.stat.mode == '0644'"
+
+- name: Repeat the last request to verify no changes
+ unarchive:
+ src: "{{ remote_tmp_dir }}/test-unarchive.tar.gz"
+ dest: "{{ remote_tmp_dir }}/test-unarchive-tar-gz"
+ remote_src: yes
+ mode: "u+rwX-x,g-wx,o-wx,g+r,o+r"
+ list_files: True
+ register: unarchive07
+
+- name: Test that the files were not changed
+ assert:
+ that:
+ - "unarchive07.changed == false"
+ # Verify that file list is generated
+ - "'files' in unarchive07"
+ - "{{unarchive07['files']| length}} == 1"
+ - "'foo-unarchive.txt' in unarchive07['files']"
+
+- name: remove our tar.gz unarchive destination
+ file:
+ path: '{{ remote_tmp_dir }}/test-unarchive-tar-gz'
+ state: absent
+
+- name: create our unarchive destination
+ file:
+ path: '{{remote_tmp_dir}}/test-unarchive-zip'
+ state: directory
+
+- name: unarchive and set mode to 0601, directories 0700
+ unarchive:
+ src: "{{ remote_tmp_dir }}/test-unarchive.zip"
+ dest: "{{ remote_tmp_dir }}/test-unarchive-zip"
+ remote_src: yes
+ mode: "u+rwX-x,g-rwx,o=x"
+ list_files: True
+ register: unarchive08
+
+- name: Test that the file modes were changed
+ stat:
+ path: "{{ remote_tmp_dir }}/test-unarchive-zip/foo-unarchive.txt"
+ register: unarchive08_stat
+
+- name: Test that the file modes were changed
+ assert:
+ that:
+ - "unarchive08.changed == true"
+ - "unarchive08_stat.stat.mode == '0601'"
+ # Verify that file list is generated
+ - "'files' in unarchive08"
+ - "{{unarchive08['files']| length}} == 3"
+ - "'foo-unarchive.txt' in unarchive08['files']"
+ - "'foo-unarchive-777.txt' in unarchive08['files']"
+ - "'FOO-UNAR.TXT' in unarchive08['files']"
+
+- name: unarchive zipfile a second time and set mode to 0601, directories 0700
+ unarchive:
+ src: "{{ remote_tmp_dir }}/test-unarchive.zip"
+ dest: "{{ remote_tmp_dir }}/test-unarchive-zip"
+ remote_src: yes
+ mode: "u+rwX-x,g-rwx,o=x"
+ list_files: True
+ register: unarchive08
+
+- name: Test that the file modes were not changed
+ stat:
+ path: "{{ remote_tmp_dir }}/test-unarchive-zip/foo-unarchive.txt"
+ register: unarchive08_stat
+
+- debug:
+ var: unarchive08
+
+- debug:
+ var: unarchive08_stat
+
+- name: Test that the files did not change
+ assert:
+ that:
+ - "unarchive08.changed == false"
+ - "unarchive08_stat.stat.mode == '0601'"
+ # Verify that file list is generated
+ - "'files' in unarchive08"
+ - "{{unarchive08['files']| length}} == 3"
+ - "'foo-unarchive.txt' in unarchive08['files']"
+ - "'foo-unarchive-777.txt' in unarchive08['files']"
+ - "'FOO-UNAR.TXT' in unarchive08['files']"
+
+- name: remove our zip unarchive destination
+ file:
+ path: '{{ remote_tmp_dir }}/test-unarchive-zip'
+ state: absent
diff --git a/test/integration/targets/unarchive/tasks/test_non_ascii_filename.yml b/test/integration/targets/unarchive/tasks/test_non_ascii_filename.yml
new file mode 100644
index 0000000..c884f49
--- /dev/null
+++ b/test/integration/targets/unarchive/tasks/test_non_ascii_filename.yml
@@ -0,0 +1,66 @@
+- name: create our unarchive destination
+ file:
+ path: "{{ remote_tmp_dir }}/test-unarchive-nonascii-ãらã¨ã¿-tar-gz"
+ state: directory
+
+- name: test that unarchive works with an archive that contains non-ascii filenames
+ unarchive:
+ # Both the filename of the tarball and the filename inside the tarball have
+ # nonascii chars
+ src: "test-unarchive-nonascii-ãらã¨ã¿.tar.gz"
+ dest: "{{ remote_tmp_dir }}/test-unarchive-nonascii-ãらã¨ã¿-tar-gz"
+ mode: "u+rwX,go+rX"
+ remote_src: no
+ register: nonascii_result0
+
+- name: Check that file is really there
+ stat:
+ path: "{{ remote_tmp_dir }}/test-unarchive-nonascii-ãらã¨ã¿-tar-gz/storage/aÌ€âæçeÌeÌ€ïiÌ‚oÌ‚Å“(copy)!@#$%^&-().jpg"
+ register: nonascii_stat0
+
+- name: Assert that nonascii tests succeeded
+ assert:
+ that:
+ - "nonascii_result0.changed == true"
+ - "nonascii_stat0.stat.exists == true"
+
+- name: remove nonascii test
+ file:
+ path: "{{ remote_tmp_dir }}/test-unarchive-nonascii-ãらã¨ã¿-tar-gz"
+ state: absent
+
+- name: test non-ascii with different LC_ALL
+ block:
+ - name: create our unarchive destination
+ file:
+ path: "{{ remote_tmp_dir }}/test-unarchive-nonascii-ãらã¨ã¿-tar-gz"
+ state: directory
+
+ - name: test that unarchive works with an archive that contains non-ascii filenames
+ unarchive:
+ # Both the filename of the tarball and the filename inside the tarball have
+ # nonascii chars
+ src: "test-unarchive-nonascii-ãらã¨ã¿.tar.gz"
+ dest: "{{ remote_tmp_dir }}/test-unarchive-nonascii-ãらã¨ã¿-tar-gz"
+ mode: "u+rwX,go+rX"
+ remote_src: no
+ register: nonascii_result0
+
+ - name: Check that file is really there
+ stat:
+ path: "{{ remote_tmp_dir }}/test-unarchive-nonascii-ãらã¨ã¿-tar-gz/storage/aÌ€âæçeÌeÌ€ïiÌ‚oÌ‚Å“(copy)!@#$%^&-().jpg"
+ register: nonascii_stat0
+
+ - name: Assert that nonascii tests succeeded
+ assert:
+ that:
+ - "nonascii_result0.changed == true"
+ - "nonascii_stat0.stat.exists == true"
+
+ - name: remove nonascii test
+ file:
+ path: "{{ remote_tmp_dir }}/test-unarchive-nonascii-ãらã¨ã¿-tar-gz"
+ state: absent
+
+ environment:
+ LC_ALL: C
diff --git a/test/integration/targets/unarchive/tasks/test_owner_group.yml b/test/integration/targets/unarchive/tasks/test_owner_group.yml
new file mode 100644
index 0000000..227ad9c
--- /dev/null
+++ b/test/integration/targets/unarchive/tasks/test_owner_group.yml
@@ -0,0 +1,164 @@
+- block:
+ - name: Create a group to chown to
+ group:
+ name: testgroup
+ register: testgroup
+
+ - name: Create a user to chown to
+ user:
+ name: testuser
+ groups:
+ - testgroup
+ register: testuser
+
+ - set_fact:
+ outdir: '{{remote_tmp_dir}}/test-unarchive-{{ ext | replace(".", "_") }}'
+
+ - debug:
+ msg: Username test
+
+ # username
+ - name: create our unarchive destinations
+ file:
+ path: '{{outdir}}'
+ state: directory
+
+ - name: unarchive a file
+ unarchive:
+ src: '{{remote_tmp_dir}}/{{archive}}'
+ dest: '{{outdir}}'
+ list_files: True
+ remote_src: yes
+ owner: testuser
+
+ - name: stat an output file
+ stat:
+ path: '{{outdir}}/{{testfile}}'
+ register: stat
+
+ - name: verify that the file has the right owner
+ assert:
+ that:
+ - stat.stat.exists
+ - stat.stat.pw_name == testuser.name
+ - stat.stat.uid == testuser.uid
+
+ - name: nuke destination
+ file:
+ path: '{{outdir}}'
+ state: absent
+
+ - debug:
+ msg: uid test
+
+ # uid
+ - name: create our unarchive destinations
+ file:
+ path: '{{outdir}}'
+ state: directory
+
+ - name: unarchive a file
+ unarchive:
+ src: '{{remote_tmp_dir}}/{{archive}}'
+ dest: '{{outdir}}'
+ list_files: True
+ remote_src: yes
+ owner: '{{ testuser.uid }}'
+
+ - name: stat an output file
+ stat:
+ path: '{{outdir}}/{{testfile}}'
+ register: stat
+
+ - name: verify that the file has the right owner
+ assert:
+ that:
+ - stat.stat.exists
+ - stat.stat.pw_name == testuser.name
+ - stat.stat.uid == testuser.uid
+
+ - name: nuke destination
+ file:
+ path: '{{outdir}}'
+ state: absent
+
+ - debug:
+ msg: groupname test
+
+ # groupname
+ - name: create our unarchive destinations
+ file:
+ path: '{{outdir}}'
+ state: directory
+
+ - name: unarchive a file
+ unarchive:
+ src: '{{remote_tmp_dir}}/{{archive}}'
+ dest: '{{outdir}}'
+ list_files: True
+ remote_src: yes
+ group: testgroup
+
+ - name: stat an output file
+ stat:
+ path: '{{outdir}}/{{testfile}}'
+ register: stat
+
+ - name: verify that the file has the right owner
+ assert:
+ that:
+ - stat.stat.exists
+ - stat.stat.gr_name == testgroup.name
+ - stat.stat.gid == testgroup.gid
+
+ - name: nuke destination
+ file:
+ path: '{{outdir}}'
+ state: absent
+
+ - debug:
+ msg: gid test
+
+ # gid
+ - name: create our unarchive destinations
+ file:
+ path: '{{outdir}}'
+ state: directory
+
+ - name: unarchive a file
+ unarchive:
+ src: '{{remote_tmp_dir}}/{{archive}}'
+ dest: '{{outdir}}'
+ list_files: True
+ remote_src: yes
+ group: '{{ testgroup.gid }}'
+
+ - name: stat an output file
+ stat:
+ path: '{{outdir}}/{{testfile}}'
+ register: stat
+
+ - name: verify that the file has the right owner
+ assert:
+ that:
+ - stat.stat.exists
+ - stat.stat.gr_name == testgroup.name
+ - stat.stat.gid == testgroup.gid
+
+ - name: nuke destination
+ file:
+ path: '{{outdir}}'
+ state: absent
+
+ always:
+ - name: Remove testuser
+ user:
+ name: testuser
+ state: absent
+ remove: yes
+ force: yes
+
+ - name: Remove testgroup
+ group:
+ name: testgroup
+ state: absent
diff --git a/test/integration/targets/unarchive/tasks/test_ownership_top_folder.yml b/test/integration/targets/unarchive/tasks/test_ownership_top_folder.yml
new file mode 100644
index 0000000..da40108
--- /dev/null
+++ b/test/integration/targets/unarchive/tasks/test_ownership_top_folder.yml
@@ -0,0 +1,50 @@
+- name: Test unarchiving as root and apply different ownership to top folder
+ vars:
+ ansible_become: yes
+ ansible_become_user: root
+ ansible_become_password: null
+ block:
+ - name: Create top folder owned by root
+ file:
+ path: "{{ test_user.home }}/tarball-top-folder"
+ state: directory
+ owner: root
+
+ - name: Add a file owned by root
+ copy:
+ src: foo.txt
+ dest: "{{ test_user.home }}/tarball-top-folder/foo-unarchive.txt"
+ mode: preserve
+
+ - name: Create a tarball as root. This tarball won't list the top folder when doing "tar tvf test-tarball.tar.gz"
+ shell: tar -czf test-tarball.tar.gz tarball-top-folder/foo-unarchive.txt
+ args:
+ chdir: "{{ test_user.home }}"
+ creates: "{{ test_user.home }}/test-tarball.tar.gz"
+
+ - name: Create unarchive destination folder in {{ test_user.home }}/unarchivetest3-unarchive
+ file:
+ path: "{{ test_user.home }}/unarchivetest3-unarchive"
+ state: directory
+ owner: "{{ test_user.name }}"
+ group: "{{ test_user.group }}"
+
+ - name: "unarchive the tarball as root. apply ownership for {{ test_user.name }}"
+ unarchive:
+ src: "{{ test_user.home }}/test-tarball.tar.gz"
+ dest: "{{ test_user.home }}/unarchivetest3-unarchive"
+ remote_src: yes
+ list_files: True
+ owner: "{{ test_user.name }}"
+ group: "{{ test_user.group }}"
+
+ - name: Stat the extracted top folder
+ stat:
+ path: "{{ test_user.home }}/unarchivetest3-unarchive/tarball-top-folder"
+ register: top_folder_info
+
+ - name: "verify that extracted top folder is owned by {{ test_user.name }}"
+ assert:
+ that:
+ - top_folder_info.stat.pw_name == test_user.name
+ - top_folder_info.stat.gid == test_user.group
diff --git a/test/integration/targets/unarchive/tasks/test_parent_not_writeable.yml b/test/integration/targets/unarchive/tasks/test_parent_not_writeable.yml
new file mode 100644
index 0000000..bfb082c
--- /dev/null
+++ b/test/integration/targets/unarchive/tasks/test_parent_not_writeable.yml
@@ -0,0 +1,32 @@
+- name: check if /tmp/foo-unarchive.text exists
+ stat:
+ path: /tmp/foo-unarchive.txt
+ ignore_errors: True
+ register: unarchive04
+
+- name: fail if the proposed destination file exists for safey
+ fail:
+ msg: /tmp/foo-unarchive.txt already exists, aborting
+ when: unarchive04.stat.exists
+
+- name: try unarchiving to /tmp
+ unarchive:
+ src: '{{remote_tmp_dir}}/test-unarchive.tar.gz'
+ dest: /tmp
+ remote_src: true
+ register: unarchive05
+
+- name: verify that the file was marked as changed
+ assert:
+ that:
+ - "unarchive05.changed == true"
+
+- name: verify that the file was unarchived
+ file:
+ path: /tmp/foo-unarchive.txt
+ state: file
+
+- name: remove our unarchive destination
+ file:
+ path: /tmp/foo-unarchive.txt
+ state: absent
diff --git a/test/integration/targets/unarchive/tasks/test_quotable_characters.yml b/test/integration/targets/unarchive/tasks/test_quotable_characters.yml
new file mode 100644
index 0000000..0a3c2cc
--- /dev/null
+++ b/test/integration/targets/unarchive/tasks/test_quotable_characters.yml
@@ -0,0 +1,38 @@
+- name: create our unarchive destination
+ file:
+ path: '{{remote_tmp_dir}}/test-unarchive-tar-gz'
+ state: directory
+
+- name: create a directory with quotable chars
+ file:
+ path: '{{ remote_tmp_dir }}/test-quotes~root'
+ state: directory
+
+- name: unarchive into directory with quotable chars
+ unarchive:
+ src: "{{ remote_tmp_dir }}/test-unarchive.tar.gz"
+ dest: "{{ remote_tmp_dir }}/test-quotes~root"
+ remote_src: yes
+ register: unarchive08
+
+- name: Test that unarchive succeeded
+ assert:
+ that:
+ - "unarchive08.changed == true"
+
+- name: unarchive into directory with quotable chars a second time
+ unarchive:
+ src: "{{ remote_tmp_dir }}/test-unarchive.tar.gz"
+ dest: "{{ remote_tmp_dir }}/test-quotes~root"
+ remote_src: yes
+ register: unarchive09
+
+- name: Test that unarchive did nothing
+ assert:
+ that:
+ - "unarchive09.changed == false"
+
+- name: remove quotable chars test
+ file:
+ path: '{{ remote_tmp_dir }}/test-quotes~root'
+ state: absent
diff --git a/test/integration/targets/unarchive/tasks/test_symlink.yml b/test/integration/targets/unarchive/tasks/test_symlink.yml
new file mode 100644
index 0000000..fcb7282
--- /dev/null
+++ b/test/integration/targets/unarchive/tasks/test_symlink.yml
@@ -0,0 +1,64 @@
+- name: Create a destination dir
+ file:
+ path: "{{ remote_tmp_dir }}/test-unarchive-tar-gz"
+ state: directory
+
+- name: Create a symlink to the detination dir
+ file:
+ path: "{{ remote_tmp_dir }}/link-to-unarchive-dir"
+ src: "{{ remote_tmp_dir }}/test-unarchive-tar-gz"
+ state: "link"
+
+- name: test that unarchive works when dest is a symlink to a dir
+ unarchive:
+ src: "{{ remote_tmp_dir }}/test-unarchive.tar.gz"
+ dest: "{{ remote_tmp_dir }}/link-to-unarchive-dir"
+ mode: "u+rwX,go+rX"
+ remote_src: yes
+ register: unarchive_11
+
+- name: Check that file is really there
+ stat:
+ path: "{{ remote_tmp_dir }}/test-unarchive-tar-gz/foo-unarchive.txt"
+ register: unarchive11_stat0
+
+- name: Assert that unarchive when dest is a symlink to a dir worked
+ assert:
+ that:
+ - "unarchive_11.changed == true"
+ - "unarchive11_stat0.stat.exists == true"
+
+- name: remove our tar.gz unarchive destination
+ file:
+ path: '{{ remote_tmp_dir }}/test-unarchive-tar-gz'
+ state: absent
+
+- name: Create a file
+ file:
+ path: "{{ remote_tmp_dir }}/test-unarchive-tar-gz"
+ state: touch
+
+- name: Create a symlink to the file
+ file:
+ src: "{{ remote_tmp_dir }}/test-unarchive-tar-gz"
+ path: "{{ remote_tmp_dir }}/link-to-unarchive-file"
+ state: "link"
+
+- name: test that unarchive fails when dest is a link to a file
+ unarchive:
+ src: "{{ remote_tmp_dir }}/test-unarchive.tar.gz"
+ dest: "{{ remote_tmp_dir }}/link-to-unarchive-file"
+ mode: "u+rwX,go+rX"
+ remote_src: yes
+ ignore_errors: True
+ register: unarchive_12
+
+- name: Assert that unarchive when dest is a file failed
+ assert:
+ that:
+ - "unarchive_12.failed == true"
+
+- name: remove our tar.gz unarchive destination
+ file:
+ path: '{{ remote_tmp_dir }}/test-unarchive-tar-gz'
+ state: absent
diff --git a/test/integration/targets/unarchive/tasks/test_tar.yml b/test/integration/targets/unarchive/tasks/test_tar.yml
new file mode 100644
index 0000000..0a02041
--- /dev/null
+++ b/test/integration/targets/unarchive/tasks/test_tar.yml
@@ -0,0 +1,33 @@
+- name: create our tar unarchive destination
+ file:
+ path: '{{remote_tmp_dir}}/test-unarchive-tar'
+ state: directory
+
+- name: unarchive a tar file
+ unarchive:
+ src: '{{remote_tmp_dir}}/test-unarchive.tar'
+ dest: '{{remote_tmp_dir}}/test-unarchive-tar'
+ remote_src: yes
+ register: unarchive01
+
+- name: verify that the file was marked as changed
+ assert:
+ that:
+ - "unarchive01.changed == true"
+
+- name: verify that the file was unarchived
+ file:
+ path: '{{remote_tmp_dir}}/test-unarchive-tar/foo-unarchive.txt'
+ state: file
+
+- name: remove our tar unarchive destination
+ file:
+ path: '{{remote_tmp_dir}}/test-unarchive-tar'
+ state: absent
+
+- name: test owner/group perms
+ include_tasks: test_owner_group.yml
+ vars:
+ ext: tar
+ archive: test-unarchive.tar
+ testfile: foo-unarchive.txt
diff --git a/test/integration/targets/unarchive/tasks/test_tar_gz.yml b/test/integration/targets/unarchive/tasks/test_tar_gz.yml
new file mode 100644
index 0000000..e88f77b
--- /dev/null
+++ b/test/integration/targets/unarchive/tasks/test_tar_gz.yml
@@ -0,0 +1,35 @@
+- name: create our tar.gz unarchive destination
+ file:
+ path: '{{remote_tmp_dir}}/test-unarchive-tar-gz'
+ state: directory
+
+- name: unarchive a tar.gz file
+ unarchive:
+ src: '{{remote_tmp_dir}}/test-unarchive.tar.gz'
+ dest: '{{remote_tmp_dir}}/test-unarchive-tar-gz'
+ remote_src: yes
+ register: unarchive02
+
+- name: verify that the file was marked as changed
+ assert:
+ that:
+ - "unarchive02.changed == true"
+ # Verify that no file list is generated
+ - "'files' not in unarchive02"
+
+- name: verify that the file was unarchived
+ file:
+ path: '{{remote_tmp_dir}}/test-unarchive-tar-gz/foo-unarchive.txt'
+ state: file
+
+- name: remove our tar.gz unarchive destination
+ file:
+ path: '{{remote_tmp_dir}}/test-unarchive-tar-gz'
+ state: absent
+
+- name: test owner/group perms
+ include_tasks: test_owner_group.yml
+ vars:
+ ext: tar.gz
+ archive: test-unarchive.tar.gz
+ testfile: foo-unarchive.txt
diff --git a/test/integration/targets/unarchive/tasks/test_tar_gz_creates.yml b/test/integration/targets/unarchive/tasks/test_tar_gz_creates.yml
new file mode 100644
index 0000000..fa3a23f
--- /dev/null
+++ b/test/integration/targets/unarchive/tasks/test_tar_gz_creates.yml
@@ -0,0 +1,53 @@
+- name: create our tar.gz unarchive destination for creates
+ file:
+ path: '{{remote_tmp_dir}}/test-unarchive-tar-gz'
+ state: directory
+
+- name: unarchive a tar.gz file with creates set
+ unarchive:
+ src: '{{remote_tmp_dir}}/test-unarchive.tar.gz'
+ dest: '{{remote_tmp_dir}}/test-unarchive-tar-gz'
+ creates: '{{remote_tmp_dir}}/test-unarchive-tar-gz/foo-unarchive.txt'
+ remote_src: yes
+ register: unarchive02b
+
+- name: verify that the file was marked as changed
+ assert:
+ that:
+ - "unarchive02b.changed == true"
+
+- name: verify that the file was unarchived
+ file:
+ path: '{{remote_tmp_dir}}/test-unarchive-tar-gz/foo-unarchive.txt'
+ state: file
+
+- name: unarchive a tar.gz file with creates over an existing file
+ unarchive:
+ src: '{{remote_tmp_dir}}/test-unarchive.tar.gz'
+ dest: '{{remote_tmp_dir}}/test-unarchive-tar-gz'
+ creates: '{{remote_tmp_dir}}/test-unarchive-tar-gz/foo-unarchive.txt'
+ remote_src: yes
+ register: unarchive02c
+
+- name: verify that the file was not marked as changed
+ assert:
+ that:
+ - "unarchive02c.changed == false"
+
+- name: unarchive a tar.gz file with creates over an existing file using complex_args
+ unarchive:
+ src: "{{remote_tmp_dir}}/test-unarchive.tar.gz"
+ dest: "{{remote_tmp_dir}}/test-unarchive-tar-gz"
+ remote_src: yes
+ creates: "{{remote_tmp_dir}}/test-unarchive-tar-gz/foo-unarchive.txt"
+ register: unarchive02d
+
+- name: verify that the file was not marked as changed
+ assert:
+ that:
+ - "unarchive02d.changed == false"
+
+- name: remove our tar.gz unarchive destination
+ file:
+ path: '{{remote_tmp_dir}}/test-unarchive-tar-gz'
+ state: absent
diff --git a/test/integration/targets/unarchive/tasks/test_tar_gz_keep_newer.yml b/test/integration/targets/unarchive/tasks/test_tar_gz_keep_newer.yml
new file mode 100644
index 0000000..aec9454
--- /dev/null
+++ b/test/integration/targets/unarchive/tasks/test_tar_gz_keep_newer.yml
@@ -0,0 +1,57 @@
+- name: create our tar.gz unarchive destination for keep-newer
+ file:
+ path: "{{remote_tmp_dir}}/test-unarchive-tar-gz"
+ state: directory
+
+- name: Create a newer file that we would replace
+ copy:
+ dest: "{{remote_tmp_dir}}/test-unarchive-tar-gz/foo-unarchive.txt"
+ content: boo
+ mode: preserve
+
+- name: unarchive a tar.gz file but avoid overwriting newer files (keep_newer=true)
+ unarchive:
+ src: "{{remote_tmp_dir}}/test-unarchive.tar.gz"
+ dest: "{{remote_tmp_dir}}/test-unarchive-tar-gz"
+ remote_src: yes
+ keep_newer: true
+ register: unarchive02f
+
+- name: Make sure the file still contains 'boo'
+ shell: cat {{remote_tmp_dir}}/test-unarchive-tar-gz/foo-unarchive.txt
+ register: unarchive02f_cat
+
+- name: remove our tar.gz unarchive destination
+ file:
+ path: "{{remote_tmp_dir}}/test-unarchive-tar-gz"
+ state: absent
+
+- name: create our tar.gz unarchive destination for keep-newer (take 2)
+ file:
+ path: "{{remote_tmp_dir}}/test-unarchive-tar-gz"
+ state: directory
+
+- name: unarchive a tar.gz file and overwrite newer files (keep_newer=false)
+ unarchive:
+ src: "{{remote_tmp_dir}}/test-unarchive.tar.gz"
+ dest: "{{remote_tmp_dir}}/test-unarchive-tar-gz"
+ remote_src: yes
+ keep_newer: false
+ register: unarchive02g
+
+- name: Make sure the file still contains 'boo'
+ shell: cat {{remote_tmp_dir}}/test-unarchive-tar-gz/foo-unarchive.txt
+ register: unarchive02g_cat
+
+- name: remove our tar.gz unarchive destination
+ file:
+ path: "{{remote_tmp_dir}}/test-unarchive-tar-gz"
+ state: absent
+
+- name: verify results
+ assert:
+ that:
+ - unarchive02f is changed
+ - unarchive02f_cat.stdout == 'boo'
+ - unarchive02g is changed
+ - unarchive02g_cat.stdout != 'boo'
diff --git a/test/integration/targets/unarchive/tasks/test_tar_gz_owner_group.yml b/test/integration/targets/unarchive/tasks/test_tar_gz_owner_group.yml
new file mode 100644
index 0000000..e99f038
--- /dev/null
+++ b/test/integration/targets/unarchive/tasks/test_tar_gz_owner_group.yml
@@ -0,0 +1,50 @@
+- block:
+ - name: Create a group to chown to
+ group:
+ name: testgroup
+
+ - name: Create a user to chown to
+ user:
+ name: testuser
+ groups:
+ - testgroup
+
+ - name: create our tar.gz unarchive destination for chown
+ file:
+ path: "{{remote_tmp_dir}}/test-unarchive-tar-gz"
+ state: directory
+
+ - name: unarchive a tar.gz file with owner and group set to the above user
+ unarchive:
+ src: "{{remote_tmp_dir}}/test-unarchive.tar.gz"
+ dest: "{{remote_tmp_dir}}/test-unarchive-tar-gz"
+ remote_src: yes
+ owner: testuser
+ group: testgroup
+ register: unarchive02e
+
+ - name: Stat a file in the directory we unarchived to
+ stat:
+ path: "{{remote_tmp_dir}}/test-unarchive-tar-gz/foo-unarchive.txt"
+ register: unarchive02e_file_stat
+
+ - name: verify results
+ assert:
+ that:
+ - unarchive02e is changed
+ - unarchive02e_file_stat.stat.exists
+ - unarchive02e_file_stat.stat.pw_name == 'testuser'
+ - unarchive02e_file_stat.stat.gr_name == 'testgroup'
+
+ always:
+ - name: Remove testuser
+ user:
+ name: testuser
+ state: absent
+ remove: yes
+ force: yes
+
+ - name: Remove testgroup
+ group:
+ name: testgroup
+ state: absent
diff --git a/test/integration/targets/unarchive/tasks/test_tar_zst.yml b/test/integration/targets/unarchive/tasks/test_tar_zst.yml
new file mode 100644
index 0000000..18b1281
--- /dev/null
+++ b/test/integration/targets/unarchive/tasks/test_tar_zst.yml
@@ -0,0 +1,40 @@
+# Only do this whole file when the "zstd" executable is present
+- when:
+ - zstd_available.rc == 0
+ - gnu_tar.stdout != ""
+ block:
+ - name: create our tar.zst unarchive destination
+ file:
+ path: '{{remote_tmp_dir}}/test-unarchive-tar-zst'
+ state: directory
+
+ - name: unarchive a tar.zst file
+ unarchive:
+ src: '{{remote_tmp_dir}}/test-unarchive.tar.zst'
+ dest: '{{remote_tmp_dir}}/test-unarchive-tar-zst'
+ remote_src: yes
+ register: unarchive02
+
+ - name: verify that the file was marked as changed
+ assert:
+ that:
+ - "unarchive02.changed == true"
+ # Verify that no file list is generated
+ - "'files' not in unarchive02"
+
+ - name: verify that the file was unarchived
+ file:
+ path: '{{remote_tmp_dir}}/test-unarchive-tar-zst/foo-unarchive.txt'
+ state: file
+
+ - name: remove our tar.zst unarchive destination
+ file:
+ path: '{{remote_tmp_dir}}/test-unarchive-tar-zst'
+ state: absent
+
+ - name: test owner/group perms
+ include_tasks: test_owner_group.yml
+ vars:
+ ext: tar.zst
+ archive: test-unarchive.tar.zst
+ testfile: foo-unarchive.txt
diff --git a/test/integration/targets/unarchive/tasks/test_unprivileged_user.yml b/test/integration/targets/unarchive/tasks/test_unprivileged_user.yml
new file mode 100644
index 0000000..8ee1db4
--- /dev/null
+++ b/test/integration/targets/unarchive/tasks/test_unprivileged_user.yml
@@ -0,0 +1,63 @@
+- name: Test unarchiving twice as unprivileged user
+ vars:
+ ansible_become: yes
+ ansible_become_user: "{{ test_user_name }}"
+ ansible_become_password: "{{ test_user_plaintext_password }}"
+ block:
+ - name: prep our file
+ copy:
+ src: foo.txt
+ dest: "{{ test_user.home }}/foo-unarchive.txt"
+ mode: preserve
+
+ - name: Prep a zip file as {{ test_user.name }} user
+ shell: zip unarchivetest1-unarchive.zip foo-unarchive.txt
+ args:
+ chdir: "{{ test_user.home }}"
+ creates: "{{ test_user.home }}/unarchivetest1-unarchive.zip"
+
+ - name: create our zip unarchive destination as {{ test_user.name }} user
+ file:
+ path: "{{ test_user.home }}/unarchivetest1-unarchive-zip"
+ state: directory
+
+ - name: unarchive a zip file as {{ test_user.name }} user
+ unarchive:
+ src: "{{ test_user.home }}/unarchivetest1-unarchive.zip"
+ dest: "{{ test_user.home }}/unarchivetest1-unarchive-zip"
+ remote_src: yes
+ list_files: True
+ register: unarchive10
+
+ - name: stat the unarchived file
+ stat:
+ path: "{{ test_user.home }}/unarchivetest1-unarchive-zip/foo-unarchive.txt"
+ register: archive_path
+
+ - name: verify that the tasks performed as expected
+ assert:
+ that:
+ - unarchive10 is changed
+ # Verify that file list is generated
+ - "'files' in unarchive10"
+ - "{{unarchive10['files']| length}} == 1"
+ - "'foo-unarchive.txt' in unarchive10['files']"
+ - archive_path.stat.exists
+
+ - name: repeat the last request to verify no changes
+ unarchive:
+ src: "{{ test_user.home }}/unarchivetest1-unarchive.zip"
+ dest: "{{ test_user.home }}/unarchivetest1-unarchive-zip"
+ remote_src: yes
+ list_files: True
+ register: unarchive10b
+
+ # Due to a bug in the date calculation used to determine if a change
+ # was made or not, this check is unreliable. This seems to only happen on
+ # Ubuntu 1604.
+ # https://github.com/ansible/ansible/blob/58145dff9ca1a713f8ed295a0076779a91c41cba/lib/ansible/modules/unarchive.py#L472-L474
+ - name: Check that unarchiving again reports no change
+ assert:
+ that:
+ - unarchive10b is not changed
+ ignore_errors: yes
diff --git a/test/integration/targets/unarchive/tasks/test_zip.yml b/test/integration/targets/unarchive/tasks/test_zip.yml
new file mode 100644
index 0000000..cf03946
--- /dev/null
+++ b/test/integration/targets/unarchive/tasks/test_zip.yml
@@ -0,0 +1,57 @@
+- name: create our zip unarchive destination
+ file:
+ path: '{{remote_tmp_dir}}/test-unarchive-zip'
+ state: directory
+
+- name: unarchive a zip file
+ unarchive:
+ src: '{{remote_tmp_dir}}/test-unarchive.zip'
+ dest: '{{remote_tmp_dir}}/test-unarchive-zip'
+ list_files: True
+ remote_src: yes
+ register: unarchive03
+
+- name: verify that the file was marked as changed
+ assert:
+ that:
+ - "unarchive03.changed == true"
+ # Verify that file list is generated
+ - "'files' in unarchive03"
+ - "{{unarchive03['files']| length}} == 3"
+ - "'foo-unarchive.txt' in unarchive03['files']"
+ - "'foo-unarchive-777.txt' in unarchive03['files']"
+ - "'FOO-UNAR.TXT' in unarchive03['files']"
+
+- name: verify that the file was unarchived
+ file:
+ path: '{{remote_tmp_dir}}/test-unarchive-zip/{{item}}'
+ state: file
+ with_items:
+ - foo-unarchive.txt
+ - foo-unarchive-777.txt
+ - FOO-UNAR.TXT
+
+- name: repeat the last request to verify no changes
+ unarchive:
+ src: '{{remote_tmp_dir}}/test-unarchive.zip'
+ dest: '{{remote_tmp_dir}}/test-unarchive-zip'
+ list_files: true
+ remote_src: true
+ register: unarchive03b
+
+- name: verify that the task was not marked as changed
+ assert:
+ that:
+ - "unarchive03b.changed == false"
+
+- name: nuke zip destination
+ file:
+ path: '{{remote_tmp_dir}}/test-unarchive-zip'
+ state: absent
+
+- name: test owner/group perms
+ include_tasks: test_owner_group.yml
+ vars:
+ ext: zip
+ archive: test-unarchive.zip
+ testfile: foo-unarchive.txt
diff --git a/test/integration/targets/unarchive/vars/Darwin.yml b/test/integration/targets/unarchive/vars/Darwin.yml
new file mode 100644
index 0000000..902feed
--- /dev/null
+++ b/test/integration/targets/unarchive/vars/Darwin.yml
@@ -0,0 +1 @@
+unarchive_packages: []
diff --git a/test/integration/targets/unarchive/vars/FreeBSD.yml b/test/integration/targets/unarchive/vars/FreeBSD.yml
new file mode 100644
index 0000000..0f5c401
--- /dev/null
+++ b/test/integration/targets/unarchive/vars/FreeBSD.yml
@@ -0,0 +1,4 @@
+unarchive_packages:
+ - unzip
+ - zip
+ - zstd
diff --git a/test/integration/targets/unarchive/vars/Linux.yml b/test/integration/targets/unarchive/vars/Linux.yml
new file mode 100644
index 0000000..6110934
--- /dev/null
+++ b/test/integration/targets/unarchive/vars/Linux.yml
@@ -0,0 +1,4 @@
+unarchive_packages:
+ - tar
+ - unzip
+ - zip
diff --git a/test/integration/targets/undefined/aliases b/test/integration/targets/undefined/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/undefined/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/undefined/tasks/main.yml b/test/integration/targets/undefined/tasks/main.yml
new file mode 100644
index 0000000..5bf4786
--- /dev/null
+++ b/test/integration/targets/undefined/tasks/main.yml
@@ -0,0 +1,16 @@
+- set_fact:
+ names: '{{ things|map(attribute="name") }}'
+ vars:
+ things:
+ - name: one
+ - name: two
+ - notname: three
+ - name: four
+ ignore_errors: true
+ register: undefined_set_fact
+
+- assert:
+ that:
+ - '("%r"|format(undefined_variable)).startswith("AnsibleUndefined")'
+ - undefined_set_fact is failed
+ - undefined_set_fact.msg is contains 'undefined variable'
diff --git a/test/integration/targets/unexpected_executor_exception/action_plugins/unexpected.py b/test/integration/targets/unexpected_executor_exception/action_plugins/unexpected.py
new file mode 100644
index 0000000..77fe58f
--- /dev/null
+++ b/test/integration/targets/unexpected_executor_exception/action_plugins/unexpected.py
@@ -0,0 +1,8 @@
+from ansible.plugins.action import ActionBase
+
+
+class ActionModule(ActionBase):
+ TRANSFERS_FILES = False
+
+ def run(self, tmp=None, task_vars=None):
+ raise Exception('boom')
diff --git a/test/integration/targets/unexpected_executor_exception/aliases b/test/integration/targets/unexpected_executor_exception/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/unexpected_executor_exception/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/unexpected_executor_exception/tasks/main.yml b/test/integration/targets/unexpected_executor_exception/tasks/main.yml
new file mode 100644
index 0000000..395b50c
--- /dev/null
+++ b/test/integration/targets/unexpected_executor_exception/tasks/main.yml
@@ -0,0 +1,7 @@
+- unexpected:
+ register: result
+ ignore_errors: true
+
+- assert:
+ that:
+ - 'result.msg == "Unexpected failure during module execution: boom"'
diff --git a/test/integration/targets/unicode/aliases b/test/integration/targets/unicode/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/unicode/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/unicode/inventory b/test/integration/targets/unicode/inventory
new file mode 100644
index 0000000..11b3560
--- /dev/null
+++ b/test/integration/targets/unicode/inventory
@@ -0,0 +1,5 @@
+[local]
+testhost ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+
+[all:vars]
+unicode_host_var=CaféEñyei
diff --git a/test/integration/targets/unicode/křížek-ansible-project/ansible.cfg b/test/integration/targets/unicode/křížek-ansible-project/ansible.cfg
new file mode 100644
index 0000000..6775889
--- /dev/null
+++ b/test/integration/targets/unicode/křížek-ansible-project/ansible.cfg
@@ -0,0 +1,2 @@
+[defaults]
+library=~/.ansible/plugins/modules:/usr/share/ansible/plugins/modules:.
diff --git a/test/integration/targets/unicode/runme.sh b/test/integration/targets/unicode/runme.sh
new file mode 100755
index 0000000..aa14783
--- /dev/null
+++ b/test/integration/targets/unicode/runme.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook unicode.yml -i inventory -v -e 'extra_var=café' "$@"
+# Test the start-at-task flag #9571
+ANSIBLE_HOST_PATTERN_MISMATCH=warning ansible-playbook unicode.yml -i inventory -v --start-at-task '*¶' -e 'start_at_task=True' "$@"
+
+# Test --version works with non-ascii ansible project paths #66617
+# Unset these so values from the project dir are used
+unset ANSIBLE_CONFIG
+unset ANSIBLE_LIBRARY
+pushd křížek-ansible-project && ansible --version; popd
diff --git a/test/integration/targets/unicode/unicode-test-script b/test/integration/targets/unicode/unicode-test-script
new file mode 100755
index 0000000..340f2a9
--- /dev/null
+++ b/test/integration/targets/unicode/unicode-test-script
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+echo "Non-ascii arguments:"
+echo $@
+
+echo "Non-ascii Env var:"
+echo $option
diff --git a/test/integration/targets/unicode/unicode.yml b/test/integration/targets/unicode/unicode.yml
new file mode 100644
index 0000000..672133d
--- /dev/null
+++ b/test/integration/targets/unicode/unicode.yml
@@ -0,0 +1,149 @@
+---
+- name: 'A play with unicode: ¢ £ ¤ ¥'
+ hosts: localhost
+ vars:
+ test_var: 'Ī ī Ĭ ĭ Į į İ ı IJ ij Ĵ ĵ Ķ ķ ĸ Ĺ ĺ Ļ ļ Ľ ľ Ŀ ŀ Šł Ń ń Ņ ņ Ň ň ʼn Ŋ ŋ Ō ŠŎ ŠŠő Œ'
+ hostnames:
+ - 'host-ϬϭϮϯϰ'
+ - 'host-fóöbär'
+ - 'host-ΙΚΛΜÎΞ'
+ - 'host-στυφχψ'
+ - 'host-ϬϭϮϯϰϱ'
+
+ tasks:
+ - name: 'A task name with unicode: è é ê ë'
+ debug: msg='hi there'
+
+ - name: 'A task with unicode parameters'
+ debug: var=test_var
+
+ # € ‚ ƒ „ … † ‡ ˆ ‰ Š ‹ Œ Ž ‘ ’ “ †• – — ˜ ™ š › œ ž Ÿ ¡ ¢ £ ¤ ¥ ¦ § ¨ © ª « ¬ ­ ®'
+
+ - name: 'A task using with_items containing unicode'
+ debug: msg='{{item}}'
+ with_items:
+ - '¯ ° ± ² ³ ´ µ ¶ · ¸ ¹ º » ¼ ½ ¾ ¿ À à Â Ã Ä Å Æ Ç È É Ê Ë Ì à Î à à Ñ Ò Ó Ô Õ Ö ×'
+ - 'Ø Ù Ú Û Ü à Þ ß à á â ã ä å æ ç è é ê ë ì í î ï ð ñ ò ó ô õ ö ÷ ø ù ú û ü ý þ ÿ Ā'
+ - 'Ä Ä‚ ă Ä„ Ä… Ć ć Ĉ ĉ ÄŠ Ä‹ ÄŒ Ä ÄŽ Ä Ä Ä‘ Ä’ Ä“ Ä” Ä• Ä– Ä— Ę Ä™ Äš Ä› Äœ Ä Äž ÄŸ Ä  Ä¡ Ä¢ Ä£ Ĥ Ä¥ Ħ ħ Ĩ Ä©'
+
+ - add_host:
+ name: '{{item}}'
+ groups: 'ĪīĬĭ'
+ ansible_ssh_host: 127.0.0.1
+ ansible_connection: local
+ ansible_python_interpreter: "{{ ansible_playbook_python }}"
+ with_items: "{{ hostnames }}"
+
+ - name: 'A task with unicode extra vars'
+ debug: var=extra_var
+
+ - name: 'A task with unicode host vars'
+ debug: var=unicode_host_var
+
+ - name: 'A task with unicode shell parameters'
+ shell: echo '¯ ° ± ² ³ ´ µ ¶ · ¸ ¹ º » ¼ ½ ¾ ¿ À à Â Ã Ä Å Æ Ç È É Ê Ë Ì à Î à à Ñ Ò Ó Ô Õ Ö ×'
+ register: output
+
+ - name: 'Assert that the unicode was echoed'
+ assert:
+ that:
+ - "'¯ ° ± ² ³ ´ µ ¶ · ¸ ¹ º » ¼ ½ ¾ ¿ À à Â Ã Ä Å Æ Ç È É Ê Ë Ì à Î à à Ñ Ò Ó Ô Õ Ö ×' in output.stdout_lines"
+
+ - name: Run raw with non-ascii options
+ raw: "/bin/echo Zażółć gęślą jaźń"
+ register: results
+
+ - name: Check that raw output the right thing
+ assert:
+ that:
+ - "'Zażółć gęślą jaźń' in results.stdout_lines"
+
+ - name: Run a script with non-ascii options and environment
+ script: unicode-test-script --option "Zażółć gęślą jaźń"
+ environment:
+ option: Zażółć
+ register: results
+
+ - name: Check that script output includes the nonascii arguments and environment values
+ assert:
+ that:
+ - "'--option Zażółć gęślą jaźń' in results.stdout_lines"
+ - "'Zażółć' in results.stdout_lines"
+
+ - name: Ping with non-ascii environment variable and option
+ ping:
+ data: "Zażółć gęślą jaźń"
+ environment:
+ option: Zażółć
+ register: results
+
+ - name: Check that ping with non-ascii data was correct
+ assert:
+ that:
+ - "'Zażółć gęślą jaźń' == results.ping"
+
+ - name: Command that echos a non-ascii env var
+ command: "echo $option"
+ environment:
+ option: Zażółć
+ register: results
+
+ - name: Check that a non-ascii env var was passed to the command module
+ assert:
+ that:
+ - "'Zażółć' in results.stdout_lines"
+
+ - name: Clean a temp directory
+ file:
+ path: /var/tmp/ansible_test_unicode_get_put
+ state: absent
+
+ - name: Create a temp directory
+ file:
+ path: /var/tmp/ansible_test_unicode_get_put
+ state: directory
+
+ - name: Create a file with a non-ascii filename
+ file:
+ path: /var/tmp/ansible_test_unicode_get_put/Zażółć
+ state: touch
+ delegate_to: localhost
+
+ - name: Put with unicode filename
+ copy:
+ src: /var/tmp/ansible_test_unicode_get_put/Zażółć
+ dest: /var/tmp/ansible_test_unicode_get_put/Zażółć2
+
+ - name: Fetch with unicode filename
+ fetch:
+ src: /var/tmp/ansible_test_unicode_get_put/Zażółć2
+ dest: /var/tmp/ansible_test_unicode_get_put/
+
+ - name: Clean a temp directory
+ file:
+ path: /var/tmp/ansible_test_unicode_get_put
+ state: absent
+
+- name: 'A play for hosts in group: ĪīĬĭ'
+ hosts: 'ĪīĬĭ'
+ gather_facts: true
+ tasks:
+ - debug: msg='Unicode is a good thing â„¢'
+ - debug: msg=ÐБВГД
+
+# Run this test by adding to the CLI: -e start_at_task=True --start-at-task '*¶'
+- name: 'Show that we can skip to unicode named tasks'
+ hosts: localhost
+ gather_facts: false
+ vars:
+ flag: 'original'
+ start_at_task: False
+ tasks:
+ - name: 'Override flag var'
+ set_fact: flag='new'
+
+ - name: 'A unicode task at the end of the playbook: ¶'
+ assert:
+ that:
+ - 'flag == "original"'
+ when: start_at_task|bool
diff --git a/test/integration/targets/unsafe_writes/aliases b/test/integration/targets/unsafe_writes/aliases
new file mode 100644
index 0000000..da1b554
--- /dev/null
+++ b/test/integration/targets/unsafe_writes/aliases
@@ -0,0 +1,7 @@
+context/target
+needs/root
+skip/freebsd
+skip/osx
+skip/macos
+shippable/posix/group2
+needs/target/setup_remote_tmp_dir
diff --git a/test/integration/targets/unsafe_writes/basic.yml b/test/integration/targets/unsafe_writes/basic.yml
new file mode 100644
index 0000000..99a3195
--- /dev/null
+++ b/test/integration/targets/unsafe_writes/basic.yml
@@ -0,0 +1,83 @@
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - import_role:
+ name: ../setup_remote_tmp_dir
+ - name: define test directory
+ set_fact:
+ testudir: '{{remote_tmp_dir}}/unsafe_writes_test'
+ - name: define test file
+ set_fact:
+ testufile: '{{testudir}}/unreplacablefile.txt'
+ - name: define test environment with unsafe writes set
+ set_fact:
+ test_env:
+ ANSIBLE_UNSAFE_WRITES: "{{ lookup('env', 'ANSIBLE_UNSAFE_WRITES') }}"
+ when: lookup('env', 'ANSIBLE_UNSAFE_WRITES')
+ - name: define test environment without unsafe writes set
+ set_fact:
+ test_env: {}
+ when: not lookup('env', 'ANSIBLE_UNSAFE_WRITES')
+ - name: test unsafe_writes on immutable dir (file cannot be atomically replaced)
+ block:
+ - name: create target dir
+ file: path={{testudir}} state=directory
+ - name: setup test file
+ copy: content=ORIGINAL dest={{testufile}}
+ - name: make target dir immutable (cannot write to file w/o unsafe_writes)
+ file: path={{testudir}} state=directory attributes="+i"
+ become: yes
+ ignore_errors: true
+ register: madeimmutable
+
+ - name: only run if immutable dir command worked, some of our test systems don't allow for it
+ when: madeimmutable is success
+ block:
+ - name: test this is actually immmutable working as we expect
+ file: path={{testufile}} state=absent
+ register: breakimmutable
+ ignore_errors: True
+
+ - name: only run if reallyh immutable dir
+ when: breakimmutable is failed
+ block:
+ - name: test overwriting file w/o unsafe
+ copy: content=NEW dest={{testufile}} unsafe_writes=False
+ ignore_errors: true
+ register: copy_without
+
+ - name: ensure we properly failed
+ assert:
+ that:
+ - copy_without is failed
+
+ - name: test overwriting file with unsafe
+ copy: content=NEWNOREALLY dest={{testufile}} unsafe_writes=True
+ register: copy_with
+
+ - name: ensure we properly changed
+ assert:
+ that:
+ - copy_with is changed
+
+ - name: test fallback env var
+ when: lookup('env', 'ANSIBLE_UNSAFE_WRITES') not in ('', None)
+ vars:
+ env_enabled: "{{lookup('env', 'ANSIBLE_UNSAFE_WRITES')|bool}}"
+ block:
+ - name: test overwriting file with unsafe depending on fallback environment setting
+ copy: content=NEWBUTNOTDIFFERENT dest={{testufile}}
+ register: copy_with_env
+ ignore_errors: True
+
+ - name: ensure we properly follow env var
+ assert:
+ msg: "Failed with envvar: {{env_enabled}}, due AUW: to {{q('env', 'ANSIBLE_UNSAFE_WRITES')}}"
+ that:
+ - env_enabled and copy_with_env is changed or not env_enabled and copy_with_env is failed
+ environment: "{{ test_env }}"
+ always:
+ - name: remove immutable flag from dir to prevent issues with cleanup
+ file: path={{testudir}} state=directory attributes="-i"
+ ignore_errors: true
+ become: yes
diff --git a/test/integration/targets/unsafe_writes/runme.sh b/test/integration/targets/unsafe_writes/runme.sh
new file mode 100755
index 0000000..619ce02
--- /dev/null
+++ b/test/integration/targets/unsafe_writes/runme.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# test w/o fallback env var
+ansible-playbook basic.yml -i ../../inventory "$@"
+
+# test enabled fallback env var
+ANSIBLE_UNSAFE_WRITES=1 ansible-playbook basic.yml -i ../../inventory "$@"
+
+# test disnabled fallback env var
+ANSIBLE_UNSAFE_WRITES=0 ansible-playbook basic.yml -i ../../inventory "$@"
diff --git a/test/integration/targets/until/action_plugins/shell_no_failed.py b/test/integration/targets/until/action_plugins/shell_no_failed.py
new file mode 100644
index 0000000..594c014
--- /dev/null
+++ b/test/integration/targets/until/action_plugins/shell_no_failed.py
@@ -0,0 +1,28 @@
+# Copyright: (c) 2017, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.action import ActionBase
+
+
+class ActionModule(ActionBase):
+
+ def run(self, tmp=None, task_vars=None):
+ del tmp # tmp no longer has any effect
+
+ try:
+ self._task.args['_raw_params'] = self._task.args.pop('cmd')
+ except KeyError:
+ pass
+ shell_action = self._shared_loader_obj.action_loader.get('ansible.legacy.shell',
+ task=self._task,
+ connection=self._connection,
+ play_context=self._play_context,
+ loader=self._loader,
+ templar=self._templar,
+ shared_loader_obj=self._shared_loader_obj)
+ result = shell_action.run(task_vars=task_vars)
+ result.pop('failed', None)
+ return result
diff --git a/test/integration/targets/until/aliases b/test/integration/targets/until/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/until/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/until/tasks/main.yml b/test/integration/targets/until/tasks/main.yml
new file mode 100644
index 0000000..2b2ac94
--- /dev/null
+++ b/test/integration/targets/until/tasks/main.yml
@@ -0,0 +1,84 @@
+- shell: '{{ ansible_python.executable }} -c "import tempfile; print(tempfile.mkstemp()[1])"'
+ register: tempfilepath
+
+- set_fact:
+ until_tempfile_path: "{{ tempfilepath.stdout }}"
+
+- name: loop with default retries
+ shell: echo "run" >> {{ until_tempfile_path }} && wc -w < {{ until_tempfile_path }} | tr -d ' '
+ register: runcount
+ until: runcount.stdout | int == 3
+ delay: 0.01
+
+- assert:
+ that: runcount.stdout | int == 3
+
+- file: path="{{ until_tempfile_path }}" state=absent
+
+- name: loop with specified max retries
+ shell: echo "run" >> {{ until_tempfile_path }}
+ until: 1==0
+ retries: 5
+ delay: 0.01
+ ignore_errors: true
+
+- name: validate output
+ shell: wc -l < {{ until_tempfile_path }}
+ register: runcount
+
+- assert:
+ that: runcount.stdout | int == 6 # initial + 5 retries
+
+- file:
+ path: "{{ until_tempfile_path }}"
+ state: absent
+
+- name: Test failed_when impacting until
+ shell: 'true'
+ register: failed_when_until
+ failed_when: True
+ until: failed_when_until is successful
+ retries: 3
+ delay: 0.5
+ ignore_errors: True
+
+- assert:
+ that:
+ - failed_when_until.attempts == 3
+
+- name: Test changed_when impacting until
+ shell: 'true'
+ register: changed_when_until
+ changed_when: False
+ until: changed_when_until is changed
+ retries: 3
+ delay: 0.5
+ ignore_errors: True
+
+- assert:
+ that:
+ - changed_when_until.attempts == 3
+
+# This task shouldn't fail, previously .attempts was not available to changed_when/failed_when
+# and would cause the conditional to fail due to ``'dict object' has no attribute 'attempts'``
+# https://github.com/ansible/ansible/issues/34139
+- name: Test access to attempts in changed_when/failed_when
+ shell: 'true'
+ register: changed_when_attempts
+ until: 1 == 0
+ retries: 5
+ delay: 0.5
+ failed_when: changed_when_attempts.attempts > 6
+
+# Test until on module that doesn't return failed, but does return rc
+- name: create counter file
+ copy:
+ dest: "{{ output_dir }}/until_counter"
+ content: 3
+
+- shell_no_failed:
+ cmd: |
+ COUNTER=$(cat "{{ output_dir }}/until_counter"); NEW=$(expr $COUNTER - 1); echo $NEW > "{{ output_dir }}/until_counter"; exit $COUNTER
+ register: counter
+ delay: 0.5
+ until: counter.rc == 0
diff --git a/test/integration/targets/unvault/aliases b/test/integration/targets/unvault/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/unvault/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/unvault/main.yml b/test/integration/targets/unvault/main.yml
new file mode 100644
index 0000000..a0f97b4
--- /dev/null
+++ b/test/integration/targets/unvault/main.yml
@@ -0,0 +1,9 @@
+- hosts: localhost
+ tasks:
+ - set_fact:
+ unvaulted: "{{ lookup('unvault', 'vault') }}"
+ - debug:
+ msg: "{{ unvaulted }}"
+ - assert:
+ that:
+ - "unvaulted == 'foo: bar\n'"
diff --git a/test/integration/targets/unvault/password b/test/integration/targets/unvault/password
new file mode 100644
index 0000000..d97c5ea
--- /dev/null
+++ b/test/integration/targets/unvault/password
@@ -0,0 +1 @@
+secret
diff --git a/test/integration/targets/unvault/runme.sh b/test/integration/targets/unvault/runme.sh
new file mode 100755
index 0000000..df4585e
--- /dev/null
+++ b/test/integration/targets/unvault/runme.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+set -eux
+
+
+ansible-playbook --vault-password-file password main.yml
diff --git a/test/integration/targets/unvault/vault b/test/integration/targets/unvault/vault
new file mode 100644
index 0000000..828d369
--- /dev/null
+++ b/test/integration/targets/unvault/vault
@@ -0,0 +1,6 @@
+$ANSIBLE_VAULT;1.1;AES256
+33386337343963393533343039333563323733646137636162346266643134323539396237646333
+3663363965336335663161656236616532346363303832310a393264356663393330346137613239
+34633765333936633466353932663166343531616230326161383365323966386434366431353839
+3838623233373231340a303166666433613439303464393661363365643765666137393137653138
+3631
diff --git a/test/integration/targets/uri/aliases b/test/integration/targets/uri/aliases
new file mode 100644
index 0000000..90ef161
--- /dev/null
+++ b/test/integration/targets/uri/aliases
@@ -0,0 +1,3 @@
+destructive
+shippable/posix/group1
+needs/httptester
diff --git a/test/integration/targets/uri/files/README b/test/integration/targets/uri/files/README
new file mode 100644
index 0000000..ef77912
--- /dev/null
+++ b/test/integration/targets/uri/files/README
@@ -0,0 +1,9 @@
+The files were taken from http://www.json.org/JSON_checker/
+> If the JSON_checker is working correctly, it must accept all of the pass*.json files and reject all of the fail*.json files.
+
+Difference with JSON_checker dataset:
+ - *${n}.json renamed to *${n-1}.json to be 0-based
+ - fail0.json renamed to pass3.json as python json module allows JSON payload to be string
+ - fail17.json renamed to pass4.json as python json module has no problems with deep structures
+ - fail32.json renamed to fail0.json to fill gap
+ - fail31.json renamed to fail17.json to fill gap
diff --git a/test/integration/targets/uri/files/fail0.json b/test/integration/targets/uri/files/fail0.json
new file mode 100644
index 0000000..ca5eb19
--- /dev/null
+++ b/test/integration/targets/uri/files/fail0.json
@@ -0,0 +1 @@
+["mismatch"} \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail1.json b/test/integration/targets/uri/files/fail1.json
new file mode 100644
index 0000000..6b7c11e
--- /dev/null
+++ b/test/integration/targets/uri/files/fail1.json
@@ -0,0 +1 @@
+["Unclosed array" \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail10.json b/test/integration/targets/uri/files/fail10.json
new file mode 100644
index 0000000..76eb95b
--- /dev/null
+++ b/test/integration/targets/uri/files/fail10.json
@@ -0,0 +1 @@
+{"Illegal expression": 1 + 2} \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail11.json b/test/integration/targets/uri/files/fail11.json
new file mode 100644
index 0000000..77580a4
--- /dev/null
+++ b/test/integration/targets/uri/files/fail11.json
@@ -0,0 +1 @@
+{"Illegal invocation": alert()} \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail12.json b/test/integration/targets/uri/files/fail12.json
new file mode 100644
index 0000000..379406b
--- /dev/null
+++ b/test/integration/targets/uri/files/fail12.json
@@ -0,0 +1 @@
+{"Numbers cannot have leading zeroes": 013} \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail13.json b/test/integration/targets/uri/files/fail13.json
new file mode 100644
index 0000000..0ed366b
--- /dev/null
+++ b/test/integration/targets/uri/files/fail13.json
@@ -0,0 +1 @@
+{"Numbers cannot be hex": 0x14} \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail14.json b/test/integration/targets/uri/files/fail14.json
new file mode 100644
index 0000000..fc8376b
--- /dev/null
+++ b/test/integration/targets/uri/files/fail14.json
@@ -0,0 +1 @@
+["Illegal backslash escape: \x15"] \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail15.json b/test/integration/targets/uri/files/fail15.json
new file mode 100644
index 0000000..3fe21d4
--- /dev/null
+++ b/test/integration/targets/uri/files/fail15.json
@@ -0,0 +1 @@
+[\naked] \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail16.json b/test/integration/targets/uri/files/fail16.json
new file mode 100644
index 0000000..62b9214
--- /dev/null
+++ b/test/integration/targets/uri/files/fail16.json
@@ -0,0 +1 @@
+["Illegal backslash escape: \017"] \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail17.json b/test/integration/targets/uri/files/fail17.json
new file mode 100644
index 0000000..45cba73
--- /dev/null
+++ b/test/integration/targets/uri/files/fail17.json
@@ -0,0 +1 @@
+{"Comma instead if closing brace": true, \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail18.json b/test/integration/targets/uri/files/fail18.json
new file mode 100644
index 0000000..3b9c46f
--- /dev/null
+++ b/test/integration/targets/uri/files/fail18.json
@@ -0,0 +1 @@
+{"Missing colon" null} \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail19.json b/test/integration/targets/uri/files/fail19.json
new file mode 100644
index 0000000..27c1af3
--- /dev/null
+++ b/test/integration/targets/uri/files/fail19.json
@@ -0,0 +1 @@
+{"Double colon":: null} \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail2.json b/test/integration/targets/uri/files/fail2.json
new file mode 100644
index 0000000..168c81e
--- /dev/null
+++ b/test/integration/targets/uri/files/fail2.json
@@ -0,0 +1 @@
+{unquoted_key: "keys must be quoted"} \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail20.json b/test/integration/targets/uri/files/fail20.json
new file mode 100644
index 0000000..6247457
--- /dev/null
+++ b/test/integration/targets/uri/files/fail20.json
@@ -0,0 +1 @@
+{"Comma instead of colon", null} \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail21.json b/test/integration/targets/uri/files/fail21.json
new file mode 100644
index 0000000..a775258
--- /dev/null
+++ b/test/integration/targets/uri/files/fail21.json
@@ -0,0 +1 @@
+["Colon instead of comma": false] \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail22.json b/test/integration/targets/uri/files/fail22.json
new file mode 100644
index 0000000..494add1
--- /dev/null
+++ b/test/integration/targets/uri/files/fail22.json
@@ -0,0 +1 @@
+["Bad value", truth] \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail23.json b/test/integration/targets/uri/files/fail23.json
new file mode 100644
index 0000000..caff239
--- /dev/null
+++ b/test/integration/targets/uri/files/fail23.json
@@ -0,0 +1 @@
+['single quote'] \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail24.json b/test/integration/targets/uri/files/fail24.json
new file mode 100644
index 0000000..8b7ad23
--- /dev/null
+++ b/test/integration/targets/uri/files/fail24.json
@@ -0,0 +1 @@
+[" tab character in string "] \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail25.json b/test/integration/targets/uri/files/fail25.json
new file mode 100644
index 0000000..845d26a
--- /dev/null
+++ b/test/integration/targets/uri/files/fail25.json
@@ -0,0 +1 @@
+["tab\ character\ in\ string\ "] \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail26.json b/test/integration/targets/uri/files/fail26.json
new file mode 100644
index 0000000..6b01a2c
--- /dev/null
+++ b/test/integration/targets/uri/files/fail26.json
@@ -0,0 +1,2 @@
+["line
+break"] \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail27.json b/test/integration/targets/uri/files/fail27.json
new file mode 100644
index 0000000..621a010
--- /dev/null
+++ b/test/integration/targets/uri/files/fail27.json
@@ -0,0 +1,2 @@
+["line\
+break"] \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail28.json b/test/integration/targets/uri/files/fail28.json
new file mode 100644
index 0000000..47ec421
--- /dev/null
+++ b/test/integration/targets/uri/files/fail28.json
@@ -0,0 +1 @@
+[0e] \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail29.json b/test/integration/targets/uri/files/fail29.json
new file mode 100644
index 0000000..8ab0bc4
--- /dev/null
+++ b/test/integration/targets/uri/files/fail29.json
@@ -0,0 +1 @@
+[0e+] \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail3.json b/test/integration/targets/uri/files/fail3.json
new file mode 100644
index 0000000..9de168b
--- /dev/null
+++ b/test/integration/targets/uri/files/fail3.json
@@ -0,0 +1 @@
+["extra comma",] \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail30.json b/test/integration/targets/uri/files/fail30.json
new file mode 100644
index 0000000..1cce602
--- /dev/null
+++ b/test/integration/targets/uri/files/fail30.json
@@ -0,0 +1 @@
+[0e+-1] \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail4.json b/test/integration/targets/uri/files/fail4.json
new file mode 100644
index 0000000..ddf3ce3
--- /dev/null
+++ b/test/integration/targets/uri/files/fail4.json
@@ -0,0 +1 @@
+["double extra comma",,] \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail5.json b/test/integration/targets/uri/files/fail5.json
new file mode 100644
index 0000000..ed91580
--- /dev/null
+++ b/test/integration/targets/uri/files/fail5.json
@@ -0,0 +1 @@
+[ , "<-- missing value"] \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail6.json b/test/integration/targets/uri/files/fail6.json
new file mode 100644
index 0000000..8a96af3
--- /dev/null
+++ b/test/integration/targets/uri/files/fail6.json
@@ -0,0 +1 @@
+["Comma after the close"], \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail7.json b/test/integration/targets/uri/files/fail7.json
new file mode 100644
index 0000000..b28479c
--- /dev/null
+++ b/test/integration/targets/uri/files/fail7.json
@@ -0,0 +1 @@
+["Extra close"]] \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail8.json b/test/integration/targets/uri/files/fail8.json
new file mode 100644
index 0000000..5815574
--- /dev/null
+++ b/test/integration/targets/uri/files/fail8.json
@@ -0,0 +1 @@
+{"Extra comma": true,} \ No newline at end of file
diff --git a/test/integration/targets/uri/files/fail9.json b/test/integration/targets/uri/files/fail9.json
new file mode 100644
index 0000000..5d8c004
--- /dev/null
+++ b/test/integration/targets/uri/files/fail9.json
@@ -0,0 +1 @@
+{"Extra value after close": true} "misplaced quoted value" \ No newline at end of file
diff --git a/test/integration/targets/uri/files/formdata.txt b/test/integration/targets/uri/files/formdata.txt
new file mode 100644
index 0000000..974c0f9
--- /dev/null
+++ b/test/integration/targets/uri/files/formdata.txt
@@ -0,0 +1 @@
+_multipart/form-data_
diff --git a/test/integration/targets/uri/files/pass0.json b/test/integration/targets/uri/files/pass0.json
new file mode 100644
index 0000000..70e2685
--- /dev/null
+++ b/test/integration/targets/uri/files/pass0.json
@@ -0,0 +1,58 @@
+[
+ "JSON Test Pattern pass1",
+ {"object with 1 member":["array with 1 element"]},
+ {},
+ [],
+ -42,
+ true,
+ false,
+ null,
+ {
+ "integer": 1234567890,
+ "real": -9876.543210,
+ "e": 0.123456789e-12,
+ "E": 1.234567890E+34,
+ "": 23456789012E66,
+ "zero": 0,
+ "one": 1,
+ "space": " ",
+ "quote": "\"",
+ "backslash": "\\",
+ "controls": "\b\f\n\r\t",
+ "slash": "/ & \/",
+ "alpha": "abcdefghijklmnopqrstuvwyz",
+ "ALPHA": "ABCDEFGHIJKLMNOPQRSTUVWYZ",
+ "digit": "0123456789",
+ "0123456789": "digit",
+ "special": "`1~!@#$%^&*()_+-={':[,]}|;.</>?",
+ "hex": "\u0123\u4567\u89AB\uCDEF\uabcd\uef4A",
+ "true": true,
+ "false": false,
+ "null": null,
+ "array":[ ],
+ "object":{ },
+ "address": "50 St. James Street",
+ "url": "http://www.JSON.org/",
+ "comment": "// /* <!-- --",
+ "# -- --> */": " ",
+ " s p a c e d " :[1,2 , 3
+
+,
+
+4 , 5 , 6 ,7 ],"compact":[1,2,3,4,5,6,7],
+ "jsontext": "{\"object with 1 member\":[\"array with 1 element\"]}",
+ "quotes": "&#34; \u0022 %22 0x22 034 &#x22;",
+ "\/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?"
+: "A key can be any string"
+ },
+ 0.5 ,98.6
+,
+99.44
+,
+
+1066,
+1e1,
+0.1e1,
+1e-1,
+1e00,2e+00,2e-00
+,"rosebud"] \ No newline at end of file
diff --git a/test/integration/targets/uri/files/pass1.json b/test/integration/targets/uri/files/pass1.json
new file mode 100644
index 0000000..d3c63c7
--- /dev/null
+++ b/test/integration/targets/uri/files/pass1.json
@@ -0,0 +1 @@
+[[[[[[[[[[[[[[[[[[["Not too deep"]]]]]]]]]]]]]]]]]]] \ No newline at end of file
diff --git a/test/integration/targets/uri/files/pass2.json b/test/integration/targets/uri/files/pass2.json
new file mode 100644
index 0000000..4528d51
--- /dev/null
+++ b/test/integration/targets/uri/files/pass2.json
@@ -0,0 +1,6 @@
+{
+ "JSON Test Pattern pass3": {
+ "The outermost value": "must be an object or array.",
+ "In this test": "It is an object."
+ }
+}
diff --git a/test/integration/targets/uri/files/pass3.json b/test/integration/targets/uri/files/pass3.json
new file mode 100644
index 0000000..6216b86
--- /dev/null
+++ b/test/integration/targets/uri/files/pass3.json
@@ -0,0 +1 @@
+"A JSON payload should be an object or array, not a string." \ No newline at end of file
diff --git a/test/integration/targets/uri/files/pass4.json b/test/integration/targets/uri/files/pass4.json
new file mode 100644
index 0000000..edac927
--- /dev/null
+++ b/test/integration/targets/uri/files/pass4.json
@@ -0,0 +1 @@
+[[[[[[[[[[[[[[[[[[[["Too deep"]]]]]]]]]]]]]]]]]]]] \ No newline at end of file
diff --git a/test/integration/targets/uri/files/testserver.py b/test/integration/targets/uri/files/testserver.py
new file mode 100644
index 0000000..24967d4
--- /dev/null
+++ b/test/integration/targets/uri/files/testserver.py
@@ -0,0 +1,23 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import sys
+
+if __name__ == '__main__':
+ if sys.version_info[0] >= 3:
+ import http.server
+ import socketserver
+ PORT = int(sys.argv[1])
+
+ class Handler(http.server.SimpleHTTPRequestHandler):
+ pass
+
+ Handler.extensions_map['.json'] = 'application/json'
+ httpd = socketserver.TCPServer(("", PORT), Handler)
+ httpd.serve_forever()
+ else:
+ import mimetypes
+ mimetypes.init()
+ mimetypes.add_type('application/json', '.json')
+ import SimpleHTTPServer
+ SimpleHTTPServer.test()
diff --git a/test/integration/targets/uri/meta/main.yml b/test/integration/targets/uri/meta/main.yml
new file mode 100644
index 0000000..2c2155a
--- /dev/null
+++ b/test/integration/targets/uri/meta/main.yml
@@ -0,0 +1,4 @@
+dependencies:
+ - prepare_tests
+ - prepare_http_tests
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/uri/tasks/ciphers.yml b/test/integration/targets/uri/tasks/ciphers.yml
new file mode 100644
index 0000000..a646d67
--- /dev/null
+++ b/test/integration/targets/uri/tasks/ciphers.yml
@@ -0,0 +1,32 @@
+- name: test good cipher
+ uri:
+ url: https://{{ httpbin_host }}/get
+ ciphers: ECDHE-RSA-AES128-SHA256
+ register: good_ciphers
+
+- name: test good cipher redirect
+ uri:
+ url: http://{{ httpbin_host }}/redirect-to?status_code=302&url=https://{{ httpbin_host }}/get
+ ciphers: ECDHE-RSA-AES128-SHA256
+ register: good_ciphers_redir
+
+- name: test bad cipher
+ uri:
+ url: https://{{ httpbin_host }}/get
+ ciphers: ECDHE-ECDSA-AES128-SHA
+ ignore_errors: true
+ register: bad_ciphers
+
+- name: test bad cipher redirect
+ uri:
+ url: http://{{ httpbin_host }}/redirect-to?status_code=302&url=https://{{ httpbin_host }}/get
+ ciphers: ECDHE-ECDSA-AES128-SHA
+ ignore_errors: true
+ register: bad_ciphers_redir
+
+- assert:
+ that:
+ - good_ciphers is successful
+ - good_ciphers_redir is successful
+ - bad_ciphers is failed
+ - bad_ciphers_redir is failed
diff --git a/test/integration/targets/uri/tasks/main.yml b/test/integration/targets/uri/tasks/main.yml
new file mode 100644
index 0000000..d821f28
--- /dev/null
+++ b/test/integration/targets/uri/tasks/main.yml
@@ -0,0 +1,779 @@
+# test code for the uri module
+# (c) 2014, Leonid Evdokimov <leon@darkk.net.ru>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <https://www.gnu.org/licenses/>.
+
+- name: set role facts
+ set_fact:
+ http_port: 15260
+ files_dir: '{{ remote_tmp_dir|expanduser }}/files'
+ checkout_dir: '{{ remote_tmp_dir }}/git'
+
+- name: create a directory to serve files from
+ file:
+ dest: "{{ files_dir }}"
+ state: directory
+
+- copy:
+ src: "{{ item }}"
+ dest: "{{files_dir}}/{{ item }}"
+ with_sequence: start=0 end=4 format=pass%d.json
+
+- copy:
+ src: "{{ item }}"
+ dest: "{{files_dir}}/{{ item }}"
+ with_sequence: start=0 end=30 format=fail%d.json
+
+- copy:
+ src: "testserver.py"
+ dest: "{{ remote_tmp_dir }}/testserver.py"
+
+- name: start SimpleHTTPServer
+ shell: cd {{ files_dir }} && {{ ansible_python.executable }} {{ remote_tmp_dir}}/testserver.py {{ http_port }}
+ async: 180 # this test is slower on remotes like FreeBSD, and running split slows it down further
+ poll: 0
+
+- wait_for: port={{ http_port }}
+
+
+- name: checksum pass_json
+ stat: path={{ files_dir }}/{{ item }}.json get_checksum=yes
+ register: pass_checksum
+ with_sequence: start=0 end=4 format=pass%d
+
+- name: fetch pass_json
+ uri: return_content=yes url=http://localhost:{{ http_port }}/{{ item }}.json
+ register: fetch_pass_json
+ with_sequence: start=0 end=4 format=pass%d
+
+- name: check pass_json
+ assert:
+ that:
+ - '"json" in item.1'
+ - item.0.stat.checksum == item.1.content | checksum
+ with_together:
+ - "{{pass_checksum.results}}"
+ - "{{fetch_pass_json.results}}"
+
+
+- name: checksum fail_json
+ stat: path={{ files_dir }}/{{ item }}.json get_checksum=yes
+ register: fail_checksum
+ with_sequence: start=0 end=30 format=fail%d
+
+- name: fetch fail_json
+ uri: return_content=yes url=http://localhost:{{ http_port }}/{{ item }}.json
+ register: fail
+ with_sequence: start=0 end=30 format=fail%d
+
+- name: check fail_json
+ assert:
+ that:
+ - item.0.stat.checksum == item.1.content | checksum
+ - '"json" not in item.1'
+ with_together:
+ - "{{fail_checksum.results}}"
+ - "{{fail.results}}"
+
+- name: test https fetch to a site with mismatched hostname and certificate
+ uri:
+ url: "https://{{ badssl_host }}/"
+ dest: "{{ remote_tmp_dir }}/shouldnotexist.html"
+ ignore_errors: True
+ register: result
+
+- stat:
+ path: "{{ remote_tmp_dir }}/shouldnotexist.html"
+ register: stat_result
+
+- name: Assert that the file was not downloaded
+ assert:
+ that:
+ - result.failed == true
+ - "'Failed to validate the SSL certificate' in result.msg or 'Hostname mismatch' in result.msg or (result.msg is match('hostname .* doesn.t match .*'))"
+ - stat_result.stat.exists == false
+ - result.status is defined
+ - result.status == -1
+ - result.url == 'https://' ~ badssl_host ~ '/'
+
+- name: Clean up any cruft from the results directory
+ file:
+ name: "{{ remote_tmp_dir }}/kreitz.html"
+ state: absent
+
+- name: test https fetch to a site with mismatched hostname and certificate and validate_certs=no
+ uri:
+ url: "https://{{ badssl_host }}/"
+ dest: "{{ remote_tmp_dir }}/kreitz.html"
+ validate_certs: no
+ register: result
+
+- stat:
+ path: "{{ remote_tmp_dir }}/kreitz.html"
+ register: stat_result
+
+- name: Assert that the file was downloaded
+ assert:
+ that:
+ - "stat_result.stat.exists == true"
+ - "result.changed == true"
+
+- name: "get ca certificate {{ self_signed_host }}"
+ get_url:
+ url: "http://{{ httpbin_host }}/ca2cert.pem"
+ dest: "{{ remote_tmp_dir }}/ca2cert.pem"
+
+- name: test https fetch to a site with self signed certificate using ca_path
+ uri:
+ url: "https://{{ self_signed_host }}:444/"
+ dest: "{{ remote_tmp_dir }}/self-signed_using_ca_path.html"
+ ca_path: "{{ remote_tmp_dir }}/ca2cert.pem"
+ validate_certs: yes
+ register: result
+
+- stat:
+ path: "{{ remote_tmp_dir }}/self-signed_using_ca_path.html"
+ register: stat_result
+
+- name: Assert that the file was downloaded
+ assert:
+ that:
+ - "stat_result.stat.exists == true"
+ - "result.changed == true"
+
+- name: test https fetch to a site with self signed certificate without using ca_path
+ uri:
+ url: "https://{{ self_signed_host }}:444/"
+ dest: "{{ remote_tmp_dir }}/self-signed-without_using_ca_path.html"
+ validate_certs: yes
+ register: result
+ ignore_errors: true
+
+- stat:
+ path: "{{ remote_tmp_dir }}/self-signed-without_using_ca_path.html"
+ register: stat_result
+
+- name: Assure that https access to a host with self-signed certificate without providing ca_path fails
+ assert:
+ that:
+ - "stat_result.stat.exists == false"
+ - result is failed
+ - "'certificate verify failed' in result.msg"
+
+- name: Locate ca-bundle
+ stat:
+ path: '{{ item }}'
+ loop:
+ - /etc/ssl/certs/ca-bundle.crt
+ - /etc/ssl/certs/ca-certificates.crt
+ - /var/lib/ca-certificates/ca-bundle.pem
+ - /usr/local/share/certs/ca-root-nss.crt
+ - '{{ cafile_path.stdout_lines|default(["/_i_dont_exist_ca.pem"])|first }}'
+ - /etc/ssl/cert.pem
+ register: ca_bundle_candidates
+
+- name: Test that ca_path can be a full bundle
+ uri:
+ url: "https://{{ httpbin_host }}/get"
+ ca_path: '{{ ca_bundle }}'
+ vars:
+ ca_bundle: '{{ ca_bundle_candidates.results|selectattr("stat.exists")|map(attribute="item")|first }}'
+
+- name: test redirect without follow_redirects
+ uri:
+ url: 'https://{{ httpbin_host }}/redirect/2'
+ follow_redirects: 'none'
+ status_code: 302
+ register: result
+
+- name: Assert location header
+ assert:
+ that:
+ - 'result.location|default("") == "https://{{ httpbin_host }}/relative-redirect/1"'
+
+- name: Check SSL with redirect
+ uri:
+ url: 'https://{{ httpbin_host }}/redirect/2'
+ register: result
+
+- name: Assert SSL with redirect
+ assert:
+ that:
+ - 'result.url|default("") == "https://{{ httpbin_host }}/get"'
+
+- name: redirect to bad SSL site
+ uri:
+ url: 'http://{{ badssl_host }}'
+ register: result
+ ignore_errors: true
+
+- name: Ensure bad SSL site reidrect fails
+ assert:
+ that:
+ - result is failed
+ - 'badssl_host in result.msg'
+
+- name: test basic auth
+ uri:
+ url: 'https://{{ httpbin_host }}/basic-auth/user/passwd'
+ user: user
+ password: passwd
+
+- name: test basic forced auth
+ uri:
+ url: 'https://{{ httpbin_host }}/hidden-basic-auth/user/passwd'
+ force_basic_auth: true
+ user: user
+ password: passwd
+
+- name: test digest auth
+ uri:
+ url: 'https://{{ httpbin_host }}/digest-auth/auth/user/passwd'
+ user: user
+ password: passwd
+ headers:
+ Cookie: "fake=fake_value"
+
+- name: test digest auth failure
+ uri:
+ url: 'https://{{ httpbin_host }}/digest-auth/auth/user/passwd'
+ user: user
+ password: wrong
+ headers:
+ Cookie: "fake=fake_value"
+ register: result
+ failed_when: result.status != 401
+
+- name: test unredirected_headers
+ uri:
+ url: 'https://{{ httpbin_host }}/redirect-to?status_code=301&url=/basic-auth/user/passwd'
+ user: user
+ password: passwd
+ force_basic_auth: true
+ unredirected_headers:
+ - authorization
+ ignore_errors: true
+ register: unredirected_headers
+
+- name: test omitting unredirected headers
+ uri:
+ url: 'https://{{ httpbin_host }}/redirect-to?status_code=301&url=/basic-auth/user/passwd'
+ user: user
+ password: passwd
+ force_basic_auth: true
+ register: redirected_headers
+
+- name: ensure unredirected_headers caused auth to fail
+ assert:
+ that:
+ - unredirected_headers is failed
+ - unredirected_headers.status == 401
+ - redirected_headers is successful
+ - redirected_headers.status == 200
+
+- name: test PUT
+ uri:
+ url: 'https://{{ httpbin_host }}/put'
+ method: PUT
+ body: 'foo=bar'
+
+- name: test OPTIONS
+ uri:
+ url: 'https://{{ httpbin_host }}/'
+ method: OPTIONS
+ register: result
+
+- name: Assert we got an allow header
+ assert:
+ that:
+ - 'result.allow.split(", ")|sort == ["GET", "HEAD", "OPTIONS"]'
+
+- name: Testing support of https_proxy (with failure expected)
+ environment:
+ https_proxy: 'https://localhost:3456'
+ uri:
+ url: 'https://httpbin.org/get'
+ register: result
+ ignore_errors: true
+
+- assert:
+ that:
+ - result is failed
+ - result.status == -1
+
+- name: Testing use_proxy=no is honored
+ environment:
+ https_proxy: 'https://localhost:3456'
+ uri:
+ url: 'https://httpbin.org/get'
+ use_proxy: no
+
+# Ubuntu12.04 doesn't have python-urllib3, this makes handling required dependencies a pain across all variations
+# We'll use this to just skip 12.04 on those tests. We should be sufficiently covered with other OSes and versions
+- name: Set fact if running on Ubuntu 12.04
+ set_fact:
+ is_ubuntu_precise: "{{ ansible_distribution == 'Ubuntu' and ansible_distribution_release == 'precise' }}"
+
+- name: Test that SNI succeeds on python versions that have SNI
+ uri:
+ url: 'https://{{ sni_host }}/'
+ return_content: true
+ when: ansible_python.has_sslcontext
+ register: result
+
+- name: Assert SNI verification succeeds on new python
+ assert:
+ that:
+ - result is successful
+ - 'sni_host in result.content'
+ when: ansible_python.has_sslcontext
+
+- name: Verify SNI verification fails on old python without urllib3 contrib
+ uri:
+ url: 'https://{{ sni_host }}'
+ ignore_errors: true
+ when: not ansible_python.has_sslcontext
+ register: result
+
+- name: Assert SNI verification fails on old python
+ assert:
+ that:
+ - result is failed
+ when: result is not skipped
+
+- name: check if urllib3 is installed as an OS package
+ package:
+ name: "{{ uri_os_packages[ansible_os_family].urllib3 }}"
+ check_mode: yes
+ when: not ansible_python.has_sslcontext and not is_ubuntu_precise|bool and uri_os_packages[ansible_os_family].urllib3|default
+ register: urllib3
+
+- name: uninstall conflicting urllib3 pip package
+ pip:
+ name: urllib3
+ state: absent
+ when: not ansible_python.has_sslcontext and not is_ubuntu_precise|bool and uri_os_packages[ansible_os_family].urllib3|default and urllib3.changed
+
+- name: install OS packages that are needed for SNI on old python
+ package:
+ name: "{{ item }}"
+ with_items: "{{ uri_os_packages[ansible_os_family].step1 | default([]) }}"
+ when: not ansible_python.has_sslcontext and not is_ubuntu_precise|bool
+
+- name: install python modules for Older Python SNI verification
+ pip:
+ name: "{{ item }}"
+ with_items:
+ - ndg-httpsclient
+ when: not ansible_python.has_sslcontext and not is_ubuntu_precise|bool
+
+- name: Verify SNI verification succeeds on old python with urllib3 contrib
+ uri:
+ url: 'https://{{ sni_host }}'
+ return_content: true
+ when: not ansible_python.has_sslcontext and not is_ubuntu_precise|bool
+ register: result
+
+- name: Assert SNI verification succeeds on old python
+ assert:
+ that:
+ - result is successful
+ - 'sni_host in result.content'
+ when: not ansible_python.has_sslcontext and not is_ubuntu_precise|bool
+
+- name: Uninstall ndg-httpsclient
+ pip:
+ name: "{{ item }}"
+ state: absent
+ with_items:
+ - ndg-httpsclient
+ when: not ansible_python.has_sslcontext and not is_ubuntu_precise|bool
+
+- name: uninstall OS packages that are needed for SNI on old python
+ package:
+ name: "{{ item }}"
+ state: absent
+ with_items: "{{ uri_os_packages[ansible_os_family].step1 | default([]) }}"
+ when: not ansible_python.has_sslcontext and not is_ubuntu_precise|bool
+
+- name: install OS packages that are needed for building cryptography
+ package:
+ name: "{{ item }}"
+ with_items: "{{ uri_os_packages[ansible_os_family].step2 | default([]) }}"
+ when: not ansible_python.has_sslcontext and not is_ubuntu_precise|bool
+
+- name: create constraints path
+ set_fact:
+ remote_constraints: "{{ remote_tmp_dir }}/constraints.txt"
+ when: not ansible_python.has_sslcontext and not is_ubuntu_precise|bool
+
+- name: create constraints file
+ copy:
+ content: |
+ cryptography == 2.1.4
+ idna == 2.5
+ pyopenssl == 17.5.0
+ six == 1.13.0
+ urllib3 == 1.23
+ dest: "{{ remote_constraints }}"
+ when: not ansible_python.has_sslcontext and not is_ubuntu_precise|bool
+
+- name: install urllib3 and pyopenssl via pip
+ pip:
+ name: "{{ item }}"
+ extra_args: "-c {{ remote_constraints }}"
+ with_items:
+ - urllib3
+ - PyOpenSSL
+ when: not ansible_python.has_sslcontext and not is_ubuntu_precise|bool
+
+- name: Verify SNI verification succeeds on old python with pip urllib3 contrib
+ uri:
+ url: 'https://{{ sni_host }}'
+ return_content: true
+ when: not ansible_python.has_sslcontext and not is_ubuntu_precise|bool
+ register: result
+
+- name: Assert SNI verification succeeds on old python with pip urllib3 contrib
+ assert:
+ that:
+ - result is successful
+ - 'sni_host in result.content'
+ when: not ansible_python.has_sslcontext and not is_ubuntu_precise|bool
+
+- name: Uninstall urllib3 and PyOpenSSL
+ pip:
+ name: "{{ item }}"
+ state: absent
+ with_items:
+ - urllib3
+ - PyOpenSSL
+ when: not ansible_python.has_sslcontext and not is_ubuntu_precise|bool
+
+- name: validate the status_codes are correct
+ uri:
+ url: "https://{{ httpbin_host }}/status/202"
+ status_code: 202
+ method: POST
+ body: foo
+
+- name: Validate body_format json does not override content-type in 2.3 or newer
+ uri:
+ url: "https://{{ httpbin_host }}/post"
+ method: POST
+ body:
+ foo: bar
+ body_format: json
+ headers:
+ 'Content-Type': 'text/json'
+ return_content: true
+ register: result
+ failed_when: result.json.headers['Content-Type'] != 'text/json'
+
+- name: Validate body_format form-urlencoded using dicts works
+ uri:
+ url: https://{{ httpbin_host }}/post
+ method: POST
+ body:
+ user: foo
+ password: bar!#@ |&82$M
+ submit: Sign in
+ body_format: form-urlencoded
+ return_content: yes
+ register: result
+
+- name: Assert form-urlencoded dict input
+ assert:
+ that:
+ - result is successful
+ - result.json.headers['Content-Type'] == 'application/x-www-form-urlencoded'
+ - result.json.form.password == 'bar!#@ |&82$M'
+
+- name: Validate body_format form-urlencoded using lists works
+ uri:
+ url: https://{{ httpbin_host }}/post
+ method: POST
+ body:
+ - [ user, foo ]
+ - [ password, bar!#@ |&82$M ]
+ - [ submit, Sign in ]
+ body_format: form-urlencoded
+ return_content: yes
+ register: result
+
+- name: Assert form-urlencoded list input
+ assert:
+ that:
+ - result is successful
+ - result.json.headers['Content-Type'] == 'application/x-www-form-urlencoded'
+ - result.json.form.password == 'bar!#@ |&82$M'
+
+- name: Validate body_format form-urlencoded of invalid input fails
+ uri:
+ url: https://{{ httpbin_host }}/post
+ method: POST
+ body:
+ - foo
+ - bar: baz
+ body_format: form-urlencoded
+ return_content: yes
+ register: result
+ ignore_errors: yes
+
+- name: Assert invalid input fails
+ assert:
+ that:
+ - result is failure
+ - "'failed to parse body as form_urlencoded: too many values to unpack' in result.msg"
+
+- name: multipart/form-data
+ uri:
+ url: https://{{ httpbin_host }}/post
+ method: POST
+ body_format: form-multipart
+ body:
+ file1:
+ filename: formdata.txt
+ file2:
+ content: text based file content
+ filename: fake.txt
+ mime_type: text/plain
+ text_form_field1: value1
+ text_form_field2:
+ content: value2
+ mime_type: text/plain
+ register: multipart
+
+- name: Assert multipart/form-data
+ assert:
+ that:
+ - multipart.json.files.file1 == '_multipart/form-data_\n'
+ - multipart.json.files.file2 == 'text based file content'
+ - multipart.json.form.text_form_field1 == 'value1'
+ - multipart.json.form.text_form_field2 == 'value2'
+
+# https://github.com/ansible/ansible/issues/74276 - verifies we don't have a traceback
+- name: multipart/form-data with invalid value
+ uri:
+ url: https://{{ httpbin_host }}/post
+ method: POST
+ body_format: form-multipart
+ body:
+ integer_value: 1
+ register: multipart_invalid
+ failed_when: 'multipart_invalid.msg != "failed to parse body as form-multipart: value must be a string, or mapping, cannot be type int"'
+
+- name: Validate invalid method
+ uri:
+ url: https://{{ httpbin_host }}/anything
+ method: UNKNOWN
+ register: result
+ ignore_errors: yes
+
+- name: Assert invalid method fails
+ assert:
+ that:
+ - result is failure
+ - result.status == 405
+ - "'METHOD NOT ALLOWED' in result.msg"
+
+- name: Test client cert auth, no certs
+ uri:
+ url: "https://ansible.http.tests/ssl_client_verify"
+ status_code: 200
+ return_content: true
+ register: result
+ failed_when: result.content != "ansible.http.tests:NONE"
+ when: has_httptester
+
+- name: Test client cert auth, with certs
+ uri:
+ url: "https://ansible.http.tests/ssl_client_verify"
+ client_cert: "{{ remote_tmp_dir }}/client.pem"
+ client_key: "{{ remote_tmp_dir }}/client.key"
+ return_content: true
+ register: result
+ failed_when: result.content != "ansible.http.tests:SUCCESS"
+ when: has_httptester
+
+- name: Test client cert auth, with no validation
+ uri:
+ url: "https://fail.ansible.http.tests/ssl_client_verify"
+ client_cert: "{{ remote_tmp_dir }}/client.pem"
+ client_key: "{{ remote_tmp_dir }}/client.key"
+ return_content: true
+ validate_certs: no
+ register: result
+ failed_when: result.content != "ansible.http.tests:SUCCESS"
+ when: has_httptester
+
+- name: Test client cert auth, with validation and ssl mismatch
+ uri:
+ url: "https://fail.ansible.http.tests/ssl_client_verify"
+ client_cert: "{{ remote_tmp_dir }}/client.pem"
+ client_key: "{{ remote_tmp_dir }}/client.key"
+ return_content: true
+ validate_certs: yes
+ register: result
+ failed_when: result is not failed
+ when: has_httptester
+
+- uri:
+ url: https://{{ httpbin_host }}/response-headers?Set-Cookie=Foo%3Dbar&Set-Cookie=Baz%3Dqux
+ register: result
+
+- assert:
+ that:
+ - result['set_cookie'] == 'Foo=bar, Baz=qux'
+ # Python sorts cookies in order of most specific (ie. longest) path first
+ # items with the same path are reversed from response order
+ - result['cookies_string'] == 'Baz=qux; Foo=bar'
+
+- name: Write out netrc template
+ template:
+ src: netrc.j2
+ dest: "{{ remote_tmp_dir }}/netrc"
+
+- name: Test netrc with port
+ uri:
+ url: "https://{{ httpbin_host }}:443/basic-auth/user/passwd"
+ environment:
+ NETRC: "{{ remote_tmp_dir }}/netrc"
+
+- name: Test JSON POST with src
+ uri:
+ url: "https://{{ httpbin_host}}/post"
+ src: pass0.json
+ method: POST
+ return_content: true
+ body_format: json
+ register: result
+
+- name: Validate POST with src works
+ assert:
+ that:
+ - result.json.json[0] == 'JSON Test Pattern pass1'
+
+- name: Copy file pass0.json to remote
+ copy:
+ src: "{{ role_path }}/files/pass0.json"
+ dest: "{{ remote_tmp_dir }}/pass0.json"
+
+- name: Test JSON POST with src and remote_src=True
+ uri:
+ url: "https://{{ httpbin_host}}/post"
+ src: "{{ remote_tmp_dir }}/pass0.json"
+ remote_src: true
+ method: POST
+ return_content: true
+ body_format: json
+ register: result
+
+- name: Validate POST with src and remote_src=True works
+ assert:
+ that:
+ - result.json.json[0] == 'JSON Test Pattern pass1'
+
+- name: Make request that includes password in JSON keys
+ uri:
+ url: "https://{{ httpbin_host}}/get?key-password=value-password"
+ user: admin
+ password: password
+ register: sanitize_keys
+
+- name: assert that keys were sanitized
+ assert:
+ that:
+ - sanitize_keys.json.args['key-********'] == 'value-********'
+
+- name: Test gzip encoding
+ uri:
+ url: "https://{{ httpbin_host }}/gzip"
+ register: result
+
+- name: Validate gzip decoding
+ assert:
+ that:
+ - result.json.gzipped
+
+- name: test gzip encoding no auto decompress
+ uri:
+ url: "https://{{ httpbin_host }}/gzip"
+ decompress: false
+ register: result
+
+- name: Assert gzip wasn't decompressed
+ assert:
+ that:
+ - result.json is undefined
+
+- name: Create a testing file
+ copy:
+ content: "content"
+ dest: "{{ remote_tmp_dir }}/output"
+
+- name: Download a file from non existing location
+ uri:
+ url: http://does/not/exist
+ dest: "{{ remote_tmp_dir }}/output"
+ ignore_errors: yes
+
+- name: Save testing file's output
+ command: "cat {{ remote_tmp_dir }}/output"
+ register: file_out
+
+- name: Test the testing file was not overwritten
+ assert:
+ that:
+ - "'content' in file_out.stdout"
+
+- name: Clean up
+ file:
+ dest: "{{ remote_tmp_dir }}/output"
+ state: absent
+
+- name: Test follow_redirects=none
+ import_tasks: redirect-none.yml
+
+- name: Test follow_redirects=safe
+ import_tasks: redirect-safe.yml
+
+- name: Test follow_redirects=urllib2
+ import_tasks: redirect-urllib2.yml
+
+- name: Test follow_redirects=all
+ import_tasks: redirect-all.yml
+
+- name: Check unexpected failures
+ import_tasks: unexpected-failures.yml
+
+- name: Check return-content
+ import_tasks: return-content.yml
+
+- name: Test use_gssapi=True
+ include_tasks:
+ file: use_gssapi.yml
+ apply:
+ environment:
+ KRB5_CONFIG: '{{ krb5_config }}'
+ KRB5CCNAME: FILE:{{ remote_tmp_dir }}/krb5.cc
+ when: krb5_config is defined
+
+- name: Test ciphers
+ import_tasks: ciphers.yml
+
+- name: Test use_netrc.yml
+ import_tasks: use_netrc.yml
diff --git a/test/integration/targets/uri/tasks/redirect-all.yml b/test/integration/targets/uri/tasks/redirect-all.yml
new file mode 100644
index 0000000..d5b47a1
--- /dev/null
+++ b/test/integration/targets/uri/tasks/redirect-all.yml
@@ -0,0 +1,272 @@
+- name: Test HTTP 301 using HEAD
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=301&url=https://{{ httpbin_host }}/anything
+ follow_redirects: all
+ return_content: yes
+ method: HEAD
+ register: http_301_head
+
+- assert:
+ that:
+ - http_301_head is successful
+ - http_301_head.json is not defined
+ - http_301_head.redirected == true
+ - http_301_head.status == 200
+ - http_301_head.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 301 using GET
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=301&url=https://{{ httpbin_host }}/anything
+ follow_redirects: all
+ return_content: yes
+ method: GET
+ register: http_301_get
+
+- assert:
+ that:
+ - http_301_get is successful
+ - http_301_get.json.data == ''
+ - http_301_get.json.method == 'GET'
+ - http_301_get.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_301_get.redirected == true
+ - http_301_get.status == 200
+ - http_301_get.url == 'https://{{ httpbin_host }}/anything'
+
+# NOTE: The HTTP POST turns into an HTTP GET
+- name: Test HTTP 301 using POST
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=301&url=https://{{ httpbin_host }}/anything
+ follow_redirects: all
+ return_content: yes
+ method: POST
+ body: '{ "foo": "bar" }'
+ body_format: json
+ register: http_301_post
+
+- assert:
+ that:
+ - http_301_post is successful
+ - http_301_post.json.data == ''
+ - http_301_post.json.method == 'GET'
+ - http_301_post.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_301_post.redirected == true
+ - http_301_post.status == 200
+ - http_301_post.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 302 using HEAD
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=302&url=https://{{ httpbin_host }}/anything
+ follow_redirects: all
+ return_content: yes
+ method: HEAD
+ register: http_302_head
+
+- assert:
+ that:
+ - http_302_head is successful
+ - http_302_head.json is not defined
+ - http_302_head.redirected == true
+ - http_302_head.status == 200
+ - http_302_head.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 302 using GET
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=302&url=https://{{ httpbin_host }}/anything
+ follow_redirects: all
+ return_content: yes
+ method: GET
+ register: http_302_get
+
+- assert:
+ that:
+ - http_302_get is successful
+ - http_302_get.json.data == ''
+ - http_302_get.json.method == 'GET'
+ - http_302_get.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_302_get.redirected == true
+ - http_302_get.status == 200
+ - http_302_get.url == 'https://{{ httpbin_host }}/anything'
+
+# NOTE: The HTTP POST turns into an HTTP GET
+- name: Test HTTP 302 using POST
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=302&url=https://{{ httpbin_host }}/anything
+ follow_redirects: all
+ return_content: yes
+ method: POST
+ body: '{ "foo": "bar" }'
+ body_format: json
+ register: http_302_post
+
+- assert:
+ that:
+ - http_302_post is successful
+ - http_302_post.json.data == ''
+ - http_302_post.json.method == 'GET'
+ - http_302_post.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_302_post.redirected == true
+ - http_302_post.status == 200
+ - http_302_post.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 303 using HEAD
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=303&url=https://{{ httpbin_host }}/anything
+ follow_redirects: all
+ return_content: yes
+ method: HEAD
+ register: http_303_head
+
+- assert:
+ that:
+ - http_303_head is successful
+ - http_303_head.json is not defined
+ - http_303_head.redirected == true
+ - http_303_head.status == 200
+ - http_303_head.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 303 using GET
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=303&url=https://{{ httpbin_host }}/anything
+ follow_redirects: all
+ return_content: yes
+ method: GET
+ register: http_303_get
+
+- assert:
+ that:
+ - http_303_get is successful
+ - http_303_get.json.data == ''
+ - http_303_get.json.method == 'GET'
+ - http_303_get.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_303_get.redirected == true
+ - http_303_get.status == 200
+ - http_303_get.url == 'https://{{ httpbin_host }}/anything'
+
+# NOTE: The HTTP POST turns into an HTTP GET
+- name: Test HTTP 303 using POST
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=303&url=https://{{ httpbin_host }}/anything
+ follow_redirects: all
+ return_content: yes
+ method: POST
+ body: '{ "foo": "bar" }'
+ body_format: json
+ register: http_303_post
+
+- assert:
+ that:
+ - http_303_post is successful
+ - http_303_post.json.data == ''
+ - http_303_post.json.method == 'GET'
+ - http_303_post.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_303_post.redirected == true
+ - http_303_post.status == 200
+ - http_303_post.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 307 using HEAD
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=307&url=https://{{ httpbin_host }}/anything
+ follow_redirects: all
+ return_content: yes
+ method: HEAD
+ register: http_307_head
+
+- assert:
+ that:
+ - http_307_head is successful
+ - http_307_head.json is not defined
+ - http_307_head.redirected == true
+ - http_307_head.status == 200
+ - http_307_head.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 307 using GET
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=307&url=https://{{ httpbin_host }}/anything
+ follow_redirects: all
+ return_content: yes
+ method: GET
+ register: http_307_get
+
+- assert:
+ that:
+ - http_307_get is successful
+ - http_307_get.json.data == ''
+ - http_307_get.json.method == 'GET'
+ - http_307_get.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_307_get.redirected == true
+ - http_307_get.status == 200
+ - http_307_get.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 307 using POST
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=307&url=https://{{ httpbin_host }}/anything
+ follow_redirects: all
+ return_content: yes
+ method: POST
+ body: '{ "foo": "bar" }'
+ body_format: json
+ register: http_307_post
+
+- assert:
+ that:
+ - http_307_post is successful
+ - http_307_post.json.json.foo == 'bar'
+ - http_307_post.json.method == 'POST'
+ - http_307_post.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_307_post.redirected == true
+ - http_307_post.status == 200
+ - http_307_post.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 308 using HEAD
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything
+ follow_redirects: all
+ return_content: yes
+ method: HEAD
+ register: http_308_head
+
+- assert:
+ that:
+ - http_308_head is successful
+ - http_308_head.json is undefined
+ - http_308_head.redirected == true
+ - http_308_head.status == 200
+ - http_308_head.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 308 using GET
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything
+ follow_redirects: all
+ return_content: yes
+ method: GET
+ register: http_308_get
+
+- assert:
+ that:
+ - http_308_get is successful
+ - http_308_get.json.data == ''
+ - http_308_get.json.method == 'GET'
+ - http_308_get.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_308_get.redirected == true
+ - http_308_get.status == 200
+ - http_308_get.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 308 using POST
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything
+ follow_redirects: all
+ return_content: yes
+ method: POST
+ body: '{ "foo": "bar" }'
+ body_format: json
+ register: http_308_post
+
+- assert:
+ that:
+ - http_308_post is successful
+ - http_308_post.json.json.foo == 'bar'
+ - http_308_post.json.method == 'POST'
+ - http_308_post.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_308_post.redirected == true
+ - http_308_post.status == 200
+ - http_308_post.url == 'https://{{ httpbin_host }}/anything'
diff --git a/test/integration/targets/uri/tasks/redirect-none.yml b/test/integration/targets/uri/tasks/redirect-none.yml
new file mode 100644
index 0000000..0d1b2b3
--- /dev/null
+++ b/test/integration/targets/uri/tasks/redirect-none.yml
@@ -0,0 +1,296 @@
+- name: Test HTTP 301 using HEAD
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=301&url=https://{{ httpbin_host }}/anything
+ follow_redirects: none
+ return_content: yes
+ method: HEAD
+ ignore_errors: yes
+ register: http_301_head
+
+- assert:
+ that:
+ - http_301_head is failure
+ - http_301_head.json is not defined
+ - http_301_head.location == 'https://{{ httpbin_host }}/anything'
+ - "http_301_head.msg == 'Status code was 301 and not [200]: HTTP Error 301: MOVED PERMANENTLY'"
+ - http_301_head.redirected == false
+ - http_301_head.status == 301
+ - http_301_head.url == 'https://{{ httpbin_host }}/redirect-to?status_code=301&url=https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 301 using GET
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=301&url=https://{{ httpbin_host }}/anything
+ follow_redirects: none
+ return_content: yes
+ method: GET
+ ignore_errors: yes
+ register: http_301_get
+
+- assert:
+ that:
+ - http_301_get is failure
+ - http_301_get.json is not defined
+ - http_301_get.location == 'https://{{ httpbin_host }}/anything'
+ - "http_301_get.msg == 'Status code was 301 and not [200]: HTTP Error 301: MOVED PERMANENTLY'"
+ - http_301_get.redirected == false
+ - http_301_get.status == 301
+ - http_301_get.url == 'https://{{ httpbin_host }}/redirect-to?status_code=301&url=https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 301 using POST
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=301&url=https://{{ httpbin_host }}/anything
+ follow_redirects: none
+ return_content: yes
+ method: POST
+ body: '{ "foo": "bar" }'
+ body_format: json
+ ignore_errors: yes
+ register: http_301_post
+
+- assert:
+ that:
+ - http_301_post is failure
+ - http_301_post.json is not defined
+ - http_301_post.location == 'https://{{ httpbin_host }}/anything'
+ - "http_301_post.msg == 'Status code was 301 and not [200]: HTTP Error 301: MOVED PERMANENTLY'"
+ - http_301_post.redirected == false
+ - http_301_post.status == 301
+ - http_301_post.url == 'https://{{ httpbin_host }}/redirect-to?status_code=301&url=https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 302 using HEAD
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=302&url=https://{{ httpbin_host }}/anything
+ follow_redirects: none
+ return_content: yes
+ method: HEAD
+ ignore_errors: yes
+ register: http_302_head
+
+- assert:
+ that:
+ - http_302_head is failure
+ - http_302_head.json is not defined
+ - http_302_head.location == 'https://{{ httpbin_host }}/anything'
+ - "http_302_head.msg == 'Status code was 302 and not [200]: HTTP Error 302: FOUND'"
+ - http_302_head.redirected == false
+ - http_302_head.status == 302
+ - http_302_head.url == 'https://{{ httpbin_host }}/redirect-to?status_code=302&url=https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 302 using GET
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=302&url=https://{{ httpbin_host }}/anything
+ follow_redirects: none
+ return_content: yes
+ method: GET
+ ignore_errors: yes
+ register: http_302_get
+
+- assert:
+ that:
+ - http_302_get is failure
+ - http_302_get.json is not defined
+ - http_302_get.location == 'https://{{ httpbin_host }}/anything'
+ - "http_302_get.msg == 'Status code was 302 and not [200]: HTTP Error 302: FOUND'"
+ - http_302_get.redirected == false
+ - http_302_get.status == 302
+ - http_302_get.url == 'https://{{ httpbin_host }}/redirect-to?status_code=302&url=https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 302 using POST
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=302&url=https://{{ httpbin_host }}/anything
+ follow_redirects: none
+ return_content: yes
+ method: POST
+ body: '{ "foo": "bar" }'
+ body_format: json
+ ignore_errors: yes
+ register: http_302_post
+
+- assert:
+ that:
+ - http_302_post is failure
+ - http_302_post.json is not defined
+ - http_302_post.location == 'https://{{ httpbin_host }}/anything'
+ - "http_302_post.msg == 'Status code was 302 and not [200]: HTTP Error 302: FOUND'"
+ - http_302_post.redirected == false
+ - http_302_post.status == 302
+ - http_302_post.url == 'https://{{ httpbin_host }}/redirect-to?status_code=302&url=https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 303 using HEAD
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=303&url=https://{{ httpbin_host }}/anything
+ follow_redirects: none
+ return_content: yes
+ method: HEAD
+ ignore_errors: yes
+ register: http_303_head
+
+- assert:
+ that:
+ - http_303_head is failure
+ - http_303_head.json is not defined
+ - http_303_head.location == 'https://{{ httpbin_host }}/anything'
+ - "http_303_head.msg == 'Status code was 303 and not [200]: HTTP Error 303: SEE OTHER'"
+ - http_303_head.redirected == false
+ - http_303_head.status == 303
+ - http_303_head.url == 'https://{{ httpbin_host }}/redirect-to?status_code=303&url=https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 303 using GET
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=303&url=https://{{ httpbin_host }}/anything
+ follow_redirects: none
+ return_content: yes
+ method: GET
+ ignore_errors: yes
+ register: http_303_get
+
+- assert:
+ that:
+ - http_303_get is failure
+ - http_303_get.json is not defined
+ - http_303_get.location == 'https://{{ httpbin_host }}/anything'
+ - "http_303_get.msg == 'Status code was 303 and not [200]: HTTP Error 303: SEE OTHER'"
+ - http_303_get.redirected == false
+ - http_303_get.status == 303
+ - http_303_get.url == 'https://{{ httpbin_host }}/redirect-to?status_code=303&url=https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 303 using POST
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=303&url=https://{{ httpbin_host }}/anything
+ follow_redirects: none
+ return_content: yes
+ method: POST
+ body: '{ "foo": "bar" }'
+ body_format: json
+ ignore_errors: yes
+ register: http_303_post
+
+- assert:
+ that:
+ - http_303_post is failure
+ - http_303_post.json is not defined
+ - http_303_post.location == 'https://{{ httpbin_host }}/anything'
+ - "http_303_post.msg == 'Status code was 303 and not [200]: HTTP Error 303: SEE OTHER'"
+ - http_303_post.redirected == false
+ - http_303_post.status == 303
+ - http_303_post.url == 'https://{{ httpbin_host }}/redirect-to?status_code=303&url=https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 307 using HEAD
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=307&url=https://{{ httpbin_host }}/anything
+ follow_redirects: none
+ return_content: yes
+ method: HEAD
+ ignore_errors: yes
+ register: http_307_head
+
+- assert:
+ that:
+ - http_307_head is failure
+ - http_307_head.json is not defined
+ - http_307_head.location == 'https://{{ httpbin_host }}/anything'
+ - "http_307_head.msg == 'Status code was 307 and not [200]: HTTP Error 307: TEMPORARY REDIRECT'"
+ - http_307_head.redirected == false
+ - http_307_head.status == 307
+ - http_307_head.url == 'https://{{ httpbin_host }}/redirect-to?status_code=307&url=https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 307 using GET
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=307&url=https://{{ httpbin_host }}/anything
+ follow_redirects: none
+ return_content: yes
+ method: GET
+ ignore_errors: yes
+ register: http_307_get
+
+- assert:
+ that:
+ - http_307_get is failure
+ - http_307_get.json is not defined
+ - http_307_get.location == 'https://{{ httpbin_host }}/anything'
+ - "http_307_get.msg == 'Status code was 307 and not [200]: HTTP Error 307: TEMPORARY REDIRECT'"
+ - http_307_get.redirected == false
+ - http_307_get.status == 307
+ - http_307_get.url == 'https://{{ httpbin_host }}/redirect-to?status_code=307&url=https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 307 using POST
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=307&url=https://{{ httpbin_host }}/anything
+ follow_redirects: none
+ return_content: yes
+ method: POST
+ body: '{ "foo": "bar" }'
+ body_format: json
+ ignore_errors: yes
+ register: http_307_post
+
+- assert:
+ that:
+ - http_307_post is failure
+ - http_307_post.json is not defined
+ - http_307_post.location == 'https://{{ httpbin_host }}/anything'
+ - "http_307_post.msg == 'Status code was 307 and not [200]: HTTP Error 307: TEMPORARY REDIRECT'"
+ - http_307_post.redirected == false
+ - http_307_post.status == 307
+ - http_307_post.url == 'https://{{ httpbin_host }}/redirect-to?status_code=307&url=https://{{ httpbin_host }}/anything'
+
+# NOTE: This is a bug, fixed in https://github.com/ansible/ansible/pull/36809
+- name: Test HTTP 308 using HEAD
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything
+ follow_redirects: none
+ return_content: yes
+ method: GET
+ ignore_errors: yes
+ register: http_308_head
+
+- assert:
+ that:
+ - http_308_head is failure
+ - http_308_head.json is not defined
+ - http_308_head.location == 'https://{{ httpbin_host }}/anything'
+ - "'Status code was 308 and not [200]: HTTP Error 308: ' in http_308_head.msg"
+ - http_308_head.redirected == false
+ - http_308_head.status == 308
+ - http_308_head.url == 'https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything'
+
+# NOTE: This is a bug, fixed in https://github.com/ansible/ansible/pull/36809
+- name: Test HTTP 308 using GET
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything
+ follow_redirects: none
+ return_content: yes
+ method: GET
+ ignore_errors: yes
+ register: http_308_get
+
+- assert:
+ that:
+ - http_308_get is failure
+ - http_308_get.json is not defined
+ - http_308_get.location == 'https://{{ httpbin_host }}/anything'
+ - "'Status code was 308 and not [200]: HTTP Error 308: ' in http_308_get.msg"
+ - http_308_get.redirected == false
+ - http_308_get.status == 308
+ - http_308_get.url == 'https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 308 using POST
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything
+ follow_redirects: none
+ return_content: yes
+ method: POST
+ body: '{ "foo": "bar" }'
+ body_format: json
+ ignore_errors: yes
+ register: http_308_post
+
+- assert:
+ that:
+ - http_308_post is failure
+ - http_308_post.json is not defined
+ - http_308_post.location == 'https://{{ httpbin_host }}/anything'
+ - "'Status code was 308 and not [200]: HTTP Error 308: ' in http_308_post.msg"
+ - http_308_post.redirected == false
+ - http_308_post.status == 308
+ - http_308_post.url == 'https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything'
diff --git a/test/integration/targets/uri/tasks/redirect-safe.yml b/test/integration/targets/uri/tasks/redirect-safe.yml
new file mode 100644
index 0000000..bcc4169
--- /dev/null
+++ b/test/integration/targets/uri/tasks/redirect-safe.yml
@@ -0,0 +1,274 @@
+- name: Test HTTP 301 using HEAD
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=301&url=https://{{ httpbin_host }}/anything
+ follow_redirects: safe
+ return_content: yes
+ method: HEAD
+ register: http_301_head
+
+- assert:
+ that:
+ - http_301_head is successful
+ - http_301_head.json is not defined
+ - http_301_head.redirected == true
+ - http_301_head.status == 200
+ - http_301_head.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 301 using GET
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=301&url=https://{{ httpbin_host }}/anything
+ follow_redirects: safe
+ return_content: yes
+ method: GET
+ register: http_301_get
+
+- assert:
+ that:
+ - http_301_get is successful
+ - http_301_get.json.data == ''
+ - http_301_get.json.method == 'GET'
+ - http_301_get.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_301_get.redirected == true
+ - http_301_get.status == 200
+ - http_301_get.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 301 using POST
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=301&url=https://{{ httpbin_host }}/anything
+ follow_redirects: safe
+ return_content: yes
+ method: POST
+ body: '{ "foo": "bar" }'
+ body_format: json
+ ignore_errors: yes
+ register: http_301_post
+
+- assert:
+ that:
+ - http_301_post is failure
+ - http_301_post.json is not defined
+ - http_301_post.location == 'https://{{ httpbin_host }}/anything'
+ - "http_301_post.msg == 'Status code was 301 and not [200]: HTTP Error 301: MOVED PERMANENTLY'"
+ - http_301_post.redirected == false
+ - http_301_post.status == 301
+ - http_301_post.url == 'https://{{ httpbin_host }}/redirect-to?status_code=301&url=https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 302 using HEAD
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=302&url=https://{{ httpbin_host }}/anything
+ follow_redirects: safe
+ return_content: yes
+ method: HEAD
+ register: http_302_head
+
+- assert:
+ that:
+ - http_302_head is successful
+ - http_302_head.json is not defined
+ - http_302_head.redirected == true
+ - http_302_head.status == 200
+ - http_302_head.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 302 using GET
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=302&url=https://{{ httpbin_host }}/anything
+ follow_redirects: safe
+ return_content: yes
+ method: GET
+ register: http_302_get
+
+- assert:
+ that:
+ - http_302_get is successful
+ - http_302_get.json.data == ''
+ - http_302_get.json.method == 'GET'
+ - http_302_get.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_302_get.redirected == true
+ - http_302_get.status == 200
+ - http_302_get.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 302 using POST
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=302&url=https://{{ httpbin_host }}/anything
+ follow_redirects: safe
+ return_content: yes
+ method: POST
+ body: '{ "foo": "bar" }'
+ body_format: json
+ ignore_errors: yes
+ register: http_302_post
+
+- assert:
+ that:
+ - http_302_post is failure
+ - http_302_post.json is not defined
+ - http_302_post.location == 'https://{{ httpbin_host }}/anything'
+ - "http_302_post.msg == 'Status code was 302 and not [200]: HTTP Error 302: FOUND'"
+ - http_302_post.redirected == false
+ - http_302_post.status == 302
+ - http_302_post.url == 'https://{{ httpbin_host }}/redirect-to?status_code=302&url=https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 303 using HEAD
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=303&url=https://{{ httpbin_host }}/anything
+ follow_redirects: safe
+ return_content: yes
+ method: HEAD
+ register: http_303_head
+
+- assert:
+ that:
+ - http_303_head is successful
+ - http_303_head.json is not defined
+ - http_303_head.redirected == true
+ - http_303_head.status == 200
+ - http_303_head.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 303 using GET
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=303&url=https://{{ httpbin_host }}/anything
+ follow_redirects: safe
+ return_content: yes
+ method: GET
+ register: http_303_get
+
+- assert:
+ that:
+ - http_303_get is successful
+ - http_303_get.json.data == ''
+ - http_303_get.json.method == 'GET'
+ - http_303_get.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_303_get.redirected == true
+ - http_303_get.status == 200
+ - http_303_get.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 303 using POST
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=303&url=https://{{ httpbin_host }}/anything
+ follow_redirects: safe
+ return_content: yes
+ method: POST
+ body: '{ "foo": "bar" }'
+ body_format: json
+ ignore_errors: yes
+ register: http_303_post
+
+- assert:
+ that:
+ - http_303_post is failure
+ - http_303_post.json is not defined
+ - http_303_post.location == 'https://{{ httpbin_host }}/anything'
+ - "http_303_post.msg == 'Status code was 303 and not [200]: HTTP Error 303: SEE OTHER'"
+ - http_303_post.redirected == false
+ - http_303_post.status == 303
+ - http_303_post.url == 'https://{{ httpbin_host }}/redirect-to?status_code=303&url=https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 307 using HEAD
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=307&url=https://{{ httpbin_host }}/anything
+ follow_redirects: safe
+ return_content: yes
+ method: HEAD
+ register: http_307_head
+
+- assert:
+ that:
+ - http_307_head is successful
+ - http_307_head.json is not defined
+ - http_307_head.redirected == true
+ - http_307_head.status == 200
+ - http_307_head.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 307 using GET
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=307&url=https://{{ httpbin_host }}/anything
+ follow_redirects: safe
+ return_content: yes
+ method: GET
+ register: http_307_get
+
+- assert:
+ that:
+ - http_307_get is successful
+ - http_307_get.json.data == ''
+ - http_307_get.json.method == 'GET'
+ - http_307_get.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_307_get.redirected == true
+ - http_307_get.status == 200
+ - http_307_get.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 307 using POST
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=307&url=https://{{ httpbin_host }}/anything
+ follow_redirects: safe
+ return_content: yes
+ method: POST
+ body: '{ "foo": "bar" }'
+ body_format: json
+ ignore_errors: yes
+ register: http_307_post
+
+- assert:
+ that:
+ - http_307_post is failure
+ - http_307_post.json is not defined
+ - http_307_post.location == 'https://{{ httpbin_host }}/anything'
+ - "http_307_post.msg == 'Status code was 307 and not [200]: HTTP Error 307: TEMPORARY REDIRECT'"
+ - http_307_post.redirected == false
+ - http_307_post.status == 307
+ - http_307_post.url == 'https://{{ httpbin_host }}/redirect-to?status_code=307&url=https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 308 using HEAD
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything
+ follow_redirects: safe
+ return_content: yes
+ method: HEAD
+ register: http_308_head
+
+- assert:
+ that:
+ - http_308_head is successful
+ - http_308_head.json is not defined
+ - http_308_head.redirected == true
+ - http_308_head.status == 200
+ - http_308_head.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 308 using GET
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything
+ follow_redirects: safe
+ return_content: yes
+ method: GET
+ register: http_308_get
+
+- assert:
+ that:
+ - http_308_get is successful
+ - http_308_get.json.data == ''
+ - http_308_get.json.method == 'GET'
+ - http_308_get.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_308_get.redirected == true
+ - http_308_get.status == 200
+ - http_308_get.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 308 using POST
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything
+ follow_redirects: safe
+ return_content: yes
+ method: POST
+ body: '{ "foo": "bar" }'
+ body_format: json
+ ignore_errors: yes
+ register: http_308_post
+
+- assert:
+ that:
+ - http_308_post is failure
+ - http_308_post.json is not defined
+ - http_308_post.location == 'https://{{ httpbin_host }}/anything'
+ - "'Status code was 308 and not [200]: HTTP Error 308: ' in http_308_post.msg"
+ - http_308_post.redirected == false
+ - http_308_post.status == 308
+ - http_308_post.url == 'https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything'
diff --git a/test/integration/targets/uri/tasks/redirect-urllib2.yml b/test/integration/targets/uri/tasks/redirect-urllib2.yml
new file mode 100644
index 0000000..6cdafdb
--- /dev/null
+++ b/test/integration/targets/uri/tasks/redirect-urllib2.yml
@@ -0,0 +1,294 @@
+# NOTE: The HTTP HEAD turns into an HTTP GET
+- name: Test HTTP 301 using HEAD
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=301&url=https://{{ httpbin_host }}/anything
+ follow_redirects: urllib2
+ return_content: yes
+ method: HEAD
+ register: http_301_head
+
+- assert:
+ that:
+ - http_301_head is successful
+ - http_301_head.json.data == ''
+ - http_301_head.json.method == 'GET'
+ - http_301_head.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_301_head.redirected == true
+ - http_301_head.status == 200
+ - http_301_head.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 301 using GET
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=301&url=https://{{ httpbin_host }}/anything
+ follow_redirects: urllib2
+ return_content: yes
+ method: GET
+ register: http_301_get
+
+- assert:
+ that:
+ - http_301_get is successful
+ - http_301_get.json.data == ''
+ - http_301_get.json.method == 'GET'
+ - http_301_get.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_301_get.redirected == true
+ - http_301_get.status == 200
+ - http_301_get.url == 'https://{{ httpbin_host }}/anything'
+
+# NOTE: The HTTP POST turns into an HTTP GET
+- name: Test HTTP 301 using POST
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=301&url=https://{{ httpbin_host }}/anything
+ follow_redirects: urllib2
+ return_content: yes
+ method: POST
+ body: '{ "foo": "bar" }'
+ body_format: json
+ register: http_301_post
+
+- assert:
+ that:
+ - http_301_post is successful
+ - http_301_post.json.data == ''
+ - http_301_post.json.method == 'GET'
+ - http_301_post.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_301_post.redirected == true
+ - http_301_post.status == 200
+ - http_301_post.url == 'https://{{ httpbin_host }}/anything'
+
+# NOTE: The HTTP HEAD turns into an HTTP GET
+- name: Test HTTP 302 using HEAD
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=302&url=https://{{ httpbin_host }}/anything
+ follow_redirects: urllib2
+ return_content: yes
+ method: HEAD
+ register: http_302_head
+
+- assert:
+ that:
+ - http_302_head is successful
+ - http_302_head.json.data == ''
+ - http_302_head.json.method == 'GET'
+ - http_302_head.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_302_head.redirected == true
+ - http_302_head.status == 200
+ - http_302_head.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 302 using GET
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=302&url=https://{{ httpbin_host }}/anything
+ follow_redirects: urllib2
+ return_content: yes
+ method: GET
+ register: http_302_get
+
+- assert:
+ that:
+ - http_302_get is successful
+ - http_302_get.json.data == ''
+ - http_302_get.json.method == 'GET'
+ - http_302_get.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_302_get.redirected == true
+ - http_302_get.status == 200
+ - http_302_get.url == 'https://{{ httpbin_host }}/anything'
+
+# NOTE: The HTTP POST turns into an HTTP GET
+- name: Test HTTP 302 using POST
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=302&url=https://{{ httpbin_host }}/anything
+ follow_redirects: urllib2
+ return_content: yes
+ method: POST
+ body: '{ "foo": "bar" }'
+ body_format: json
+ register: http_302_post
+
+- assert:
+ that:
+ - http_302_post is successful
+ - http_302_post.json.data == ''
+ - http_302_post.json.method == 'GET'
+ - http_302_post.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_302_post.redirected == true
+ - http_302_post.status == 200
+ - http_302_post.url == 'https://{{ httpbin_host }}/anything'
+
+# NOTE: The HTTP HEAD turns into an HTTP GET
+- name: Test HTTP 303 using HEAD
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=303&url=https://{{ httpbin_host }}/anything
+ follow_redirects: urllib2
+ return_content: yes
+ method: HEAD
+ register: http_303_head
+
+- assert:
+ that:
+ - http_303_head is successful
+ - http_303_head.json.data == ''
+ - http_303_head.json.method == 'GET'
+ - http_303_head.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_303_head.redirected == true
+ - http_303_head.status == 200
+ - http_303_head.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 303 using GET
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=303&url=https://{{ httpbin_host }}/anything
+ follow_redirects: urllib2
+ return_content: yes
+ method: GET
+ register: http_303_get
+
+- assert:
+ that:
+ - http_303_get is successful
+ - http_303_get.json.data == ''
+ - http_303_get.json.method == 'GET'
+ - http_303_get.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_303_get.redirected == true
+ - http_303_get.status == 200
+ - http_303_get.url == 'https://{{ httpbin_host }}/anything'
+
+# NOTE: The HTTP POST turns into an HTTP GET
+- name: Test HTTP 303 using POST
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=303&url=https://{{ httpbin_host }}/anything
+ follow_redirects: urllib2
+ return_content: yes
+ method: POST
+ body: '{ "foo": "bar" }'
+ body_format: json
+ register: http_303_post
+
+- assert:
+ that:
+ - http_303_post is successful
+ - http_303_post.json.data == ''
+ - http_303_post.json.method == 'GET'
+ - http_303_post.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_303_post.redirected == true
+ - http_303_post.status == 200
+ - http_303_post.url == 'https://{{ httpbin_host }}/anything'
+
+# NOTE: The HTTP HEAD turns into an HTTP GET
+- name: Test HTTP 307 using HEAD
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=307&url=https://{{ httpbin_host }}/anything
+ follow_redirects: urllib2
+ return_content: yes
+ method: HEAD
+ register: http_307_head
+
+- assert:
+ that:
+ - http_307_head is successful
+ - http_307_head.json.data == ''
+ - http_307_head.json.method == 'GET'
+ - http_307_head.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_307_head.redirected == true
+ - http_307_head.status == 200
+ - http_307_head.url == 'https://{{ httpbin_host }}/anything'
+
+- name: Test HTTP 307 using GET
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=307&url=https://{{ httpbin_host }}/anything
+ follow_redirects: urllib2
+ return_content: yes
+ method: GET
+ register: http_307_get
+
+- assert:
+ that:
+ - http_307_get is successful
+ - http_307_get.json.data == ''
+ - http_307_get.json.method == 'GET'
+ - http_307_get.json.url == 'https://{{ httpbin_host }}/anything'
+ - http_307_get.redirected == true
+ - http_307_get.status == 200
+ - http_307_get.url == 'https://{{ httpbin_host }}/anything'
+
+# FIXME: This is fixed in https://github.com/ansible/ansible/pull/36809
+- name: Test HTTP 307 using POST
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=307&url=https://{{ httpbin_host }}/anything
+ follow_redirects: urllib2
+ return_content: yes
+ method: POST
+ body: '{ "foo": "bar" }'
+ body_format: json
+ ignore_errors: yes
+ register: http_307_post
+
+- assert:
+ that:
+ - http_307_post is failure
+ - http_307_post.json is not defined
+ - http_307_post.location == 'https://{{ httpbin_host }}/anything'
+ - "http_307_post.msg == 'Status code was 307 and not [200]: HTTP Error 307: TEMPORARY REDIRECT'"
+ - http_307_post.redirected == false
+ - http_307_post.status == 307
+ - http_307_post.url == 'https://{{ httpbin_host }}/redirect-to?status_code=307&url=https://{{ httpbin_host }}/anything'
+
+# FIXME: This is fixed in https://github.com/ansible/ansible/pull/36809
+- name: Test HTTP 308 using HEAD
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything
+ follow_redirects: urllib2
+ return_content: yes
+ method: GET
+ ignore_errors: yes
+ register: http_308_head
+
+- assert:
+ that:
+ - http_308_head is failure
+ - http_308_head.json is not defined
+ - http_308_head.location == 'https://{{ httpbin_host }}/anything'
+ - "'Status code was 308 and not [200]: HTTP Error 308: ' in http_308_head.msg"
+ - http_308_head.redirected == false
+ - http_308_head.status == 308
+ - http_308_head.url == 'https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything'
+
+# FIXME: This is fixed in https://github.com/ansible/ansible/pull/36809
+- name: Test HTTP 308 using GET
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything
+ follow_redirects: urllib2
+ return_content: yes
+ method: GET
+ ignore_errors: yes
+ register: http_308_get
+
+- assert:
+ that:
+ - http_308_get is failure
+ - http_308_get.json is not defined
+ - http_308_get.location == 'https://{{ httpbin_host }}/anything'
+ - "'Status code was 308 and not [200]: HTTP Error 308: ' in http_308_get.msg"
+ - http_308_get.redirected == false
+ - http_308_get.status == 308
+ - http_308_get.url == 'https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything'
+
+# FIXME: This is fixed in https://github.com/ansible/ansible/pull/36809
+- name: Test HTTP 308 using POST
+ uri:
+ url: https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything
+ follow_redirects: urllib2
+ return_content: yes
+ method: POST
+ body: '{ "foo": "bar" }'
+ body_format: json
+ ignore_errors: yes
+ register: http_308_post
+
+- assert:
+ that:
+ - http_308_post is failure
+ - http_308_post.json is not defined
+ - http_308_post.location == 'https://{{ httpbin_host }}/anything'
+ - "'Status code was 308 and not [200]: HTTP Error 308: ' in http_308_post.msg"
+ - http_308_post.redirected == false
+ - http_308_post.status == 308
+ - http_308_post.url == 'https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything'
diff --git a/test/integration/targets/uri/tasks/return-content.yml b/test/integration/targets/uri/tasks/return-content.yml
new file mode 100644
index 0000000..5a9b97e
--- /dev/null
+++ b/test/integration/targets/uri/tasks/return-content.yml
@@ -0,0 +1,49 @@
+- name: Test when return_content is yes
+ uri:
+ url: https://{{ httpbin_host }}/get
+ return_content: yes
+ register: result
+
+- name: Assert content exists when return_content is yes and request succeeds
+ assert:
+ that:
+ - result is successful
+ - "'content' in result"
+
+- name: Test when return_content is yes
+ uri:
+ url: http://does/not/exist
+ return_content: yes
+ register: result
+ ignore_errors: true
+
+- name: Assert content exists when return_content is yes and request fails
+ assert:
+ that:
+ - result is failed
+ - "'content' in result"
+
+- name: Test when return_content is no
+ uri:
+ url: https://{{ httpbin_host }}/get
+ return_content: no
+ register: result
+
+- name: Assert content does not exist when return_content is no and request succeeds
+ assert:
+ that:
+ - result is successful
+ - "'content' not in result"
+
+- name: Test when return_content is no
+ uri:
+ url: http://does/not/exist
+ return_content: no
+ register: result
+ ignore_errors: true
+
+- name: Assert content does not exist when return_content is no and request fails
+ assert:
+ that:
+ - result is failed
+ - "'content' not in result" \ No newline at end of file
diff --git a/test/integration/targets/uri/tasks/unexpected-failures.yml b/test/integration/targets/uri/tasks/unexpected-failures.yml
new file mode 100644
index 0000000..341b66e
--- /dev/null
+++ b/test/integration/targets/uri/tasks/unexpected-failures.yml
@@ -0,0 +1,26 @@
+---
+# same as expanduser & expandvars called on managed host
+- command: 'echo {{ remote_tmp_dir }}'
+ register: echo
+
+- set_fact:
+ remote_dir_expanded: '{{ echo.stdout }}'
+
+- name: ensure test directory doesn't exist
+ file:
+ path: '{{ remote_tmp_dir }}/non/existent/path'
+ state: absent
+
+- name: destination doesn't exist
+ uri:
+ url: 'https://{{ httpbin_host }}/get'
+ dest: '{{ remote_tmp_dir }}/non/existent/path'
+ ignore_errors: true
+ register: ret
+
+- name: check that unexpected failure didn't happen
+ assert:
+ that:
+ - ret is failed
+ - "not ret.msg.startswith('MODULE FAILURE')"
+ - '"Could not replace file" in ret.msg'
diff --git a/test/integration/targets/uri/tasks/use_gssapi.yml b/test/integration/targets/uri/tasks/use_gssapi.yml
new file mode 100644
index 0000000..6629cee
--- /dev/null
+++ b/test/integration/targets/uri/tasks/use_gssapi.yml
@@ -0,0 +1,62 @@
+- name: test that endpoint offers Negotiate auth
+ uri:
+ url: http://{{ httpbin_host }}/gssapi
+ status_code: 401
+ register: no_auth_failure
+ failed_when: no_auth_failure.www_authenticate != 'Negotiate'
+
+- name: test Negotiate auth over HTTP with explicit credentials
+ uri:
+ url: http://{{ httpbin_host }}/gssapi
+ use_gssapi: yes
+ url_username: '{{ krb5_username }}'
+ url_password: '{{ krb5_password }}'
+ return_content: yes
+ register: http_explicit
+
+- name: test Negotiate auth over HTTPS with explicit credentials
+ uri:
+ url: https://{{ httpbin_host }}/gssapi
+ use_gssapi: yes
+ url_username: '{{ krb5_username }}'
+ url_password: '{{ krb5_password }}'
+ return_content: yes
+ register: https_explicit
+
+- name: assert test Negotiate auth with implicit credentials
+ assert:
+ that:
+ - http_explicit.status == 200
+ - http_explicit.content | trim == 'Microsoft Rulz'
+ - https_explicit.status == 200
+ - https_explicit.content | trim == 'Microsoft Rulz'
+
+- name: skip tests on macOS, I cannot seem to get it to read a credential from a custom ccache
+ when: ansible_facts.distribution != 'MacOSX'
+ block:
+ - name: get Kerberos ticket for implicit auth tests
+ httptester_kinit:
+ username: '{{ krb5_username }}'
+ password: '{{ krb5_password }}'
+
+ - name: test Negotiate auth over HTTP with implicit credentials
+ uri:
+ url: http://{{ httpbin_host }}/gssapi
+ use_gssapi: yes
+ return_content: yes
+ register: http_implicit
+
+ - name: test Negotiate auth over HTTPS with implicit credentials
+ uri:
+ url: https://{{ httpbin_host }}/gssapi
+ use_gssapi: yes
+ return_content: yes
+ register: https_implicit
+
+ - name: assert test Negotiate auth with implicit credentials
+ assert:
+ that:
+ - http_implicit.status == 200
+ - http_implicit.content | trim == 'Microsoft Rulz'
+ - https_implicit.status == 200
+ - https_implicit.content | trim == 'Microsoft Rulz'
diff --git a/test/integration/targets/uri/tasks/use_netrc.yml b/test/integration/targets/uri/tasks/use_netrc.yml
new file mode 100644
index 0000000..da745b8
--- /dev/null
+++ b/test/integration/targets/uri/tasks/use_netrc.yml
@@ -0,0 +1,51 @@
+- name: Write out netrc
+ copy:
+ dest: "{{ remote_tmp_dir }}/netrc"
+ content: |
+ machine {{ httpbin_host }}
+ login foo
+ password bar
+
+- name: Test Bearer authorization is failed with netrc
+ uri:
+ url: https://{{ httpbin_host }}/bearer
+ return_content: yes
+ headers:
+ Authorization: Bearer foobar
+ ignore_errors: yes
+ environment:
+ NETRC: "{{ remote_tmp_dir }}/netrc"
+ register: response_failed
+
+- name: assert Test Bearer authorization is failed with netrc
+ assert:
+ that:
+ - response_failed.json.token != 'foobar'
+ - "'Zm9vOmJhcg==' in response_failed.json.token"
+ fail_msg: "Was expecting 'foo:bar' in base64, but received: {{ response_failed }}"
+ success_msg: "Expected to fail because netrc is using Basic authentication by default"
+
+- name: Test Bearer authorization is successfull with use_netrc=False
+ uri:
+ url: https://{{ httpbin_host }}/bearer
+ use_netrc: false
+ return_content: yes
+ headers:
+ Authorization: Bearer foobar
+ environment:
+ NETRC: "{{ remote_tmp_dir }}/netrc"
+ register: response
+
+- name: assert Test Bearer authorization is successfull with use_netrc=False
+ assert:
+ that:
+ - response.status == 200
+ - response.json.token == 'foobar'
+ - response.url == 'https://{{ httpbin_host }}/bearer'
+ fail_msg: "Was expecting successful Bearer authentication, but received: {{ response }}"
+ success_msg: "Bearer authentication successfull when netrc is ignored."
+
+- name: Clean up
+ file:
+ dest: "{{ remote_tmp_dir }}/netrc"
+ state: absent \ No newline at end of file
diff --git a/test/integration/targets/uri/templates/netrc.j2 b/test/integration/targets/uri/templates/netrc.j2
new file mode 100644
index 0000000..3a100d5
--- /dev/null
+++ b/test/integration/targets/uri/templates/netrc.j2
@@ -0,0 +1,3 @@
+machine {{ httpbin_host }}
+login user
+password passwd
diff --git a/test/integration/targets/uri/vars/main.yml b/test/integration/targets/uri/vars/main.yml
new file mode 100644
index 0000000..83a740b
--- /dev/null
+++ b/test/integration/targets/uri/vars/main.yml
@@ -0,0 +1,20 @@
+uri_os_packages:
+ RedHat:
+ urllib3: python-urllib3
+ step1:
+ - python-pyasn1
+ - pyOpenSSL
+ - python-urllib3
+ step2:
+ - libffi-devel
+ - openssl-devel
+ - python-devel
+ Debian:
+ step1:
+ - python-pyasn1
+ - python-openssl
+ - python-urllib3
+ step2:
+ - libffi-dev
+ - libssl-dev
+ - python-dev
diff --git a/test/integration/targets/user/aliases b/test/integration/targets/user/aliases
new file mode 100644
index 0000000..a4c92ef
--- /dev/null
+++ b/test/integration/targets/user/aliases
@@ -0,0 +1,2 @@
+destructive
+shippable/posix/group1
diff --git a/test/integration/targets/user/files/userlist.sh b/test/integration/targets/user/files/userlist.sh
new file mode 100644
index 0000000..96a83b2
--- /dev/null
+++ b/test/integration/targets/user/files/userlist.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+
+#- name: make a list of groups
+# shell: |
+# cat /etc/group | cut -d: -f1
+# register: group_names
+# when: 'ansible_distribution != "MacOSX"'
+
+#- name: make a list of groups [mac]
+# shell: dscl localhost -list /Local/Default/Groups
+# register: group_names
+# when: 'ansible_distribution == "MacOSX"'
+
+DISTRO="$*"
+
+if [[ "$DISTRO" == "MacOSX" ]]; then
+ dscl localhost -list /Local/Default/Users
+else
+ grep -E -v ^\# /etc/passwd | cut -d: -f1
+fi
diff --git a/test/integration/targets/user/meta/main.yml b/test/integration/targets/user/meta/main.yml
new file mode 100644
index 0000000..07faa21
--- /dev/null
+++ b/test/integration/targets/user/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - prepare_tests
diff --git a/test/integration/targets/user/tasks/main.yml b/test/integration/targets/user/tasks/main.yml
new file mode 100644
index 0000000..9d36bfc
--- /dev/null
+++ b/test/integration/targets/user/tasks/main.yml
@@ -0,0 +1,42 @@
+# Test code for the user module.
+# (c) 2017, James Tanner <tanner.jc@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+#
+
+- name: skip broken distros
+ meta: end_host
+ when: ansible_distribution == 'Alpine'
+
+- import_tasks: test_create_user.yml
+- import_tasks: test_create_system_user.yml
+- import_tasks: test_create_user_uid.yml
+- import_tasks: test_create_user_password.yml
+- import_tasks: test_create_user_home.yml
+- import_tasks: test_remove_user.yml
+- import_tasks: test_no_home_fallback.yml
+- import_tasks: test_expires.yml
+- import_tasks: test_expires_new_account.yml
+- import_tasks: test_expires_new_account_epoch_negative.yml
+- import_tasks: test_expires_min_max.yml
+- import_tasks: test_shadow_backup.yml
+- import_tasks: test_ssh_key_passphrase.yml
+- import_tasks: test_password_lock.yml
+- import_tasks: test_password_lock_new_user.yml
+- import_tasks: test_local.yml
+ when: not (ansible_distribution == 'openSUSE Leap' and ansible_distribution_version is version('15.4', '>='))
+- import_tasks: test_umask.yml
+ when: ansible_facts.system == 'Linux'
diff --git a/test/integration/targets/user/tasks/test_create_system_user.yml b/test/integration/targets/user/tasks/test_create_system_user.yml
new file mode 100644
index 0000000..da746c5
--- /dev/null
+++ b/test/integration/targets/user/tasks/test_create_system_user.yml
@@ -0,0 +1,12 @@
+# create system user
+
+- name: remove user
+ user:
+ name: ansibulluser
+ state: absent
+
+- name: create system user
+ user:
+ name: ansibulluser
+ state: present
+ system: yes
diff --git a/test/integration/targets/user/tasks/test_create_user.yml b/test/integration/targets/user/tasks/test_create_user.yml
new file mode 100644
index 0000000..bced790
--- /dev/null
+++ b/test/integration/targets/user/tasks/test_create_user.yml
@@ -0,0 +1,67 @@
+- name: remove the test user
+ user:
+ name: ansibulluser
+ state: absent
+
+- name: try to create a user
+ user:
+ name: ansibulluser
+ state: present
+ register: user_test0_0
+
+- name: create the user again
+ user:
+ name: ansibulluser
+ state: present
+ register: user_test0_1
+
+- debug:
+ var: user_test0
+ verbosity: 2
+
+- name: make a list of users
+ script: userlist.sh {{ ansible_facts.distribution }}
+ register: user_names
+
+- debug:
+ var: user_names
+ verbosity: 2
+
+- name: validate results for testcase 0
+ assert:
+ that:
+ - user_test0_0 is changed
+ - user_test0_1 is not changed
+ - '"ansibulluser" in user_names.stdout_lines'
+
+- name: run existing user check tests
+ user:
+ name: "{{ user_names.stdout_lines | random }}"
+ state: present
+ create_home: no
+ loop: "{{ range(1, 5+1) | list }}"
+ register: user_test1
+
+- debug:
+ var: user_test1
+ verbosity: 2
+
+- name: validate results for testcase 1
+ assert:
+ that:
+ - user_test1.results is defined
+ - user_test1.results | length == 5
+
+- name: validate changed results for testcase 1
+ assert:
+ that:
+ - "user_test1.results[0] is not changed"
+ - "user_test1.results[1] is not changed"
+ - "user_test1.results[2] is not changed"
+ - "user_test1.results[3] is not changed"
+ - "user_test1.results[4] is not changed"
+ - "user_test1.results[0]['state'] == 'present'"
+ - "user_test1.results[1]['state'] == 'present'"
+ - "user_test1.results[2]['state'] == 'present'"
+ - "user_test1.results[3]['state'] == 'present'"
+ - "user_test1.results[4]['state'] == 'present'"
diff --git a/test/integration/targets/user/tasks/test_create_user_home.yml b/test/integration/targets/user/tasks/test_create_user_home.yml
new file mode 100644
index 0000000..1b529f7
--- /dev/null
+++ b/test/integration/targets/user/tasks/test_create_user_home.yml
@@ -0,0 +1,136 @@
+# https://github.com/ansible/ansible/issues/42484
+# Skipping macOS for now since there is a bug when changing home directory
+- name: Test home directory creation
+ when: ansible_facts.system != 'Darwin'
+ block:
+ - name: create user specifying home
+ user:
+ name: ansibulluser
+ state: present
+ home: "{{ user_home_prefix[ansible_facts.system] }}/ansibulluser"
+ register: user_test3_0
+
+ - name: create user again specifying home
+ user:
+ name: ansibulluser
+ state: present
+ home: "{{ user_home_prefix[ansible_facts.system] }}/ansibulluser"
+ register: user_test3_1
+
+ - name: change user home
+ user:
+ name: ansibulluser
+ state: present
+ home: "{{ user_home_prefix[ansible_facts.system] }}/ansibulluser-mod"
+ register: user_test3_2
+
+ - name: change user home back
+ user:
+ name: ansibulluser
+ state: present
+ home: "{{ user_home_prefix[ansible_facts.system] }}/ansibulluser"
+ register: user_test3_3
+
+ - name: validate results for testcase 3
+ assert:
+ that:
+ - user_test3_0 is not changed
+ - user_test3_1 is not changed
+ - user_test3_2 is changed
+ - user_test3_3 is changed
+
+# https://github.com/ansible/ansible/issues/41393
+# Create a new user account with a path that has parent directories that do not exist
+- name: Create user with home path that has parents that do not exist
+ user:
+ name: ansibulluser2
+ state: present
+ home: "{{ user_home_prefix[ansible_facts.system] }}/in2deep/ansibulluser2"
+ register: create_home_with_no_parent_1
+
+- name: Create user with home path that has parents that do not exist again
+ user:
+ name: ansibulluser2
+ state: present
+ home: "{{ user_home_prefix[ansible_facts.system] }}/in2deep/ansibulluser2"
+ register: create_home_with_no_parent_2
+
+- name: Check the created home directory
+ stat:
+ path: "{{ user_home_prefix[ansible_facts.system] }}/in2deep/ansibulluser2"
+ register: home_with_no_parent_3
+
+- name: Ensure user with non-existing parent paths was created successfully
+ assert:
+ that:
+ - create_home_with_no_parent_1 is changed
+ - create_home_with_no_parent_1.home == user_home_prefix[ansible_facts.system] ~ '/in2deep/ansibulluser2'
+ - create_home_with_no_parent_2 is not changed
+ - home_with_no_parent_3.stat.uid == create_home_with_no_parent_1.uid
+ - home_with_no_parent_3.stat.gr_name == default_user_group[ansible_facts.distribution] | default('ansibulluser2')
+
+- name: Cleanup test account
+ user:
+ name: ansibulluser2
+ home: "{{ user_home_prefix[ansible_facts.system] }}/in2deep/ansibulluser2"
+ state: absent
+ remove: yes
+
+- name: Remove testing dir
+ file:
+ path: "{{ user_home_prefix[ansible_facts.system] }}/in2deep/"
+ state: absent
+
+
+# https://github.com/ansible/ansible/issues/60307
+# Make sure we can create a user when the home directory is missing
+- name: Create user with home path that does not exist
+ user:
+ name: ansibulluser3
+ state: present
+ home: "{{ user_home_prefix[ansible_facts.system] }}/nosuchdir"
+ createhome: no
+
+- name: Cleanup test account
+ user:
+ name: ansibulluser3
+ state: absent
+ remove: yes
+
+# https://github.com/ansible/ansible/issues/70589
+# Create user with create_home: no and parent directory does not exist.
+- name: "Check if parent dir for home dir for user exists (before)"
+ stat:
+ path: "{{ user_home_prefix[ansible_facts.system] }}/thereisnodir"
+ register: create_user_no_create_home_with_no_parent_parent_dir_before
+
+- name: "Create user with create_home == no and home path parent dir does not exist"
+ user:
+ name: randomuser
+ state: present
+ create_home: false
+ home: "{{ user_home_prefix[ansible_facts.system] }}/thereisnodir/randomuser"
+ register: create_user_no_create_home_with_no_parent
+
+- name: "Check if parent dir for home dir for user exists (after)"
+ stat:
+ path: "{{ user_home_prefix[ansible_facts.system] }}/thereisnodir"
+ register: create_user_no_create_home_with_no_parent_parent_dir_after
+
+- name: "Check if home for user is created"
+ stat:
+ path: "{{ user_home_prefix[ansible_facts.system] }}/thereisnodir/randomuser"
+ register: create_user_no_create_home_with_no_parent_home_dir
+
+- name: "Ensure user with non-existing parent paths with create_home: no was created successfully"
+ assert:
+ that:
+ - not create_user_no_create_home_with_no_parent_parent_dir_before.stat.exists
+ - not create_user_no_create_home_with_no_parent_parent_dir_after.stat.isdir is defined
+ - not create_user_no_create_home_with_no_parent_home_dir.stat.exists
+
+- name: Cleanup test account
+ user:
+ name: randomuser
+ state: absent
+ remove: yes
diff --git a/test/integration/targets/user/tasks/test_create_user_password.yml b/test/integration/targets/user/tasks/test_create_user_password.yml
new file mode 100644
index 0000000..02aae00
--- /dev/null
+++ b/test/integration/targets/user/tasks/test_create_user_password.yml
@@ -0,0 +1,90 @@
+# test user add with password
+- name: add an encrypted password for user
+ user:
+ name: ansibulluser
+ password: "$6$rounds=656000$TT4O7jz2M57npccl$33LF6FcUMSW11qrESXL1HX0BS.bsiT6aenFLLiVpsQh6hDtI9pJh5iY7x8J7ePkN4fP8hmElidHXaeD51pbGS."
+ state: present
+ update_password: always
+ register: test_user_encrypt0
+
+- name: there should not be warnings
+ assert:
+ that: "'warnings' not in test_user_encrypt0"
+
+# https://github.com/ansible/ansible/issues/65711
+- name: Test updating password only on creation
+ user:
+ name: ansibulluser
+ password: '*'
+ update_password: on_create
+ register: test_user_update_password
+
+- name: Ensure password was not changed
+ assert:
+ that:
+ - test_user_update_password is not changed
+
+- name: Verify password hash for Linux
+ when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse']
+ block:
+ - name: LINUX | Get shadow entry for ansibulluser
+ getent:
+ database: shadow
+ key: ansibulluser
+
+ - name: LINUX | Ensure password hash was not removed
+ assert:
+ that:
+ - getent_shadow['ansibulluser'][1] != '*'
+
+- name: Test plaintext warning
+ when: ansible_facts.system != 'Darwin'
+ block:
+ - name: add an plaintext password for user
+ user:
+ name: ansibulluser
+ password: "plaintextpassword"
+ state: present
+ update_password: always
+ register: test_user_encrypt1
+
+ - name: there should be a warning complains that the password is plaintext
+ assert:
+ that: "'warnings' in test_user_encrypt1"
+
+ - name: add an invalid hashed password
+ user:
+ name: ansibulluser
+ password: "$6$rounds=656000$tgK3gYTyRLUmhyv2$lAFrYUQwn7E6VsjPOwQwoSx30lmpiU9r/E0Al7tzKrR9mkodcMEZGe9OXD0H/clOn6qdsUnaL4zefy5fG+++++"
+ state: present
+ update_password: always
+ register: test_user_encrypt2
+
+ - name: there should be a warning complains about the character set of password
+ assert:
+ that: "'warnings' in test_user_encrypt2"
+
+ - name: change password to '!'
+ user:
+ name: ansibulluser
+ password: '!'
+ register: test_user_encrypt3
+
+ - name: change password to '*'
+ user:
+ name: ansibulluser
+ password: '*'
+ register: test_user_encrypt4
+
+ - name: change password to '*************'
+ user:
+ name: ansibulluser
+ password: '*************'
+ register: test_user_encrypt5
+
+ - name: there should be no warnings when setting the password to '!', '*' or '*************'
+ assert:
+ that:
+ - "'warnings' not in test_user_encrypt3"
+ - "'warnings' not in test_user_encrypt4"
+ - "'warnings' not in test_user_encrypt5"
diff --git a/test/integration/targets/user/tasks/test_create_user_uid.yml b/test/integration/targets/user/tasks/test_create_user_uid.yml
new file mode 100644
index 0000000..9ac8a96
--- /dev/null
+++ b/test/integration/targets/user/tasks/test_create_user_uid.yml
@@ -0,0 +1,26 @@
+# test adding user with uid
+# https://github.com/ansible/ansible/issues/62969
+- name: remove the test user
+ user:
+ name: ansibulluser
+ state: absent
+
+- name: try to create a user with uid
+ user:
+ name: ansibulluser
+ state: present
+ uid: 572
+ register: user_test01_0
+
+- name: create the user again
+ user:
+ name: ansibulluser
+ state: present
+ uid: 572
+ register: user_test01_1
+
+- name: validate results for testcase 0
+ assert:
+ that:
+ - user_test01_0 is changed
+ - user_test01_1 is not changed
diff --git a/test/integration/targets/user/tasks/test_expires.yml b/test/integration/targets/user/tasks/test_expires.yml
new file mode 100644
index 0000000..8c23893
--- /dev/null
+++ b/test/integration/targets/user/tasks/test_expires.yml
@@ -0,0 +1,147 @@
+# Date is March 3, 2050
+- name: Set user expiration
+ user:
+ name: ansibulluser
+ state: present
+ expires: 2529881062
+ register: user_test_expires1
+ tags:
+ - timezone
+
+- name: Set user expiration again to ensure no change is made
+ user:
+ name: ansibulluser
+ state: present
+ expires: 2529881062
+ register: user_test_expires2
+ tags:
+ - timezone
+
+- name: Ensure that account with expiration was created and did not change on subsequent run
+ assert:
+ that:
+ - user_test_expires1 is changed
+ - user_test_expires2 is not changed
+
+- name: Verify expiration date for Linux
+ block:
+ - name: LINUX | Get expiration date for ansibulluser
+ getent:
+ database: shadow
+ key: ansibulluser
+
+ - name: LINUX | Ensure proper expiration date was set
+ assert:
+ that:
+ - getent_shadow['ansibulluser'][6] == '29281'
+ when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse']
+
+
+- name: Verify expiration date for BSD
+ block:
+ - name: BSD | Get expiration date for ansibulluser
+ shell: 'grep ansibulluser /etc/master.passwd | cut -d: -f 7'
+ changed_when: no
+ register: bsd_account_expiration
+
+ - name: BSD | Ensure proper expiration date was set
+ assert:
+ that:
+ - bsd_account_expiration.stdout == '2529881062'
+ when: ansible_facts.os_family == 'FreeBSD'
+
+- name: Change timezone
+ timezone:
+ name: America/Denver
+ register: original_timezone
+ tags:
+ - timezone
+
+- name: Change system timezone to make sure expiration comparison works properly
+ block:
+ - name: Create user with expiration again to ensure no change is made in a new timezone
+ user:
+ name: ansibulluser
+ state: present
+ expires: 2529881062
+ register: user_test_different_tz
+ tags:
+ - timezone
+
+ - name: Ensure that no change was reported
+ assert:
+ that:
+ - user_test_different_tz is not changed
+ tags:
+ - timezone
+
+ always:
+ - name: Restore original timezone - {{ original_timezone.diff.before.name }}
+ timezone:
+ name: "{{ original_timezone.diff.before.name }}"
+ when: original_timezone.diff.before.name != "n/a"
+ tags:
+ - timezone
+
+ - name: Restore original timezone when n/a
+ file:
+ path: /etc/sysconfig/clock
+ state: absent
+ when:
+ - original_timezone.diff.before.name == "n/a"
+ - "'/etc/sysconfig/clock' in original_timezone.msg"
+ tags:
+ - timezone
+
+
+- name: Unexpire user
+ user:
+ name: ansibulluser
+ state: present
+ expires: -1
+ register: user_test_expires3
+
+- name: Verify un expiration date for Linux
+ block:
+ - name: LINUX | Get expiration date for ansibulluser
+ getent:
+ database: shadow
+ key: ansibulluser
+
+ - name: LINUX | Ensure proper expiration date was set
+ assert:
+ msg: "expiry is supposed to be empty or -1, not {{ getent_shadow['ansibulluser'][6] }}"
+ that:
+ - not getent_shadow['ansibulluser'][6] or getent_shadow['ansibulluser'][6] | int < 0
+ when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse']
+
+- name: Verify un expiration date for Linux/BSD
+ block:
+ - name: Unexpire user again to check for change
+ user:
+ name: ansibulluser
+ state: present
+ expires: -1
+ register: user_test_expires4
+
+ - name: Ensure first expiration reported a change and second did not
+ assert:
+ msg: The second run of the expiration removal task reported a change when it should not
+ that:
+ - user_test_expires3 is changed
+ - user_test_expires4 is not changed
+ when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse', 'FreeBSD']
+
+- name: Verify un expiration date for BSD
+ block:
+ - name: BSD | Get expiration date for ansibulluser
+ shell: 'grep ansibulluser /etc/master.passwd | cut -d: -f 7'
+ changed_when: no
+ register: bsd_account_expiration
+
+ - name: BSD | Ensure proper expiration date was set
+ assert:
+ msg: "expiry is supposed to be '0', not {{ bsd_account_expiration.stdout }}"
+ that:
+ - bsd_account_expiration.stdout == '0'
+ when: ansible_facts.os_family == 'FreeBSD'
diff --git a/test/integration/targets/user/tasks/test_expires_min_max.yml b/test/integration/targets/user/tasks/test_expires_min_max.yml
new file mode 100644
index 0000000..0b80379
--- /dev/null
+++ b/test/integration/targets/user/tasks/test_expires_min_max.yml
@@ -0,0 +1,73 @@
+# https://github.com/ansible/ansible/issues/68775
+- name: Test setting maximum expiration
+ when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse']
+ block:
+ - name: create user
+ user:
+ name: ansibulluser
+ state: present
+
+ - name: add maximum expire date for password
+ user:
+ name: ansibulluser
+ password_expire_max: 10
+ register: pass_max_1_0
+
+ - name: again add maximum expire date for password
+ user:
+ name: ansibulluser
+ password_expire_max: 10
+ register: pass_max_1_1
+
+ - name: validate result for maximum expire date
+ assert:
+ that:
+ - pass_max_1_0 is changed
+ - pass_max_1_1 is not changed
+
+ - name: add minimum expire date for password
+ user:
+ name: ansibulluser
+ password_expire_min: 5
+ register: pass_min_2_0
+
+ - name: again add minimum expire date for password
+ user:
+ name: ansibulluser
+ password_expire_min: 5
+ register: pass_min_2_1
+
+ - name: validate result for minimum expire date
+ assert:
+ that:
+ - pass_min_2_0 is changed
+ - pass_min_2_1 is not changed
+
+ - name: Get shadow data for ansibulluser
+ getent:
+ database: shadow
+ key: ansibulluser
+
+ - name: Ensure password expiration was set properly
+ assert:
+ that:
+ - ansible_facts.getent_shadow['ansibulluser'][2] == '5'
+ - ansible_facts.getent_shadow['ansibulluser'][3] == '10'
+
+ - name: Set min and max at the same time
+ user:
+ name: ansibulluser
+ # also checks that assigning 0 works
+ password_expire_min: 0
+ password_expire_max: 0
+
+ - name: Get shadow data for ansibulluser
+ getent:
+ database: shadow
+ key: ansibulluser
+
+ - name: Ensure password expiration was set properly
+ assert:
+ that:
+ - ansible_facts.getent_shadow['ansibulluser'][2] == '0'
+ - ansible_facts.getent_shadow['ansibulluser'][3] == '0'
diff --git a/test/integration/targets/user/tasks/test_expires_new_account.yml b/test/integration/targets/user/tasks/test_expires_new_account.yml
new file mode 100644
index 0000000..b77d137
--- /dev/null
+++ b/test/integration/targets/user/tasks/test_expires_new_account.yml
@@ -0,0 +1,55 @@
+# Test setting no expiration when creating a new account
+# https://github.com/ansible/ansible/issues/44155
+- name: Remove ansibulluser
+ user:
+ name: ansibulluser
+ state: absent
+
+- name: Create user account without expiration
+ user:
+ name: ansibulluser
+ state: present
+ expires: -1
+ register: user_test_create_no_expires_1
+
+- name: Create user account without expiration again
+ user:
+ name: ansibulluser
+ state: present
+ expires: -1
+ register: user_test_create_no_expires_2
+
+- name: Ensure changes were made appropriately
+ assert:
+ msg: Setting 'expires='-1 resulted in incorrect changes
+ that:
+ - user_test_create_no_expires_1 is changed
+ - user_test_create_no_expires_2 is not changed
+
+- name: Verify un expiration date for Linux
+ block:
+ - name: LINUX | Get expiration date for ansibulluser
+ getent:
+ database: shadow
+ key: ansibulluser
+
+ - name: LINUX | Ensure proper expiration date was set
+ assert:
+ msg: "expiry is supposed to be empty or -1, not {{ getent_shadow['ansibulluser'][6] }}"
+ that:
+ - not getent_shadow['ansibulluser'][6] or getent_shadow['ansibulluser'][6] | int < 0
+ when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse']
+
+- name: Verify un expiration date for BSD
+ block:
+ - name: BSD | Get expiration date for ansibulluser
+ shell: 'grep ansibulluser /etc/master.passwd | cut -d: -f 7'
+ changed_when: no
+ register: bsd_account_expiration
+
+ - name: BSD | Ensure proper expiration date was set
+ assert:
+ msg: "expiry is supposed to be '0', not {{ bsd_account_expiration.stdout }}"
+ that:
+ - bsd_account_expiration.stdout == '0'
+ when: ansible_facts.os_family == 'FreeBSD'
diff --git a/test/integration/targets/user/tasks/test_expires_new_account_epoch_negative.yml b/test/integration/targets/user/tasks/test_expires_new_account_epoch_negative.yml
new file mode 100644
index 0000000..77a07c4
--- /dev/null
+++ b/test/integration/targets/user/tasks/test_expires_new_account_epoch_negative.yml
@@ -0,0 +1,112 @@
+# Test setting epoch 0 expiration when creating a new account, then removing the expiry
+# https://github.com/ansible/ansible/issues/47114
+- name: Remove ansibulluser
+ user:
+ name: ansibulluser
+ state: absent
+
+- name: Create user account with epoch 0 expiration
+ user:
+ name: ansibulluser
+ state: present
+ expires: 0
+ register: user_test_expires_create0_1
+
+- name: Create user account with epoch 0 expiration again
+ user:
+ name: ansibulluser
+ state: present
+ expires: 0
+ register: user_test_expires_create0_2
+
+- name: Change the user account to remove the expiry time
+ user:
+ name: ansibulluser
+ expires: -1
+ register: user_test_remove_expires_1
+
+- name: Change the user account to remove the expiry time again
+ user:
+ name: ansibulluser
+ expires: -1
+ register: user_test_remove_expires_2
+
+
+- name: Verify un expiration date for Linux
+ block:
+ - name: LINUX | Ensure changes were made appropriately
+ assert:
+ msg: Creating an account with 'expries=0' then removing that expriation with 'expires=-1' resulted in incorrect changes
+ that:
+ - user_test_expires_create0_1 is changed
+ - user_test_expires_create0_2 is not changed
+ - user_test_remove_expires_1 is changed
+ - user_test_remove_expires_2 is not changed
+
+ - name: LINUX | Get expiration date for ansibulluser
+ getent:
+ database: shadow
+ key: ansibulluser
+
+ - name: LINUX | Ensure proper expiration date was set
+ assert:
+ msg: "expiry is supposed to be empty or -1, not {{ getent_shadow['ansibulluser'][6] }}"
+ that:
+ - not getent_shadow['ansibulluser'][6] or getent_shadow['ansibulluser'][6] | int < 0
+ when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse']
+
+
+- name: Verify proper expiration behavior for BSD
+ block:
+ - name: BSD | Ensure changes were made appropriately
+ assert:
+ msg: Creating an account with 'expries=0' then removing that expriation with 'expires=-1' resulted in incorrect changes
+ that:
+ - user_test_expires_create0_1 is changed
+ - user_test_expires_create0_2 is not changed
+ - user_test_remove_expires_1 is not changed
+ - user_test_remove_expires_2 is not changed
+ when: ansible_facts.os_family == 'FreeBSD'
+
+
+# Test expiration with a very large negative number. This should have the same
+# result as setting -1.
+- name: Set expiration date using very long negative number
+ user:
+ name: ansibulluser
+ state: present
+ expires: -2529881062
+ register: user_test_expires5
+
+- name: Ensure no change was made
+ assert:
+ that:
+ - user_test_expires5 is not changed
+
+- name: Verify un expiration date for Linux
+ block:
+ - name: LINUX | Get expiration date for ansibulluser
+ getent:
+ database: shadow
+ key: ansibulluser
+
+ - name: LINUX | Ensure proper expiration date was set
+ assert:
+ msg: "expiry is supposed to be empty or -1, not {{ getent_shadow['ansibulluser'][6] }}"
+ that:
+ - not getent_shadow['ansibulluser'][6] or getent_shadow['ansibulluser'][6] | int < 0
+ when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse']
+
+- name: Verify un expiration date for BSD
+ block:
+ - name: BSD | Get expiration date for ansibulluser
+ shell: 'grep ansibulluser /etc/master.passwd | cut -d: -f 7'
+ changed_when: no
+ register: bsd_account_expiration
+
+ - name: BSD | Ensure proper expiration date was set
+ assert:
+ msg: "expiry is supposed to be '0', not {{ bsd_account_expiration.stdout }}"
+ that:
+ - bsd_account_expiration.stdout == '0'
+ when: ansible_facts.os_family == 'FreeBSD'
diff --git a/test/integration/targets/user/tasks/test_local.yml b/test/integration/targets/user/tasks/test_local.yml
new file mode 100644
index 0000000..67c24a2
--- /dev/null
+++ b/test/integration/targets/user/tasks/test_local.yml
@@ -0,0 +1,196 @@
+## Check local mode
+# Even if we don't have a system that is bound to a directory, it's useful
+# to run with local: true to exercise the code path that reads through the local
+# user database file.
+# https://github.com/ansible/ansible/issues/50947
+
+- name: Create /etc/gshadow
+ file:
+ path: /etc/gshadow
+ state: touch
+ when: ansible_facts.os_family == 'Suse'
+ tags:
+ - user_test_local_mode
+
+- name: Create /etc/libuser.conf
+ file:
+ path: /etc/libuser.conf
+ state: touch
+ when:
+ - ansible_facts.distribution == 'Ubuntu'
+ - ansible_facts.distribution_major_version is version_compare('16', '==')
+ tags:
+ - user_test_local_mode
+
+- name: Ensure luseradd is present
+ action: "{{ ansible_facts.pkg_mgr }}"
+ args:
+ name: libuser
+ state: present
+ when: ansible_facts.system in ['Linux']
+ tags:
+ - user_test_local_mode
+
+- name: Create local account that already exists to check for warning
+ user:
+ name: root
+ local: yes
+ register: local_existing
+ tags:
+ - user_test_local_mode
+
+- name: Create local_ansibulluser
+ user:
+ name: local_ansibulluser
+ state: present
+ local: yes
+ register: local_user_test_1
+ tags:
+ - user_test_local_mode
+
+- name: Create local_ansibulluser again
+ user:
+ name: local_ansibulluser
+ state: present
+ local: yes
+ register: local_user_test_2
+ tags:
+ - user_test_local_mode
+
+- name: Remove local_ansibulluser
+ user:
+ name: local_ansibulluser
+ state: absent
+ remove: yes
+ local: yes
+ register: local_user_test_remove_1
+ tags:
+ - user_test_local_mode
+
+- name: Remove local_ansibulluser again
+ user:
+ name: local_ansibulluser
+ state: absent
+ remove: yes
+ local: yes
+ register: local_user_test_remove_2
+ tags:
+ - user_test_local_mode
+
+- name: Create test groups
+ group:
+ name: "{{ item }}"
+ loop:
+ - testgroup1
+ - testgroup2
+ - testgroup3
+ - testgroup4
+ - testgroup5
+ - local_ansibulluser
+ tags:
+ - user_test_local_mode
+
+- name: Create local_ansibulluser with groups
+ user:
+ name: local_ansibulluser
+ state: present
+ local: yes
+ groups: ['testgroup1', 'testgroup2']
+ register: local_user_test_3
+ ignore_errors: yes
+ tags:
+ - user_test_local_mode
+
+- name: Append groups for local_ansibulluser
+ user:
+ name: local_ansibulluser
+ state: present
+ local: yes
+ groups: ['testgroup3', 'testgroup4']
+ append: yes
+ register: local_user_test_4
+ ignore_errors: yes
+ tags:
+ - user_test_local_mode
+
+- name: Test append without groups for local_ansibulluser
+ user:
+ name: local_ansibulluser
+ state: present
+ append: yes
+ register: local_user_test_5
+ ignore_errors: yes
+ tags:
+ - user_test_local_mode
+
+- name: Assign named group for local_ansibulluser
+ user:
+ name: local_ansibulluser
+ state: present
+ local: yes
+ group: testgroup5
+ register: local_user_test_6
+ tags:
+ - user_test_local_mode
+
+# If we don't re-assign, then "Set user expiration" will
+# fail.
+- name: Re-assign named group for local_ansibulluser
+ user:
+ name: local_ansibulluser
+ state: present
+ local: yes
+ group: local_ansibulluser
+ ignore_errors: yes
+ tags:
+ - user_test_local_mode
+
+- name: Remove local_ansibulluser again
+ user:
+ name: local_ansibulluser
+ state: absent
+ remove: yes
+ local: yes
+ tags:
+ - user_test_local_mode
+
+- name: Remove test groups
+ group:
+ name: "{{ item }}"
+ state: absent
+ loop:
+ - testgroup1
+ - testgroup2
+ - testgroup3
+ - testgroup4
+ - testgroup5
+ - local_ansibulluser
+ tags:
+ - user_test_local_mode
+
+- name: Ensure local user accounts were created and removed properly
+ assert:
+ that:
+ - local_user_test_1 is changed
+ - local_user_test_2 is not changed
+ - local_user_test_3 is changed
+ - local_user_test_4 is changed
+ - local_user_test_6 is changed
+ - local_user_test_remove_1 is changed
+ - local_user_test_remove_2 is not changed
+ tags:
+ - user_test_local_mode
+
+- name: Ensure warnings were displayed properly
+ assert:
+ that:
+ - local_user_test_1['warnings'] | length > 0
+ - local_user_test_1['warnings'] | first is search('The local user account may already exist')
+ - local_user_test_5['warnings'] is search("'append' is set, but no 'groups' are specified. Use 'groups'")
+ - local_existing['warnings'] is not defined
+ when: ansible_facts.system in ['Linux']
+ tags:
+ - user_test_local_mode
+
+- name: Test expires for local users
+ import_tasks: test_local_expires.yml
diff --git a/test/integration/targets/user/tasks/test_local_expires.yml b/test/integration/targets/user/tasks/test_local_expires.yml
new file mode 100644
index 0000000..e662035
--- /dev/null
+++ b/test/integration/targets/user/tasks/test_local_expires.yml
@@ -0,0 +1,333 @@
+---
+## local user expires
+# Date is March 3, 2050
+
+- name: Remove local_ansibulluser
+ user:
+ name: local_ansibulluser
+ state: absent
+ remove: yes
+ local: yes
+ tags:
+ - user_test_local_mode
+
+- name: Set user expiration
+ user:
+ name: local_ansibulluser
+ state: present
+ local: yes
+ expires: 2529881062
+ register: user_test_local_expires1
+ tags:
+ - timezone
+ - user_test_local_mode
+
+- name: Set user expiration again to ensure no change is made
+ user:
+ name: local_ansibulluser
+ state: present
+ local: yes
+ expires: 2529881062
+ register: user_test_local_expires2
+ tags:
+ - timezone
+ - user_test_local_mode
+
+- name: Ensure that account with expiration was created and did not change on subsequent run
+ assert:
+ that:
+ - user_test_local_expires1 is changed
+ - user_test_local_expires2 is not changed
+ tags:
+ - user_test_local_mode
+
+- name: Verify expiration date for Linux
+ block:
+ - name: LINUX | Get expiration date for local_ansibulluser
+ getent:
+ database: shadow
+ key: local_ansibulluser
+ tags:
+ - user_test_local_mode
+
+ - name: LINUX | Ensure proper expiration date was set
+ assert:
+ that:
+ - getent_shadow['local_ansibulluser'][6] == '29281'
+ tags:
+ - user_test_local_mode
+ when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse']
+
+- name: Change timezone
+ timezone:
+ name: America/Denver
+ register: original_timezone
+ tags:
+ - timezone
+ - user_test_local_mode
+
+- name: Change system timezone to make sure expiration comparison works properly
+ block:
+ - name: Create user with expiration again to ensure no change is made in a new timezone
+ user:
+ name: local_ansibulluser
+ state: present
+ local: yes
+ expires: 2529881062
+ register: user_test_local_different_tz
+ tags:
+ - timezone
+ - user_test_local_mode
+
+ - name: Ensure that no change was reported
+ assert:
+ that:
+ - user_test_local_different_tz is not changed
+ tags:
+ - timezone
+ - user_test_local_mode
+
+ always:
+ - name: Restore original timezone - {{ original_timezone.diff.before.name }}
+ timezone:
+ name: "{{ original_timezone.diff.before.name }}"
+ when: original_timezone.diff.before.name != "n/a"
+ tags:
+ - timezone
+ - user_test_local_mode
+
+ - name: Restore original timezone when n/a
+ file:
+ path: /etc/sysconfig/clock
+ state: absent
+ when:
+ - original_timezone.diff.before.name == "n/a"
+ - "'/etc/sysconfig/clock' in original_timezone.msg"
+ tags:
+ - timezone
+ - user_test_local_mode
+
+
+- name: Unexpire user
+ user:
+ name: local_ansibulluser
+ state: present
+ local: yes
+ expires: -1
+ register: user_test_local_expires3
+ tags:
+ - user_test_local_mode
+
+- name: Verify un expiration date for Linux
+ block:
+ - name: LINUX | Get expiration date for local_ansibulluser
+ getent:
+ database: shadow
+ key: local_ansibulluser
+ tags:
+ - user_test_local_mode
+
+ - name: LINUX | Ensure proper expiration date was set
+ assert:
+ msg: "expiry is supposed to be empty or -1, not {{ getent_shadow['local_ansibulluser'][6] }}"
+ that:
+ - not getent_shadow['local_ansibulluser'][6] or getent_shadow['local_ansibulluser'][6] | int < 0
+ tags:
+ - user_test_local_mode
+ when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse']
+
+- name: Verify un expiration date for Linux/BSD
+ block:
+ - name: Unexpire user again to check for change
+ user:
+ name: local_ansibulluser
+ state: present
+ local: yes
+ expires: -1
+ register: user_test_local_expires4
+ tags:
+ - user_test_local_mode
+
+ - name: Ensure first expiration reported a change and second did not
+ assert:
+ msg: The second run of the expiration removal task reported a change when it should not
+ that:
+ - user_test_local_expires3 is changed
+ - user_test_local_expires4 is not changed
+ tags:
+ - user_test_local_mode
+ when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse', 'FreeBSD']
+
+# Test setting no expiration when creating a new account
+# https://github.com/ansible/ansible/issues/44155
+- name: Remove local_ansibulluser
+ user:
+ name: local_ansibulluser
+ state: absent
+ remove: yes
+ local: yes
+ tags:
+ - user_test_local_mode
+
+- name: Create user account without expiration
+ user:
+ name: local_ansibulluser
+ state: present
+ local: yes
+ expires: -1
+ register: user_test_local_create_no_expires_1
+ tags:
+ - user_test_local_mode
+
+- name: Create user account without expiration again
+ user:
+ name: local_ansibulluser
+ state: present
+ local: yes
+ expires: -1
+ register: user_test_local_create_no_expires_2
+ tags:
+ - user_test_local_mode
+
+- name: Ensure changes were made appropriately
+ assert:
+ msg: Setting 'expires='-1 resulted in incorrect changes
+ that:
+ - user_test_local_create_no_expires_1 is changed
+ - user_test_local_create_no_expires_2 is not changed
+ tags:
+ - user_test_local_mode
+
+- name: Verify un expiration date for Linux
+ block:
+ - name: LINUX | Get expiration date for local_ansibulluser
+ getent:
+ database: shadow
+ key: local_ansibulluser
+ tags:
+ - user_test_local_mode
+
+ - name: LINUX | Ensure proper expiration date was set
+ assert:
+ msg: "expiry is supposed to be empty or -1, not {{ getent_shadow['local_ansibulluser'][6] }}"
+ that:
+ - not getent_shadow['local_ansibulluser'][6] or getent_shadow['local_ansibulluser'][6] | int < 0
+ tags:
+ - user_test_local_mode
+ when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse']
+
+# Test setting epoch 0 expiration when creating a new account, then removing the expiry
+# https://github.com/ansible/ansible/issues/47114
+- name: Remove local_ansibulluser
+ user:
+ name: local_ansibulluser
+ state: absent
+ remove: yes
+ local: yes
+ tags:
+ - user_test_local_mode
+
+- name: Create user account with epoch 0 expiration
+ user:
+ name: local_ansibulluser
+ state: present
+ local: yes
+ expires: 0
+ register: user_test_local_expires_create0_1
+ tags:
+ - user_test_local_mode
+
+- name: Create user account with epoch 0 expiration again
+ user:
+ name: local_ansibulluser
+ state: present
+ local: yes
+ expires: 0
+ register: user_test_local_expires_create0_2
+ tags:
+ - user_test_local_mode
+
+- name: Change the user account to remove the expiry time
+ user:
+ name: local_ansibulluser
+ expires: -1
+ local: yes
+ register: user_test_local_remove_expires_1
+ tags:
+ - user_test_local_mode
+
+- name: Change the user account to remove the expiry time again
+ user:
+ name: local_ansibulluser
+ expires: -1
+ local: yes
+ register: user_test_local_remove_expires_2
+ tags:
+ - user_test_local_mode
+
+
+- name: Verify un expiration date for Linux
+ block:
+ - name: LINUX | Ensure changes were made appropriately
+ assert:
+ msg: Creating an account with 'expries=0' then removing that expriation with 'expires=-1' resulted in incorrect changes
+ that:
+ - user_test_local_expires_create0_1 is changed
+ - user_test_local_expires_create0_2 is not changed
+ - user_test_local_remove_expires_1 is changed
+ - user_test_local_remove_expires_2 is not changed
+ tags:
+ - user_test_local_mode
+
+ - name: LINUX | Get expiration date for local_ansibulluser
+ getent:
+ database: shadow
+ key: local_ansibulluser
+ tags:
+ - user_test_local_mode
+
+ - name: LINUX | Ensure proper expiration date was set
+ assert:
+ msg: "expiry is supposed to be empty or -1, not {{ getent_shadow['local_ansibulluser'][6] }}"
+ that:
+ - not getent_shadow['local_ansibulluser'][6] or getent_shadow['local_ansibulluser'][6] | int < 0
+ tags:
+ - user_test_local_mode
+ when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse']
+
+# Test expiration with a very large negative number. This should have the same
+# result as setting -1.
+- name: Set expiration date using very long negative number
+ user:
+ name: local_ansibulluser
+ state: present
+ local: yes
+ expires: -2529881062
+ register: user_test_local_expires5
+ tags:
+ - user_test_local_mode
+
+- name: Ensure no change was made
+ assert:
+ that:
+ - user_test_local_expires5 is not changed
+ tags:
+ - user_test_local_mode
+
+- name: Verify un expiration date for Linux
+ block:
+ - name: LINUX | Get expiration date for local_ansibulluser
+ getent:
+ database: shadow
+ key: local_ansibulluser
+ tags:
+ - user_test_local_mode
+
+ - name: LINUX | Ensure proper expiration date was set
+ assert:
+ msg: "expiry is supposed to be empty or -1, not {{ getent_shadow['local_ansibulluser'][6] }}"
+ that:
+ - not getent_shadow['local_ansibulluser'][6] or getent_shadow['local_ansibulluser'][6] | int < 0
+ tags:
+ - user_test_local_mode
+ when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse']
diff --git a/test/integration/targets/user/tasks/test_no_home_fallback.yml b/test/integration/targets/user/tasks/test_no_home_fallback.yml
new file mode 100644
index 0000000..f7627fa
--- /dev/null
+++ b/test/integration/targets/user/tasks/test_no_home_fallback.yml
@@ -0,0 +1,106 @@
+## create user without home and test fallback home dir create
+
+- name: Test home directory creation
+ when: ansible_facts.system != 'Darwin'
+ block:
+ - name: create the user
+ user:
+ name: ansibulluser
+
+ - name: delete the user and home dir
+ user:
+ name: ansibulluser
+ state: absent
+ force: true
+ remove: true
+
+ - name: create the user without home
+ user:
+ name: ansibulluser
+ create_home: no
+
+ - name: create the user home dir
+ user:
+ name: ansibulluser
+ register: user_create_home_fallback
+
+ - name: stat home dir
+ stat:
+ path: '{{ user_create_home_fallback.home }}'
+ register: user_create_home_fallback_dir
+
+ - name: read UMASK from /etc/login.defs and return mode
+ shell: |
+ import re
+ import os
+ try:
+ for line in open('/etc/login.defs').readlines():
+ m = re.match(r'^UMASK\s+(\d+)$', line)
+ if m:
+ umask = int(m.group(1), 8)
+ except:
+ umask = os.umask(0)
+ mode = oct(0o777 & ~umask)
+ print(str(mode).replace('o', ''))
+ args:
+ executable: "{{ ansible_python_interpreter }}"
+ register: user_login_defs_umask
+
+ - name: validate that user home dir is created
+ assert:
+ that:
+ - user_create_home_fallback is changed
+ - user_create_home_fallback_dir.stat.exists
+ - user_create_home_fallback_dir.stat.isdir
+ - user_create_home_fallback_dir.stat.pw_name == 'ansibulluser'
+ - user_create_home_fallback_dir.stat.mode == user_login_defs_umask.stdout
+
+- name: Create non-system user
+ when: ansible_facts.distribution == "MacOSX"
+ block:
+ - name: create non-system user on macOS to test the shell is set to /bin/bash
+ user:
+ name: macosuser
+ register: macosuser_output
+
+ - name: validate the shell is set to /bin/bash
+ assert:
+ that:
+ - 'macosuser_output.shell == "/bin/bash"'
+
+ - name: cleanup
+ user:
+ name: macosuser
+ state: absent
+
+ - name: create system user on macOS to test the shell is set to /usr/bin/false
+ user:
+ name: macosuser
+ system: yes
+ register: macosuser_output
+
+ - name: validate the shell is set to /usr/bin/false
+ assert:
+ that:
+ - 'macosuser_output.shell == "/usr/bin/false"'
+
+ - name: cleanup
+ user:
+ name: macosuser
+ state: absent
+
+ - name: create non-system user on macos and set the shell to /bin/sh
+ user:
+ name: macosuser
+ shell: /bin/sh
+ register: macosuser_output
+
+ - name: validate the shell is set to /bin/sh
+ assert:
+ that:
+ - 'macosuser_output.shell == "/bin/sh"'
+
+ - name: cleanup
+ user:
+ name: macosuser
+ state: absent
diff --git a/test/integration/targets/user/tasks/test_password_lock.yml b/test/integration/targets/user/tasks/test_password_lock.yml
new file mode 100644
index 0000000..dde374e
--- /dev/null
+++ b/test/integration/targets/user/tasks/test_password_lock.yml
@@ -0,0 +1,140 @@
+- name: Test password lock
+ when: ansible_facts.system in ['FreeBSD', 'OpenBSD', 'Linux']
+ block:
+ - name: Remove ansibulluser
+ user:
+ name: ansibulluser
+ state: absent
+ remove: yes
+
+ - name: Create ansibulluser with password
+ user:
+ name: ansibulluser
+ password: "$6$rounds=656000$TT4O7jz2M57npccl$33LF6FcUMSW11qrESXL1HX0BS.bsiT6aenFLLiVpsQh6hDtI9pJh5iY7x8J7ePkN4fP8hmElidHXaeD51pbGS."
+
+ - name: Lock account without password parameter
+ user:
+ name: ansibulluser
+ password_lock: yes
+ register: password_lock_1
+
+ - name: Lock account without password parameter again
+ user:
+ name: ansibulluser
+ password_lock: yes
+ register: password_lock_2
+
+ - name: Unlock account without password parameter
+ user:
+ name: ansibulluser
+ password_lock: no
+ register: password_lock_3
+
+ - name: Unlock account without password parameter again
+ user:
+ name: ansibulluser
+ password_lock: no
+ register: password_lock_4
+
+ - name: Lock account with password parameter
+ user:
+ name: ansibulluser
+ password_lock: yes
+ password: "$6$rounds=656000$TT4O7jz2M57npccl$33LF6FcUMSW11qrESXL1HX0BS.bsiT6aenFLLiVpsQh6hDtI9pJh5iY7x8J7ePkN4fP8hmElidHXaeD51pbGS."
+ register: password_lock_5
+
+ - name: Lock account with password parameter again
+ user:
+ name: ansibulluser
+ password_lock: yes
+ password: "$6$rounds=656000$TT4O7jz2M57npccl$33LF6FcUMSW11qrESXL1HX0BS.bsiT6aenFLLiVpsQh6hDtI9pJh5iY7x8J7ePkN4fP8hmElidHXaeD51pbGS."
+ register: password_lock_6
+
+ - name: Unlock account with password parameter
+ user:
+ name: ansibulluser
+ password_lock: no
+ password: "$6$rounds=656000$TT4O7jz2M57npccl$33LF6FcUMSW11qrESXL1HX0BS.bsiT6aenFLLiVpsQh6hDtI9pJh5iY7x8J7ePkN4fP8hmElidHXaeD51pbGS."
+ register: password_lock_7
+
+ - name: Unlock account with password parameter again
+ user:
+ name: ansibulluser
+ password_lock: no
+ password: "$6$rounds=656000$TT4O7jz2M57npccl$33LF6FcUMSW11qrESXL1HX0BS.bsiT6aenFLLiVpsQh6hDtI9pJh5iY7x8J7ePkN4fP8hmElidHXaeD51pbGS."
+ register: password_lock_8
+
+ - name: Ensure task reported changes appropriately
+ assert:
+ msg: The password_lock tasks did not make changes appropriately
+ that:
+ - password_lock_1 is changed
+ - password_lock_2 is not changed
+ - password_lock_3 is changed
+ - password_lock_4 is not changed
+ - password_lock_5 is changed
+ - password_lock_6 is not changed
+ - password_lock_7 is changed
+ - password_lock_8 is not changed
+
+ - name: Lock account
+ user:
+ name: ansibulluser
+ password_lock: yes
+
+ - name: Verify account lock for BSD
+ when: ansible_facts.system in ['FreeBSD', 'OpenBSD']
+ block:
+ - name: BSD | Get account status
+ shell: "{{ status_command[ansible_facts['system']] }}"
+ register: account_status_locked
+
+ - name: Unlock account
+ user:
+ name: ansibulluser
+ password_lock: no
+
+ - name: BSD | Get account status
+ shell: "{{ status_command[ansible_facts['system']] }}"
+ register: account_status_unlocked
+
+ - name: FreeBSD | Ensure account is locked
+ assert:
+ that:
+ - "'LOCKED' in account_status_locked.stdout"
+ - "'LOCKED' not in account_status_unlocked.stdout"
+ when: ansible_facts['system'] == 'FreeBSD'
+
+ - name: Verify account lock for Linux
+ when: ansible_facts.system == 'Linux'
+ block:
+ - name: LINUX | Get account status
+ getent:
+ database: shadow
+ key: ansibulluser
+
+ - name: LINUX | Ensure account is locked
+ assert:
+ that:
+ - getent_shadow['ansibulluser'][0].startswith('!')
+
+ - name: Unlock account
+ user:
+ name: ansibulluser
+ password_lock: no
+
+ - name: LINUX | Get account status
+ getent:
+ database: shadow
+ key: ansibulluser
+
+ - name: LINUX | Ensure account is unlocked
+ assert:
+ that:
+ - not getent_shadow['ansibulluser'][0].startswith('!')
+
+ always:
+ - name: Unlock account
+ user:
+ name: ansibulluser
+ password_lock: no
diff --git a/test/integration/targets/user/tasks/test_password_lock_new_user.yml b/test/integration/targets/user/tasks/test_password_lock_new_user.yml
new file mode 100644
index 0000000..dd4f23d
--- /dev/null
+++ b/test/integration/targets/user/tasks/test_password_lock_new_user.yml
@@ -0,0 +1,63 @@
+- name: Test password lock
+ when: ansible_facts.system in ['FreeBSD', 'OpenBSD', 'Linux']
+ block:
+ - name: Remove ansibulluser
+ user:
+ name: ansibulluser
+ state: absent
+ remove: yes
+
+ - name: Create ansibulluser with password and locked
+ user:
+ name: ansibulluser
+ password: "$6$rounds=656000$TT4O7jz2M57npccl$33LF6FcUMSW11qrESXL1HX0BS.bsiT6aenFLLiVpsQh6hDtI9pJh5iY7x8J7ePkN4fP8hmElidHXaeD51pbGS."
+ password_lock: yes
+ register: create_with_lock_1
+
+ - name: Create ansibulluser with password and locked again
+ user:
+ name: ansibulluser
+ password: "$6$rounds=656000$TT4O7jz2M57npccl$33LF6FcUMSW11qrESXL1HX0BS.bsiT6aenFLLiVpsQh6hDtI9pJh5iY7x8J7ePkN4fP8hmElidHXaeD51pbGS."
+ password_lock: yes
+ register: create_with_lock_2
+
+ - name: Ensure task reported changes appropriately
+ assert:
+ msg: The password_lock tasks did not make changes appropriately
+ that:
+ - create_with_lock_1 is changed
+ - create_with_lock_2 is not changed
+
+ - name: Verify account lock for BSD
+ when: ansible_facts.system in ['FreeBSD', 'OpenBSD']
+ block:
+ - name: BSD | Get account status
+ shell: "{{ status_command[ansible_facts['system']] }}"
+ register: account_status_locked
+
+ - name: FreeBSD | Ensure account is locked
+ assert:
+ that:
+ - "'LOCKED' in account_status_locked.stdout"
+ when: ansible_facts.system == 'FreeBSD'
+
+
+ - name: Verify account lock for Linux
+ when: ansible_facts.system == 'Linux'
+ block:
+ - name: LINUX | Get account status
+ getent:
+ database: shadow
+ key: ansibulluser
+
+ - name: LINUX | Ensure account is locked
+ assert:
+ that:
+ - getent_shadow['ansibulluser'][0].startswith('!')
+
+
+ always:
+ - name: Unlock account
+ user:
+ name: ansibulluser
+ password_lock: no
diff --git a/test/integration/targets/user/tasks/test_remove_user.yml b/test/integration/targets/user/tasks/test_remove_user.yml
new file mode 100644
index 0000000..dea71cb
--- /dev/null
+++ b/test/integration/targets/user/tasks/test_remove_user.yml
@@ -0,0 +1,19 @@
+- name: try to delete the user
+ user:
+ name: ansibulluser
+ state: absent
+ force: true
+ register: user_test2
+
+- name: make a new list of users
+ script: userlist.sh {{ ansible_facts.distribution }}
+ register: user_names2
+
+- debug:
+ var: user_names2
+ verbosity: 2
+
+- name: validate results for testcase 2
+ assert:
+ that:
+ - '"ansibulluser" not in user_names2.stdout_lines'
diff --git a/test/integration/targets/user/tasks/test_shadow_backup.yml b/test/integration/targets/user/tasks/test_shadow_backup.yml
new file mode 100644
index 0000000..2655fbf
--- /dev/null
+++ b/test/integration/targets/user/tasks/test_shadow_backup.yml
@@ -0,0 +1,21 @@
+- name: Test shadow backup on Solaris
+ when: ansible_facts.os_family == 'Solaris'
+ block:
+ - name: Create a user to test shadow file backup
+ user:
+ name: ansibulluser
+ state: present
+ register: result
+
+ - name: Find shadow backup files
+ find:
+ path: /etc
+ patterns: 'shadow\..*~$'
+ use_regex: yes
+ register: shadow_backups
+
+ - name: Assert that a backup file was created
+ assert:
+ that:
+ - result.bakup
+ - shadow_backups.files | map(attribute='path') | list | length > 0
diff --git a/test/integration/targets/user/tasks/test_ssh_key_passphrase.yml b/test/integration/targets/user/tasks/test_ssh_key_passphrase.yml
new file mode 100644
index 0000000..f0725ed
--- /dev/null
+++ b/test/integration/targets/user/tasks/test_ssh_key_passphrase.yml
@@ -0,0 +1,30 @@
+# Test creating ssh key with passphrase
+- name: Remove ansibulluser
+ user:
+ name: ansibulluser
+ state: absent
+
+- name: Create user with ssh key
+ user:
+ name: ansibulluser
+ state: present
+ generate_ssh_key: yes
+ force: yes
+ ssh_key_file: .ssh/test_id_rsa
+ ssh_key_passphrase: secret_passphrase
+ register: ansibulluser_create_with_ssh_key
+
+- name: Unlock ssh key
+ command: "ssh-keygen -y -f {{ ansibulluser_create_with_ssh_key.ssh_key_file|quote }} -P secret_passphrase"
+ register: result
+
+- name: Check that ssh key was unlocked successfully
+ assert:
+ that:
+ - result.rc == 0
+
+- name: Clean ssh key
+ file:
+ path: "{{ ansibulluser_create_with_ssh_key.ssh_key_file }}"
+ state: absent
+ when: ansible_os_family == 'FreeBSD'
diff --git a/test/integration/targets/user/tasks/test_umask.yml b/test/integration/targets/user/tasks/test_umask.yml
new file mode 100644
index 0000000..9e16297
--- /dev/null
+++ b/test/integration/targets/user/tasks/test_umask.yml
@@ -0,0 +1,57 @@
+---
+- name: remove comments of /etc/login.defs
+ command: sed -e '/^[ \t]*#/d' /etc/login.defs
+ register: logindefs
+
+- block:
+ - name: Create user with 000 umask
+ user:
+ name: umaskuser_test_1
+ umask: "000"
+ register: umaskuser_test_1
+
+ - name: Create user with 077 umask
+ user:
+ name: umaskuser_test_2
+ umask: "077"
+ register: umaskuser_test_2
+
+ - name: check permissions on created home folder
+ stat:
+ path: "{{ user_home_prefix[ansible_facts.system] }}/umaskuser_test_1"
+ register: umaskuser_test_1_path
+
+ - name: check permissions on created home folder
+ stat:
+ path: "{{ user_home_prefix[ansible_facts.system] }}/umaskuser_test_2"
+ register: umaskuser_test_2_path
+
+ - name: remove created users
+ user:
+ name: "{{ item }}"
+ state: absent
+ register: umaskuser_test_remove
+ loop:
+ - umaskuser_test_1
+ - umaskuser_test_2
+
+ - name: Ensure correct umask has been set on created users
+ assert:
+ that:
+ - umaskuser_test_1_path.stat.mode == "0777"
+ - umaskuser_test_2_path.stat.mode == "0700"
+ - umaskuser_test_remove is changed
+ when: logindefs.stdout_lines is not search ("HOME_MODE")
+
+- name: Create user with setting both umask and local
+ user:
+ name: umaskuser_test_3
+ umask: "077"
+ local: true
+ register: umaskuser_test_3
+ ignore_errors: true
+
+- name: Ensure task has been failed
+ assert:
+ that:
+ - umaskuser_test_3 is failed
diff --git a/test/integration/targets/user/vars/main.yml b/test/integration/targets/user/vars/main.yml
new file mode 100644
index 0000000..4b328f7
--- /dev/null
+++ b/test/integration/targets/user/vars/main.yml
@@ -0,0 +1,13 @@
+user_home_prefix:
+ Linux: '/home'
+ FreeBSD: '/home'
+ SunOS: '/home'
+ Darwin: '/Users'
+
+status_command:
+ OpenBSD: "grep ansibulluser /etc/master.passwd | cut -d ':' -f 2"
+ FreeBSD: 'pw user show ansibulluser'
+
+default_user_group:
+ openSUSE Leap: users
+ MacOSX: admin
diff --git a/test/integration/targets/var_blending/aliases b/test/integration/targets/var_blending/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/var_blending/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/var_blending/group_vars/all b/test/integration/targets/var_blending/group_vars/all
new file mode 100644
index 0000000..30aa3d6
--- /dev/null
+++ b/test/integration/targets/var_blending/group_vars/all
@@ -0,0 +1,9 @@
+a: 999
+b: 998
+c: 997
+d: 996
+uno: 1
+dos: 2
+tres: 3
+etest: 'from group_vars'
+inventory_beats_default: 'narf'
diff --git a/test/integration/targets/var_blending/group_vars/local b/test/integration/targets/var_blending/group_vars/local
new file mode 100644
index 0000000..8feb93f
--- /dev/null
+++ b/test/integration/targets/var_blending/group_vars/local
@@ -0,0 +1 @@
+tres: 'three'
diff --git a/test/integration/targets/var_blending/host_vars/testhost b/test/integration/targets/var_blending/host_vars/testhost
new file mode 100644
index 0000000..49271ae
--- /dev/null
+++ b/test/integration/targets/var_blending/host_vars/testhost
@@ -0,0 +1,4 @@
+a: 1
+b: 2
+c: 3
+d: 4
diff --git a/test/integration/targets/var_blending/inventory b/test/integration/targets/var_blending/inventory
new file mode 100644
index 0000000..f0afb18
--- /dev/null
+++ b/test/integration/targets/var_blending/inventory
@@ -0,0 +1,26 @@
+[local]
+testhost ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+testhost2 ansible_connection=local # connections are never made to this host, only host vars are accessed
+
+# the following inline declarations are accompanied
+# by (preferred) group_vars/ and host_vars/ variables
+# and are used in testing of variable precedence
+
+[arbitrary_parent:children]
+local
+
+[local:vars]
+parent_var=6000
+groups_tree_var=5000
+
+[arbitrary_parent:vars]
+groups_tree_var=4000
+overridden_in_parent=1000
+
+[arbitrary_grandparent:children]
+arbitrary_parent
+
+[arbitrary_grandparent:vars]
+groups_tree_var=3000
+grandparent_var=2000
+overridden_in_parent=2000
diff --git a/test/integration/targets/var_blending/roles/test_var_blending/defaults/main.yml b/test/integration/targets/var_blending/roles/test_var_blending/defaults/main.yml
new file mode 100644
index 0000000..671a127
--- /dev/null
+++ b/test/integration/targets/var_blending/roles/test_var_blending/defaults/main.yml
@@ -0,0 +1,4 @@
+etest: "from role defaults"
+role_var_beats_default: "shouldn't see this"
+parameterized_beats_default: "shouldn't see this"
+inventory_beats_default: "shouldn't see this"
diff --git a/test/integration/targets/var_blending/roles/test_var_blending/files/foo.txt b/test/integration/targets/var_blending/roles/test_var_blending/files/foo.txt
new file mode 100644
index 0000000..d51be39
--- /dev/null
+++ b/test/integration/targets/var_blending/roles/test_var_blending/files/foo.txt
@@ -0,0 +1,77 @@
+The value of groups_tree_var = 5000.
+This comes from host, not the parents or grandparents.
+
+The value of the grandparent variable grandparent_var is
+not overridden and is = 2000
+
+The value of the parent variable is not overridden and
+is = 6000
+
+The variable 'overridden_in_parent' is set in the parent
+and grandparent, so the parent wins. It's value is = 1000.
+
+The values of 'uno', 'dos', and 'tres' are set in group_vars/all but 'tres' is
+set to the value of 'three' in group_vars/local, which should override it.
+
+uno = 1
+dos = 2
+tres = three
+
+The values of 'a', 'b', 'c', and 'd' are set in host_vars/local and should not
+be clobbered by values that are also set in group_vars.
+
+a = 1
+b = 2
+c = 3
+d = 4
+
+The value of 'badwolf' is set via the include_vars plugin.
+
+badwolf = badwolf
+
+The value of 'winter' is set via the main.yml in the role.
+
+winter = coming
+
+Here's an arbitrary variable set as vars_files in the playbook.
+
+vars_file_var = 321
+
+And vars.
+
+vars = 123
+
+Variables about other hosts can be looked up via hostvars. This includes
+facts but here we'll just access a variable defined in the groups.
+
+999
+
+Ansible has pretty basic precedence rules for variable overriding. We already have
+some tests above about group order. Here are a few more.
+
+ * -e variables always win
+ * then comes "most everything else"
+ * then comes variables defined in inventory
+ * then "role defaults", which are the most "defaulty" and lose in priority to everything.
+
+Given the above rules, here's a test that a -e variable overrides inventory,
+and also defaults, and role vars.
+
+etest = from -e
+
+Now a test to make sure role variables can override inventory variables.
+
+role_var_beats_inventory = chevron 5 encoded
+
+Role variables should also beat defaults.
+
+role_var_beats_default = chevron 6 encoded
+
+But defaults are lower priority than inventory, so inventory should win.
+
+inventory_beats_default = narf
+
+That's the end of the precedence tests for now, but more are welcome.
+
+
+
diff --git a/test/integration/targets/var_blending/roles/test_var_blending/tasks/main.yml b/test/integration/targets/var_blending/roles/test_var_blending/tasks/main.yml
new file mode 100644
index 0000000..f2b2e54
--- /dev/null
+++ b/test/integration/targets/var_blending/roles/test_var_blending/tasks/main.yml
@@ -0,0 +1,57 @@
+# test code
+# (c) 2014, Michael DeHaan <michael.dehaan@gmail.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- include_vars: more_vars.yml
+
+- set_fact:
+ output_dir: "{{ lookup('env', 'OUTPUT_DIR') }}"
+
+- name: deploy a template that will use variables at various levels
+ template: src=foo.j2 dest={{output_dir}}/foo.templated
+ register: template_result
+
+- name: copy known good into place
+ copy: src=foo.txt dest={{output_dir}}/foo.txt
+
+- name: compare templated file to known good
+ shell: diff {{output_dir}}/foo.templated {{output_dir}}/foo.txt
+ register: diff_result
+
+- name: verify templated file matches known good
+ assert:
+ that:
+ - 'diff_result.stdout == ""'
+
+- name: check debug variable with same name as var content
+ debug: var=same_value_as_var_name_var
+ register: same_value_as_var_name
+
+- name: check debug variable output when variable is undefined
+ debug: var=undefined_variable
+ register: var_undefined
+
+- assert:
+ that:
+ - "'VARIABLE IS NOT DEFINED!' in var_undefined.undefined_variable"
+ - same_value_as_var_name.same_value_as_var_name_var == 'same_value_as_var_name_var'
+
+- name: cleanup temporary template output
+ file: path={{output_dir}}/foo.templated state=absent
+
+- name: cleanup temporary copy
+ file: path={{output_dir}}/foo.txt state=absent
diff --git a/test/integration/targets/var_blending/roles/test_var_blending/templates/foo.j2 b/test/integration/targets/var_blending/roles/test_var_blending/templates/foo.j2
new file mode 100644
index 0000000..10709b1
--- /dev/null
+++ b/test/integration/targets/var_blending/roles/test_var_blending/templates/foo.j2
@@ -0,0 +1,77 @@
+The value of groups_tree_var = {{ groups_tree_var }}.
+This comes from host, not the parents or grandparents.
+
+The value of the grandparent variable grandparent_var is
+not overridden and is = {{ grandparent_var }}
+
+The value of the parent variable is not overridden and
+is = {{ parent_var }}
+
+The variable 'overridden_in_parent' is set in the parent
+and grandparent, so the parent wins. It's value is = {{ overridden_in_parent }}.
+
+The values of 'uno', 'dos', and 'tres' are set in group_vars/all but 'tres' is
+set to the value of 'three' in group_vars/local, which should override it.
+
+uno = {{ uno }}
+dos = {{ dos }}
+tres = {{ tres }}
+
+The values of 'a', 'b', 'c', and 'd' are set in host_vars/local and should not
+be clobbered by values that are also set in group_vars.
+
+a = {{ a }}
+b = {{ b }}
+c = {{ c }}
+d = {{ d }}
+
+The value of 'badwolf' is set via the include_vars plugin.
+
+badwolf = {{ badwolf }}
+
+The value of 'winter' is set via the main.yml in the role.
+
+winter = {{ winter }}
+
+Here's an arbitrary variable set as vars_files in the playbook.
+
+vars_file_var = {{ vars_file_var }}
+
+And vars.
+
+vars = {{ vars_var }}
+
+Variables about other hosts can be looked up via hostvars. This includes
+facts but here we'll just access a variable defined in the groups.
+
+{{ hostvars['testhost2']['a'] }}
+
+Ansible has pretty basic precedence rules for variable overriding. We already have
+some tests above about group order. Here are a few more.
+
+ * -e variables always win
+ * then comes "most everything else"
+ * then comes variables defined in inventory
+ * then "role defaults", which are the most "defaulty" and lose in priority to everything.
+
+Given the above rules, here's a test that a -e variable overrides inventory,
+and also defaults, and role vars.
+
+etest = {{ etest }}
+
+Now a test to make sure role variables can override inventory variables.
+
+role_var_beats_inventory = {{ role_var_beats_inventory }}
+
+Role variables should also beat defaults.
+
+role_var_beats_default = {{ role_var_beats_default }}
+
+But defaults are lower priority than inventory, so inventory should win.
+
+inventory_beats_default = {{ inventory_beats_default }}
+
+That's the end of the precedence tests for now, but more are welcome.
+
+
+
diff --git a/test/integration/targets/var_blending/roles/test_var_blending/vars/main.yml b/test/integration/targets/var_blending/roles/test_var_blending/vars/main.yml
new file mode 100644
index 0000000..1bb08bf
--- /dev/null
+++ b/test/integration/targets/var_blending/roles/test_var_blending/vars/main.yml
@@ -0,0 +1,4 @@
+winter: coming
+etest: 'from role vars'
+role_var_beats_inventory: 'chevron 5 encoded'
+role_var_beats_default: 'chevron 6 encoded'
diff --git a/test/integration/targets/var_blending/roles/test_var_blending/vars/more_vars.yml b/test/integration/targets/var_blending/roles/test_var_blending/vars/more_vars.yml
new file mode 100644
index 0000000..bac93d3
--- /dev/null
+++ b/test/integration/targets/var_blending/roles/test_var_blending/vars/more_vars.yml
@@ -0,0 +1,3 @@
+badwolf: badwolf
+
+same_value_as_var_name_var: "same_value_as_var_name_var"
diff --git a/test/integration/targets/var_blending/runme.sh b/test/integration/targets/var_blending/runme.sh
new file mode 100755
index 0000000..d0cf7f0
--- /dev/null
+++ b/test/integration/targets/var_blending/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook test_var_blending.yml -i inventory -e @test_vars.yml -v "$@"
diff --git a/test/integration/targets/var_blending/test_var_blending.yml b/test/integration/targets/var_blending/test_var_blending.yml
new file mode 100644
index 0000000..88a35b2
--- /dev/null
+++ b/test/integration/targets/var_blending/test_var_blending.yml
@@ -0,0 +1,8 @@
+- hosts: testhost
+ vars_files:
+ - vars_file.yml
+ vars:
+ vars_var: 123
+ gather_facts: True
+ roles:
+ - { role: test_var_blending, parameterized_beats_default: 1234, tags: test_var_blending }
diff --git a/test/integration/targets/var_blending/test_vars.yml b/test/integration/targets/var_blending/test_vars.yml
new file mode 100644
index 0000000..abb71a5
--- /dev/null
+++ b/test/integration/targets/var_blending/test_vars.yml
@@ -0,0 +1 @@
+etest: 'from -e'
diff --git a/test/integration/targets/var_blending/vars_file.yml b/test/integration/targets/var_blending/vars_file.yml
new file mode 100644
index 0000000..971e16a
--- /dev/null
+++ b/test/integration/targets/var_blending/vars_file.yml
@@ -0,0 +1,12 @@
+# this file is here to support testing vars_files in the blending tests only.
+# in general define test data in the individual role:
+# roles/role_name/vars/main.yml
+
+foo: "Hello"
+things1:
+ - 1
+ - 2
+things2:
+ - "{{ foo }}"
+ - "{{ foob | default('') }}"
+vars_file_var: 321
diff --git a/test/integration/targets/var_inheritance/aliases b/test/integration/targets/var_inheritance/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/var_inheritance/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/var_inheritance/tasks/main.yml b/test/integration/targets/var_inheritance/tasks/main.yml
new file mode 100644
index 0000000..48d7b3d
--- /dev/null
+++ b/test/integration/targets/var_inheritance/tasks/main.yml
@@ -0,0 +1,16 @@
+- name: outer
+ vars:
+ myvar: abc
+ block:
+ - assert:
+ that:
+ - "myvar == 'abc'"
+
+ - name: inner block
+ vars:
+ myvar: 123
+ block:
+
+ - assert:
+ that:
+ - myvar|int == 123
diff --git a/test/integration/targets/var_precedence/aliases b/test/integration/targets/var_precedence/aliases
new file mode 100644
index 0000000..8278ec8
--- /dev/null
+++ b/test/integration/targets/var_precedence/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group3
+context/controller
diff --git a/test/integration/targets/var_precedence/ansible-var-precedence-check.py b/test/integration/targets/var_precedence/ansible-var-precedence-check.py
new file mode 100755
index 0000000..fc31688
--- /dev/null
+++ b/test/integration/targets/var_precedence/ansible-var-precedence-check.py
@@ -0,0 +1,544 @@
+#!/usr/bin/env python
+
+# A tool to check the order of precedence for ansible variables
+# https://github.com/ansible/ansible/blob/devel/test/integration/test_var_precedence.yml
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+import os
+import sys
+import shutil
+import stat
+import subprocess
+import tempfile
+import yaml
+from pprint import pprint
+from optparse import OptionParser
+from jinja2 import Environment
+
+ENV = Environment()
+TESTDIR = tempfile.mkdtemp()
+
+
+def run_command(args, cwd=None):
+ p = subprocess.Popen(
+ args,
+ stderr=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ shell=True,
+ cwd=cwd,
+ )
+ (so, se) = p.communicate()
+ return (p.returncode, so, se)
+
+
+def clean_test_dir():
+ if os.path.isdir(TESTDIR):
+ shutil.rmtree(TESTDIR)
+ os.makedirs(TESTDIR)
+
+
+class Role(object):
+ def __init__(self, name):
+ self.name = name
+ self.load = True
+ self.dependencies = []
+ self.defaults = False
+ self.vars = False
+ self.tasks = []
+ self.params = dict()
+
+ def write_role(self):
+
+ fpath = os.path.join(TESTDIR, 'roles', self.name)
+ if not os.path.isdir(fpath):
+ os.makedirs(fpath)
+
+ if self.defaults:
+ # roles/x/defaults/main.yml
+ fpath = os.path.join(TESTDIR, 'roles', self.name, 'defaults')
+ if not os.path.isdir(fpath):
+ os.makedirs(fpath)
+ fname = os.path.join(fpath, 'main.yml')
+ with open(fname, 'w') as f:
+ f.write('findme: %s\n' % self.name)
+
+ if self.vars:
+ # roles/x/vars/main.yml
+ fpath = os.path.join(TESTDIR, 'roles', self.name, 'vars')
+ if not os.path.isdir(fpath):
+ os.makedirs(fpath)
+ fname = os.path.join(fpath, 'main.yml')
+ with open(fname, 'w') as f:
+ f.write('findme: %s\n' % self.name)
+
+ if self.dependencies:
+ fpath = os.path.join(TESTDIR, 'roles', self.name, 'meta')
+ if not os.path.isdir(fpath):
+ os.makedirs(fpath)
+ fname = os.path.join(fpath, 'main.yml')
+ with open(fname, 'w') as f:
+ f.write('dependencies:\n')
+ for dep in self.dependencies:
+ f.write('- { role: %s }\n' % dep)
+
+
+class DynamicInventory(object):
+ BASESCRIPT = '''#!/usr/bin/python
+import json
+data = """{{ data }}"""
+data = json.loads(data)
+print(json.dumps(data, indent=2, sort_keys=True))
+'''
+
+ BASEINV = {
+ '_meta': {
+ 'hostvars': {
+ 'testhost': {}
+ }
+ }
+ }
+
+ def __init__(self, features):
+ self.ENV = Environment()
+ self.features = features
+ self.fpath = None
+ self.inventory = self.BASEINV.copy()
+ self.build()
+
+ def build(self):
+ xhost = 'testhost'
+ if 'script_host' in self.features:
+ self.inventory['_meta']['hostvars'][xhost]['findme'] = 'script_host'
+ else:
+ self.inventory['_meta']['hostvars'][xhost] = {}
+
+ if 'script_child' in self.features:
+ self.inventory['child'] = {
+ 'hosts': [xhost],
+ 'vars': {'findme': 'script_child'}
+ }
+
+ if 'script_parent' in self.features:
+
+ self.inventory['parent'] = {
+ 'vars': {'findme': 'script_parent'}
+ }
+
+ if 'script_child' in self.features:
+ self.inventory['parent']['children'] = ['child']
+ else:
+ self.inventory['parent']['hosts'] = [xhost]
+
+ if 'script_all' in self.features:
+ self.inventory['all'] = {
+ 'hosts': [xhost],
+ 'vars': {
+ 'findme': 'script_all'
+ },
+ }
+ else:
+ self.inventory['all'] = {
+ 'hosts': [xhost],
+ }
+
+ def write_script(self):
+ fdir = os.path.join(TESTDIR, 'inventory')
+ if not os.path.isdir(fdir):
+ os.makedirs(fdir)
+ fpath = os.path.join(fdir, 'hosts')
+ # fpath = os.path.join(TESTDIR, 'inventory')
+ self.fpath = fpath
+
+ data = json.dumps(self.inventory)
+ t = self.ENV.from_string(self.BASESCRIPT)
+ fdata = t.render(data=data)
+ with open(fpath, 'w') as f:
+ f.write(fdata + '\n')
+ st = os.stat(fpath)
+ os.chmod(fpath, st.st_mode | stat.S_IEXEC)
+
+
+class VarTestMaker(object):
+ def __init__(self, features, dynamic_inventory=False):
+ clean_test_dir()
+ self.dynamic_inventory = dynamic_inventory
+ self.di = None
+ self.features = features[:]
+ self.inventory = ''
+ self.playvars = dict()
+ self.varsfiles = []
+ self.playbook = dict(hosts='testhost', gather_facts=False)
+ self.tasks = []
+ self.roles = []
+ self.ansible_command = None
+ self.stdout = None
+
+ def write_playbook(self):
+ fname = os.path.join(TESTDIR, 'site.yml')
+ pb_copy = self.playbook.copy()
+
+ if self.playvars:
+ pb_copy['vars'] = self.playvars
+ if self.varsfiles:
+ pb_copy['vars_files'] = self.varsfiles
+ if self.roles:
+ pb_copy['roles'] = []
+ for role in self.roles:
+ role.write_role()
+ role_def = dict(role=role.name)
+ role_def.update(role.params)
+ pb_copy['roles'].append(role_def)
+ if self.tasks:
+ pb_copy['tasks'] = self.tasks
+
+ with open(fname, 'w') as f:
+ pb_yaml = yaml.dump([pb_copy], f, default_flow_style=False, indent=2)
+
+ def build(self):
+
+ if self.dynamic_inventory:
+ # python based inventory file
+ self.di = DynamicInventory(self.features)
+ self.di.write_script()
+ else:
+ # ini based inventory file
+ if 'ini_host' in self.features:
+ self.inventory += 'testhost findme=ini_host\n'
+ else:
+ self.inventory += 'testhost\n'
+ self.inventory += '\n'
+
+ if 'ini_child' in self.features:
+ self.inventory += '[child]\n'
+ self.inventory += 'testhost\n'
+ self.inventory += '\n'
+ self.inventory += '[child:vars]\n'
+ self.inventory += 'findme=ini_child\n'
+ self.inventory += '\n'
+
+ if 'ini_parent' in self.features:
+ if 'ini_child' in self.features:
+ self.inventory += '[parent:children]\n'
+ self.inventory += 'child\n'
+ else:
+ self.inventory += '[parent]\n'
+ self.inventory += 'testhost\n'
+ self.inventory += '\n'
+ self.inventory += '[parent:vars]\n'
+ self.inventory += 'findme=ini_parent\n'
+ self.inventory += '\n'
+
+ if 'ini_all' in self.features:
+ self.inventory += '[all:vars]\n'
+ self.inventory += 'findme=ini_all\n'
+ self.inventory += '\n'
+
+ # default to a single file called inventory
+ invfile = os.path.join(TESTDIR, 'inventory', 'hosts')
+ ipath = os.path.join(TESTDIR, 'inventory')
+ if not os.path.isdir(ipath):
+ os.makedirs(ipath)
+
+ with open(invfile, 'w') as f:
+ f.write(self.inventory)
+
+ hpath = os.path.join(TESTDIR, 'inventory', 'host_vars')
+ if not os.path.isdir(hpath):
+ os.makedirs(hpath)
+ gpath = os.path.join(TESTDIR, 'inventory', 'group_vars')
+ if not os.path.isdir(gpath):
+ os.makedirs(gpath)
+
+ if 'ini_host_vars_file' in self.features:
+ hfile = os.path.join(hpath, 'testhost')
+ with open(hfile, 'w') as f:
+ f.write('findme: ini_host_vars_file\n')
+
+ if 'ini_group_vars_file_all' in self.features:
+ hfile = os.path.join(gpath, 'all')
+ with open(hfile, 'w') as f:
+ f.write('findme: ini_group_vars_file_all\n')
+
+ if 'ini_group_vars_file_child' in self.features:
+ hfile = os.path.join(gpath, 'child')
+ with open(hfile, 'w') as f:
+ f.write('findme: ini_group_vars_file_child\n')
+
+ if 'ini_group_vars_file_parent' in self.features:
+ hfile = os.path.join(gpath, 'parent')
+ with open(hfile, 'w') as f:
+ f.write('findme: ini_group_vars_file_parent\n')
+
+ if 'pb_host_vars_file' in self.features:
+ os.makedirs(os.path.join(TESTDIR, 'host_vars'))
+ fname = os.path.join(TESTDIR, 'host_vars', 'testhost')
+ with open(fname, 'w') as f:
+ f.write('findme: pb_host_vars_file\n')
+
+ if 'pb_group_vars_file_parent' in self.features:
+ if not os.path.isdir(os.path.join(TESTDIR, 'group_vars')):
+ os.makedirs(os.path.join(TESTDIR, 'group_vars'))
+ fname = os.path.join(TESTDIR, 'group_vars', 'parent')
+ with open(fname, 'w') as f:
+ f.write('findme: pb_group_vars_file_parent\n')
+
+ if 'pb_group_vars_file_child' in self.features:
+ if not os.path.isdir(os.path.join(TESTDIR, 'group_vars')):
+ os.makedirs(os.path.join(TESTDIR, 'group_vars'))
+ fname = os.path.join(TESTDIR, 'group_vars', 'child')
+ with open(fname, 'w') as f:
+ f.write('findme: pb_group_vars_file_child\n')
+
+ if 'pb_group_vars_file_all' in self.features:
+ if not os.path.isdir(os.path.join(TESTDIR, 'group_vars')):
+ os.makedirs(os.path.join(TESTDIR, 'group_vars'))
+ fname = os.path.join(TESTDIR, 'group_vars', 'all')
+ with open(fname, 'w') as f:
+ f.write('findme: pb_group_vars_file_all\n')
+
+ if 'play_var' in self.features:
+ self.playvars['findme'] = 'play_var'
+
+ if 'set_fact' in self.features:
+ self.tasks.append(dict(set_fact='findme="set_fact"'))
+
+ if 'vars_file' in self.features:
+ self.varsfiles.append('varsfile.yml')
+ fname = os.path.join(TESTDIR, 'varsfile.yml')
+ with open(fname, 'w') as f:
+ f.write('findme: vars_file\n')
+
+ if 'include_vars' in self.features:
+ self.tasks.append(dict(include_vars='included_vars.yml'))
+ fname = os.path.join(TESTDIR, 'included_vars.yml')
+ with open(fname, 'w') as f:
+ f.write('findme: include_vars\n')
+
+ if 'role_var' in self.features:
+ role = Role('role_var')
+ role.vars = True
+ role.load = True
+ self.roles.append(role)
+
+ if 'role_parent_default' in self.features:
+ role = Role('role_default')
+ role.load = False
+ role.defaults = True
+ self.roles.append(role)
+
+ role = Role('role_parent_default')
+ role.dependencies.append('role_default')
+ role.defaults = True
+ role.load = True
+ if 'role_params' in self.features:
+ role.params = dict(findme='role_params')
+ self.roles.append(role)
+
+ elif 'role_default' in self.features:
+ role = Role('role_default')
+ role.defaults = True
+ role.load = True
+ if 'role_params' in self.features:
+ role.params = dict(findme='role_params')
+ self.roles.append(role)
+
+ debug_task = dict(debug='var=findme')
+ test_task = {'assert': dict(that=['findme == "%s"' % self.features[0]])}
+ if 'task_vars' in self.features:
+ test_task['vars'] = dict(findme="task_vars")
+ if 'registered_vars' in self.features:
+ test_task['register'] = 'findme'
+
+ if 'block_vars' in self.features:
+ block_wrapper = [
+ debug_task,
+ {
+ 'block': [test_task],
+ 'vars': dict(findme="block_vars"),
+ }
+ ]
+ else:
+ block_wrapper = [debug_task, test_task]
+
+ if 'include_params' in self.features:
+ self.tasks.append(dict(name='including tasks', include='included_tasks.yml', vars=dict(findme='include_params')))
+ else:
+ self.tasks.append(dict(include='included_tasks.yml'))
+
+ fname = os.path.join(TESTDIR, 'included_tasks.yml')
+ with open(fname, 'w') as f:
+ f.write(yaml.dump(block_wrapper))
+
+ self.write_playbook()
+
+ def run(self):
+ '''
+ if self.dynamic_inventory:
+ cmd = 'ansible-playbook -c local -i inventory/hosts site.yml'
+ else:
+ cmd = 'ansible-playbook -c local -i inventory site.yml'
+ '''
+ cmd = 'ansible-playbook -c local -i inventory site.yml'
+ if 'extra_vars' in self.features:
+ cmd += ' --extra-vars="findme=extra_vars"'
+ cmd = cmd + ' -vvvvv'
+ self.ansible_command = cmd
+ (rc, so, se) = run_command(cmd, cwd=TESTDIR)
+ self.stdout = so
+
+ if rc != 0:
+ raise Exception("playbook failed (rc=%s), stdout: '%s' stderr: '%s'" % (rc, so, se))
+
+ def show_tree(self):
+ print('## TREE')
+ cmd = 'tree %s' % TESTDIR
+ (rc, so, se) = run_command(cmd)
+ lines = so.split('\n')
+ lines = lines[:-3]
+ print('\n'.join(lines))
+
+ def show_content(self):
+ print('## CONTENT')
+ cmd = 'find %s -type f | xargs tail -n +1' % TESTDIR
+ (rc, so, se) = run_command(cmd)
+ print(so)
+
+ def show_stdout(self):
+ print('## COMMAND')
+ print(self.ansible_command)
+ print('## STDOUT')
+ print(self.stdout)
+
+
+def main():
+ features = [
+ 'extra_vars',
+ 'include_params',
+ # 'role_params', # FIXME: we don't yet validate tasks within a role
+ 'set_fact',
+ # 'registered_vars', # FIXME: hard to simulate
+ 'include_vars',
+ # 'role_dep_params',
+ 'task_vars',
+ 'block_vars',
+ 'role_var',
+ 'vars_file',
+ 'play_var',
+ # 'host_facts', # FIXME: hard to simulate
+ 'pb_host_vars_file',
+ 'ini_host_vars_file',
+ 'ini_host',
+ 'pb_group_vars_file_child',
+ # 'ini_group_vars_file_child', #FIXME: this contradicts documented precedence pb group vars files should override inventory ones
+ 'pb_group_vars_file_parent',
+ 'ini_group_vars_file_parent',
+ 'pb_group_vars_file_all',
+ 'ini_group_vars_file_all',
+ 'ini_child',
+ 'ini_parent',
+ 'ini_all',
+ 'role_parent_default',
+ 'role_default',
+ ]
+
+ parser = OptionParser()
+ parser.add_option('-f', '--feature', action='append')
+ parser.add_option('--use_dynamic_inventory', action='store_true')
+ parser.add_option('--show_tree', action='store_true')
+ parser.add_option('--show_content', action='store_true')
+ parser.add_option('--show_stdout', action='store_true')
+ parser.add_option('--copy_testcases_to_local_dir', action='store_true')
+ (options, args) = parser.parse_args()
+
+ if options.feature:
+ for f in options.feature:
+ if f not in features:
+ print('%s is not a valid feature' % f)
+ sys.exit(1)
+ features = list(options.feature)
+
+ fdesc = {
+ 'ini_host': 'host var inside the ini',
+ 'script_host': 'host var inside the script _meta',
+ 'ini_child': 'child group var inside the ini',
+ 'script_child': 'child group var inside the script',
+ 'ini_parent': 'parent group var inside the ini',
+ 'script_parent': 'parent group var inside the script',
+ 'ini_all': 'all group var inside the ini',
+ 'script_all': 'all group var inside the script',
+ 'ini_host_vars_file': 'var in inventory/host_vars/host',
+ 'ini_group_vars_file_parent': 'var in inventory/group_vars/parent',
+ 'ini_group_vars_file_child': 'var in inventory/group_vars/child',
+ 'ini_group_vars_file_all': 'var in inventory/group_vars/all',
+ 'pb_group_vars_file_parent': 'var in playbook/group_vars/parent',
+ 'pb_group_vars_file_child': 'var in playbook/group_vars/child',
+ 'pb_group_vars_file_all': 'var in playbook/group_vars/all',
+ 'pb_host_vars_file': 'var in playbook/host_vars/host',
+ 'play_var': 'var set in playbook header',
+ 'role_parent_default': 'var in roles/role_parent/defaults/main.yml',
+ 'role_default': 'var in roles/role/defaults/main.yml',
+ 'role_var': 'var in ???',
+ 'include_vars': 'var in included file',
+ 'set_fact': 'var made by set_fact',
+ 'vars_file': 'var in file added by vars_file',
+ 'block_vars': 'vars defined on the block',
+ 'task_vars': 'vars defined on the task',
+ 'extra_vars': 'var passed via the cli'
+ }
+
+ dinv = options.use_dynamic_inventory
+ if dinv:
+ # some features are specific to ini, so swap those
+ for (idx, x) in enumerate(features):
+ if x.startswith('ini_') and 'vars_file' not in x:
+ features[idx] = x.replace('ini_', 'script_')
+
+ dinv = options.use_dynamic_inventory
+
+ index = 1
+ while features:
+ VTM = VarTestMaker(features, dynamic_inventory=dinv)
+ VTM.build()
+
+ if options.show_tree or options.show_content or options.show_stdout:
+ print('')
+ if options.show_tree:
+ VTM.show_tree()
+ if options.show_content:
+ VTM.show_content()
+
+ try:
+ print("CHECKING: %s (%s)" % (features[0], fdesc.get(features[0], '')))
+ res = VTM.run()
+ if options.show_stdout:
+ VTM.show_stdout()
+
+ features.pop(0)
+
+ if options.copy_testcases_to_local_dir:
+ topdir = 'testcases'
+ if index == 1 and os.path.isdir(topdir):
+ shutil.rmtree(topdir)
+ if not os.path.isdir(topdir):
+ os.makedirs(topdir)
+ thisindex = str(index)
+ if len(thisindex) == 1:
+ thisindex = '0' + thisindex
+ thisdir = os.path.join(topdir, '%s.%s' % (thisindex, res))
+ shutil.copytree(TESTDIR, thisdir)
+
+ except Exception as e:
+ print("ERROR !!!")
+ print(e)
+ print('feature: %s failed' % features[0])
+ sys.exit(1)
+ finally:
+ shutil.rmtree(TESTDIR)
+ index += 1
+
+
+if __name__ == "__main__":
+ main()
diff --git a/test/integration/targets/var_precedence/host_vars/testhost b/test/integration/targets/var_precedence/host_vars/testhost
new file mode 100644
index 0000000..7d53355
--- /dev/null
+++ b/test/integration/targets/var_precedence/host_vars/testhost
@@ -0,0 +1,2 @@
+# Var precedence testing
+defaults_file_var_role3: "overridden from inventory"
diff --git a/test/integration/targets/var_precedence/inventory b/test/integration/targets/var_precedence/inventory
new file mode 100644
index 0000000..3b52d04
--- /dev/null
+++ b/test/integration/targets/var_precedence/inventory
@@ -0,0 +1,13 @@
+[local]
+testhost ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+
+[all:vars]
+extra_var_override=FROM_INVENTORY
+inven_var=inventory_var
+
+[inven_overridehosts]
+invenoverride ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
+
+[inven_overridehosts:vars]
+foo=foo
+var_dir=vars
diff --git a/test/integration/targets/var_precedence/roles/test_var_precedence/meta/main.yml b/test/integration/targets/var_precedence/roles/test_var_precedence/meta/main.yml
new file mode 100644
index 0000000..423b94e
--- /dev/null
+++ b/test/integration/targets/var_precedence/roles/test_var_precedence/meta/main.yml
@@ -0,0 +1,4 @@
+dependencies:
+ - { role: test_var_precedence_role1, param_var: "param_var_role1" }
+ - { role: test_var_precedence_role2, param_var: "param_var_role2" }
+ - { role: test_var_precedence_role3, param_var: "param_var_role3" }
diff --git a/test/integration/targets/var_precedence/roles/test_var_precedence/tasks/main.yml b/test/integration/targets/var_precedence/roles/test_var_precedence/tasks/main.yml
new file mode 100644
index 0000000..7850e6b
--- /dev/null
+++ b/test/integration/targets/var_precedence/roles/test_var_precedence/tasks/main.yml
@@ -0,0 +1,10 @@
+- debug: var=extra_var
+- debug: var=vars_var
+- debug: var=vars_files_var
+- debug: var=vars_files_var_role
+- assert:
+ that:
+ - 'extra_var == "extra_var"'
+ - 'vars_var == "vars_var"'
+ - 'vars_files_var == "vars_files_var"'
+ - 'vars_files_var_role == "vars_files_var_role3"'
diff --git a/test/integration/targets/var_precedence/roles/test_var_precedence_dep/defaults/main.yml b/test/integration/targets/var_precedence/roles/test_var_precedence_dep/defaults/main.yml
new file mode 100644
index 0000000..dda4224
--- /dev/null
+++ b/test/integration/targets/var_precedence/roles/test_var_precedence_dep/defaults/main.yml
@@ -0,0 +1,5 @@
+---
+# should be overridden by vars_files in the main play
+vars_files_var: "BAD!"
+# should be seen in role1 (no override)
+defaults_file_var_role1: "defaults_file_var_role1"
diff --git a/test/integration/targets/var_precedence/roles/test_var_precedence_dep/tasks/main.yml b/test/integration/targets/var_precedence/roles/test_var_precedence_dep/tasks/main.yml
new file mode 100644
index 0000000..2f8e170
--- /dev/null
+++ b/test/integration/targets/var_precedence/roles/test_var_precedence_dep/tasks/main.yml
@@ -0,0 +1,14 @@
+- debug: var=extra_var
+- debug: var=param_var
+- debug: var=vars_var
+- debug: var=vars_files_var
+- debug: var=vars_files_var_role
+- debug: var=defaults_file_var_role1
+- assert:
+ that:
+ - 'extra_var == "extra_var"'
+ - 'param_var == "param_var_role1"'
+ - 'vars_var == "vars_var"'
+ - 'vars_files_var == "vars_files_var"'
+ - 'vars_files_var_role == "vars_files_var_dep"'
+ - 'defaults_file_var_role1 == "defaults_file_var_role1"'
diff --git a/test/integration/targets/var_precedence/roles/test_var_precedence_dep/vars/main.yml b/test/integration/targets/var_precedence/roles/test_var_precedence_dep/vars/main.yml
new file mode 100644
index 0000000..a69efad
--- /dev/null
+++ b/test/integration/targets/var_precedence/roles/test_var_precedence_dep/vars/main.yml
@@ -0,0 +1,4 @@
+---
+# should override the global vars_files_var since it's local to the role
+# but will be set to the value in the last role included which defines it
+vars_files_var_role: "vars_files_var_dep"
diff --git a/test/integration/targets/var_precedence/roles/test_var_precedence_inven_override/tasks/main.yml b/test/integration/targets/var_precedence/roles/test_var_precedence_inven_override/tasks/main.yml
new file mode 100644
index 0000000..942ae4e
--- /dev/null
+++ b/test/integration/targets/var_precedence/roles/test_var_precedence_inven_override/tasks/main.yml
@@ -0,0 +1,5 @@
+---
+- debug: var=foo
+- assert:
+ that:
+ - 'foo == "bar"'
diff --git a/test/integration/targets/var_precedence/roles/test_var_precedence_role1/defaults/main.yml b/test/integration/targets/var_precedence/roles/test_var_precedence_role1/defaults/main.yml
new file mode 100644
index 0000000..dda4224
--- /dev/null
+++ b/test/integration/targets/var_precedence/roles/test_var_precedence_role1/defaults/main.yml
@@ -0,0 +1,5 @@
+---
+# should be overridden by vars_files in the main play
+vars_files_var: "BAD!"
+# should be seen in role1 (no override)
+defaults_file_var_role1: "defaults_file_var_role1"
diff --git a/test/integration/targets/var_precedence/roles/test_var_precedence_role1/meta/main.yml b/test/integration/targets/var_precedence/roles/test_var_precedence_role1/meta/main.yml
new file mode 100644
index 0000000..c8b410b
--- /dev/null
+++ b/test/integration/targets/var_precedence/roles/test_var_precedence_role1/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - test_var_precedence_dep
diff --git a/test/integration/targets/var_precedence/roles/test_var_precedence_role1/tasks/main.yml b/test/integration/targets/var_precedence/roles/test_var_precedence_role1/tasks/main.yml
new file mode 100644
index 0000000..95b2a0b
--- /dev/null
+++ b/test/integration/targets/var_precedence/roles/test_var_precedence_role1/tasks/main.yml
@@ -0,0 +1,14 @@
+- debug: var=extra_var
+- debug: var=param_var
+- debug: var=vars_var
+- debug: var=vars_files_var
+- debug: var=vars_files_var_role
+- debug: var=defaults_file_var_role1
+- assert:
+ that:
+ - 'extra_var == "extra_var"'
+ - 'param_var == "param_var_role1"'
+ - 'vars_var == "vars_var"'
+ - 'vars_files_var == "vars_files_var"'
+ - 'vars_files_var_role == "vars_files_var_role1"'
+ - 'defaults_file_var_role1 == "defaults_file_var_role1"'
diff --git a/test/integration/targets/var_precedence/roles/test_var_precedence_role1/vars/main.yml b/test/integration/targets/var_precedence/roles/test_var_precedence_role1/vars/main.yml
new file mode 100644
index 0000000..2f7613d
--- /dev/null
+++ b/test/integration/targets/var_precedence/roles/test_var_precedence_role1/vars/main.yml
@@ -0,0 +1,4 @@
+---
+# should override the global vars_files_var since it's local to the role
+# but will be set to the value in the last role included which defines it
+vars_files_var_role: "vars_files_var_role1"
diff --git a/test/integration/targets/var_precedence/roles/test_var_precedence_role2/defaults/main.yml b/test/integration/targets/var_precedence/roles/test_var_precedence_role2/defaults/main.yml
new file mode 100644
index 0000000..8ed63ce
--- /dev/null
+++ b/test/integration/targets/var_precedence/roles/test_var_precedence_role2/defaults/main.yml
@@ -0,0 +1,5 @@
+---
+# should be overridden by vars_files in the main play
+vars_files_var: "BAD!"
+# should be overridden by the vars file in role2
+defaults_file_var_role2: "BAD!"
diff --git a/test/integration/targets/var_precedence/roles/test_var_precedence_role2/tasks/main.yml b/test/integration/targets/var_precedence/roles/test_var_precedence_role2/tasks/main.yml
new file mode 100644
index 0000000..a862389
--- /dev/null
+++ b/test/integration/targets/var_precedence/roles/test_var_precedence_role2/tasks/main.yml
@@ -0,0 +1,14 @@
+- debug: var=extra_var
+- debug: var=param_var
+- debug: var=vars_var
+- debug: var=vars_files_var
+- debug: var=vars_files_var_role
+- debug: var=defaults_file_var_role1
+- assert:
+ that:
+ - 'extra_var == "extra_var"'
+ - 'param_var == "param_var_role2"'
+ - 'vars_var == "vars_var"'
+ - 'vars_files_var == "vars_files_var"'
+ - 'vars_files_var_role == "vars_files_var_role2"'
+ - 'defaults_file_var_role2 == "overridden by role vars"'
diff --git a/test/integration/targets/var_precedence/roles/test_var_precedence_role2/vars/main.yml b/test/integration/targets/var_precedence/roles/test_var_precedence_role2/vars/main.yml
new file mode 100644
index 0000000..483c5ea
--- /dev/null
+++ b/test/integration/targets/var_precedence/roles/test_var_precedence_role2/vars/main.yml
@@ -0,0 +1,5 @@
+---
+# should override the global vars_files_var since it's local to the role
+vars_files_var_role: "vars_files_var_role2"
+# should override the value in defaults/main.yml for role 2
+defaults_file_var_role2: "overridden by role vars"
diff --git a/test/integration/targets/var_precedence/roles/test_var_precedence_role3/defaults/main.yml b/test/integration/targets/var_precedence/roles/test_var_precedence_role3/defaults/main.yml
new file mode 100644
index 0000000..763b0d5
--- /dev/null
+++ b/test/integration/targets/var_precedence/roles/test_var_precedence_role3/defaults/main.yml
@@ -0,0 +1,7 @@
+---
+# should be overridden by vars_files in the main play
+vars_files_var: "BAD!"
+# should override the defaults var for role 1 and 2
+defaults_file_var: "last one wins"
+# should be overridden from the inventory value
+defaults_file_var_role3: "BAD!"
diff --git a/test/integration/targets/var_precedence/roles/test_var_precedence_role3/tasks/main.yml b/test/integration/targets/var_precedence/roles/test_var_precedence_role3/tasks/main.yml
new file mode 100644
index 0000000..12346ec
--- /dev/null
+++ b/test/integration/targets/var_precedence/roles/test_var_precedence_role3/tasks/main.yml
@@ -0,0 +1,14 @@
+- debug: var=extra_var
+- debug: var=param_var
+- debug: var=vars_var
+- debug: var=vars_files_var
+- debug: var=vars_files_var_role
+- debug: var=defaults_file_var_role1
+- assert:
+ that:
+ - 'extra_var == "extra_var"'
+ - 'param_var == "param_var_role3"'
+ - 'vars_var == "vars_var"'
+ - 'vars_files_var == "vars_files_var"'
+ - 'vars_files_var_role == "vars_files_var_role3"'
+ - 'defaults_file_var_role3 == "overridden from inventory"'
diff --git a/test/integration/targets/var_precedence/roles/test_var_precedence_role3/vars/main.yml b/test/integration/targets/var_precedence/roles/test_var_precedence_role3/vars/main.yml
new file mode 100644
index 0000000..3cfb1b1
--- /dev/null
+++ b/test/integration/targets/var_precedence/roles/test_var_precedence_role3/vars/main.yml
@@ -0,0 +1,3 @@
+---
+# should override the global vars_files_var since it's local to the role
+vars_files_var_role: "vars_files_var_role3"
diff --git a/test/integration/targets/var_precedence/runme.sh b/test/integration/targets/var_precedence/runme.sh
new file mode 100755
index 0000000..0f0811c
--- /dev/null
+++ b/test/integration/targets/var_precedence/runme.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook test_var_precedence.yml -i inventory -v "$@" \
+ -e 'extra_var=extra_var' \
+ -e 'extra_var_override=extra_var_override'
+
+./ansible-var-precedence-check.py
diff --git a/test/integration/targets/var_precedence/test_var_precedence.yml b/test/integration/targets/var_precedence/test_var_precedence.yml
new file mode 100644
index 0000000..58584bf
--- /dev/null
+++ b/test/integration/targets/var_precedence/test_var_precedence.yml
@@ -0,0 +1,44 @@
+---
+- hosts: testhost
+ vars:
+ - ansible_hostname: "BAD!"
+ - vars_var: "vars_var"
+ - param_var: "BAD!"
+ - vars_files_var: "BAD!"
+ - extra_var_override_once_removed: "{{ extra_var_override }}"
+ - from_inventory_once_removed: "{{ inven_var | default('BAD!') }}"
+ vars_files:
+ - vars/test_var_precedence.yml
+ roles:
+ - { role: test_var_precedence, param_var: "param_var" }
+ tasks:
+ - name: register a result
+ command: echo 'BAD!'
+ register: registered_var
+ - name: use set_fact to override the registered_var
+ set_fact: registered_var="this is from set_fact"
+ - debug: var=extra_var
+ - debug: var=extra_var_override_once_removed
+ - debug: var=vars_var
+ - debug: var=vars_files_var
+ - debug: var=vars_files_var_role
+ - debug: var=registered_var
+ - debug: var=from_inventory_once_removed
+ - assert:
+ that: item
+ with_items:
+ - 'extra_var == "extra_var"'
+ - 'extra_var_override == "extra_var_override"'
+ - 'extra_var_override_once_removed == "extra_var_override"'
+ - 'vars_var == "vars_var"'
+ - 'vars_files_var == "vars_files_var"'
+ - 'vars_files_var_role == "vars_files_var_role3"'
+ - 'registered_var == "this is from set_fact"'
+ - 'from_inventory_once_removed == "inventory_var"'
+
+- hosts: inven_overridehosts
+ vars_files:
+ - "test_var_precedence.yml"
+ roles:
+ - role: test_var_precedence_inven_override
+ foo: bar
diff --git a/test/integration/targets/var_precedence/vars/test_var_precedence.yml b/test/integration/targets/var_precedence/vars/test_var_precedence.yml
new file mode 100644
index 0000000..19d65cb
--- /dev/null
+++ b/test/integration/targets/var_precedence/vars/test_var_precedence.yml
@@ -0,0 +1,5 @@
+---
+extra_var: "BAD!"
+role_var: "BAD!"
+vars_files_var: "vars_files_var"
+vars_files_var_role: "should be overridden by roles"
diff --git a/test/integration/targets/var_reserved/aliases b/test/integration/targets/var_reserved/aliases
new file mode 100644
index 0000000..498fedd
--- /dev/null
+++ b/test/integration/targets/var_reserved/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group4
+context/controller
diff --git a/test/integration/targets/var_reserved/reserved_varname_warning.yml b/test/integration/targets/var_reserved/reserved_varname_warning.yml
new file mode 100644
index 0000000..1bdb34a
--- /dev/null
+++ b/test/integration/targets/var_reserved/reserved_varname_warning.yml
@@ -0,0 +1,6 @@
+- hosts: localhost
+ gather_facts: false
+ vars:
+ lipsum: jinja2 uses me internally
+ tasks:
+ - debug:
diff --git a/test/integration/targets/var_reserved/runme.sh b/test/integration/targets/var_reserved/runme.sh
new file mode 100755
index 0000000..3c3befb
--- /dev/null
+++ b/test/integration/targets/var_reserved/runme.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -eux
+
+ansible-playbook reserved_varname_warning.yml "$@" 2>&1| grep 'Found variable using reserved name: lipsum'
diff --git a/test/integration/targets/var_templating/aliases b/test/integration/targets/var_templating/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/var_templating/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/var_templating/ansible_debug_template.j2 b/test/integration/targets/var_templating/ansible_debug_template.j2
new file mode 100644
index 0000000..8fe25f9
--- /dev/null
+++ b/test/integration/targets/var_templating/ansible_debug_template.j2
@@ -0,0 +1 @@
+{{ hello }}
diff --git a/test/integration/targets/var_templating/group_vars/all.yml b/test/integration/targets/var_templating/group_vars/all.yml
new file mode 100644
index 0000000..4eae7c1
--- /dev/null
+++ b/test/integration/targets/var_templating/group_vars/all.yml
@@ -0,0 +1,7 @@
+---
+x: 100
+y: "{{ x }}"
+nested_x:
+ value:
+ x: 100
+nested_y: "{{ nested_x }}"
diff --git a/test/integration/targets/var_templating/runme.sh b/test/integration/targets/var_templating/runme.sh
new file mode 100755
index 0000000..bcf0924
--- /dev/null
+++ b/test/integration/targets/var_templating/runme.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# this should succeed since we override the undefined variable
+ansible-playbook undefined.yml -i inventory -v "$@" -e '{"mytest": False}'
+
+# this should still work, just show that var is undefined in debug
+ansible-playbook undefined.yml -i inventory -v "$@"
+
+# this should work since we dont use the variable
+ansible-playbook undall.yml -i inventory -v "$@"
+
+# test hostvars templating
+ansible-playbook task_vars_templating.yml -v "$@"
+
+# there should be an attempt to use 'sudo' in the connection debug output
+ANSIBLE_BECOME_ALLOW_SAME_USER=true ansible-playbook test_connection_vars.yml -vvvv "$@" | tee /dev/stderr | grep 'sudo \-H \-S'
+
+# smoke test usage of VarsWithSources that is used when ANSIBLE_DEBUG=1
+ANSIBLE_DEBUG=1 ansible-playbook test_vars_with_sources.yml -v "$@"
diff --git a/test/integration/targets/var_templating/task_vars_templating.yml b/test/integration/targets/var_templating/task_vars_templating.yml
new file mode 100644
index 0000000..88e1e60
--- /dev/null
+++ b/test/integration/targets/var_templating/task_vars_templating.yml
@@ -0,0 +1,58 @@
+---
+- hosts: localhost
+ connection: local
+ gather_facts: no
+ tasks:
+ - add_host:
+ name: host1
+ ansible_connection: local
+ ansible_host: 127.0.0.1
+
+- hosts: all
+ gather_facts: no
+ tasks:
+ - debug:
+ msg: "{{ hostvars['host1']['x'] }}"
+ register: x_1
+ - debug:
+ msg: "{{ hostvars['host1']['y'] }}"
+ register: y_1
+ - debug:
+ msg: "{{ hostvars_['x'] }}"
+ vars:
+ hostvars_: "{{ hostvars['host1'] }}"
+ register: x_2
+ - debug:
+ msg: "{{ hostvars_['y'] }}"
+ vars:
+ hostvars_: "{{ hostvars['host1'] }}"
+ register: y_2
+
+ - assert:
+ that:
+ - x_1 == x_2
+ - y_1 == y_2
+ - x_1 == y_1
+
+ - debug:
+ msg: "{{ hostvars['host1']['nested_x']['value'] }}"
+ register: x_1
+ - debug:
+ msg: "{{ hostvars['host1']['nested_y']['value'] }}"
+ register: y_1
+ - debug:
+ msg: "{{ hostvars_['nested_x']['value'] }}"
+ vars:
+ hostvars_: "{{ hostvars['host1'] }}"
+ register: x_2
+ - debug:
+ msg: "{{ hostvars_['nested_y']['value'] }}"
+ vars:
+ hostvars_: "{{ hostvars['host1'] }}"
+ register: y_2
+
+ - assert:
+ that:
+ - x_1 == x_2
+ - y_1 == y_2
+ - x_1 == y_1
diff --git a/test/integration/targets/var_templating/test_connection_vars.yml b/test/integration/targets/var_templating/test_connection_vars.yml
new file mode 100644
index 0000000..2b22eea
--- /dev/null
+++ b/test/integration/targets/var_templating/test_connection_vars.yml
@@ -0,0 +1,26 @@
+---
+- hosts: localhost
+ gather_facts: no
+ vars:
+ my_var:
+ become_method: sudo
+ connection: local
+ become: 1
+ tasks:
+
+ - include_vars: "./vars/connection.yml"
+
+ - command: whoami
+ ignore_errors: yes
+ register: result
+ failed_when: result is not success and (result.module_stderr is defined or result.module_stderr is defined)
+
+ - assert:
+ that:
+ - "'sudo' in result.module_stderr"
+ when: result is not success and result.module_stderr is defined
+
+ - assert:
+ that:
+ - "'Invalid become method specified' not in result.msg"
+ when: result is not success and result.msg is defined
diff --git a/test/integration/targets/var_templating/test_vars_with_sources.yml b/test/integration/targets/var_templating/test_vars_with_sources.yml
new file mode 100644
index 0000000..0b8c990
--- /dev/null
+++ b/test/integration/targets/var_templating/test_vars_with_sources.yml
@@ -0,0 +1,9 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - template:
+ src: ansible_debug_template.j2
+ dest: "{{ output_dir }}/ansible_debug_templated.txt"
+ vars:
+ output_dir: "{{ lookup('env', 'OUTPUT_DIR') }}"
+ hello: hello
diff --git a/test/integration/targets/var_templating/undall.yml b/test/integration/targets/var_templating/undall.yml
new file mode 100644
index 0000000..9ea9f1d
--- /dev/null
+++ b/test/integration/targets/var_templating/undall.yml
@@ -0,0 +1,6 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - debug:
+ vars:
+ mytest: '{{ und }}'
diff --git a/test/integration/targets/var_templating/undefined.yml b/test/integration/targets/var_templating/undefined.yml
new file mode 100644
index 0000000..cf083d5
--- /dev/null
+++ b/test/integration/targets/var_templating/undefined.yml
@@ -0,0 +1,13 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: show defined/undefined var
+ debug: var=mytest
+ vars:
+ mytest: '{{ und }}'
+ register: var_undefined
+
+ - name: ensure either mytest is defined or debug finds it to be undefined
+ assert:
+ that:
+ - mytest is defined or 'VARIABLE IS NOT DEFINED!' in var_undefined['mytest']
diff --git a/test/integration/targets/var_templating/vars/connection.yml b/test/integration/targets/var_templating/vars/connection.yml
new file mode 100644
index 0000000..263929a
--- /dev/null
+++ b/test/integration/targets/var_templating/vars/connection.yml
@@ -0,0 +1,3 @@
+ansible_become: "{{ my_var.become }}"
+ansible_become_method: "{{ my_var.become_method }}"
+ansible_connection: "{{ my_var.connection }}"
diff --git a/test/integration/targets/wait_for/aliases b/test/integration/targets/wait_for/aliases
new file mode 100644
index 0000000..a4c92ef
--- /dev/null
+++ b/test/integration/targets/wait_for/aliases
@@ -0,0 +1,2 @@
+destructive
+shippable/posix/group1
diff --git a/test/integration/targets/wait_for/files/testserver.py b/test/integration/targets/wait_for/files/testserver.py
new file mode 100644
index 0000000..2b728b6
--- /dev/null
+++ b/test/integration/targets/wait_for/files/testserver.py
@@ -0,0 +1,19 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import sys
+
+if __name__ == '__main__':
+ if sys.version_info[0] >= 3:
+ import http.server
+ import socketserver
+ PORT = int(sys.argv[1])
+ Handler = http.server.SimpleHTTPRequestHandler
+ httpd = socketserver.TCPServer(("", PORT), Handler)
+ httpd.serve_forever()
+ else:
+ import mimetypes
+ mimetypes.init()
+ mimetypes.add_type('application/json', '.json')
+ import SimpleHTTPServer
+ SimpleHTTPServer.test()
diff --git a/test/integration/targets/wait_for/files/write_utf16.py b/test/integration/targets/wait_for/files/write_utf16.py
new file mode 100644
index 0000000..6079ed3
--- /dev/null
+++ b/test/integration/targets/wait_for/files/write_utf16.py
@@ -0,0 +1,20 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import sys
+
+# utf16 encoded bytes
+# to ensure wait_for doesn't have any encoding errors
+data = (
+ b'\xff\xfep\x00r\x00e\x00m\x00i\x00\xe8\x00r\x00e\x00 \x00i\x00s\x00 '
+ b'\x00f\x00i\x00r\x00s\x00t\x00\n\x00p\x00r\x00e\x00m\x00i\x00e\x00'
+ b'\x00\x03r\x00e\x00 \x00i\x00s\x00 \x00s\x00l\x00i\x00g\x00h\x00t\x00'
+ b'l\x00y\x00 \x00d\x00i\x00f\x00f\x00e\x00r\x00e\x00n\x00t\x00\n\x00\x1a'
+ b'\x048\x04@\x048\x04;\x04;\x048\x04F\x040\x04 \x00i\x00s\x00 \x00C\x00y'
+ b'\x00r\x00i\x00l\x00l\x00i\x00c\x00\n\x00\x01\xd8\x00\xdc \x00a\x00m'
+ b'\x00 \x00D\x00e\x00s\x00e\x00r\x00e\x00t\x00\n\x00\n'
+ b'completed\n'
+)
+
+with open(sys.argv[1], 'wb') as f:
+ f.write(data)
diff --git a/test/integration/targets/wait_for/files/zombie.py b/test/integration/targets/wait_for/files/zombie.py
new file mode 100644
index 0000000..913074e
--- /dev/null
+++ b/test/integration/targets/wait_for/files/zombie.py
@@ -0,0 +1,13 @@
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import os
+import sys
+import time
+
+child_pid = os.fork()
+
+if child_pid > 0:
+ time.sleep(60)
+else:
+ sys.exit()
diff --git a/test/integration/targets/wait_for/meta/main.yml b/test/integration/targets/wait_for/meta/main.yml
new file mode 100644
index 0000000..cb6005d
--- /dev/null
+++ b/test/integration/targets/wait_for/meta/main.yml
@@ -0,0 +1,3 @@
+dependencies:
+ - prepare_tests
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/wait_for/tasks/main.yml b/test/integration/targets/wait_for/tasks/main.yml
new file mode 100644
index 0000000..f71ddbd
--- /dev/null
+++ b/test/integration/targets/wait_for/tasks/main.yml
@@ -0,0 +1,199 @@
+---
+- name: test wait_for with delegate_to
+ wait_for:
+ timeout: 2
+ delegate_to: localhost
+ register: waitfor
+
+- assert:
+ that:
+ - waitfor is successful
+ - waitfor.elapsed >= 2
+
+- name: setup create a directory to serve files from
+ file:
+ dest: "{{ files_dir }}"
+ state: directory
+
+- name: setup webserver
+ copy:
+ src: "testserver.py"
+ dest: "{{ remote_tmp_dir }}/testserver.py"
+
+- name: setup a path
+ file:
+ path: "{{ remote_tmp_dir }}/wait_for_file"
+ state: touch
+
+- name: setup remove a file after 3s
+ shell: sleep 3 && rm {{ remote_tmp_dir }}/wait_for_file
+ async: 20
+ poll: 0
+
+- name: test for absent path
+ wait_for:
+ path: "{{ remote_tmp_dir }}/wait_for_file"
+ state: absent
+ timeout: 20
+ register: waitfor
+- name: verify test for absent path
+ assert:
+ that:
+ - waitfor is successful
+ - waitfor.path == "{{ remote_tmp_dir | expanduser }}/wait_for_file"
+ - waitfor.elapsed >= 2
+ - waitfor.elapsed <= 15
+
+- name: setup create a file after 3s
+ shell: sleep 3 && touch {{ remote_tmp_dir }}/wait_for_file
+ async: 20
+ poll: 0
+
+- name: test for present path
+ wait_for:
+ path: "{{ remote_tmp_dir }}/wait_for_file"
+ timeout: 5
+ register: waitfor
+- name: verify test for absent path
+ assert:
+ that:
+ - waitfor is successful
+ - waitfor.path == "{{ remote_tmp_dir | expanduser }}/wait_for_file"
+ - waitfor.elapsed >= 2
+ - waitfor.elapsed <= 15
+
+- name: setup write keyword to file after 3s
+ shell: sleep 3 && echo completed > {{remote_tmp_dir}}/wait_for_keyword
+ async: 20
+ poll: 0
+
+- name: test wait for keyword in file
+ wait_for:
+ path: "{{remote_tmp_dir}}/wait_for_keyword"
+ search_regex: completed
+ timeout: 5
+ register: waitfor
+
+- name: verify test wait for keyword in file
+ assert:
+ that:
+ - waitfor is successful
+ - "waitfor.search_regex == 'completed'"
+ - waitfor.elapsed >= 2
+ - waitfor.elapsed <= 15
+
+- name: setup write keyword to file after 3s
+ shell: sleep 3 && echo "completed data 123" > {{remote_tmp_dir}}/wait_for_keyword
+ async: 20
+ poll: 0
+
+- name: test wait for keyword in file with match groups
+ wait_for:
+ path: "{{remote_tmp_dir}}/wait_for_keyword"
+ search_regex: completed (?P<foo>\w+) ([0-9]+)
+ timeout: 5
+ register: waitfor
+
+- name: verify test wait for keyword in file with match groups
+ assert:
+ that:
+ - waitfor is successful
+ - waitfor.elapsed >= 2
+ - waitfor.elapsed <= 15
+ - waitfor['match_groupdict'] | length == 1
+ - waitfor['match_groupdict']['foo'] == 'data'
+ - waitfor['match_groups'] == ['data', '123']
+
+- name: write non-ascii file
+ script: write_utf16.py "{{remote_tmp_dir}}/utf16.txt"
+ args:
+ executable: '{{ ansible_facts.python.executable }}'
+
+- name: test non-ascii file
+ wait_for:
+ path: "{{remote_tmp_dir}}/utf16.txt"
+ search_regex: completed
+
+- name: test wait for port timeout
+ wait_for:
+ port: 12121
+ timeout: 3
+ register: waitfor
+ ignore_errors: true
+- name: verify test wait for port timeout
+ assert:
+ that:
+ - waitfor is failed
+ - waitfor.elapsed == 3
+ - "waitfor.msg == 'Timeout when waiting for 127.0.0.1:12121'"
+
+- name: test fail with custom msg
+ wait_for:
+ port: 12121
+ msg: fail with custom message
+ timeout: 3
+ register: waitfor
+ ignore_errors: true
+- name: verify test fail with custom msg
+ assert:
+ that:
+ - waitfor is failed
+ - waitfor.elapsed == 3
+ - "waitfor.msg == 'fail with custom message'"
+
+- name: setup start SimpleHTTPServer
+ shell: sleep 3 && cd {{ files_dir }} && {{ ansible_python.executable }} {{ remote_tmp_dir}}/testserver.py {{ http_port }}
+ async: 120 # this test set can take ~1m to run on FreeBSD (via Shippable)
+ poll: 0
+
+- name: test wait for port with sleep
+ wait_for:
+ port: "{{ http_port }}"
+ sleep: 3
+ register: waitfor
+- name: verify test wait for port sleep
+ assert:
+ that:
+ - waitfor is successful
+ - waitfor is not changed
+ - "waitfor.port == {{ http_port }}"
+
+- name: install psutil using pip (non-Linux only)
+ pip:
+ name: psutil==5.8.0
+ when: ansible_system != 'Linux'
+
+- name: Copy zombie.py
+ copy:
+ src: zombie.py
+ dest: "{{ remote_tmp_dir }}"
+
+- name: Create zombie process
+ shell: "{{ ansible_python.executable }} {{ remote_tmp_dir }}/zombie"
+ async: 90
+ poll: 0
+
+- name: test wait for port drained
+ wait_for:
+ port: "{{ http_port }}"
+ state: drained
+ register: waitfor
+
+- name: verify test wait for port
+ assert:
+ that:
+ - waitfor is successful
+ - waitfor is not changed
+ - "waitfor.port == {{ http_port }}"
+
+- name: test wait_for with delay
+ wait_for:
+ timeout: 2
+ delay: 2
+ register: waitfor
+
+- name: verify test wait_for with delay
+ assert:
+ that:
+ - waitfor is successful
+ - waitfor.elapsed >= 4
diff --git a/test/integration/targets/wait_for/vars/main.yml b/test/integration/targets/wait_for/vars/main.yml
new file mode 100644
index 0000000..d15b6d7
--- /dev/null
+++ b/test/integration/targets/wait_for/vars/main.yml
@@ -0,0 +1,4 @@
+---
+http_port: 15261
+files_dir: '{{ remote_tmp_dir|expanduser }}/files'
+checkout_dir: '{{ remote_tmp_dir }}/git'
diff --git a/test/integration/targets/wait_for_connection/aliases b/test/integration/targets/wait_for_connection/aliases
new file mode 100644
index 0000000..7ab3bd0
--- /dev/null
+++ b/test/integration/targets/wait_for_connection/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group1
+shippable/windows/group1
diff --git a/test/integration/targets/wait_for_connection/tasks/main.yml b/test/integration/targets/wait_for_connection/tasks/main.yml
new file mode 100644
index 0000000..19749e6
--- /dev/null
+++ b/test/integration/targets/wait_for_connection/tasks/main.yml
@@ -0,0 +1,30 @@
+- name: Test normal connection to target node
+ wait_for_connection:
+ connect_timeout: 5
+ sleep: 1
+ timeout: 10
+
+- name: Test normal connection to target node with delay
+ wait_for_connection:
+ connect_timeout: 5
+ sleep: 1
+ timeout: 10
+ delay: 3
+ register: result
+
+- name: Verify delay was honored
+ assert:
+ that:
+ - result.elapsed >= 3
+
+- name: Use invalid parameter
+ wait_for_connection:
+ foo: bar
+ ignore_errors: yes
+ register: invalid_parameter
+
+- name: Ensure task fails with error
+ assert:
+ that:
+ - invalid_parameter is failed
+ - "invalid_parameter.msg == 'Invalid options for wait_for_connection: foo'"
diff --git a/test/integration/targets/want_json_modules_posix/aliases b/test/integration/targets/want_json_modules_posix/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/want_json_modules_posix/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/want_json_modules_posix/library/helloworld.py b/test/integration/targets/want_json_modules_posix/library/helloworld.py
new file mode 100644
index 0000000..80f8761
--- /dev/null
+++ b/test/integration/targets/want_json_modules_posix/library/helloworld.py
@@ -0,0 +1,34 @@
+#!/usr/bin/python
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# WANT_JSON
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+import sys
+
+try:
+ with open(sys.argv[1], 'r') as f:
+ data = json.load(f)
+except (IOError, OSError, IndexError):
+ print(json.dumps(dict(msg="No argument file provided", failed=True)))
+ sys.exit(1)
+
+salutation = data.get('salutation', 'Hello')
+name = data.get('name', 'World')
+print(json.dumps(dict(msg='%s, %s!' % (salutation, name))))
diff --git a/test/integration/targets/want_json_modules_posix/meta/main.yml b/test/integration/targets/want_json_modules_posix/meta/main.yml
new file mode 100644
index 0000000..1810d4b
--- /dev/null
+++ b/test/integration/targets/want_json_modules_posix/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/want_json_modules_posix/tasks/main.yml b/test/integration/targets/want_json_modules_posix/tasks/main.yml
new file mode 100644
index 0000000..27e9f78
--- /dev/null
+++ b/test/integration/targets/want_json_modules_posix/tasks/main.yml
@@ -0,0 +1,43 @@
+- name: Hello, World!
+ helloworld:
+ register: hello_world
+
+- assert:
+ that:
+ - 'hello_world.msg == "Hello, World!"'
+
+- name: Hello, Ansible!
+ helloworld:
+ args:
+ name: Ansible
+ register: hello_ansible
+
+- assert:
+ that:
+ - 'hello_ansible.msg == "Hello, Ansible!"'
+
+- name: Goodbye, Ansible!
+ helloworld:
+ args:
+ salutation: Goodbye
+ name: Ansible
+ register: goodbye_ansible
+
+- assert:
+ that:
+ - 'goodbye_ansible.msg == "Goodbye, Ansible!"'
+
+- name: Copy module to remote
+ copy:
+ src: "{{ role_path }}/library/helloworld.py"
+ dest: "{{ remote_tmp_dir }}/helloworld.py"
+
+- name: Execute module directly
+ command: '{{ ansible_python_interpreter|default(ansible_playbook_python) }} {{ remote_tmp_dir }}/helloworld.py'
+ register: direct
+ ignore_errors: true
+
+- assert:
+ that:
+ - direct is failed
+ - 'direct.stdout | from_json == {"msg": "No argument file provided", "failed": true}'
diff --git a/test/integration/targets/win_async_wrapper/aliases b/test/integration/targets/win_async_wrapper/aliases
new file mode 100644
index 0000000..59dda5e
--- /dev/null
+++ b/test/integration/targets/win_async_wrapper/aliases
@@ -0,0 +1,3 @@
+async_status
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/win_async_wrapper/library/async_test.ps1 b/test/integration/targets/win_async_wrapper/library/async_test.ps1
new file mode 100644
index 0000000..3b4c1c8
--- /dev/null
+++ b/test/integration/targets/win_async_wrapper/library/async_test.ps1
@@ -0,0 +1,47 @@
+#!powershell
+
+# Copyright: (c) 2018, Ansible Project
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+$parsed_args = Parse-Args $args
+
+$sleep_delay_sec = Get-AnsibleParam -obj $parsed_args -name "sleep_delay_sec" -type "int" -default 0
+$fail_mode = Get-AnsibleParam -obj $parsed_args -name "fail_mode" -type "str" -default "success" -validateset "success", "graceful", "exception"
+
+If ($fail_mode -isnot [array]) {
+ $fail_mode = @($fail_mode)
+}
+
+$result = @{
+ changed = $true
+ module_pid = $pid
+ module_tempdir = $PSScriptRoot
+}
+
+If ($sleep_delay_sec -gt 0) {
+ Sleep -Seconds $sleep_delay_sec
+ $result["slept_sec"] = $sleep_delay_sec
+}
+
+If ($fail_mode -contains "leading_junk") {
+ Write-Output "leading junk before module output"
+}
+
+If ($fail_mode -contains "graceful") {
+ Fail-Json $result "failed gracefully"
+}
+
+Try {
+
+ If ($fail_mode -contains "exception") {
+ Throw "failing via exception"
+ }
+
+ Exit-Json $result
+}
+Finally {
+ If ($fail_mode -contains "trailing_junk") {
+ Write-Output "trailing junk after module output"
+ }
+}
diff --git a/test/integration/targets/win_async_wrapper/tasks/main.yml b/test/integration/targets/win_async_wrapper/tasks/main.yml
new file mode 100644
index 0000000..91b4584
--- /dev/null
+++ b/test/integration/targets/win_async_wrapper/tasks/main.yml
@@ -0,0 +1,257 @@
+- name: capture timestamp before fire and forget
+ set_fact:
+ start_timestamp: "{{ lookup('pipe', 'date +%s') }}"
+
+- name: async fire and forget
+ async_test:
+ sleep_delay_sec: 15
+ async: 20
+ poll: 0
+ register: asyncresult
+
+- name: validate response
+ assert:
+ that:
+ - asyncresult.ansible_job_id is match('\d+\.\d+')
+ - asyncresult.started == 1
+ - asyncresult is started
+ - asyncresult.finished == 0
+ - asyncresult is not finished
+ - asyncresult.results_file is search('\.ansible_async.+\d+\.\d+')
+ # ensure that async is actually async- this test will fail if # hosts > forks or if the target host is VERY slow
+ - (lookup('pipe', 'date +%s') | int) - (start_timestamp | int) < 15
+
+- name: async poll immediate success
+ async_test:
+ sleep_delay_sec: 0
+ async: 10
+ poll: 1
+ register: asyncresult
+
+- name: validate response
+ assert:
+ that:
+ - asyncresult.ansible_job_id is match('\d+\.\d+')
+ - asyncresult.finished == 1
+ - asyncresult is finished
+ - asyncresult is changed
+ - asyncresult.ansible_async_watchdog_pid is number
+# - asyncresult.module_tempdir is search('ansible-tmp-')
+ - asyncresult.module_pid is number
+
+# this part of the test is flaky- Windows PIDs are reused aggressively, so this occasionally fails due to a new process with the same ID
+# FUTURE: consider having the test module hook to a kernel object we can poke at that gets signaled/released on exit
+#- name: ensure that watchdog and module procs have exited
+# raw: Get-Process | Where { $_.Id -in ({{ asyncresult.ansible_async_watchdog_pid }}, {{ asyncresult.module_pid }}) }
+# register: proclist
+#
+#- name: validate no running watchdog/module processes were returned
+# assert:
+# that:
+# - proclist.stdout.strip() == ''
+
+#- name: ensure that module_tempdir was deleted
+# raw: Test-Path {{ asyncresult.module_tempdir }}
+# register: tempdircheck
+#
+#- name: validate tempdir response
+# assert:
+# that:
+# - tempdircheck.stdout is search('False')
+
+- name: async poll retry
+ async_test:
+ sleep_delay_sec: 5
+ async: 10
+ poll: 1
+ register: asyncresult
+
+- name: validate response
+ assert:
+ that:
+ - asyncresult.ansible_job_id is match('\d+\.\d+')
+ - asyncresult.finished == 1
+ - asyncresult is finished
+ - asyncresult is changed
+# - asyncresult.module_tempdir is search('ansible-tmp-')
+ - asyncresult.module_pid is number
+
+# this part of the test is flaky- Windows PIDs are reused aggressively, so this occasionally fails due to a new process with the same ID
+# FUTURE: consider having the test module hook to a kernel object we can poke at that gets signaled/released on exit
+#- name: ensure that watchdog and module procs have exited
+# raw: Get-Process | Where { $_.Id -in ({{ asyncresult.ansible_async_watchdog_pid }}, {{ asyncresult.module_pid }}) }
+# register: proclist
+#
+#- name: validate no running watchdog/module processes were returned
+# assert:
+# that:
+# - proclist.stdout.strip() == ''
+
+#- name: ensure that module_tempdir was deleted
+# raw: Test-Path {{ asyncresult.module_tempdir }}
+# register: tempdircheck
+#
+#- name: validate tempdir response
+# assert:
+# that:
+# - tempdircheck.stdout is search('False')
+
+- name: async poll timeout
+ async_test:
+ sleep_delay_sec: 5
+ async: 3
+ poll: 1
+ register: asyncresult
+ ignore_errors: true
+
+- name: validate response
+ assert:
+ that:
+ - asyncresult.ansible_job_id is match('\d+\.\d+')
+ - asyncresult.finished == 1
+ - asyncresult is finished
+ - asyncresult is not changed
+ - asyncresult is failed
+ - asyncresult.msg is search('timed out')
+
+- name: async poll graceful module failure
+ async_test:
+ fail_mode: graceful
+ async: 5
+ poll: 1
+ register: asyncresult
+ ignore_errors: true
+
+- name: validate response
+ assert:
+ that:
+ - asyncresult.ansible_job_id is match('\d+\.\d+')
+ - asyncresult.finished == 1
+ - asyncresult is finished
+ - asyncresult is changed
+ - asyncresult is failed
+ - asyncresult.msg == 'failed gracefully'
+
+- name: async poll exception module failure
+ async_test:
+ fail_mode: exception
+ async: 5
+ poll: 1
+ register: asyncresult
+ ignore_errors: true
+
+- name: validate response
+ assert:
+ that:
+ - asyncresult.ansible_job_id is match('\d+\.\d+')
+ - asyncresult.finished == 1
+ - asyncresult is finished
+ - asyncresult is not changed
+ - asyncresult is failed
+ - 'asyncresult.msg == "Unhandled exception while executing module: failing via exception"'
+
+- name: echo some non ascii characters
+ win_command: cmd.exe /c echo über den Fußgängerübergang gehen
+ async: 10
+ poll: 1
+ register: nonascii_output
+
+- name: assert echo some non ascii characters
+ assert:
+ that:
+ - nonascii_output is changed
+ - nonascii_output.rc == 0
+ - nonascii_output.stdout_lines|count == 1
+ - nonascii_output.stdout_lines[0] == 'über den Fußgängerübergang gehen'
+ - nonascii_output.stderr == ''
+
+- name: test async with custom async dir
+ win_shell: echo hi
+ register: async_custom_dir
+ async: 5
+ vars:
+ ansible_async_dir: '{{win_output_dir}}'
+
+- name: assert results file is in the remote tmp specified
+ assert:
+ that:
+ - async_custom_dir.results_file == win_output_dir + '\\' + async_custom_dir.ansible_job_id
+
+- name: test async fire and forget with custom async dir
+ win_shell: echo hi
+ register: async_custom_dir_poll
+ async: 5
+ poll: 0
+ vars:
+ ansible_async_dir: '{{win_output_dir}}'
+
+- name: poll with different dir - fail
+ async_status:
+ jid: '{{ async_custom_dir_poll.ansible_job_id }}'
+ register: fail_async_custom_dir_poll
+ ignore_errors: yes
+
+- name: poll with different dir - success
+ async_status:
+ jid: '{{ async_custom_dir_poll.ansible_job_id }}'
+ register: success_async_custom_dir_poll
+ vars:
+ ansible_async_dir: '{{win_output_dir}}'
+
+- name: assert test async fire and forget with custom async dir
+ assert:
+ that:
+ - fail_async_custom_dir_poll.failed
+ - '"could not find job at ''" + nonascii_output.results_file|win_dirname + "''" in fail_async_custom_dir_poll.msg'
+ - not success_async_custom_dir_poll.failed
+ - success_async_custom_dir_poll.results_file == win_output_dir + '\\' + async_custom_dir_poll.ansible_job_id
+
+# FUTURE: figure out why the last iteration of this test often fails on shippable
+#- name: loop async success
+# async_test:
+# sleep_delay_sec: 3
+# async: 10
+# poll: 0
+# with_sequence: start=1 end=4
+# register: async_many
+#
+#- name: wait for completion
+# async_status:
+# jid: "{{ item }}"
+# register: asyncout
+# until: asyncout is finished
+# retries: 10
+# delay: 1
+# with_items: "{{ async_many.results | map(attribute='ansible_job_id') | list }}"
+#
+#- name: validate results
+# assert:
+# that:
+# - item.finished == 1
+# - item is finished
+# - item.slept_sec == 3
+# - item is changed
+# - item.ansible_job_id is match('\d+\.\d+')
+# with_items: "{{ asyncout.results }}"
+
+# this part of the test is flaky- Windows PIDs are reused aggressively, so this occasionally fails due to a new process with the same ID
+# FUTURE: consider having the test module hook to a kernel object we can poke at that gets signaled/released on exit
+#- name: ensure that all watchdog and module procs have exited
+# raw: Get-Process | Where { $_.Id -in ({{ asyncout.results | join(',', attribute='ansible_async_watchdog_pid') }}, {{ asyncout.results | join(',', attribute='module_pid') }}) }
+# register: proclist
+#
+#- name: validate no processes were returned
+# assert:
+# that:
+# - proclist.stdout.strip() == ""
+
+# FUTURE: test junk before/after JSON
+# FUTURE: verify tempdir stays through module exec
+# FUTURE: verify tempdir is deleted after module exec
+# FUTURE: verify tempdir is permanent with ANSIBLE_KEEP_REMOTE_FILES=1 (how?)
+# FUTURE: verify binary modules work
+
+# FUTURE: test status/return
+# FUTURE: test status/cleanup
+# FUTURE: test reboot/connection failure
+# FUTURE: figure out how to ensure that processes and tempdirs are cleaned up in all exceptional cases
diff --git a/test/integration/targets/win_become/aliases b/test/integration/targets/win_become/aliases
new file mode 100644
index 0000000..1eed2ec
--- /dev/null
+++ b/test/integration/targets/win_become/aliases
@@ -0,0 +1,2 @@
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/win_become/tasks/main.yml b/test/integration/targets/win_become/tasks/main.yml
new file mode 100644
index 0000000..a075958
--- /dev/null
+++ b/test/integration/targets/win_become/tasks/main.yml
@@ -0,0 +1,251 @@
+- set_fact:
+ become_test_username: ansible_become_test
+ become_test_admin_username: ansible_become_admin
+ gen_pw: "{{ 'password123!' + lookup('password', '/dev/null chars=ascii_letters,digits length=8') }}"
+
+- name: create unprivileged user
+ win_user:
+ name: "{{ become_test_username }}"
+ password: "{{ gen_pw }}"
+ update_password: always
+ groups: Users
+ register: user_limited_result
+
+- name: create a privileged user
+ win_user:
+ name: "{{ become_test_admin_username }}"
+ password: "{{ gen_pw }}"
+ update_password: always
+ groups: Administrators
+ register: user_admin_result
+
+- name: add requisite logon rights for test user
+ win_user_right:
+ name: '{{item}}'
+ users: '{{become_test_username}}'
+ action: add
+ with_items:
+ - SeNetworkLogonRight
+ - SeInteractiveLogonRight
+ - SeBatchLogonRight
+
+- name: fetch current target date/time for log filtering
+ raw: '[datetime]::now | Out-String'
+ register: test_starttime
+
+- name: execute tests and ensure that test user is deleted regardless of success/failure
+ block:
+ - name: ensure current user is not the become user
+ win_whoami:
+ register: whoami_out
+ failed_when: whoami_out.account.sid == user_limited_result.sid or whoami_out.account.sid == user_admin_result.sid
+
+ - name: get become user profile dir so we can clean it up later
+ vars: &become_vars
+ ansible_become_user: "{{ become_test_username }}"
+ ansible_become_password: "{{ gen_pw }}"
+ ansible_become_method: runas
+ ansible_become: yes
+ win_shell: $env:USERPROFILE
+ register: profile_dir_out
+
+ - name: ensure profile dir contains test username (eg, if become fails silently, prevent deletion of real user profile)
+ assert:
+ that:
+ - become_test_username in profile_dir_out.stdout_lines[0]
+
+ - name: get become admin user profile dir so we can clean it up later
+ vars: &admin_become_vars
+ ansible_become_user: "{{ become_test_admin_username }}"
+ ansible_become_password: "{{ gen_pw }}"
+ ansible_become_method: runas
+ ansible_become: yes
+ win_shell: $env:USERPROFILE
+ register: admin_profile_dir_out
+
+ - name: ensure profile dir contains admin test username
+ assert:
+ that:
+ - become_test_admin_username in admin_profile_dir_out.stdout_lines[0]
+
+ - name: test become runas via task vars (underprivileged user)
+ vars: *become_vars
+ win_whoami:
+ register: whoami_out
+
+ - name: verify output
+ assert:
+ that:
+ - whoami_out.account.sid == user_limited_result.sid
+ - whoami_out.account.account_name == become_test_username
+ - whoami_out.label.account_name == 'Medium Mandatory Level'
+ - whoami_out.label.sid == 'S-1-16-8192'
+ - whoami_out.logon_type == 'Interactive'
+
+ - name: test become runas via task vars (privileged user)
+ vars: *admin_become_vars
+ win_whoami:
+ register: whoami_out
+
+ - name: verify output
+ assert:
+ that:
+ - whoami_out.account.sid == user_admin_result.sid
+ - whoami_out.account.account_name == become_test_admin_username
+ - whoami_out.label.account_name == 'High Mandatory Level'
+ - whoami_out.label.sid == 'S-1-16-12288'
+ - whoami_out.logon_type == 'Interactive'
+
+ - name: test become runas via task keywords
+ vars:
+ ansible_become_password: "{{ gen_pw }}"
+ become: yes
+ become_method: runas
+ become_user: "{{ become_test_username }}"
+ win_shell: whoami
+ register: whoami_out
+
+ - name: verify output
+ assert:
+ that:
+ - whoami_out.stdout_lines[0].endswith(become_test_username)
+
+ - name: test become via block vars
+ vars: *become_vars
+ block:
+ - name: ask who the current user is
+ win_whoami:
+ register: whoami_out
+
+ - name: verify output
+ assert:
+ that:
+ - whoami_out.account.sid == user_limited_result.sid
+ - whoami_out.account.account_name == become_test_username
+ - whoami_out.label.account_name == 'Medium Mandatory Level'
+ - whoami_out.label.sid == 'S-1-16-8192'
+ - whoami_out.logon_type == 'Interactive'
+
+ - name: test with module that will return non-zero exit code (https://github.com/ansible/ansible/issues/30468)
+ vars: *become_vars
+ setup:
+
+ - name: test become with invalid password
+ win_whoami:
+ vars:
+ ansible_become_pass: '{{ gen_pw }}abc'
+ become: yes
+ become_method: runas
+ become_user: '{{ become_test_username }}'
+ register: become_invalid_pass
+ failed_when:
+ - '"Failed to become user " + become_test_username not in become_invalid_pass.msg'
+ - '"LogonUser failed" not in become_invalid_pass.msg'
+ - '"Win32ErrorCode 1326 - 0x0000052E)" not in become_invalid_pass.msg'
+
+ - name: test become password precedence
+ win_whoami:
+ become: yes
+ become_method: runas
+ become_user: '{{ become_test_username }}'
+ vars:
+ ansible_become_pass: broken
+ ansible_runas_pass: '{{ gen_pw }}' # should have a higher precedence than ansible_become_pass
+
+ - name: test become + async
+ vars: *become_vars
+ win_command: whoami
+ async: 10
+ register: whoami_out
+
+ - name: verify become + async worked
+ assert:
+ that:
+ - whoami_out is successful
+ - become_test_username in whoami_out.stdout
+
+ - name: test failure with string become invalid key
+ vars: *become_vars
+ win_whoami:
+ become_flags: logon_type=batch invalid_flags=a
+ become_method: runas
+ register: failed_flags_invalid_key
+ failed_when: "failed_flags_invalid_key.msg != \"internal error: failed to parse become_flags 'logon_type=batch invalid_flags=a': become_flags key 'invalid_flags' is not a valid runas flag, must be 'logon_type' or 'logon_flags'\""
+
+ - name: test failure with invalid logon_type
+ vars: *become_vars
+ win_whoami:
+ become_flags: logon_type=invalid
+ register: failed_flags_invalid_type
+ failed_when: "failed_flags_invalid_type.msg != \"internal error: failed to parse become_flags 'logon_type=invalid': become_flags logon_type value 'invalid' is not valid, valid values are: interactive, network, batch, service, unlock, network_cleartext, new_credentials\""
+
+ - name: test failure with invalid logon_flag
+ vars: *become_vars
+ win_whoami:
+ become_flags: logon_flags=with_profile,invalid
+ register: failed_flags_invalid_flag
+ failed_when: "failed_flags_invalid_flag.msg != \"internal error: failed to parse become_flags 'logon_flags=with_profile,invalid': become_flags logon_flags value 'invalid' is not valid, valid values are: with_profile, netcredentials_only\""
+
+ - name: echo some non ascii characters
+ win_command: cmd.exe /c echo über den Fußgängerübergang gehen
+ vars: *become_vars
+ register: nonascii_output
+
+ - name: assert echo some non ascii characters
+ assert:
+ that:
+ - nonascii_output is changed
+ - nonascii_output.rc == 0
+ - nonascii_output.stdout_lines|count == 1
+ - nonascii_output.stdout_lines[0] == 'über den Fußgängerübergang gehen'
+ - nonascii_output.stderr == ''
+
+ - name: get PS events containing password or module args created since test start
+ raw: |
+ $dt=[datetime]"{{ test_starttime.stdout|trim }}"
+ (Get-WinEvent -LogName Microsoft-Windows-Powershell/Operational |
+ ? { $_.TimeCreated -ge $dt -and $_.Message -match "{{ gen_pw }}" }).Count
+ register: ps_log_count
+
+ - name: assert no PS events contain password or module args
+ assert:
+ that:
+ - ps_log_count.stdout | int == 0
+
+# FUTURE: test raw + script become behavior once they're running under the exec wrapper again
+# FUTURE: add standalone playbook tests to include password prompting and play become keywords
+
+ always:
+ - name: remove explicit logon rights for test user
+ win_user_right:
+ name: '{{item}}'
+ users: '{{become_test_username}}'
+ action: remove
+ with_items:
+ - SeNetworkLogonRight
+ - SeInteractiveLogonRight
+ - SeBatchLogonRight
+
+ - name: ensure underprivileged test user is deleted
+ win_user:
+ name: "{{ become_test_username }}"
+ state: absent
+
+ - name: ensure privileged test user is deleted
+ win_user:
+ name: "{{ become_test_admin_username }}"
+ state: absent
+
+ - name: ensure underprivileged test user profile is deleted
+ # NB: have to work around powershell limitation of long filenames until win_file fixes it
+ win_shell: rmdir /S /Q {{ profile_dir_out.stdout_lines[0] }}
+ args:
+ executable: cmd.exe
+ when: become_test_username in profile_dir_out.stdout_lines[0]
+
+ - name: ensure privileged test user profile is deleted
+ # NB: have to work around powershell limitation of long filenames until win_file fixes it
+ win_shell: rmdir /S /Q {{ admin_profile_dir_out.stdout_lines[0] }}
+ args:
+ executable: cmd.exe
+ when: become_test_admin_username in admin_profile_dir_out.stdout_lines[0]
diff --git a/test/integration/targets/win_exec_wrapper/aliases b/test/integration/targets/win_exec_wrapper/aliases
new file mode 100644
index 0000000..1eed2ec
--- /dev/null
+++ b/test/integration/targets/win_exec_wrapper/aliases
@@ -0,0 +1,2 @@
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/win_exec_wrapper/library/test_all_options.ps1 b/test/integration/targets/win_exec_wrapper/library/test_all_options.ps1
new file mode 100644
index 0000000..7c2c9c7
--- /dev/null
+++ b/test/integration/targets/win_exec_wrapper/library/test_all_options.ps1
@@ -0,0 +1,12 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.SID
+#Requires -Version 3.0
+#AnsibleRequires -OSVersion 6
+#AnsibleRequires -Become
+
+$output = &whoami.exe
+$sid = Convert-ToSID -account_name $output.Trim()
+
+Exit-Json -obj @{ output = $sid; changed = $false }
diff --git a/test/integration/targets/win_exec_wrapper/library/test_common_functions.ps1 b/test/integration/targets/win_exec_wrapper/library/test_common_functions.ps1
new file mode 100644
index 0000000..dde1ebc
--- /dev/null
+++ b/test/integration/targets/win_exec_wrapper/library/test_common_functions.ps1
@@ -0,0 +1,43 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+$ErrorActionPreference = "Stop"
+
+Function Assert-Equal($actual, $expected) {
+ if ($actual -cne $expected) {
+ $call_stack = (Get-PSCallStack)[1]
+ $error_msg = -join @(
+ "AssertionError:`r`nActual: `"$actual`" != Expected: `"$expected`"`r`nLine: "
+ "$($call_stack.ScriptLineNumber), Method: $($call_stack.Position.Text)"
+ )
+ Fail-Json -obj $result -message $error_msg
+ }
+}
+
+$result = @{
+ changed = $false
+}
+
+#ConvertFrom-AnsibleJso
+$input_json = '{"string":"string","float":3.1415926,"dict":{"string":"string","int":1},"list":["entry 1","entry 2"],"null":null,"int":1}'
+$actual = ConvertFrom-AnsibleJson -InputObject $input_json
+Assert-Equal -actual $actual.GetType() -expected ([Hashtable])
+Assert-Equal -actual $actual.string.GetType() -expected ([String])
+Assert-Equal -actual $actual.string -expected "string"
+Assert-Equal -actual $actual.int.GetType() -expected ([Int32])
+Assert-Equal -actual $actual.int -expected 1
+Assert-Equal -actual $actual.null -expected $null
+Assert-Equal -actual $actual.float.GetType() -expected ([Decimal])
+Assert-Equal -actual $actual.float -expected 3.1415926
+Assert-Equal -actual $actual.list.GetType() -expected ([Object[]])
+Assert-Equal -actual $actual.list.Count -expected 2
+Assert-Equal -actual $actual.list[0] -expected "entry 1"
+Assert-Equal -actual $actual.list[1] -expected "entry 2"
+Assert-Equal -actual $actual.GetType() -expected ([Hashtable])
+Assert-Equal -actual $actual.dict.string -expected "string"
+Assert-Equal -actual $actual.dict.int -expected 1
+
+$result.msg = "good"
+Exit-Json -obj $result
+
diff --git a/test/integration/targets/win_exec_wrapper/library/test_fail.ps1 b/test/integration/targets/win_exec_wrapper/library/test_fail.ps1
new file mode 100644
index 0000000..72b89c6
--- /dev/null
+++ b/test/integration/targets/win_exec_wrapper/library/test_fail.ps1
@@ -0,0 +1,66 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+$params = Parse-Args $args -supports_check_mode $true
+
+$data = Get-AnsibleParam -obj $params -name "data" -type "str" -default "normal"
+$result = @{
+ changed = $false
+}
+
+<#
+This module tests various error events in PowerShell to verify our hidden trap
+catches them all and outputs a pretty error message with a traceback to help
+users debug the actual issue
+
+normal - normal execution, no errors
+fail - Calls Fail-Json like normal
+throw - throws an exception
+error - Write-Error with ErrorActionPreferenceStop
+cmdlet_error - Calls a Cmdlet with an invalid error
+dotnet_exception - Calls a .NET function that will throw an error
+function_throw - Throws an exception in a function
+proc_exit_fine - calls an executable with a non-zero exit code with Exit-Json
+proc_exit_fail - calls an executable with a non-zero exit code with Fail-Json
+#>
+
+Function Test-ThrowException {
+ throw "exception in function"
+}
+
+if ($data -eq "normal") {
+ Exit-Json -obj $result
+}
+elseif ($data -eq "fail") {
+ Fail-Json -obj $result -message "fail message"
+}
+elseif ($data -eq "throw") {
+ throw [ArgumentException]"module is thrown"
+}
+elseif ($data -eq "error") {
+ Write-Error -Message $data
+}
+elseif ($data -eq "cmdlet_error") {
+ Get-Item -Path "fake:\path"
+}
+elseif ($data -eq "dotnet_exception") {
+ [System.IO.Path]::GetFullPath($null)
+}
+elseif ($data -eq "function_throw") {
+ Test-ThrowException
+}
+elseif ($data -eq "proc_exit_fine") {
+ # verifies that if no error was actually fired and we have an output, we
+ # don't use the RC to validate if the module failed
+ &cmd.exe /c exit 2
+ Exit-Json -obj $result
+}
+elseif ($data -eq "proc_exit_fail") {
+ &cmd.exe /c exit 2
+ Fail-Json -obj $result -message "proc_exit_fail"
+}
+
+# verify no exception were silently caught during our tests
+Fail-Json -obj $result -message "end of module"
+
diff --git a/test/integration/targets/win_exec_wrapper/library/test_invalid_requires.ps1 b/test/integration/targets/win_exec_wrapper/library/test_invalid_requires.ps1
new file mode 100644
index 0000000..89727ef
--- /dev/null
+++ b/test/integration/targets/win_exec_wrapper/library/test_invalid_requires.ps1
@@ -0,0 +1,9 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+# Requires -Version 20
+# AnsibleRequires -OSVersion 20
+
+# requires statement must be straight after the original # with now space, this module won't fail
+
+Exit-Json -obj @{ output = "output"; changed = $false }
diff --git a/test/integration/targets/win_exec_wrapper/library/test_min_os_version.ps1 b/test/integration/targets/win_exec_wrapper/library/test_min_os_version.ps1
new file mode 100644
index 0000000..39b1ded
--- /dev/null
+++ b/test/integration/targets/win_exec_wrapper/library/test_min_os_version.ps1
@@ -0,0 +1,8 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#AnsibleRequires -OSVersion 20.0
+
+# this shouldn't run as no Windows OS will meet the version of 20.0
+
+Exit-Json -obj @{ output = "output"; changed = $false }
diff --git a/test/integration/targets/win_exec_wrapper/library/test_min_ps_version.ps1 b/test/integration/targets/win_exec_wrapper/library/test_min_ps_version.ps1
new file mode 100644
index 0000000..bb5fd0f
--- /dev/null
+++ b/test/integration/targets/win_exec_wrapper/library/test_min_ps_version.ps1
@@ -0,0 +1,8 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Version 20.0.0.0
+
+# this shouldn't run as no PS Version will be at 20 in the near future
+
+Exit-Json -obj @{ output = "output"; changed = $false }
diff --git a/test/integration/targets/win_exec_wrapper/tasks/main.yml b/test/integration/targets/win_exec_wrapper/tasks/main.yml
new file mode 100644
index 0000000..8fc54f7
--- /dev/null
+++ b/test/integration/targets/win_exec_wrapper/tasks/main.yml
@@ -0,0 +1,274 @@
+---
+- name: fetch current target date/time for log filtering
+ raw: '[datetime]::now | Out-String'
+ register: test_starttime
+
+- name: test normal module execution
+ test_fail:
+ register: normal
+
+- name: assert test normal module execution
+ assert:
+ that:
+ - not normal is failed
+
+- name: test fail module execution
+ test_fail:
+ data: fail
+ register: fail_module
+ ignore_errors: yes
+
+- name: assert test fail module execution
+ assert:
+ that:
+ - fail_module is failed
+ - fail_module.msg == "fail message"
+ - not fail_module.exception is defined
+
+- name: test module with exception thrown
+ test_fail:
+ data: throw
+ register: throw_module
+ ignore_errors: yes
+
+- name: assert test module with exception thrown
+ assert:
+ that:
+ - throw_module is failed
+ - 'throw_module.msg == "Unhandled exception while executing module: module is thrown"'
+ - '"throw [ArgumentException]\"module is thrown\"" in throw_module.exception'
+
+- name: test module with error msg
+ test_fail:
+ data: error
+ register: error_module
+ ignore_errors: yes
+ vars:
+ # Running with coverage means the module is run from a script and not as a psuedo script in a pipeline. This
+ # results in a different error message being returned so we disable coverage collection for this task.
+ _ansible_coverage_remote_output: ''
+
+- name: assert test module with error msg
+ assert:
+ that:
+ - error_module is failed
+ - 'error_module.msg == "Unhandled exception while executing module: error"'
+ - '"Write-Error -Message $data" in error_module.exception'
+
+- name: test module with cmdlet error
+ test_fail:
+ data: cmdlet_error
+ register: cmdlet_error
+ ignore_errors: yes
+
+- name: assert test module with cmdlet error
+ assert:
+ that:
+ - cmdlet_error is failed
+ - 'cmdlet_error.msg == "Unhandled exception while executing module: Cannot find drive. A drive with the name ''fake'' does not exist."'
+ - '"Get-Item -Path \"fake:\\path\"" in cmdlet_error.exception'
+
+- name: test module with .NET exception
+ test_fail:
+ data: dotnet_exception
+ register: dotnet_exception
+ ignore_errors: yes
+
+- name: assert test module with .NET exception
+ assert:
+ that:
+ - dotnet_exception is failed
+ - 'dotnet_exception.msg == "Unhandled exception while executing module: Exception calling \"GetFullPath\" with \"1\" argument(s): \"The path is not of a legal form.\""'
+ - '"[System.IO.Path]::GetFullPath($null)" in dotnet_exception.exception'
+
+- name: test module with function exception
+ test_fail:
+ data: function_throw
+ register: function_exception
+ ignore_errors: yes
+ vars:
+ _ansible_coverage_remote_output: ''
+
+- name: assert test module with function exception
+ assert:
+ that:
+ - function_exception is failed
+ - 'function_exception.msg == "Unhandled exception while executing module: exception in function"'
+ - '"throw \"exception in function\"" in function_exception.exception'
+ - '"at Test-ThrowException, <No file>: line" in function_exception.exception'
+
+- name: test module with fail process but Exit-Json
+ test_fail:
+ data: proc_exit_fine
+ register: proc_exit_fine
+
+- name: assert test module with fail process but Exit-Json
+ assert:
+ that:
+ - not proc_exit_fine is failed
+
+- name: test module with fail process but Fail-Json
+ test_fail:
+ data: proc_exit_fail
+ register: proc_exit_fail
+ ignore_errors: yes
+
+- name: assert test module with fail process but Fail-Json
+ assert:
+ that:
+ - proc_exit_fail is failed
+ - proc_exit_fail.msg == "proc_exit_fail"
+ - not proc_exit_fail.exception is defined
+
+- name: test out invalid options
+ test_invalid_requires:
+ register: invalid_options
+
+- name: assert test out invalid options
+ assert:
+ that:
+ - invalid_options is successful
+ - invalid_options.output == "output"
+
+- name: test out invalid os version
+ test_min_os_version:
+ register: invalid_os_version
+ ignore_errors: yes
+
+- name: assert test out invalid os version
+ assert:
+ that:
+ - invalid_os_version is failed
+ - '"This module cannot run on this OS as it requires a minimum version of 20.0, actual was " in invalid_os_version.msg'
+
+- name: test out invalid powershell version
+ test_min_ps_version:
+ register: invalid_ps_version
+ ignore_errors: yes
+
+- name: assert test out invalid powershell version
+ assert:
+ that:
+ - invalid_ps_version is failed
+ - '"This module cannot run as it requires a minimum PowerShell version of 20.0.0.0, actual was " in invalid_ps_version.msg'
+
+- name: test out environment block for task
+ win_shell: set
+ args:
+ executable: cmd.exe
+ environment:
+ String: string value
+ Int: 1234
+ Bool: True
+ double_quote: 'double " quote'
+ single_quote: "single ' quote"
+ hyphen-var: abc@123
+ '_-(){}[]<>*+-/\?"''!@#$%^&|;:i,.`~0': '_-(){}[]<>*+-/\?"''!@#$%^&|;:i,.`~0'
+ '‘key': 'value‚'
+ register: environment_block
+
+- name: assert environment block for task
+ assert:
+ that:
+ - '"String=string value" in environment_block.stdout_lines'
+ - '"Int=1234" in environment_block.stdout_lines'
+ - '"Bool=True" in environment_block.stdout_lines'
+ - '"double_quote=double \" quote" in environment_block.stdout_lines'
+ - '"single_quote=single '' quote" in environment_block.stdout_lines'
+ - '"hyphen-var=abc@123" in environment_block.stdout_lines'
+ # yaml escaping rules - (\\ == \), (\" == "), ('' == ')
+ - '"_-(){}[]<>*+-/\\?\"''!@#$%^&|;:i,.`~0=_-(){}[]<>*+-/\\?\"''!@#$%^&|;:i,.`~0" in environment_block.stdout_lines'
+ - '"‘key=value‚" in environment_block.stdout_lines'
+
+- name: test out become requires without become_user set
+ test_all_options:
+ register: become_system
+
+- name: assert become requires without become_user set
+ assert:
+ that:
+ - become_system is successful
+ - become_system.output == "S-1-5-18"
+
+- set_fact:
+ become_test_username: ansible_become_test
+ gen_pw: "{{ 'password123!' + lookup('password', '/dev/null chars=ascii_letters,digits length=8') }}"
+
+- name: create unprivileged user
+ win_user:
+ name: "{{ become_test_username }}"
+ password: "{{ gen_pw }}"
+ update_password: always
+ groups: Users
+ register: become_test_user_result
+
+- name: execute tests and ensure that test user is deleted regardless of success/failure
+ block:
+ - name: ensure current user is not the become user
+ win_shell: whoami
+ register: whoami_out
+
+ - name: verify output
+ assert:
+ that:
+ - not whoami_out.stdout_lines[0].endswith(become_test_username)
+
+ - name: get become user profile dir so we can clean it up later
+ vars: &become_vars
+ ansible_become_user: "{{ become_test_username }}"
+ ansible_become_password: "{{ gen_pw }}"
+ ansible_become_method: runas
+ ansible_become: yes
+ win_shell: $env:USERPROFILE
+ register: profile_dir_out
+
+ - name: ensure profile dir contains test username (eg, if become fails silently, prevent deletion of real user profile)
+ assert:
+ that:
+ - become_test_username in profile_dir_out.stdout_lines[0]
+
+ - name: test out become requires when become_user set
+ test_all_options:
+ vars: *become_vars
+ register: become_system
+
+ - name: assert become requires when become_user set
+ assert:
+ that:
+ - become_system is successful
+ - become_system.output == become_test_user_result.sid
+
+ always:
+ - name: ensure test user is deleted
+ win_user:
+ name: "{{ become_test_username }}"
+ state: absent
+
+ - name: ensure test user profile is deleted
+ # NB: have to work around powershell limitation of long filenames until win_file fixes it
+ win_shell: rmdir /S /Q {{ profile_dir_out.stdout_lines[0] }}
+ args:
+ executable: cmd.exe
+ when: become_test_username in profile_dir_out.stdout_lines[0]
+
+- name: test common functions in exec
+ test_common_functions:
+ register: common_functions_res
+
+- name: assert test common functions in exec
+ assert:
+ that:
+ - not common_functions_res is failed
+ - common_functions_res.msg == "good"
+
+- name: get PS events containing module args or envvars created since test start
+ raw: |
+ $dt=[datetime]"{{ test_starttime.stdout|trim }}"
+ (Get-WinEvent -LogName Microsoft-Windows-Powershell/Operational |
+ ? { $_.TimeCreated -ge $dt -and $_.Message -match "fail_module|hyphen-var" }).Count
+ register: ps_log_count
+
+- name: assert no PS events contain module args or envvars
+ assert:
+ that:
+ - ps_log_count.stdout | int == 0
diff --git a/test/integration/targets/win_fetch/aliases b/test/integration/targets/win_fetch/aliases
new file mode 100644
index 0000000..4cd27b3
--- /dev/null
+++ b/test/integration/targets/win_fetch/aliases
@@ -0,0 +1 @@
+shippable/windows/group1
diff --git a/test/integration/targets/win_fetch/meta/main.yml b/test/integration/targets/win_fetch/meta/main.yml
new file mode 100644
index 0000000..9f37e96
--- /dev/null
+++ b/test/integration/targets/win_fetch/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+- setup_remote_tmp_dir
diff --git a/test/integration/targets/win_fetch/tasks/main.yml b/test/integration/targets/win_fetch/tasks/main.yml
new file mode 100644
index 0000000..78b6fa0
--- /dev/null
+++ b/test/integration/targets/win_fetch/tasks/main.yml
@@ -0,0 +1,212 @@
+# test code for the fetch module when using winrm connection
+# (c) 2014, Chris Church <chris@ninemoreminutes.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: define host-specific host_output_dir
+ set_fact:
+ host_output_dir: "{{ output_dir }}/{{ inventory_hostname }}"
+
+- name: clean out the test directory
+ file: name={{ host_output_dir|mandatory }} state=absent
+ delegate_to: localhost
+ run_once: true
+
+- name: create the test directory
+ file: name={{ host_output_dir }} state=directory
+ delegate_to: localhost
+ run_once: true
+
+- name: fetch a small file
+ fetch: src="C:/Windows/win.ini" dest={{ host_output_dir }}
+ register: fetch_small
+
+- name: check fetch small result
+ assert:
+ that:
+ - "fetch_small.changed"
+
+- name: check file created by fetch small
+ stat: path={{ fetch_small.dest }}
+ delegate_to: localhost
+ register: fetch_small_stat
+
+- name: verify fetched small file exists locally
+ assert:
+ that:
+ - "fetch_small_stat.stat.exists"
+ - "fetch_small_stat.stat.isreg"
+ - "fetch_small_stat.stat.checksum == fetch_small.checksum"
+
+- name: fetch the same small file
+ fetch: src="C:/Windows/win.ini" dest={{ host_output_dir }}
+ register: fetch_small_again
+
+- name: check fetch small result again
+ assert:
+ that:
+ - "not fetch_small_again.changed"
+
+- name: fetch a small file to flat namespace
+ fetch: src="C:/Windows/win.ini" dest="{{ host_output_dir }}/" flat=yes
+ register: fetch_flat
+
+- name: check fetch flat result
+ assert:
+ that:
+ - "fetch_flat.changed"
+
+- name: check file created by fetch flat
+ stat: path="{{ host_output_dir }}/win.ini"
+ delegate_to: localhost
+ register: fetch_flat_stat
+
+- name: verify fetched file exists locally in host_output_dir
+ assert:
+ that:
+ - "fetch_flat_stat.stat.exists"
+ - "fetch_flat_stat.stat.isreg"
+ - "fetch_flat_stat.stat.checksum == fetch_flat.checksum"
+
+#- name: fetch a small file to flat directory (without trailing slash)
+# fetch: src="C:/Windows/win.ini" dest="{{ host_output_dir }}" flat=yes
+# register: fetch_flat_dir
+
+#- name: check fetch flat to directory result
+# assert:
+# that:
+# - "fetch_flat_dir is not changed"
+
+- name: fetch a large binary file
+ fetch: src="C:/Windows/explorer.exe" dest={{ host_output_dir }}
+ register: fetch_large
+
+- name: check fetch large binary file result
+ assert:
+ that:
+ - "fetch_large.changed"
+
+- name: check file created by fetch large binary
+ stat: path={{ fetch_large.dest }}
+ delegate_to: localhost
+ register: fetch_large_stat
+
+- name: verify fetched large file exists locally
+ assert:
+ that:
+ - "fetch_large_stat.stat.exists"
+ - "fetch_large_stat.stat.isreg"
+ - "fetch_large_stat.stat.checksum == fetch_large.checksum"
+
+- name: fetch a large binary file again
+ fetch: src="C:/Windows/explorer.exe" dest={{ host_output_dir }}
+ register: fetch_large_again
+
+- name: check fetch large binary file result again
+ assert:
+ that:
+ - "not fetch_large_again.changed"
+
+- name: fetch a small file using backslashes in src path
+ fetch: src="C:\\Windows\\system.ini" dest={{ host_output_dir }}
+ register: fetch_small_bs
+
+- name: check fetch small result with backslashes
+ assert:
+ that:
+ - "fetch_small_bs.changed"
+
+- name: check file created by fetch small with backslashes
+ stat: path={{ fetch_small_bs.dest }}
+ delegate_to: localhost
+ register: fetch_small_bs_stat
+
+- name: verify fetched small file with backslashes exists locally
+ assert:
+ that:
+ - "fetch_small_bs_stat.stat.exists"
+ - "fetch_small_bs_stat.stat.isreg"
+ - "fetch_small_bs_stat.stat.checksum == fetch_small_bs.checksum"
+
+- name: attempt to fetch a non-existent file - do not fail on missing
+ fetch: src="C:/this_file_should_not_exist.txt" dest={{ host_output_dir }} fail_on_missing=no
+ register: fetch_missing_nofail
+
+- name: check fetch missing no fail result
+ assert:
+ that:
+ - "fetch_missing_nofail is not failed"
+ - "fetch_missing_nofail.msg"
+ - "fetch_missing_nofail is not changed"
+
+- name: attempt to fetch a non-existent file - fail on missing
+ fetch: src="~/this_file_should_not_exist.txt" dest={{ host_output_dir }} fail_on_missing=yes
+ register: fetch_missing
+ ignore_errors: true
+
+- name: check fetch missing with failure
+ assert:
+ that:
+ - "fetch_missing is failed"
+ - "fetch_missing.msg"
+ - "fetch_missing is not changed"
+
+- name: attempt to fetch a non-existent file - fail on missing implicit
+ fetch: src="~/this_file_should_not_exist.txt" dest={{ host_output_dir }}
+ register: fetch_missing_implicit
+ ignore_errors: true
+
+- name: check fetch missing with failure on implicit
+ assert:
+ that:
+ - "fetch_missing_implicit is failed"
+ - "fetch_missing_implicit.msg"
+ - "fetch_missing_implicit is not changed"
+
+- name: attempt to fetch a directory
+ fetch: src="C:\\Windows" dest={{ host_output_dir }}
+ register: fetch_dir
+ ignore_errors: true
+
+- name: check fetch directory result
+ assert:
+ that:
+ # Doesn't fail anymore, only returns a message.
+ - "fetch_dir is not changed"
+ - "fetch_dir.msg"
+
+- name: create file with special characters
+ raw: Set-Content -LiteralPath '{{ remote_tmp_dir }}\abc$not var''quote‘‘' -Value 'abc'
+
+- name: fetch file with special characters
+ fetch:
+ src: '{{ remote_tmp_dir }}\abc$not var''quote‘'
+ dest: '{{ host_output_dir }}/'
+ flat: yes
+ register: fetch_special_file
+
+- name: get content of fetched file
+ command: cat {{ (host_output_dir ~ "/abc$not var'quote‘") | quote }}
+ register: fetch_special_file_actual
+ delegate_to: localhost
+
+- name: assert fetch file with special characters
+ assert:
+ that:
+ - fetch_special_file is changed
+ - fetch_special_file.checksum == '34d4150adc3347f1dd8ce19fdf65b74d971ab602'
+ - fetch_special_file.dest == host_output_dir + "/abc$not var'quote‘"
+ - fetch_special_file_actual.stdout == 'abc'
diff --git a/test/integration/targets/win_module_utils/aliases b/test/integration/targets/win_module_utils/aliases
new file mode 100644
index 0000000..1eed2ec
--- /dev/null
+++ b/test/integration/targets/win_module_utils/aliases
@@ -0,0 +1,2 @@
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/win_module_utils/library/csharp_util.ps1 b/test/integration/targets/win_module_utils/library/csharp_util.ps1
new file mode 100644
index 0000000..cf2dc45
--- /dev/null
+++ b/test/integration/targets/win_module_utils/library/csharp_util.ps1
@@ -0,0 +1,12 @@
+#1powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#AnsibleRequires -CSharpUtil Ansible.Test
+
+$result = @{
+ res = [Ansible.Test.OutputTest]::GetString()
+ changed = $false
+}
+
+Exit-Json -obj $result
+
diff --git a/test/integration/targets/win_module_utils/library/legacy_only_new_way.ps1 b/test/integration/targets/win_module_utils/library/legacy_only_new_way.ps1
new file mode 100644
index 0000000..045ca75
--- /dev/null
+++ b/test/integration/targets/win_module_utils/library/legacy_only_new_way.ps1
@@ -0,0 +1,5 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+Exit-Json @{ data = "success" }
diff --git a/test/integration/targets/win_module_utils/library/legacy_only_new_way_win_line_ending.ps1 b/test/integration/targets/win_module_utils/library/legacy_only_new_way_win_line_ending.ps1
new file mode 100644
index 0000000..837a516
--- /dev/null
+++ b/test/integration/targets/win_module_utils/library/legacy_only_new_way_win_line_ending.ps1
@@ -0,0 +1,6 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+Exit-Json @{ data = "success" }
+
diff --git a/test/integration/targets/win_module_utils/library/legacy_only_old_way.ps1 b/test/integration/targets/win_module_utils/library/legacy_only_old_way.ps1
new file mode 100644
index 0000000..3c6b083
--- /dev/null
+++ b/test/integration/targets/win_module_utils/library/legacy_only_old_way.ps1
@@ -0,0 +1,5 @@
+#!powershell
+
+# POWERSHELL_COMMON
+
+Exit-Json @{ data = "success" }
diff --git a/test/integration/targets/win_module_utils/library/legacy_only_old_way_win_line_ending.ps1 b/test/integration/targets/win_module_utils/library/legacy_only_old_way_win_line_ending.ps1
new file mode 100644
index 0000000..afe7548
--- /dev/null
+++ b/test/integration/targets/win_module_utils/library/legacy_only_old_way_win_line_ending.ps1
@@ -0,0 +1,4 @@
+#!powershell
+# POWERSHELL_COMMON
+
+Exit-Json @{ data = "success" }
diff --git a/test/integration/targets/win_module_utils/library/recursive_requires.ps1 b/test/integration/targets/win_module_utils/library/recursive_requires.ps1
new file mode 100644
index 0000000..db8c23e
--- /dev/null
+++ b/test/integration/targets/win_module_utils/library/recursive_requires.ps1
@@ -0,0 +1,13 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.Recursive3
+#Requires -Version 2
+
+$ErrorActionPreference = "Stop"
+
+$result = @{
+ changed = $false
+ value = Get-Test3
+}
+Exit-Json -obj $result
diff --git a/test/integration/targets/win_module_utils/library/uses_bogus_utils.ps1 b/test/integration/targets/win_module_utils/library/uses_bogus_utils.ps1
new file mode 100644
index 0000000..3886aec
--- /dev/null
+++ b/test/integration/targets/win_module_utils/library/uses_bogus_utils.ps1
@@ -0,0 +1,6 @@
+#!powershell
+
+# this should fail
+#Requires -Module Ansible.ModuleUtils.BogusModule
+
+Exit-Json @{ data = "success" }
diff --git a/test/integration/targets/win_module_utils/library/uses_local_utils.ps1 b/test/integration/targets/win_module_utils/library/uses_local_utils.ps1
new file mode 100644
index 0000000..48c2757
--- /dev/null
+++ b/test/integration/targets/win_module_utils/library/uses_local_utils.ps1
@@ -0,0 +1,9 @@
+#!powershell
+
+# use different cases, spacing and plural of 'module' to exercise flexible powershell dialect
+#ReQuiReS -ModUleS Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.ValidTestModule
+
+$o = CustomFunction
+
+Exit-Json @{data = $o }
diff --git a/test/integration/targets/win_module_utils/module_utils/Ansible.ModuleUtils.Recursive1.psm1 b/test/integration/targets/win_module_utils/module_utils/Ansible.ModuleUtils.Recursive1.psm1
new file mode 100644
index 0000000..a63ece3
--- /dev/null
+++ b/test/integration/targets/win_module_utils/module_utils/Ansible.ModuleUtils.Recursive1.psm1
@@ -0,0 +1,9 @@
+Function Get-Test1 {
+ <#
+ .SYNOPSIS
+ Test function
+ #>
+ return "Get-Test1"
+}
+
+Export-ModuleMember -Function Get-Test1
diff --git a/test/integration/targets/win_module_utils/module_utils/Ansible.ModuleUtils.Recursive2.psm1 b/test/integration/targets/win_module_utils/module_utils/Ansible.ModuleUtils.Recursive2.psm1
new file mode 100644
index 0000000..f9c07ca
--- /dev/null
+++ b/test/integration/targets/win_module_utils/module_utils/Ansible.ModuleUtils.Recursive2.psm1
@@ -0,0 +1,12 @@
+#Requires -Module Ansible.ModuleUtils.Recursive1
+#Requires -Module Ansible.ModuleUtils.Recursive3
+
+Function Get-Test2 {
+ <#
+ .SYNOPSIS
+ Test function
+ #>
+ return "Get-Test2, 1: $(Get-Test1), 3: $(Get-NewTest3)"
+}
+
+Export-ModuleMember -Function Get-Test2
diff --git a/test/integration/targets/win_module_utils/module_utils/Ansible.ModuleUtils.Recursive3.psm1 b/test/integration/targets/win_module_utils/module_utils/Ansible.ModuleUtils.Recursive3.psm1
new file mode 100644
index 0000000..ce6e70c
--- /dev/null
+++ b/test/integration/targets/win_module_utils/module_utils/Ansible.ModuleUtils.Recursive3.psm1
@@ -0,0 +1,20 @@
+#Requires -Module Ansible.ModuleUtils.Recursive2
+#Requires -Version 3.0
+
+Function Get-Test3 {
+ <#
+ .SYNOPSIS
+ Test function
+ #>
+ return "Get-Test3: 2: $(Get-Test2)"
+}
+
+Function Get-NewTest3 {
+ <#
+ .SYNOPSIS
+ Test function
+ #>
+ return "Get-NewTest3"
+}
+
+Export-ModuleMember -Function Get-Test3, Get-NewTest3
diff --git a/test/integration/targets/win_module_utils/module_utils/Ansible.ModuleUtils.ValidTestModule.psm1 b/test/integration/targets/win_module_utils/module_utils/Ansible.ModuleUtils.ValidTestModule.psm1
new file mode 100644
index 0000000..a60b799
--- /dev/null
+++ b/test/integration/targets/win_module_utils/module_utils/Ansible.ModuleUtils.ValidTestModule.psm1
@@ -0,0 +1,3 @@
+Function CustomFunction {
+ return "ValueFromCustomFunction"
+}
diff --git a/test/integration/targets/win_module_utils/module_utils/Ansible.Test.cs b/test/integration/targets/win_module_utils/module_utils/Ansible.Test.cs
new file mode 100644
index 0000000..9556d9a
--- /dev/null
+++ b/test/integration/targets/win_module_utils/module_utils/Ansible.Test.cs
@@ -0,0 +1,26 @@
+//AssemblyReference -Name System.Web.Extensions.dll
+
+using System;
+using System.Collections.Generic;
+using System.Web.Script.Serialization;
+
+namespace Ansible.Test
+{
+ public class OutputTest
+ {
+ public static string GetString()
+ {
+ Dictionary<string, object> obj = new Dictionary<string, object>();
+ obj["a"] = "a";
+ obj["b"] = 1;
+ return ToJson(obj);
+ }
+
+ private static string ToJson(object obj)
+ {
+ JavaScriptSerializer jss = new JavaScriptSerializer();
+ return jss.Serialize(obj);
+ }
+ }
+}
+
diff --git a/test/integration/targets/win_module_utils/tasks/main.yml b/test/integration/targets/win_module_utils/tasks/main.yml
new file mode 100644
index 0000000..87f2592
--- /dev/null
+++ b/test/integration/targets/win_module_utils/tasks/main.yml
@@ -0,0 +1,71 @@
+- name: call old WANTS_JSON module
+ legacy_only_old_way:
+ register: old_way
+
+- assert:
+ that:
+ - old_way.data == 'success'
+
+- name: call module with only legacy requires
+ legacy_only_new_way:
+ register: new_way
+
+- assert:
+ that:
+ - new_way.data == 'success'
+
+- name: call old WANTS_JSON module with windows line endings
+ legacy_only_old_way_win_line_ending:
+ register: old_way_win
+
+- assert:
+ that:
+ - old_way_win.data == 'success'
+
+- name: call module with only legacy requires and windows line endings
+ legacy_only_new_way_win_line_ending:
+ register: new_way_win
+
+- assert:
+ that:
+ - new_way_win.data == 'success'
+
+- name: call module with local module_utils
+ uses_local_utils:
+ register: local_utils
+
+- assert:
+ that:
+ - local_utils.data == "ValueFromCustomFunction"
+
+- name: call module that imports bogus Ansible-named module_utils
+ uses_bogus_utils:
+ ignore_errors: true
+ register: bogus_utils
+
+- assert:
+ that:
+ - bogus_utils is failed
+ - bogus_utils.msg is search("Could not find")
+
+- name: call module that imports module_utils with further imports
+ recursive_requires:
+ register: recursive_requires
+ vars:
+ # Our coverage runner does not work with recursive required. This is a limitation on PowerShell so we need to
+ # disable coverage for this task
+ _ansible_coverage_remote_output: ''
+
+- assert:
+ that:
+ - 'recursive_requires.value == "Get-Test3: 2: Get-Test2, 1: Get-Test1, 3: Get-NewTest3"'
+
+- name: call module with C# reference
+ csharp_util:
+ register: csharp_res
+
+- name: assert call module with C# reference
+ assert:
+ that:
+ - not csharp_res is failed
+ - csharp_res.res == '{"a":"a","b":1}'
diff --git a/test/integration/targets/win_raw/aliases b/test/integration/targets/win_raw/aliases
new file mode 100644
index 0000000..1eed2ec
--- /dev/null
+++ b/test/integration/targets/win_raw/aliases
@@ -0,0 +1,2 @@
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/win_raw/tasks/main.yml b/test/integration/targets/win_raw/tasks/main.yml
new file mode 100644
index 0000000..31f90b8
--- /dev/null
+++ b/test/integration/targets/win_raw/tasks/main.yml
@@ -0,0 +1,143 @@
+# test code for the raw module when using winrm connection
+# (c) 2014, Chris Church <chris@ninemoreminutes.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: run getmac
+ raw: getmac
+ register: getmac_result
+
+- name: assert that getmac ran
+ assert:
+ that:
+ - "getmac_result.rc == 0"
+ - "getmac_result.stdout"
+ - "not getmac_result.stderr"
+ - "getmac_result is not failed"
+ - "getmac_result is changed"
+
+- name: run ipconfig with /all argument
+ raw: ipconfig /all
+ register: ipconfig_result
+
+- name: assert that ipconfig ran with /all argument
+ assert:
+ that:
+ - "ipconfig_result.rc == 0"
+ - "ipconfig_result.stdout"
+ - "'Physical Address' in ipconfig_result.stdout"
+ - "not ipconfig_result.stderr"
+ - "ipconfig_result is not failed"
+ - "ipconfig_result is changed"
+
+- name: run ipconfig with invalid argument
+ raw: ipconfig /badswitch
+ register: ipconfig_invalid_result
+ ignore_errors: true
+
+- name: assert that ipconfig with invalid argument failed
+ assert:
+ that:
+ - "ipconfig_invalid_result.rc != 0"
+ - "ipconfig_invalid_result.stdout" # ipconfig displays errors on stdout.
+# - "not ipconfig_invalid_result.stderr"
+ - "ipconfig_invalid_result is failed"
+ - "ipconfig_invalid_result is changed"
+
+- name: run an unknown command
+ raw: uname -a
+ register: unknown_result
+ ignore_errors: true
+
+- name: assert that an unknown command failed
+ assert:
+ that:
+ - "unknown_result.rc != 0"
+ - "not unknown_result.stdout"
+ - "unknown_result.stderr" # An unknown command displays error on stderr.
+ - "unknown_result is failed"
+ - "unknown_result is changed"
+
+- name: run a command that takes longer than 60 seconds
+ raw: Start-Sleep -s 75
+ register: sleep_command
+
+- name: assert that the sleep command ran
+ assert:
+ that:
+ - "sleep_command.rc == 0"
+ - "not sleep_command.stdout"
+ - "not sleep_command.stderr"
+ - "sleep_command is not failed"
+ - "sleep_command is changed"
+
+- name: run a raw command with key=value arguments
+ raw: echo wwe=raw
+ register: raw_result
+
+- name: make sure raw is really raw and not removing key=value arguments
+ assert:
+ that:
+ - "raw_result.stdout_lines[0] == 'wwe=raw'"
+
+- name: unicode tests for winrm
+ when: ansible_connection != 'psrp' # Write-Host does not work over PSRP
+ block:
+ - name: run a raw command with unicode chars and quoted args (from https://github.com/ansible/ansible-modules-core/issues/1929)
+ raw: Write-Host --% icacls D:\somedir\ /grant "! ЗÐО. РуководÑтво":F
+ register: raw_result2
+
+ - name: make sure raw passes command as-is and doesn't split/rejoin args
+ assert:
+ that:
+ - "raw_result2.stdout_lines[0] == '--% icacls D:\\\\somedir\\\\ /grant \"! ЗÐО. РуководÑтво\":F'"
+
+- name: unicode tests for psrp
+ when: ansible_connection == 'psrp'
+ block:
+ # Cannot test unicode passed into separate exec as PSRP doesn't run with a preset CP of 65001 which reuslts in ? for unicode chars
+ - name: run a raw command with unicode chars
+ raw: Write-Output "! ЗÐО. РуководÑтво"
+ register: raw_result2
+
+ - name: make sure raw passes command as-is and doesn't split/rejoin args
+ assert:
+ that:
+ - "raw_result2.stdout_lines[0] == '! ЗÐО. РуководÑтво'"
+
+# Assumes MaxShellsPerUser == 30 (the default)
+
+- name: test raw + with_items to verify that winrm connection is reused for each item
+ raw: echo "{{item}}"
+ with_items: "{{range(32)|list}}"
+ register: raw_with_items_result
+
+- name: check raw + with_items result
+ assert:
+ that:
+ - "raw_with_items_result is not failed"
+ - "raw_with_items_result.results|length == 32"
+
+# TODO: this test fails, since we're back to passing raw commands without modification
+#- name: test raw with job to ensure that preamble-free InputEncoding is working
+# raw: Start-Job { echo yo } | Receive-Job -Wait
+# register: raw_job_result
+#
+#- name: check raw with job result
+# assert:
+# that:
+# - raw_job_result is successful
+# - raw_job_result.stdout_lines[0] == 'yo'
diff --git a/test/integration/targets/win_script/aliases b/test/integration/targets/win_script/aliases
new file mode 100644
index 0000000..1eed2ec
--- /dev/null
+++ b/test/integration/targets/win_script/aliases
@@ -0,0 +1,2 @@
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/win_script/defaults/main.yml b/test/integration/targets/win_script/defaults/main.yml
new file mode 100644
index 0000000..a2c6475
--- /dev/null
+++ b/test/integration/targets/win_script/defaults/main.yml
@@ -0,0 +1,5 @@
+---
+
+# Parameters to pass to test scripts.
+test_win_script_value: VaLuE
+test_win_script_splat: "@{This='THIS'; That='THAT'; Other='OTHER'}"
diff --git a/test/integration/targets/win_script/files/fail.bat b/test/integration/targets/win_script/files/fail.bat
new file mode 100644
index 0000000..02562a8
--- /dev/null
+++ b/test/integration/targets/win_script/files/fail.bat
@@ -0,0 +1 @@
+bang-run-a-thing-that-doesnt-exist
diff --git a/test/integration/targets/win_script/files/space path/test_script.ps1 b/test/integration/targets/win_script/files/space path/test_script.ps1
new file mode 100644
index 0000000..10dd9c8
--- /dev/null
+++ b/test/integration/targets/win_script/files/space path/test_script.ps1
@@ -0,0 +1 @@
+Write-Output "Ansible supports spaces in the path to the script."
diff --git a/test/integration/targets/win_script/files/test_script.bat b/test/integration/targets/win_script/files/test_script.bat
new file mode 100644
index 0000000..05cc2d1
--- /dev/null
+++ b/test/integration/targets/win_script/files/test_script.bat
@@ -0,0 +1,2 @@
+@ECHO OFF
+ECHO We can even run a batch file!
diff --git a/test/integration/targets/win_script/files/test_script.cmd b/test/integration/targets/win_script/files/test_script.cmd
new file mode 100644
index 0000000..0e36312
--- /dev/null
+++ b/test/integration/targets/win_script/files/test_script.cmd
@@ -0,0 +1,2 @@
+@ECHO OFF
+ECHO We can even run a batch file with cmd extension!
diff --git a/test/integration/targets/win_script/files/test_script.ps1 b/test/integration/targets/win_script/files/test_script.ps1
new file mode 100644
index 0000000..9978f36
--- /dev/null
+++ b/test/integration/targets/win_script/files/test_script.ps1
@@ -0,0 +1,2 @@
+# Test script to make sure the Ansible script module works.
+Write-Host "Woohoo! We can run a PowerShell script via Ansible!"
diff --git a/test/integration/targets/win_script/files/test_script_bool.ps1 b/test/integration/targets/win_script/files/test_script_bool.ps1
new file mode 100644
index 0000000..d5116f3
--- /dev/null
+++ b/test/integration/targets/win_script/files/test_script_bool.ps1
@@ -0,0 +1,6 @@
+Param(
+ [bool]$boolvariable
+)
+
+Write-Output $boolvariable.GetType().FullName
+Write-Output $boolvariable
diff --git a/test/integration/targets/win_script/files/test_script_creates_file.ps1 b/test/integration/targets/win_script/files/test_script_creates_file.ps1
new file mode 100644
index 0000000..3a7c3a9
--- /dev/null
+++ b/test/integration/targets/win_script/files/test_script_creates_file.ps1
@@ -0,0 +1,3 @@
+# Test script to create a file.
+
+Write-Output $null > $args[0]
diff --git a/test/integration/targets/win_script/files/test_script_removes_file.ps1 b/test/integration/targets/win_script/files/test_script_removes_file.ps1
new file mode 100644
index 0000000..f0549a5
--- /dev/null
+++ b/test/integration/targets/win_script/files/test_script_removes_file.ps1
@@ -0,0 +1,3 @@
+# Test script to remove a file.
+
+Remove-Item $args[0] -Force
diff --git a/test/integration/targets/win_script/files/test_script_whoami.ps1 b/test/integration/targets/win_script/files/test_script_whoami.ps1
new file mode 100644
index 0000000..79a1c47
--- /dev/null
+++ b/test/integration/targets/win_script/files/test_script_whoami.ps1
@@ -0,0 +1,2 @@
+whoami.exe
+Write-Output "finished"
diff --git a/test/integration/targets/win_script/files/test_script_with_args.ps1 b/test/integration/targets/win_script/files/test_script_with_args.ps1
new file mode 100644
index 0000000..01bb37f
--- /dev/null
+++ b/test/integration/targets/win_script/files/test_script_with_args.ps1
@@ -0,0 +1,6 @@
+# Test script to make sure the Ansible script module works when arguments are
+# passed to the script.
+
+foreach ($i in $args) {
+ Write-Host $i;
+}
diff --git a/test/integration/targets/win_script/files/test_script_with_env.ps1 b/test/integration/targets/win_script/files/test_script_with_env.ps1
new file mode 100644
index 0000000..b54fd92
--- /dev/null
+++ b/test/integration/targets/win_script/files/test_script_with_env.ps1
@@ -0,0 +1 @@
+$env:taskenv \ No newline at end of file
diff --git a/test/integration/targets/win_script/files/test_script_with_errors.ps1 b/test/integration/targets/win_script/files/test_script_with_errors.ps1
new file mode 100644
index 0000000..56f9773
--- /dev/null
+++ b/test/integration/targets/win_script/files/test_script_with_errors.ps1
@@ -0,0 +1,8 @@
+# Test script to make sure we handle non-zero exit codes.
+
+trap {
+ Write-Error -ErrorRecord $_
+ exit 1;
+}
+
+throw "Oh noes I has an error"
diff --git a/test/integration/targets/win_script/files/test_script_with_splatting.ps1 b/test/integration/targets/win_script/files/test_script_with_splatting.ps1
new file mode 100644
index 0000000..429a9a3
--- /dev/null
+++ b/test/integration/targets/win_script/files/test_script_with_splatting.ps1
@@ -0,0 +1,6 @@
+# Test script to make sure the Ansible script module works when arguments are
+# passed via splatting (http://technet.microsoft.com/en-us/magazine/gg675931.aspx)
+
+Write-Host $args.This
+Write-Host $args.That
+Write-Host $args.Other
diff --git a/test/integration/targets/win_script/tasks/main.yml b/test/integration/targets/win_script/tasks/main.yml
new file mode 100644
index 0000000..4d57eda
--- /dev/null
+++ b/test/integration/targets/win_script/tasks/main.yml
@@ -0,0 +1,316 @@
+# test code for the script module when using winrm connection
+# (c) 2014, Chris Church <chris@ninemoreminutes.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: run setup to allow skipping OS-specific tests
+ setup:
+ gather_subset: min
+
+- name: get tempdir path
+ raw: $env:TEMP
+ register: tempdir
+
+- name: set script path dynamically
+ set_fact:
+ test_win_script_filename: "{{ tempdir.stdout_lines[0] }}/testing_win_script.txt"
+
+- name: run simple test script
+ script: test_script.ps1
+ register: test_script_result
+
+- name: check that script ran
+ assert:
+ that:
+ - "test_script_result.rc == 0"
+ - "test_script_result.stdout"
+ - "'Woohoo' in test_script_result.stdout"
+ - "not test_script_result.stderr"
+ - "test_script_result is not failed"
+ - "test_script_result is changed"
+
+- name: run test script that takes arguments including a unicode char
+ script: test_script_with_args.ps1 /this /that /Ó¦ther
+ register: test_script_with_args_result
+
+- name: check that script ran and received arguments and returned unicode
+ assert:
+ that:
+ - "test_script_with_args_result.rc == 0"
+ - "test_script_with_args_result.stdout"
+ - "test_script_with_args_result.stdout_lines[0] == '/this'"
+ - "test_script_with_args_result.stdout_lines[1] == '/that'"
+ - "test_script_with_args_result.stdout_lines[2] == '/Ó¦ther'"
+ - "not test_script_with_args_result.stderr"
+ - "test_script_with_args_result is not failed"
+ - "test_script_with_args_result is changed"
+
+# Bug: https://github.com/ansible/ansible/issues/32850
+- name: set fact of long string
+ set_fact:
+ long_string: "{{ lookup('pipe', 'printf \"a%.0s\" {1..1000}') }}"
+
+- name: run test script with args that exceed the stdin buffer
+ script: test_script_with_args.ps1 {{ long_string }}
+ register: test_script_with_large_args_result
+
+- name: check that script ran and received arguments correctly
+ assert:
+ that:
+ - test_script_with_large_args_result.rc == 0
+ - not test_script_with_large_args_result.stderr
+ - test_script_with_large_args_result is not failed
+ - test_script_with_large_args_result is changed
+
+- name: check that script ran and received arguments correctly with winrm output
+ assert:
+ that:
+ - test_script_with_large_args_result.stdout == long_string + "\r\n"
+ when: ansible_connection != 'psrp'
+
+- name: check that script ran and received arguments correctly with psrp output
+ assert:
+ that:
+ - test_script_with_large_args_result.stdout == long_string
+ when: ansible_connection == 'psrp'
+
+- name: run test script that takes parameters passed via splatting
+ script: test_script_with_splatting.ps1 @{ This = 'this'; That = '{{ test_win_script_value }}'; Other = 'other'}
+ register: test_script_with_splatting_result
+
+- name: check that script ran and received parameters via splatting
+ assert:
+ that:
+ - "test_script_with_splatting_result.rc == 0"
+ - "test_script_with_splatting_result.stdout"
+ - "test_script_with_splatting_result.stdout_lines[0] == 'this'"
+ - "test_script_with_splatting_result.stdout_lines[1] == test_win_script_value"
+ - "test_script_with_splatting_result.stdout_lines[2] == 'other'"
+ - "not test_script_with_splatting_result.stderr"
+ - "test_script_with_splatting_result is not failed"
+ - "test_script_with_splatting_result is changed"
+
+- name: run test script that takes splatted parameters from a variable
+ script: test_script_with_splatting.ps1 {{ test_win_script_splat }}
+ register: test_script_with_splatting2_result
+
+- name: check that script ran and received parameters via splatting from a variable
+ assert:
+ that:
+ - "test_script_with_splatting2_result.rc == 0"
+ - "test_script_with_splatting2_result.stdout"
+ - "test_script_with_splatting2_result.stdout_lines[0] == 'THIS'"
+ - "test_script_with_splatting2_result.stdout_lines[1] == 'THAT'"
+ - "test_script_with_splatting2_result.stdout_lines[2] == 'OTHER'"
+ - "not test_script_with_splatting2_result.stderr"
+ - "test_script_with_splatting2_result is not failed"
+ - "test_script_with_splatting2_result is changed"
+
+- name: run test script that has errors
+ script: test_script_with_errors.ps1
+ register: test_script_with_errors_result
+ ignore_errors: true
+
+- name: check that script ran but failed with errors
+ assert:
+ that:
+ - "test_script_with_errors_result.rc != 0"
+ - "not test_script_with_errors_result.stdout"
+ - "test_script_with_errors_result.stderr"
+ - "test_script_with_errors_result is failed"
+ - "test_script_with_errors_result is changed"
+
+- name: cleanup test file if it exists
+ raw: Remove-Item "{{ test_win_script_filename }}" -Force
+ ignore_errors: true
+
+- name: run test script that creates a file
+ script: test_script_creates_file.ps1 {{ test_win_script_filename }}
+ args:
+ creates: "{{ test_win_script_filename }}"
+ register: test_script_creates_file_result
+
+- name: check that script ran and indicated a change
+ assert:
+ that:
+ - "test_script_creates_file_result.rc == 0"
+ - "not test_script_creates_file_result.stdout"
+ - "not test_script_creates_file_result.stderr"
+ - "test_script_creates_file_result is not failed"
+ - "test_script_creates_file_result is changed"
+
+- name: run test script that creates a file again
+ script: test_script_creates_file.ps1 {{ test_win_script_filename }}
+ args:
+ creates: "{{ test_win_script_filename }}"
+ register: test_script_creates_file_again_result
+
+- name: check that the script did not run since the remote file exists
+ assert:
+ that:
+ - "test_script_creates_file_again_result is not failed"
+ - "test_script_creates_file_again_result is not changed"
+ - "test_script_creates_file_again_result is skipped"
+
+- name: run test script that removes a file
+ script: test_script_removes_file.ps1 {{ test_win_script_filename }}
+ args:
+ removes: "{{ test_win_script_filename }}"
+ register: test_script_removes_file_result
+
+- name: check that the script ran since the remote file exists
+ assert:
+ that:
+ - "test_script_removes_file_result.rc == 0"
+ - "not test_script_removes_file_result.stdout"
+ - "not test_script_removes_file_result.stderr"
+ - "test_script_removes_file_result is not failed"
+ - "test_script_removes_file_result is changed"
+
+- name: run test script that removes a file again
+ script: test_script_removes_file.ps1 {{ test_win_script_filename }}
+ args:
+ removes: "{{ test_win_script_filename }}"
+ register: test_script_removes_file_again_result
+
+- name: check that the script did not run since the remote file does not exist
+ assert:
+ that:
+ - "test_script_removes_file_again_result is not failed"
+ - "test_script_removes_file_again_result is not changed"
+ - "test_script_removes_file_again_result is skipped"
+
+- name: skip batch tests on 6.0 (UTF8 codepage prevents it from working, see https://github.com/ansible/ansible/issues/21915)
+ block:
+ - name: run simple batch file
+ script: test_script.bat
+ register: test_batch_result
+
+ - name: check that batch file ran
+ assert:
+ that:
+ - "test_batch_result.rc == 0"
+ - "test_batch_result.stdout"
+ - "'batch' in test_batch_result.stdout"
+ - "not test_batch_result.stderr"
+ - "test_batch_result is not failed"
+ - "test_batch_result is changed"
+
+ - name: run simple batch file with .cmd extension
+ script: test_script.cmd
+ register: test_cmd_result
+
+ - name: check that batch file with .cmd extension ran
+ assert:
+ that:
+ - "test_cmd_result.rc == 0"
+ - "test_cmd_result.stdout"
+ - "'cmd extension' in test_cmd_result.stdout"
+ - "not test_cmd_result.stderr"
+ - "test_cmd_result is not failed"
+ - "test_cmd_result is changed"
+
+ - name: run simple batch file with .bat extension that fails
+ script: fail.bat
+ ignore_errors: true
+ register: test_batch_result
+
+ - name: check that batch file with .bat extension reported failure
+ assert:
+ that:
+ - test_batch_result.rc == 1
+ - test_batch_result.stdout
+ - test_batch_result.stderr
+ - test_batch_result is failed
+ - test_batch_result is changed
+ when: not ansible_distribution_version.startswith('6.0')
+
+- name: run test script that takes a boolean parameter
+ script: test_script_bool.ps1 $false # use false as that can pick up more errors
+ register: test_script_bool_result
+
+- name: check that the script ran and the parameter was treated as a boolean
+ assert:
+ that:
+ - test_script_bool_result.stdout_lines[0] == 'System.Boolean'
+ - test_script_bool_result.stdout_lines[1] == 'False'
+
+- name: run test script that uses envvars
+ script: test_script_with_env.ps1
+ environment:
+ taskenv: task
+ register: test_script_env_result
+
+- name: ensure that script ran and that environment var was passed
+ assert:
+ that:
+ - test_script_env_result is successful
+ - test_script_env_result.stdout_lines[0] == 'task'
+
+# check mode
+- name: Run test script that creates a file in check mode
+ script: test_script_creates_file.ps1 {{ test_win_script_filename }}
+ args:
+ creates: "{{ test_win_script_filename }}"
+ check_mode: yes
+ register: test_script_creates_file_check_mode
+
+- name: Get state of file created by script
+ win_stat:
+ path: "{{ test_win_script_filename }}"
+ register: create_file_stat
+
+- name: Assert that a change was reported but the script did not make changes
+ assert:
+ that:
+ - test_script_creates_file_check_mode is changed
+ - not create_file_stat.stat.exists
+
+- name: Run test script that creates a file
+ script: test_script_creates_file.ps1 {{ test_win_script_filename }}
+ args:
+ creates: "{{ test_win_script_filename }}"
+
+- name: Run test script that removes a file in check mode
+ script: test_script_removes_file.ps1 {{ test_win_script_filename }}
+ args:
+ removes: "{{ test_win_script_filename }}"
+ check_mode: yes
+ register: test_script_removes_file_check_mode
+
+- name: Get state of file removed by script
+ win_stat:
+ path: "{{ test_win_script_filename }}"
+ register: remove_file_stat
+
+- name: Assert that a change was reported but the script did not make changes
+ assert:
+ that:
+ - test_script_removes_file_check_mode is changed
+ - remove_file_stat.stat.exists
+
+- name: run test script with become that outputs 2 lines
+ script: test_script_whoami.ps1
+ register: test_script_result_become
+ become: yes
+ become_user: SYSTEM
+ become_method: runas
+
+- name: check that the script ran and we get both outputs on new lines
+ assert:
+ that:
+ - test_script_result_become.stdout_lines[0]|lower == 'nt authority\\system'
+ - test_script_result_become.stdout_lines[1] == 'finished'
diff --git a/test/integration/targets/windows-minimal/aliases b/test/integration/targets/windows-minimal/aliases
new file mode 100644
index 0000000..479948a
--- /dev/null
+++ b/test/integration/targets/windows-minimal/aliases
@@ -0,0 +1,4 @@
+shippable/windows/group1
+shippable/windows/minimal
+shippable/windows/smoketest
+windows
diff --git a/test/integration/targets/windows-minimal/library/win_ping.ps1 b/test/integration/targets/windows-minimal/library/win_ping.ps1
new file mode 100644
index 0000000..c848b91
--- /dev/null
+++ b/test/integration/targets/windows-minimal/library/win_ping.ps1
@@ -0,0 +1,21 @@
+#!powershell
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$spec = @{
+ options = @{
+ data = @{ type = "str"; default = "pong" }
+ }
+ supports_check_mode = $true
+}
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+$data = $module.Params.data
+
+if ($data -eq "crash") {
+ throw "boom"
+}
+
+$module.Result.ping = $data
+$module.ExitJson()
diff --git a/test/integration/targets/windows-minimal/library/win_ping.py b/test/integration/targets/windows-minimal/library/win_ping.py
new file mode 100644
index 0000000..6d35f37
--- /dev/null
+++ b/test/integration/targets/windows-minimal/library/win_ping.py
@@ -0,0 +1,55 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>, and others
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# this is a windows documentation stub. actual code lives in the .ps1
+# file of the same name
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['stableinterface'],
+ 'supported_by': 'core'}
+
+DOCUMENTATION = r'''
+---
+module: win_ping
+version_added: "1.7"
+short_description: A windows version of the classic ping module
+description:
+ - Checks management connectivity of a windows host.
+ - This is NOT ICMP ping, this is just a trivial test module.
+ - For non-Windows targets, use the M(ping) module instead.
+ - For Network targets, use the M(net_ping) module instead.
+options:
+ data:
+ description:
+ - Alternate data to return instead of 'pong'.
+ - If this parameter is set to C(crash), the module will cause an exception.
+ type: str
+ default: pong
+seealso:
+- module: ping
+author:
+- Chris Church (@cchurch)
+'''
+
+EXAMPLES = r'''
+# Test connectivity to a windows host
+# ansible winserver -m win_ping
+
+- name: Example from an Ansible Playbook
+ win_ping:
+
+- name: Induce an exception to see what happens
+ win_ping:
+ data: crash
+'''
+
+RETURN = r'''
+ping:
+ description: Value provided with the data parameter.
+ returned: success
+ type: str
+ sample: pong
+'''
diff --git a/test/integration/targets/windows-minimal/library/win_ping_set_attr.ps1 b/test/integration/targets/windows-minimal/library/win_ping_set_attr.ps1
new file mode 100644
index 0000000..f170496
--- /dev/null
+++ b/test/integration/targets/windows-minimal/library/win_ping_set_attr.ps1
@@ -0,0 +1,31 @@
+#!powershell
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# POWERSHELL_COMMON
+
+$params = Parse-Args $args $true;
+
+$data = Get-Attr $params "data" "pong";
+
+$result = @{
+ changed = $false
+ ping = "pong"
+};
+
+# Test that Set-Attr will replace an existing attribute.
+Set-Attr $result "ping" $data
+
+Exit-Json $result;
diff --git a/test/integration/targets/windows-minimal/library/win_ping_strict_mode_error.ps1 b/test/integration/targets/windows-minimal/library/win_ping_strict_mode_error.ps1
new file mode 100644
index 0000000..508174a
--- /dev/null
+++ b/test/integration/targets/windows-minimal/library/win_ping_strict_mode_error.ps1
@@ -0,0 +1,30 @@
+#!powershell
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# POWERSHELL_COMMON
+
+$params = Parse-Args $args $true;
+
+$params.thisPropertyDoesNotExist
+
+$data = Get-Attr $params "data" "pong";
+
+$result = @{
+ changed = $false
+ ping = $data
+};
+
+Exit-Json $result;
diff --git a/test/integration/targets/windows-minimal/library/win_ping_syntax_error.ps1 b/test/integration/targets/windows-minimal/library/win_ping_syntax_error.ps1
new file mode 100644
index 0000000..d4c9f07
--- /dev/null
+++ b/test/integration/targets/windows-minimal/library/win_ping_syntax_error.ps1
@@ -0,0 +1,30 @@
+#!powershell
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# POWERSHELL_COMMON
+
+$blah = 'I can't quote my strings correctly.'
+
+$params = Parse-Args $args $true;
+
+$data = Get-Attr $params "data" "pong";
+
+$result = @{
+ changed = $false
+ ping = $data
+};
+
+Exit-Json $result;
diff --git a/test/integration/targets/windows-minimal/library/win_ping_throw.ps1 b/test/integration/targets/windows-minimal/library/win_ping_throw.ps1
new file mode 100644
index 0000000..7306f4d
--- /dev/null
+++ b/test/integration/targets/windows-minimal/library/win_ping_throw.ps1
@@ -0,0 +1,30 @@
+#!powershell
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# POWERSHELL_COMMON
+
+throw
+
+$params = Parse-Args $args $true;
+
+$data = Get-Attr $params "data" "pong";
+
+$result = @{
+ changed = $false
+ ping = $data
+};
+
+Exit-Json $result;
diff --git a/test/integration/targets/windows-minimal/library/win_ping_throw_string.ps1 b/test/integration/targets/windows-minimal/library/win_ping_throw_string.ps1
new file mode 100644
index 0000000..09e3b7c
--- /dev/null
+++ b/test/integration/targets/windows-minimal/library/win_ping_throw_string.ps1
@@ -0,0 +1,30 @@
+#!powershell
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# POWERSHELL_COMMON
+
+throw "no ping for you"
+
+$params = Parse-Args $args $true;
+
+$data = Get-Attr $params "data" "pong";
+
+$result = @{
+ changed = $false
+ ping = $data
+};
+
+Exit-Json $result;
diff --git a/test/integration/targets/windows-minimal/tasks/main.yml b/test/integration/targets/windows-minimal/tasks/main.yml
new file mode 100644
index 0000000..a7e6ba7
--- /dev/null
+++ b/test/integration/targets/windows-minimal/tasks/main.yml
@@ -0,0 +1,67 @@
+# test code for the win_ping module
+# (c) 2014, Chris Church <chris@ninemoreminutes.com>
+
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+- name: test win_ping
+ action: win_ping
+ register: win_ping_result
+
+- name: check win_ping result
+ assert:
+ that:
+ - win_ping_result is not failed
+ - win_ping_result is not changed
+ - win_ping_result.ping == 'pong'
+
+- name: test win_ping with data
+ win_ping:
+ data: ☠
+ register: win_ping_with_data_result
+
+- name: check win_ping result with data
+ assert:
+ that:
+ - win_ping_with_data_result is not failed
+ - win_ping_with_data_result is not changed
+ - win_ping_with_data_result.ping == '☠'
+
+- name: test win_ping.ps1 with data as complex args
+ # win_ping.ps1: # TODO: do we want to actually support this? no other tests that I can see...
+ win_ping:
+ data: bleep
+ register: win_ping_ps1_result
+
+- name: check win_ping.ps1 result with data
+ assert:
+ that:
+ - win_ping_ps1_result is not failed
+ - win_ping_ps1_result is not changed
+ - win_ping_ps1_result.ping == 'bleep'
+
+- name: test win_ping using data=crash so that it throws an exception
+ win_ping:
+ data: crash
+ register: win_ping_crash_result
+ ignore_errors: yes
+
+- name: check win_ping_crash result
+ assert:
+ that:
+ - win_ping_crash_result is failed
+ - win_ping_crash_result is not changed
+ - 'win_ping_crash_result.msg == "Unhandled exception while executing module: boom"'
+ - '"throw \"boom\"" in win_ping_crash_result.exception'
diff --git a/test/integration/targets/windows-paths/aliases b/test/integration/targets/windows-paths/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/windows-paths/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/windows-paths/tasks/main.yml b/test/integration/targets/windows-paths/tasks/main.yml
new file mode 100644
index 0000000..4d22265
--- /dev/null
+++ b/test/integration/targets/windows-paths/tasks/main.yml
@@ -0,0 +1,191 @@
+- name: Set variables in YAML syntax
+ set_fact:
+ no_quotes_single: C:\Windows\Temp
+ single_quotes_single: 'C:\Windows\Temp'
+# double_quotes_single: "C:\Windows\Temp"
+ no_quotes_double: C:\\Windows\\Temp
+ single_quotes_double: 'C:\\Windows\\Temp'
+ double_quotes_double: "C:\\Windows\\Temp"
+ no_quotes_slash: C:/Windows/Temp
+ no_quotes_trailing: C:\Windows\Temp\
+ single_quotes_trailing: 'C:\Windows\Temp\'
+# double_quotes_trailing: "C:\Windows\Temp\"
+ good: C:\Windows\Temp
+ works1: C:\\Windows\\Temp
+ works2: C:/Windows/Temp
+# fail: "C:\Windows\Temp"
+ trailing: C:\Windows\Temp\
+ register: yaml_syntax
+
+- assert:
+ that:
+ - no_quotes_single == good
+ - single_quotes_single == good
+# - double_quotes_single == fail
+ - no_quotes_double == works1
+ - single_quotes_double == works1
+ - double_quotes_double == good
+ - no_quotes_slash == works2
+ - no_quotes_trailing == trailing
+ - single_quotes_trailing == trailing
+# - double_quotes_trailing == fail
+ - good != works1
+ - good != works2
+ - good != trailing
+ - works1 != works2
+ - works1 != trailing
+ - works2 != trailing
+
+- name: Test good path {{ good }}
+ win_stat:
+ path: '{{ good }}'
+ register: good_result
+
+- assert:
+ that:
+ - good_result is successful
+ - good_result.stat.attributes == 'Directory'
+ - good_result.stat.exists == true
+ - good_result.stat.path == good
+
+- name: Test works1 path {{ works1 }}
+ win_stat:
+ path: '{{ works1 }}'
+ register: works1_result
+
+- assert:
+ that:
+ - works1_result is successful
+ - works1_result.stat.attributes == 'Directory'
+ - works1_result.stat.exists == true
+ - works1_result.stat.path == good
+
+- name: Test works2 path {{ works2 }}
+ win_stat:
+ path: '{{ works2 }}'
+ register: works2_result
+
+- assert:
+ that:
+ - works2_result is successful
+ - works2_result.stat.attributes == 'Directory'
+ - works2_result.stat.exists == true
+ - works2_result.stat.path == good
+
+- name: Test trailing path {{ trailing }}
+ win_stat:
+ path: '{{ trailing }}'
+ register: trailing_result
+
+- assert:
+ that:
+ - trailing_result is successful
+ - trailing_result.stat.attributes == 'Directory'
+ - trailing_result.stat.exists == true
+ - trailing_result.stat.path == trailing
+
+- name: Set variables in key=value syntax
+ set_fact:
+ no_quotes_single=C:\Windows\Temp
+ single_quotes_single='C:\Windows\Temp'
+ double_quotes_single="C:\Windows\Temp"
+ no_quotes_single_tab=C:\Windows\temp
+ single_quotes_single_tab='C:\Windows\temp'
+ double_quotes_single_tab="C:\Windows\temp"
+ no_quotes_double=C:\\Windows\\Temp
+ single_quotes_double='C:\\Windows\\Temp'
+ double_quotes_double="C:\\Windows\\Temp"
+ no_quotes_slash=C:/Windows/Temp
+ no_quotes_trailing=C:\Windows\Temp\
+ good=C:\Windows\Temp
+ works1=C:\\Windows\\Temp
+ works2=C:/Windows/Temp
+ fail="C:\Windows\Temp"
+ trailing=C:\Windows\Temp\
+ tab=C:\Windows\x09emp
+ eof=foobar
+# single_quotes_trailing='C:\Windows\Temp\'
+# double_quotes_trailing="C:\Windows\Temp\"
+ register: legacy_syntax
+
+- assert:
+ that:
+ - no_quotes_single == good
+ - single_quotes_single == good
+ - double_quotes_single == good
+ - no_quotes_double == works1
+ - single_quotes_double == works1
+ - double_quotes_double == works1
+ - no_quotes_slash == works2
+ - no_quotes_single_tab == tab
+ - single_quotes_single_tab == tab
+ - double_quotes_single_tab == tab
+ - no_quotes_trailing == trailing
+ - good == works1
+ - good != works2
+ - good != tab
+ - good != trailing
+ - works1 != works2
+ - works1 != tab
+ - works1 != trailing
+ - works2 != tab
+ - works2 != trailing
+ - tab != trailing
+
+- name: Test good path {{ good }}
+ win_stat:
+ path: '{{ good }}'
+ register: good_result
+
+- assert:
+ that:
+ - good_result is successful
+ - good_result.stat.attributes == 'Directory'
+ - good_result.stat.exists == true
+ - good_result.stat.path == good
+
+- name: Test works1 path {{ works1 }}
+ win_stat:
+ path: '{{ works1 }}'
+ register: works1_result
+
+- assert:
+ that:
+ - works1_result is successful
+ - works1_result.stat.attributes == 'Directory'
+ - works1_result.stat.exists == true
+ - works1_result.stat.path == good
+
+- name: Test works2 path {{ works2 }}
+ win_stat:
+ path: '{{ works2 }}'
+ register: works2_result
+
+- assert:
+ that:
+ - works2_result is successful
+ - works2_result.stat.attributes == 'Directory'
+ - works2_result.stat.exists == true
+ - works2_result.stat.path == good
+
+- name: Test trailing path {{ trailing }}
+ win_stat:
+ path: '{{ trailing }}'
+ register: trailing_result
+
+- assert:
+ that:
+ - trailing_result is successful
+ - trailing_result.stat.attributes == 'Directory'
+ - trailing_result.stat.exists == true
+ - trailing_result.stat.path == trailing
+
+- name: Test tab path {{ tab }}
+ win_stat:
+ path: '{{ tab }}'
+ register: tab_result
+ ignore_errors: yes
+
+- assert:
+ that:
+ - tab_result is failed
diff --git a/test/integration/targets/yaml_parsing/aliases b/test/integration/targets/yaml_parsing/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/yaml_parsing/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/yaml_parsing/playbook.yml b/test/integration/targets/yaml_parsing/playbook.yml
new file mode 100644
index 0000000..b3c9d5b
--- /dev/null
+++ b/test/integration/targets/yaml_parsing/playbook.yml
@@ -0,0 +1,5 @@
+- hosts: localhost
+ gather_facts: false
+ vars:
+ foo: bar
+ foo: baz # yamllint disable rule:key-duplicates
diff --git a/test/integration/targets/yaml_parsing/tasks/main.yml b/test/integration/targets/yaml_parsing/tasks/main.yml
new file mode 100644
index 0000000..7d9c4aa
--- /dev/null
+++ b/test/integration/targets/yaml_parsing/tasks/main.yml
@@ -0,0 +1,37 @@
+- name: Test ANSIBLE_DUPLICATE_YAML_DICT_KEY=warn
+ command: ansible-playbook {{ verbosity }} {{ role_path }}/playbook.yml
+ environment:
+ ANSIBLE_DUPLICATE_YAML_DICT_KEY: warn
+ register: duplicate_warn
+
+- assert:
+ that:
+ - '"found a duplicate dict key (foo)" in duplicate_warn.stderr'
+ - duplicate_warn.rc == 0
+
+- name: Test ANSIBLE_DUPLICATE_YAML_DICT_KEY=error
+ command: ansible-playbook {{ verbosity }} {{ role_path }}/playbook.yml
+ failed_when: duplicate_error.rc != 4
+ environment:
+ ANSIBLE_DUPLICATE_YAML_DICT_KEY: error
+ register: duplicate_error
+
+- assert:
+ that:
+ - '"found a duplicate dict key (foo)" in duplicate_error.stderr'
+ - duplicate_error.rc == 4
+
+- name: Test ANSIBLE_DUPLICATE_YAML_DICT_KEY=ignore
+ command: ansible-playbook {{ verbosity }} {{ role_path }}/playbook.yml
+ environment:
+ ANSIBLE_DUPLICATE_YAML_DICT_KEY: ignore
+ register: duplicate_ignore
+
+- assert:
+ that:
+ - '"found a duplicate dict key (foo)" not in duplicate_ignore.stderr'
+ - duplicate_ignore.rc == 0
+
+
+- name: test unsafe YAMLism
+ import_tasks: unsafe.yml
diff --git a/test/integration/targets/yaml_parsing/tasks/unsafe.yml b/test/integration/targets/yaml_parsing/tasks/unsafe.yml
new file mode 100644
index 0000000..8d9d627
--- /dev/null
+++ b/test/integration/targets/yaml_parsing/tasks/unsafe.yml
@@ -0,0 +1,36 @@
+- name: ensure no templating unsafe
+ block:
+ - name: check unsafe string
+ assert:
+ that:
+ - regstr != resolved
+ - "'Fail' not in regstr"
+ - "'{' in regstr"
+ - "'}' in regstr"
+ vars:
+ regstr: !unsafe b{{nottemplate}}
+
+ - name: check unsafe string in list
+ assert:
+ that:
+ - ulist[0] != resolved
+ - "'Fail' not in ulist[0]"
+ - "'{' in ulist[0]"
+ - "'}' in ulist[0]"
+ vars:
+ ulist: !unsafe [ 'b{{nottemplate}}', 'c', 'd']
+
+ - name: check unsafe string in dict
+ assert:
+ that:
+ - udict['a'] != resolved
+ - "'Fail' not in udict['a']"
+ - "'{' in udict['a']"
+ - "'}' in udict['a']"
+ vars:
+ udict: !unsafe
+ a: b{{nottemplate}}
+ c: d
+ vars:
+ nottemplate: FAIL
+ resolved: 'b{{nottemplate}}'
diff --git a/test/integration/targets/yaml_parsing/vars/main.yml b/test/integration/targets/yaml_parsing/vars/main.yml
new file mode 100644
index 0000000..ea65e0b
--- /dev/null
+++ b/test/integration/targets/yaml_parsing/vars/main.yml
@@ -0,0 +1 @@
+verbosity: "{{ '' if not ansible_verbosity else '-' ~ ('v' * ansible_verbosity) }}"
diff --git a/test/integration/targets/yum/aliases b/test/integration/targets/yum/aliases
new file mode 100644
index 0000000..1d49133
--- /dev/null
+++ b/test/integration/targets/yum/aliases
@@ -0,0 +1,5 @@
+destructive
+shippable/posix/group1
+skip/freebsd
+skip/osx
+skip/macos
diff --git a/test/integration/targets/yum/files/yum.conf b/test/integration/targets/yum/files/yum.conf
new file mode 100644
index 0000000..5a5fca6
--- /dev/null
+++ b/test/integration/targets/yum/files/yum.conf
@@ -0,0 +1,5 @@
+[main]
+gpgcheck=1
+installonly_limit=3
+clean_requirements_on_remove=True
+tsflags=nodocs
diff --git a/test/integration/targets/yum/filter_plugins/filter_list_of_tuples_by_first_param.py b/test/integration/targets/yum/filter_plugins/filter_list_of_tuples_by_first_param.py
new file mode 100644
index 0000000..27f38ce
--- /dev/null
+++ b/test/integration/targets/yum/filter_plugins/filter_list_of_tuples_by_first_param.py
@@ -0,0 +1,25 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.errors import AnsibleError, AnsibleFilterError
+
+
+def filter_list_of_tuples_by_first_param(lst, search, startswith=False):
+ out = []
+ for element in lst:
+ if startswith:
+ if element[0].startswith(search):
+ out.append(element)
+ else:
+ if search in element[0]:
+ out.append(element)
+ return out
+
+
+class FilterModule(object):
+ ''' filter '''
+
+ def filters(self):
+ return {
+ 'filter_list_of_tuples_by_first_param': filter_list_of_tuples_by_first_param,
+ }
diff --git a/test/integration/targets/yum/meta/main.yml b/test/integration/targets/yum/meta/main.yml
new file mode 100644
index 0000000..34d8126
--- /dev/null
+++ b/test/integration/targets/yum/meta/main.yml
@@ -0,0 +1,4 @@
+dependencies:
+ - prepare_tests
+ - setup_rpm_repo
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/yum/tasks/cacheonly.yml b/test/integration/targets/yum/tasks/cacheonly.yml
new file mode 100644
index 0000000..03cbd0e
--- /dev/null
+++ b/test/integration/targets/yum/tasks/cacheonly.yml
@@ -0,0 +1,16 @@
+---
+- name: Test cacheonly (clean before testing)
+ command: yum clean all
+
+- name: Try installing from cache where it has been cleaned
+ yum:
+ name: sos
+ state: latest
+ cacheonly: true
+ register: yum_result
+ ignore_errors: true
+
+- name: Verify yum failure
+ assert:
+ that:
+ - "yum_result is failed"
diff --git a/test/integration/targets/yum/tasks/check_mode_consistency.yml b/test/integration/targets/yum/tasks/check_mode_consistency.yml
new file mode 100644
index 0000000..e2a99d9
--- /dev/null
+++ b/test/integration/targets/yum/tasks/check_mode_consistency.yml
@@ -0,0 +1,61 @@
+- name: install htop in check mode to verify changes dict returned
+ yum:
+ name: htop
+ state: present
+ check_mode: yes
+ register: yum_changes_check_mode_result
+
+- name: install verify changes dict returned in check mode
+ assert:
+ that:
+ - "yum_changes_check_mode_result is success"
+ - "yum_changes_check_mode_result is changed"
+ - "'changes' in yum_changes_check_mode_result"
+ - "'installed' in yum_changes_check_mode_result['changes']"
+ - "'htop' in yum_changes_check_mode_result['changes']['installed']"
+
+- name: install htop to verify changes dict returned
+ yum:
+ name: htop
+ state: present
+ register: yum_changes_result
+
+- name: install verify changes dict returned
+ assert:
+ that:
+ - "yum_changes_result is success"
+ - "yum_changes_result is changed"
+ - "'changes' in yum_changes_result"
+ - "'installed' in yum_changes_result['changes']"
+ - "'htop' in yum_changes_result['changes']['installed']"
+
+- name: remove htop in check mode to verify changes dict returned
+ yum:
+ name: htop
+ state: absent
+ check_mode: yes
+ register: yum_changes_check_mode_result
+
+- name: remove verify changes dict returned in check mode
+ assert:
+ that:
+ - "yum_changes_check_mode_result is success"
+ - "yum_changes_check_mode_result is changed"
+ - "'changes' in yum_changes_check_mode_result"
+ - "'removed' in yum_changes_check_mode_result['changes']"
+ - "'htop' in yum_changes_check_mode_result['changes']['removed']"
+
+- name: remove htop to verify changes dict returned
+ yum:
+ name: htop
+ state: absent
+ register: yum_changes_result
+
+- name: remove verify changes dict returned
+ assert:
+ that:
+ - "yum_changes_result is success"
+ - "yum_changes_result is changed"
+ - "'changes' in yum_changes_result"
+ - "'removed' in yum_changes_result['changes']"
+ - "'htop' in yum_changes_result['changes']['removed']"
diff --git a/test/integration/targets/yum/tasks/lock.yml b/test/integration/targets/yum/tasks/lock.yml
new file mode 100644
index 0000000..3f585c1
--- /dev/null
+++ b/test/integration/targets/yum/tasks/lock.yml
@@ -0,0 +1,28 @@
+- block:
+ - name: Make sure testing package is not installed
+ yum:
+ name: sos
+ state: absent
+
+ - name: Create bogus lock file
+ copy:
+ content: bogus content for this lock file
+ dest: /var/run/yum.pid
+
+ - name: Install a package, lock file should be deleted by the module
+ yum:
+ name: sos
+ state: present
+ register: yum_result
+
+ - assert:
+ that:
+ - yum_result is success
+
+ always:
+ - name: Clean up
+ yum:
+ name: sos
+ state: absent
+
+ when: ansible_pkg_mgr == 'yum'
diff --git a/test/integration/targets/yum/tasks/main.yml b/test/integration/targets/yum/tasks/main.yml
new file mode 100644
index 0000000..157124a
--- /dev/null
+++ b/test/integration/targets/yum/tasks/main.yml
@@ -0,0 +1,82 @@
+# (c) 2014, James Tanner <tanner.jc@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Note: We install the yum package onto Fedora so that this will work on dnf systems
+# We want to test that for people who don't want to upgrade their systems.
+
+- block:
+ - name: ensure test packages are removed before starting
+ yum:
+ name:
+ - sos
+ state: absent
+
+ - import_tasks: yum.yml
+ always:
+ - name: remove installed packages
+ yum:
+ name:
+ - sos
+ state: absent
+
+ - name: remove installed group
+ yum:
+ name: "@Custom Group"
+ state: absent
+
+ - name: On Fedora 28 the above won't remove the group which results in a failure in repo.yml below
+ yum:
+ name: dinginessentail
+ state: absent
+ when:
+ - ansible_distribution in ['Fedora']
+
+ when:
+ - ansible_distribution in ['RedHat', 'CentOS', 'ScientificLinux', 'Fedora']
+
+
+- block:
+ - import_tasks: repo.yml
+ - import_tasks: yum_group_remove.yml
+ when:
+ - ansible_distribution in ['RedHat', 'CentOS', 'ScientificLinux']
+ always:
+ - yum_repository:
+ name: "{{ item }}"
+ state: absent
+ loop: "{{ repos }}"
+
+ - command: yum clean metadata
+ when:
+ - ansible_distribution in ['RedHat', 'CentOS', 'ScientificLinux', 'Fedora']
+
+
+- import_tasks: yuminstallroot.yml
+ when:
+ - ansible_distribution in ['RedHat', 'CentOS', 'ScientificLinux', 'Fedora']
+
+
+- import_tasks: proxy.yml
+ when:
+ - ansible_distribution in ['RedHat', 'CentOS', 'ScientificLinux', 'Fedora']
+
+
+- import_tasks: check_mode_consistency.yml
+ when:
+ - (ansible_distribution in ['RedHat', 'CentOS', 'ScientificLinux'] and ansible_distribution_major_version|int == 7)
+
+
+- import_tasks: lock.yml
+ when:
+ - ansible_distribution in ['RedHat', 'CentOS', 'ScientificLinux']
+
+- import_tasks: multiarch.yml
+ when:
+ - ansible_distribution in ['RedHat', 'CentOS', 'ScientificLinux']
+ - ansible_architecture == 'x86_64'
+ # Our output parsing expects us to be on yum, not dnf
+ - ansible_distribution_major_version is version('7', '<=')
+
+- import_tasks: cacheonly.yml
+ when:
+ - ansible_distribution in ['RedHat', 'CentOS', 'ScientificLinux', 'Fedora']
diff --git a/test/integration/targets/yum/tasks/multiarch.yml b/test/integration/targets/yum/tasks/multiarch.yml
new file mode 100644
index 0000000..bced634
--- /dev/null
+++ b/test/integration/targets/yum/tasks/multiarch.yml
@@ -0,0 +1,154 @@
+- block:
+ - name: Set up test yum repo
+ yum_repository:
+ name: multiarch-test-repo
+ description: ansible-test multiarch test repo
+ baseurl: "{{ multiarch_repo_baseurl }}"
+ gpgcheck: no
+ repo_gpgcheck: no
+
+ - name: Install two out of date packages from the repo
+ yum:
+ name:
+ - multiarch-a-1.0
+ - multiarch-b-1.0
+ register: outdated
+
+ - name: See what we installed
+ command: rpm -q multiarch-a multiarch-b
+ register: rpm_q
+
+ # Here we assume we're running on x86_64 (and limit to this in main.yml)
+ # (avoid comparing ansible_architecture because we only have test RPMs
+ # for i686 and x86_64 and ansible_architecture could be other things.)
+ - name: Assert that we got the right architecture
+ assert:
+ that:
+ - outdated is changed
+ - outdated.changes.installed | length == 2
+ - rpm_q.stdout_lines | length == 2
+ - rpm_q.stdout_lines[0].endswith('x86_64')
+ - rpm_q.stdout_lines[1].endswith('x86_64')
+
+ - name: Install the same versions, but i686 instead
+ yum:
+ name:
+ - multiarch-a-1.0*.i686
+ - multiarch-b-1.0*.i686
+ register: outdated_i686
+
+ - name: See what we installed
+ command: rpm -q multiarch-a multiarch-b
+ register: rpm_q
+
+ - name: Assert that all four are installed
+ assert:
+ that:
+ - outdated_i686 is changed
+ - outdated.changes.installed | length == 2
+ - rpm_q.stdout_lines | length == 4
+
+ - name: Update them all to 2.0
+ yum:
+ name: multiarch-*
+ state: latest
+ update_only: true
+ register: yum_latest
+
+ - name: Assert that all were updated and shown in results
+ assert:
+ that:
+ - yum_latest is changed
+ # This is just testing UI stability. The behavior is arguably not
+ # correct, because multiple packages are being updated. But the
+ # "because of (at least)..." wording kinda locks us in to only
+ # showing one update in this case. :(
+ - yum_latest.changes.updated | length == 1
+
+ - name: Downgrade them so we can upgrade them a different way
+ yum:
+ name:
+ - multiarch-a-1.0*
+ - multiarch-b-1.0*
+ allow_downgrade: true
+ register: downgrade
+
+ - name: See what we installed
+ command: rpm -q multiarch-a multiarch-b --queryformat '%{name}-%{version}.%{arch}\n'
+ register: rpm_q
+
+ - name: Ensure downgrade worked
+ assert:
+ that:
+ - downgrade is changed
+ - rpm_q.stdout_lines | sort == ['multiarch-a-1.0.i686', 'multiarch-a-1.0.x86_64', 'multiarch-b-1.0.i686', 'multiarch-b-1.0.x86_64']
+
+ # This triggers a different branch of logic that the partial wildcard
+ # above, but we're limited to check_mode here since it's '*'.
+ - name: Upgrade with full wildcard
+ yum:
+ name: '*'
+ state: latest
+ update_only: true
+ update_cache: true
+ check_mode: true
+ register: full_wildcard
+
+ # https://github.com/ansible/ansible/issues/73284
+ - name: Ensure we report things correctly (both arches)
+ assert:
+ that:
+ - full_wildcard is changed
+ - full_wildcard.changes.updated | filter_list_of_tuples_by_first_param('multiarch', startswith=True) | length == 4
+
+ - name: Downgrade them so we can upgrade them a different way
+ yum:
+ name:
+ - multiarch-a-1.0*
+ - multiarch-b-1.0*
+ allow_downgrade: true
+ register: downgrade
+
+ - name: Try to install again via virtual provides, should be unchanged
+ yum:
+ name:
+ - virtual-provides-multiarch-a
+ - virtual-provides-multiarch-b
+ state: present
+ register: install_vp
+
+ - name: Ensure the above did not change
+ assert:
+ that:
+ - install_vp is not changed
+
+ - name: Try to upgrade via virtual provides
+ yum:
+ name:
+ - virtual-provides-multiarch-a
+ - virtual-provides-multiarch-b
+ state: latest
+ update_only: true
+ register: upgrade_vp
+
+ - name: Ensure we report things correctly (both arches)
+ assert:
+ that:
+ - upgrade_vp is changed
+ # This is just testing UI stability, like above.
+ # We'll only have one package in "updated" per spec, even though
+ # (in this case) two are getting updated per spec.
+ - upgrade_vp.changes.updated | length == 2
+
+ always:
+ - name: Remove test yum repo
+ yum_repository:
+ name: multiarch-test-repo
+ state: absent
+
+ - name: Remove all test packages installed
+ yum:
+ name:
+ - multiarch-*
+ - virtual-provides-multiarch-*
+ state: absent
diff --git a/test/integration/targets/yum/tasks/proxy.yml b/test/integration/targets/yum/tasks/proxy.yml
new file mode 100644
index 0000000..b011d11
--- /dev/null
+++ b/test/integration/targets/yum/tasks/proxy.yml
@@ -0,0 +1,186 @@
+- name: test yum proxy settings
+ block:
+ - name: install tinyproxy
+ yum:
+ name: 'https://ci-files.testing.ansible.com/test/integration/targets/yum/tinyproxy-1.10.0-3.el7.x86_64.rpm'
+ state: installed
+
+ # systemd doesn't play nice with this in a container for some reason
+ - name: start tinyproxy (systemd with tiny proxy does not work in container)
+ shell: tinyproxy
+ changed_when: false
+
+ # test proxy without auth
+ - name: set unauthenticated proxy in yum.conf
+ lineinfile:
+ path: /etc/yum.conf
+ line: "proxy=http://127.0.0.1:8888"
+ state: present
+
+ - name: clear proxy logs
+ shell: ': > /var/log/tinyproxy/tinyproxy.log'
+ changed_when: false
+ args:
+ executable: /usr/bin/bash
+
+ - name: install ninvaders with unauthenticated proxy
+ yum:
+ name: 'https://ci-files.testing.ansible.com/test/integration/targets/yum/ninvaders-0.1.1-18.el7.x86_64.rpm'
+ state: installed
+ register: yum_proxy_result
+
+ - assert:
+ that:
+ - "yum_proxy_result.changed"
+ - "'msg' in yum_proxy_result"
+ - "'rc' in yum_proxy_result"
+
+ - name: check that it install via unauthenticated proxy
+ command: grep -q Request /var/log/tinyproxy/tinyproxy.log
+
+ - name: uninstall ninvaders with unauthenticated proxy
+ yum:
+ name: ninvaders
+ state: absent
+ register: yum_proxy_result
+
+ - assert:
+ that:
+ - "yum_proxy_result.changed"
+ - "'msg' in yum_proxy_result"
+ - "'rc' in yum_proxy_result"
+
+ - name: unset unauthenticated proxy in yum.conf
+ lineinfile:
+ path: /etc/yum.conf
+ line: "proxy=http://127.0.0.1:8888"
+ state: absent
+
+ # test proxy with auth
+ - name: set authenticated proxy config in tinyproxy.conf
+ lineinfile:
+ path: /etc/tinyproxy/tinyproxy.conf
+ line: "BasicAuth 1testuser 1testpassword"
+ state: present
+
+ # systemd doesn't play nice with this in a container for some reason
+ - name: SIGHUP tinyproxy to reload config (workaround because of systemd+tinyproxy in container)
+ shell: kill -HUP $(ps -ef | grep tinyproxy | grep -v grep | awk '{print $2}')
+ changed_when: false
+ args:
+ executable: /usr/bin/bash
+
+ - name: set authenticated proxy config in yum.conf
+ lineinfile:
+ path: /etc/yum.conf
+ line: "proxy=http://1testuser:1testpassword@127.0.0.1:8888"
+ state: present
+
+ - name: clear proxy logs
+ shell: ': > /var/log/tinyproxy/tinyproxy.log'
+ changed_when: false
+ args:
+ executable: /usr/bin/bash
+
+ - name: install ninvaders with authenticated proxy
+ yum:
+ name: 'https://ci-files.testing.ansible.com/test/integration/targets/yum/ninvaders-0.1.1-18.el7.x86_64.rpm'
+ state: installed
+ register: yum_proxy_result
+
+ - assert:
+ that:
+ - "yum_proxy_result.changed"
+ - "'msg' in yum_proxy_result"
+ - "'rc' in yum_proxy_result"
+
+ - name: check that it install via authenticated proxy
+ command: grep -q Request /var/log/tinyproxy/tinyproxy.log
+
+ - name: uninstall ninvaders with authenticated proxy
+ yum:
+ name: ninvaders
+ state: absent
+
+ - name: unset authenticated proxy config in yum.conf
+ lineinfile:
+ path: /etc/yum.conf
+ line: "proxy=http://1testuser:1testpassword@127.0.0.1:8888"
+ state: absent
+
+ - name: set proxy config in yum.conf
+ lineinfile:
+ path: /etc/yum.conf
+ line: "proxy=http://127.0.0.1:8888"
+ state: present
+
+ - name: set proxy_username config in yum.conf
+ lineinfile:
+ path: /etc/yum.conf
+ line: "proxy_username=1testuser"
+ state: present
+
+ - name: set proxy_password config in yum.conf
+ lineinfile:
+ path: /etc/yum.conf
+ line: "proxy_password=1testpassword"
+ state: present
+
+ - name: clear proxy logs
+ shell: ': > /var/log/tinyproxy/tinyproxy.log'
+ changed_when: false
+ args:
+ executable: /usr/bin/bash
+
+ - name: install ninvaders with proxy, proxy_username, and proxy_password config in yum.conf
+ yum:
+ name: 'https://ci-files.testing.ansible.com/test/integration/targets/yum/ninvaders-0.1.1-18.el7.x86_64.rpm'
+ state: installed
+ register: yum_proxy_result
+
+ - assert:
+ that:
+ - "yum_proxy_result.changed"
+ - "'msg' in yum_proxy_result"
+ - "'rc' in yum_proxy_result"
+
+ - name: check that it install via proxy with proxy_username, proxy_password config in yum.conf
+ command: grep -q Request /var/log/tinyproxy/tinyproxy.log
+
+ always:
+ #cleanup
+ - name: uninstall tinyproxy
+ yum:
+ name: tinyproxy
+ state: absent
+
+ - name: uninstall ninvaders
+ yum:
+ name: ninvaders
+ state: absent
+
+ - name: ensure unset authenticated proxy
+ lineinfile:
+ path: /etc/yum.conf
+ line: "proxy=http://1testuser:1testpassword@127.0.0.1:8888"
+ state: absent
+
+ - name: ensure unset proxy
+ lineinfile:
+ path: /etc/yum.conf
+ line: "proxy=http://127.0.0.1:8888"
+ state: absent
+
+ - name: ensure unset proxy_username
+ lineinfile:
+ path: /etc/yum.conf
+ line: "proxy_username=1testuser"
+ state: absent
+
+ - name: ensure unset proxy_password
+ lineinfile:
+ path: /etc/yum.conf
+ line: "proxy_password=1testpassword"
+ state: absent
+ when:
+ - (ansible_distribution in ['RedHat', 'CentOS', 'ScientificLinux'] and ansible_distribution_major_version|int == 7 and ansible_architecture in ['x86_64'])
diff --git a/test/integration/targets/yum/tasks/repo.yml b/test/integration/targets/yum/tasks/repo.yml
new file mode 100644
index 0000000..f312b1c
--- /dev/null
+++ b/test/integration/targets/yum/tasks/repo.yml
@@ -0,0 +1,729 @@
+- block:
+ - name: Install dinginessentail-1.0-1
+ yum:
+ name: dinginessentail-1.0-1
+ state: present
+ register: yum_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "yum_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.0-1')"
+
+ - name: Verify yum module outputs
+ assert:
+ that:
+ - "'msg' in yum_result"
+ - "'rc' in yum_result"
+ - "'results' in yum_result"
+ # ============================================================================
+ - name: Install dinginessentail-1.0-1 again
+ yum:
+ name: dinginessentail-1.0-1
+ state: present
+ register: yum_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "not yum_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.0-1')"
+
+ - name: Verify yum module outputs
+ assert:
+ that:
+ - "'msg' in yum_result"
+ - "'rc' in yum_result"
+ - "'results' in yum_result"
+ # ============================================================================
+ - name: Install dinginessentail-1:1.0-2
+ yum:
+ name: "dinginessentail-1:1.0-2.{{ ansible_architecture }}"
+ state: present
+ register: yum_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "yum_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.0-2')"
+
+ - name: Verify yum module outputs
+ assert:
+ that:
+ - "'msg' in yum_result"
+ - "'rc' in yum_result"
+ - "'results' in yum_result"
+
+ - name: Remove dinginessentail
+ yum:
+ name: dinginessentail
+ state: absent
+ # ============================================================================
+ - name: Downgrade dinginessentail
+ yum:
+ name: dinginessentail-1.0-1
+ state: present
+ allow_downgrade: yes
+ register: yum_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "yum_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.0-1')"
+
+ - name: Verify yum module outputs
+ assert:
+ that:
+ - "'msg' in yum_result"
+ - "'rc' in yum_result"
+ - "'results' in yum_result"
+ # ============================================================================
+ - name: Update to the latest dinginessentail
+ yum:
+ name: dinginessentail
+ state: latest
+ register: yum_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "yum_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.1-1')"
+
+ - name: Verify yum module outputs
+ assert:
+ that:
+ - "'msg' in yum_result"
+ - "'rc' in yum_result"
+ - "'results' in yum_result"
+ # ============================================================================
+ - name: Install dinginessentail-1.0-1 from a file (higher version is already installed)
+ yum:
+ name: "{{ repodir }}/dinginessentail-1.0-1.{{ ansible_architecture }}.rpm"
+ state: present
+ register: yum_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "not yum_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.1-1')"
+
+ - name: Verify yum module outputs
+ assert:
+ that:
+ - "'msg' in yum_result"
+ - "'rc' in yum_result"
+ - "'results' in yum_result"
+
+ - name: Remove dinginessentail
+ yum:
+ name: dinginessentail
+ state: absent
+ # ============================================================================
+ - name: Install dinginessentail-1.0-1 from a file
+ yum:
+ name: "{{ repodir }}/dinginessentail-1.0-1.{{ ansible_architecture }}.rpm"
+ state: present
+ disable_gpg_check: true
+ register: yum_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "yum_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.0-1')"
+
+ - name: Verify yum module outputs
+ assert:
+ that:
+ - "'msg' in yum_result"
+ - "'rc' in yum_result"
+ - "'results' in yum_result"
+ # ============================================================================
+ - name: Install dinginessentail-1.0-1 from a file again
+ yum:
+ name: "{{ repodir }}/dinginessentail-1.0-1.{{ ansible_architecture }}.rpm"
+ state: present
+ disable_gpg_check: true
+ register: yum_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "not yum_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.0-1')"
+
+ - name: Verify yum module outputs
+ assert:
+ that:
+ - "'msg' in yum_result"
+ - "'rc' in yum_result"
+ - "'results' in yum_result"
+ # ============================================================================
+ - name: Install dinginessentail-1.0-2 from a file
+ yum:
+ name: "{{ repodir }}/dinginessentail-1.0-2.{{ ansible_architecture }}.rpm"
+ state: present
+ disable_gpg_check: true
+ register: yum_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "yum_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.0-2')"
+
+ - name: Verify yum module outputs
+ assert:
+ that:
+ - "'msg' in yum_result"
+ - "'rc' in yum_result"
+ - "'results' in yum_result"
+ # ============================================================================
+ - name: Install dinginessentail-1.0-2 from a file again
+ yum:
+ name: "{{ repodir }}/dinginessentail-1.0-2.{{ ansible_architecture }}.rpm"
+ state: present
+ disable_gpg_check: true
+ register: yum_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "not yum_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.0-2')"
+
+ - name: Verify yum module outputs
+ assert:
+ that:
+ - "'msg' in yum_result"
+ - "'rc' in yum_result"
+ - "'results' in yum_result"
+ # ============================================================================
+ - name: Try to downgrade dinginessentail without allow_downgrade being set
+ yum:
+ name: dinginessentail-1.0-1
+ state: present
+ allow_downgrade: no
+ register: yum_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "not yum_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.0-2')"
+
+ - name: Verify yum module outputs
+ assert:
+ that:
+ - "'msg' in yum_result"
+ - "'rc' in yum_result"
+ - "'results' in yum_result"
+ # ============================================================================
+ - name: Update dinginessentail with update_only set
+ yum:
+ name: dinginessentail
+ state: latest
+ update_only: yes
+ register: yum_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "yum_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.1-1')"
+
+ - name: Verify yum module outputs
+ assert:
+ that:
+ - "'msg' in yum_result"
+ - "'rc' in yum_result"
+ - "'results' in yum_result"
+
+ - name: Remove dinginessentail
+ yum:
+ name: dinginessentail
+ state: absent
+ # ============================================================================
+ - name: Try to update dinginessentail which is not installed, update_only is set
+ yum:
+ name: dinginessentail
+ state: latest
+ update_only: yes
+ register: yum_result
+ ignore_errors: yes
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+ ignore_errors: yes
+
+ - name: Verify installation
+ assert:
+ that:
+ - "rpm_result.rc == 1"
+ - "yum_result.rc == 0"
+ - "not yum_result.changed"
+ - "not yum_result is failed"
+
+ - name: Verify yum module outputs
+ assert:
+ that:
+ - "'msg' in yum_result"
+ - "'rc' in yum_result"
+ - "'results' in yum_result"
+ # ============================================================================
+ - name: Try to install incompatible arch
+ yum:
+ name: "{{ repodir_ppc64 }}/dinginessentail-1.0-1.ppc64.rpm"
+ state: present
+ register: yum_result
+ ignore_errors: yes
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+ ignore_errors: yes
+
+ - name: Verify installation
+ assert:
+ that:
+ - "rpm_result.rc == 1"
+ - "yum_result.rc == 1"
+ - "not yum_result.changed"
+ - "yum_result is failed"
+
+ - name: Verify yum module outputs
+ assert:
+ that:
+ - "'msg' in yum_result"
+ - "'rc' in yum_result"
+ - "'results' in yum_result"
+ # ============================================================================
+ - name: Make sure latest dinginessentail is installed
+ yum:
+ name: dinginessentail
+ state: latest
+
+ - name: Downgrade dinginessentail using rpm file
+ yum:
+ name: "{{ repodir }}/dinginessentail-1.0-1.{{ ansible_architecture }}.rpm"
+ state: present
+ allow_downgrade: yes
+ disable_gpg_check: yes
+ register: yum_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "yum_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.0-1')"
+
+ - name: Verify yum module outputs
+ assert:
+ that:
+ - "'msg' in yum_result"
+ - "'rc' in yum_result"
+ - "'results' in yum_result"
+ # ============================================================================
+ - block:
+ - name: make sure dinginessentail is not installed
+ yum:
+ name: dinginessentail
+ state: absent
+
+ - name: install dinginessentail both archs
+ yum:
+ name: "{{ pkgs }}"
+ state: present
+ disable_gpg_check: true
+ vars:
+ pkgs:
+ - "{{ repodir }}/dinginessentail-1.1-1.x86_64.rpm"
+ - "{{ repodir_i686 }}/dinginessentail-1.1-1.i686.rpm"
+
+ - name: try to install lower version of dinginessentail from rpm file, without allow_downgrade, just one arch
+ yum:
+ name: "{{ repodir_i686 }}/dinginessentail-1.0-1.i686.rpm"
+ state: present
+ register: yum_result
+
+ - name: check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: verify installation
+ assert:
+ that:
+ - "not yum_result.changed"
+ - "rpm_result.stdout_lines[0].startswith('dinginessentail-1.1-1')"
+ - "rpm_result.stdout_lines[1].startswith('dinginessentail-1.1-1')"
+
+ - name: verify yum module outputs
+ assert:
+ that:
+ - "'msg' in yum_result"
+ - "'rc' in yum_result"
+ - "'results' in yum_result"
+ when: ansible_architecture == "x86_64"
+ # ============================================================================
+ - block:
+ - name: make sure dinginessentail is not installed
+ yum:
+ name: dinginessentail
+ state: absent
+
+ - name: install dinginessentail both archs
+ yum:
+ name: "{{ pkgs }}"
+ state: present
+ disable_gpg_check: true
+ vars:
+ pkgs:
+ - "{{ repodir }}/dinginessentail-1.0-1.x86_64.rpm"
+ - "{{ repodir_i686 }}/dinginessentail-1.0-1.i686.rpm"
+
+ - name: Update both arch in one task using rpm files
+ yum:
+ name: "{{ repodir }}/dinginessentail-1.1-1.x86_64.rpm,{{ repodir_i686 }}/dinginessentail-1.1-1.i686.rpm"
+ state: present
+ disable_gpg_check: yes
+ register: yum_result
+
+ - name: check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: verify installation
+ assert:
+ that:
+ - "yum_result.changed"
+ - "rpm_result.stdout_lines[0].startswith('dinginessentail-1.1-1')"
+ - "rpm_result.stdout_lines[1].startswith('dinginessentail-1.1-1')"
+
+ - name: verify yum module outputs
+ assert:
+ that:
+ - "'msg' in yum_result"
+ - "'rc' in yum_result"
+ - "'results' in yum_result"
+ when: ansible_architecture == "x86_64"
+ # ============================================================================
+ always:
+ - name: Clean up
+ yum:
+ name: dinginessentail
+ state: absent
+
+# FIXME: dnf currently doesn't support epoch as part of it's pkg_spec for
+# finding install candidates
+# https://bugzilla.redhat.com/show_bug.cgi?id=1619687
+- block:
+ - name: Install 1:dinginessentail-1.0-2
+ yum:
+ name: "1:dinginessentail-1.0-2.{{ ansible_architecture }}"
+ state: present
+ disable_gpg_check: true
+ register: yum_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "yum_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.0-2')"
+
+ - name: Verify yum module outputs
+ assert:
+ that:
+ - "'msg' in yum_result"
+ - "'rc' in yum_result"
+ - "'results' in yum_result"
+ always:
+ - name: Clean up
+ yum:
+ name: dinginessentail
+ state: absent
+
+ when: ansible_pkg_mgr == 'yum'
+
+# DNF1 (Fedora < 26) had some issues:
+# - did not accept architecture tag as valid component of a package spec unless
+# installing a file (i.e. can't search the repo)
+# - doesn't handle downgrade transactions via the API properly, marks it as a
+# conflict
+#
+# NOTE: Both DNF1 and Fedora < 26 have long been EOL'd by their respective
+# upstreams
+- block:
+ # ============================================================================
+ - name: Install dinginessentail-1.0-2
+ yum:
+ name: "dinginessentail-1.0-2.{{ ansible_architecture }}"
+ state: present
+ disable_gpg_check: true
+ register: yum_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "yum_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.0-2')"
+
+ - name: Verify yum module outputs
+ assert:
+ that:
+ - "'msg' in yum_result"
+ - "'rc' in yum_result"
+ - "'results' in yum_result"
+
+ - name: Install dinginessentail-1.0-2 again
+ yum:
+ name: dinginessentail-1.0-2
+ state: present
+ register: yum_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "not yum_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.0-2')"
+
+ - name: Verify yum module outputs
+ assert:
+ that:
+ - "'msg' in yum_result"
+ - "'rc' in yum_result"
+ - "'results' in yum_result"
+ always:
+ - name: Clean up
+ yum:
+ name: dinginessentail
+ state: absent
+ when: not (ansible_distribution == "Fedora" and ansible_distribution_major_version|int < 26)
+
+# https://github.com/ansible/ansible/issues/47689
+- block:
+ - name: Install dinginessentail == 1.0
+ yum:
+ name: "dinginessentail == 1.0"
+ state: present
+ register: yum_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "yum_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.0-1')"
+
+ - name: Verify yum module outputs
+ assert:
+ that:
+ - "'msg' in yum_result"
+ - "'rc' in yum_result"
+ - "'results' in yum_result"
+ always:
+ - name: Clean up
+ yum:
+ name: dinginessentail
+ state: absent
+
+ when: ansible_pkg_mgr == 'yum'
+
+
+# https://github.com/ansible/ansible/pull/54603
+- block:
+ - name: Install dinginessentail < 1.1
+ yum:
+ name: "dinginessentail < 1.1"
+ state: present
+ register: yum_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "yum_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.0')"
+
+ - name: Install dinginessentail >= 1.1
+ yum:
+ name: "dinginessentail >= 1.1"
+ state: present
+ register: yum_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify installation
+ assert:
+ that:
+ - "yum_result.changed"
+ - "rpm_result.stdout.startswith('dinginessentail-1.1')"
+
+ - name: Verify yum module outputs
+ assert:
+ that:
+ - "'msg' in yum_result"
+ - "'rc' in yum_result"
+ - "'results' in yum_result"
+
+ always:
+ - name: Clean up
+ yum:
+ name: dinginessentail
+ state: absent
+
+ when: ansible_pkg_mgr == 'yum'
+
+# https://github.com/ansible/ansible/issues/45250
+- block:
+ - name: Install dinginessentail-1.0, dinginessentail-olive-1.0, landsidescalping-1.0
+ yum:
+ name: "dinginessentail-1.0,dinginessentail-olive-1.0,landsidescalping-1.0"
+ state: present
+
+ - name: Upgrade dinginessentail*
+ yum:
+ name: dinginessentail*
+ state: latest
+ register: yum_result
+
+ - name: Check dinginessentail with rpm
+ shell: rpm -q dinginessentail
+ register: rpm_result
+
+ - name: Verify update of dinginessentail
+ assert:
+ that:
+ - "rpm_result.stdout.startswith('dinginessentail-1.1-1')"
+
+ - name: Check dinginessentail-olive with rpm
+ shell: rpm -q dinginessentail-olive
+ register: rpm_result
+
+ - name: Verify update of dinginessentail-olive
+ assert:
+ that:
+ - "rpm_result.stdout.startswith('dinginessentail-olive-1.1-1')"
+
+ - name: Check landsidescalping with rpm
+ shell: rpm -q landsidescalping
+ register: rpm_result
+
+ - name: Verify landsidescalping did NOT get updated
+ assert:
+ that:
+ - "rpm_result.stdout.startswith('landsidescalping-1.0-1')"
+
+ - name: Verify yum module outputs
+ assert:
+ that:
+ - "yum_result is changed"
+ - "'msg' in yum_result"
+ - "'rc' in yum_result"
+ - "'results' in yum_result"
+ always:
+ - name: Clean up
+ yum:
+ name: dinginessentail,dinginessentail-olive,landsidescalping
+ state: absent
+
+- block:
+ - yum:
+ name: dinginessentail
+ state: present
+
+ - yum:
+ list: dinginessentail*
+ register: list_out
+
+ - set_fact:
+ passed: true
+ loop: "{{ list_out.results }}"
+ when: item.yumstate == 'installed'
+
+ - name: Test that there is yumstate=installed in the result
+ assert:
+ that:
+ - passed is defined
+ always:
+ - name: Clean up
+ yum:
+ name: dinginessentail
+ state: absent
diff --git a/test/integration/targets/yum/tasks/yum.yml b/test/integration/targets/yum/tasks/yum.yml
new file mode 100644
index 0000000..511c577
--- /dev/null
+++ b/test/integration/targets/yum/tasks/yum.yml
@@ -0,0 +1,884 @@
+# Setup by setup_rpm_repo
+- set_fact:
+ package1: dinginessentail
+ package2: dinginessentail-olive
+
+# UNINSTALL
+- name: uninstall {{ package1 }}
+ yum: name={{ package1 }} state=removed
+ register: yum_result
+
+- name: check {{ package1 }} with rpm
+ shell: rpm -q {{ package1 }}
+ ignore_errors: True
+ register: rpm_result
+
+- name: verify uninstallation of {{ package1 }}
+ assert:
+ that:
+ - "yum_result is success"
+ - "rpm_result is failed"
+
+# UNINSTALL AGAIN
+- name: uninstall {{ package1 }} again in check mode
+ yum: name={{ package1 }} state=removed
+ check_mode: true
+ register: yum_result
+
+- name: verify no change on re-uninstall in check mode
+ assert:
+ that:
+ - "not yum_result is changed"
+
+- name: uninstall {{ package1 }} again
+ yum: name={{ package1 }} state=removed
+ register: yum_result
+
+- name: verify no change on re-uninstall
+ assert:
+ that:
+ - "not yum_result is changed"
+
+# INSTALL
+- name: install {{ package1 }} in check mode
+ yum: name={{ package1 }} state=present
+ check_mode: true
+ register: yum_result
+
+- name: verify installation of {{ package1 }} in check mode
+ assert:
+ that:
+ - "yum_result is changed"
+
+- name: install {{ package1 }}
+ yum: name={{ package1 }} state=present
+ register: yum_result
+
+- name: verify installation of {{ package1 }}
+ assert:
+ that:
+ - "yum_result is success"
+ - "yum_result is changed"
+
+- name: verify yum module outputs
+ assert:
+ that:
+ - "'changed' in yum_result"
+ - "'msg' in yum_result"
+ - "'results' in yum_result"
+
+- name: check {{ package1 }} with rpm
+ shell: rpm -q {{ package1 }}
+
+# INSTALL AGAIN
+- name: install {{ package1 }} again in check mode
+ yum: name={{ package1 }} state=present
+ check_mode: true
+ register: yum_result
+- name: verify no change on second install in check mode
+ assert:
+ that:
+ - "not yum_result is changed"
+
+- name: install {{ package1 }} again
+ yum: name={{ package1 }} state=present
+ register: yum_result
+- name: verify no change on second install
+ assert:
+ that:
+ - "not yum_result is changed"
+
+- name: install {{ package1 }} again with empty string enablerepo
+ yum: name={{ package1 }} state=present enablerepo=""
+ register: yum_result
+- name: verify no change on third install with empty string enablerepo
+ assert:
+ that:
+ - "yum_result is success"
+ - "not yum_result is changed"
+
+# This test case is unfortunately distro specific because we have to specify
+# repo names which are not the same across Fedora/RHEL/CentOS for base/updates
+- name: install {{ package1 }} again with missing repo enablerepo
+ yum:
+ name: '{{ package1 }}'
+ state: present
+ enablerepo: '{{ repos + ["thisrepodoesnotexist"] }}'
+ disablerepo: "*"
+ register: yum_result
+ when: ansible_distribution == 'CentOS'
+- name: verify no change on fourth install with missing repo enablerepo (yum)
+ assert:
+ that:
+ - "yum_result is success"
+ - "yum_result is not changed"
+ when: ansible_distribution == 'CentOS'
+
+# This test case is unfortunately distro specific because we have to specify
+# repo names which are not the same across Fedora/RHEL/CentOS for base/updates
+- name: install repos again with disable all and enable select repo(s)
+ yum:
+ name: '{{ package1 }}'
+ state: present
+ enablerepo: '{{ repos }}'
+ disablerepo: "*"
+ register: yum_result
+ when: ansible_distribution == 'CentOS'
+- name: verify no change on fourth install with missing repo enablerepo (yum)
+ assert:
+ that:
+ - "yum_result is success"
+ - "yum_result is not changed"
+ when: ansible_distribution == 'CentOS'
+
+- name: install {{ package1 }} again with only missing repo enablerepo
+ yum:
+ name: '{{ package1 }}'
+ state: present
+ enablerepo: "thisrepodoesnotexist"
+ ignore_errors: true
+ register: yum_result
+- name: verify no change on fifth install with only missing repo enablerepo (yum)
+ assert:
+ that:
+ - "yum_result is not success"
+ when: ansible_pkg_mgr == 'yum'
+- name: verify no change on fifth install with only missing repo enablerepo (dnf)
+ assert:
+ that:
+ - "yum_result is success"
+ when: ansible_pkg_mgr == 'dnf'
+
+# INSTALL AGAIN WITH LATEST
+- name: install {{ package1 }} again with state latest in check mode
+ yum: name={{ package1 }} state=latest
+ check_mode: true
+ register: yum_result
+- name: verify install {{ package1 }} again with state latest in check mode
+ assert:
+ that:
+ - "not yum_result is changed"
+
+- name: install {{ package1 }} again with state latest idempotence
+ yum: name={{ package1 }} state=latest
+ register: yum_result
+- name: verify install {{ package1 }} again with state latest idempotence
+ assert:
+ that:
+ - "not yum_result is changed"
+
+# INSTALL WITH LATEST
+- name: uninstall {{ package1 }}
+ yum: name={{ package1 }} state=removed
+ register: yum_result
+- name: verify uninstall {{ package1 }}
+ assert:
+ that:
+ - "yum_result is successful"
+
+- name: copy yum.conf file in case it is missing
+ copy:
+ src: yum.conf
+ dest: /etc/yum.conf
+ force: False
+ register: yum_conf_copy
+
+- block:
+ - name: install {{ package1 }} with state latest in check mode with config file param
+ yum: name={{ package1 }} state=latest conf_file=/etc/yum.conf
+ check_mode: true
+ register: yum_result
+ - name: verify install {{ package1 }} with state latest in check mode with config file param
+ assert:
+ that:
+ - "yum_result is changed"
+
+ always:
+ - name: remove tmp yum.conf file if we created it
+ file:
+ path: /etc/yum.conf
+ state: absent
+ when: yum_conf_copy is changed
+
+- name: install {{ package1 }} with state latest in check mode
+ yum: name={{ package1 }} state=latest
+ check_mode: true
+ register: yum_result
+- name: verify install {{ package1 }} with state latest in check mode
+ assert:
+ that:
+ - "yum_result is changed"
+
+- name: install {{ package1 }} with state latest
+ yum: name={{ package1 }} state=latest
+ register: yum_result
+- name: verify install {{ package1 }} with state latest
+ assert:
+ that:
+ - "yum_result is changed"
+
+- name: install {{ package1 }} with state latest idempotence
+ yum: name={{ package1 }} state=latest
+ register: yum_result
+- name: verify install {{ package1 }} with state latest idempotence
+ assert:
+ that:
+ - "not yum_result is changed"
+
+- name: install {{ package1 }} with state latest idempotence with config file param
+ yum: name={{ package1 }} state=latest
+ register: yum_result
+- name: verify install {{ package1 }} with state latest idempotence with config file param
+ assert:
+ that:
+ - "not yum_result is changed"
+
+
+# Multiple packages
+- name: uninstall {{ package1 }} and {{ package2 }}
+ yum: name={{ package1 }},{{ package2 }} state=removed
+
+- name: check {{ package1 }} with rpm
+ shell: rpm -q {{ package1 }}
+ ignore_errors: True
+ register: rpm_package1_result
+
+- name: check {{ package2 }} with rpm
+ shell: rpm -q {{ package2 }}
+ ignore_errors: True
+ register: rpm_package2_result
+
+- name: verify packages installed
+ assert:
+ that:
+ - "rpm_package1_result is failed"
+ - "rpm_package2_result is failed"
+
+- name: install {{ package1 }} and {{ package2 }} as comma separated
+ yum: name={{ package1 }},{{ package2 }} state=present
+ register: yum_result
+
+- name: verify packages installed
+ assert:
+ that:
+ - "yum_result is success"
+ - "yum_result is changed"
+
+- name: check {{ package1 }} with rpm
+ shell: rpm -q {{ package1 }}
+
+- name: check {{ package2 }} with rpm
+ shell: rpm -q {{ package2 }}
+
+- name: uninstall {{ package1 }} and {{ package2 }}
+ yum: name={{ package1 }},{{ package2 }} state=removed
+ register: yum_result
+
+- name: install {{ package1 }} and {{ package2 }} as list
+ yum:
+ name:
+ - '{{ package1 }}'
+ - '{{ package2 }}'
+ state: present
+ register: yum_result
+
+- name: verify packages installed
+ assert:
+ that:
+ - "yum_result is success"
+ - "yum_result is changed"
+
+- name: check {{ package1 }} with rpm
+ shell: rpm -q {{ package1 }}
+
+- name: check {{ package2 }} with rpm
+ shell: rpm -q {{ package2 }}
+
+- name: uninstall {{ package1 }} and {{ package2 }}
+ yum: name={{ package1 }},{{ package2 }} state=removed
+ register: yum_result
+
+- name: install {{ package1 }} and {{ package2 }} as comma separated with spaces
+ yum:
+ name: "{{ package1 }}, {{ package2 }}"
+ state: present
+ register: yum_result
+
+- name: verify packages installed
+ assert:
+ that:
+ - "yum_result is success"
+ - "yum_result is changed"
+
+- name: check {{ package1 }} with rpm
+ shell: rpm -q {{ package1 }}
+
+- name: check {{ package2 }} with rpm
+ shell: rpm -q {{ package2 }}
+
+- name: uninstall {{ package1 }} and {{ package2 }}
+ yum: name={{ package1 }},{{ package2 }} state=removed
+
+- name: install non-existent rpm
+ yum:
+ name: does-not-exist
+ register: non_existent_rpm
+ ignore_errors: True
+
+- name: check non-existent rpm install failed
+ assert:
+ that:
+ - non_existent_rpm is failed
+
+# Install in installroot='/'
+- name: install {{ package1 }}
+ yum: name={{ package1 }} state=present installroot='/'
+ register: yum_result
+
+- name: verify installation of {{ package1 }}
+ assert:
+ that:
+ - "yum_result is success"
+ - "yum_result is changed"
+
+- name: verify yum module outputs
+ assert:
+ that:
+ - "'changed' in yum_result"
+ - "'msg' in yum_result"
+ - "'results' in yum_result"
+
+- name: check {{ package1 }} with rpm
+ shell: rpm -q {{ package1 }} --root=/
+
+- name: uninstall {{ package1 }}
+ yum:
+ name: '{{ package1 }}'
+ installroot: '/'
+ state: removed
+ register: yum_result
+
+# Seems like some yum versions won't download a package from local file repository, continue to use sos for this test.
+# https://stackoverflow.com/questions/58295660/yum-downloadonly-ignores-packages-in-local-repo
+- name: Test download_only
+ yum:
+ name: sos
+ state: latest
+ download_only: true
+ register: yum_result
+
+- name: verify download of sos (part 1 -- yum "install" succeeded)
+ assert:
+ that:
+ - "yum_result is success"
+ - "yum_result is changed"
+
+- name: uninstall sos (noop)
+ yum:
+ name: sos
+ state: removed
+ register: yum_result
+
+- name: verify download of sos (part 2 -- nothing removed during uninstall)
+ assert:
+ that:
+ - "yum_result is success"
+ - "not yum_result is changed"
+
+- name: uninstall sos for downloadonly/downloaddir test
+ yum:
+ name: sos
+ state: absent
+
+- name: Test download_only/download_dir
+ yum:
+ name: sos
+ state: latest
+ download_only: true
+ download_dir: "/var/tmp/packages"
+ register: yum_result
+
+- name: verify yum output
+ assert:
+ that:
+ - "yum_result is success"
+ - "yum_result is changed"
+
+- command: "ls /var/tmp/packages"
+ register: ls_out
+
+- name: Verify specified download_dir was used
+ assert:
+ that:
+ - "'sos' in ls_out.stdout"
+
+- name: install group
+ yum:
+ name: "@Custom Group"
+ state: present
+ register: yum_result
+
+- name: verify installation of the group
+ assert:
+ that:
+ - "yum_result is success"
+ - "yum_result is changed"
+
+- name: verify yum module outputs
+ assert:
+ that:
+ - "'changed' in yum_result"
+ - "'msg' in yum_result"
+ - "'results' in yum_result"
+
+- name: install the group again
+ yum:
+ name: "@Custom Group"
+ state: present
+ register: yum_result
+
+- name: verify nothing changed
+ assert:
+ that:
+ - "yum_result is success"
+ - "not yum_result is changed"
+
+- name: verify yum module outputs
+ assert:
+ that:
+ - "'changed' in yum_result"
+ - "'msg' in yum_result"
+ - "'results' in yum_result"
+
+- name: install the group again but also with a package that is not yet installed
+ yum:
+ name:
+ - "@Custom Group"
+ - '{{ package2 }}'
+ state: present
+ register: yum_result
+
+- name: verify {{ package3 }} is installed
+ assert:
+ that:
+ - "yum_result is success"
+ - "yum_result is changed"
+
+- name: verify yum module outputs
+ assert:
+ that:
+ - "'changed' in yum_result"
+ - "'msg' in yum_result"
+ - "'results' in yum_result"
+
+- name: try to install the group again, with --check to check 'changed'
+ yum:
+ name: "@Custom Group"
+ state: present
+ check_mode: yes
+ register: yum_result
+
+- name: verify nothing changed
+ assert:
+ that:
+ - "not yum_result is changed"
+
+- name: verify yum module outputs
+ assert:
+ that:
+ - "'changed' in yum_result"
+ - "'msg' in yum_result"
+ - "'results' in yum_result"
+
+- name: try to install non existing group
+ yum:
+ name: "@non-existing-group"
+ state: present
+ register: yum_result
+ ignore_errors: True
+
+- name: verify installation of the non existing group failed
+ assert:
+ that:
+ - "yum_result is failed"
+ - "not yum_result is changed"
+ - "yum_result is failed"
+
+- name: verify yum module outputs
+ assert:
+ that:
+ - "'changed' in yum_result"
+ - "'msg' in yum_result"
+ - "'results' in yum_result"
+
+- name: try to install non existing file
+ yum:
+ name: /tmp/non-existing-1.0.0.fc26.noarch.rpm
+ state: present
+ register: yum_result
+ ignore_errors: yes
+
+- name: verify installation failed
+ assert:
+ that:
+ - "yum_result is failed"
+ - "not yum_result is changed"
+
+- name: verify yum module outputs
+ assert:
+ that:
+ - "'changed' in yum_result"
+ - "'msg' in yum_result"
+
+- name: try to install from non existing url
+ yum:
+ name: https://ci-files.testing.ansible.com/test/integration/targets/yum/non-existing-1.0.0.fc26.noarch.rpm
+ state: present
+ register: yum_result
+ ignore_errors: yes
+
+- name: verify installation failed
+ assert:
+ that:
+ - "yum_result is failed"
+ - "not yum_result is changed"
+
+- name: verify yum module outputs
+ assert:
+ that:
+ - "'changed' in yum_result"
+ - "'msg' in yum_result"
+
+- name: use latest to install httpd
+ yum:
+ name: httpd
+ state: latest
+ register: yum_result
+
+- name: verify httpd was installed
+ assert:
+ that:
+ - "'changed' in yum_result"
+
+- name: uninstall httpd
+ yum:
+ name: httpd
+ state: removed
+
+- name: update httpd only if it exists
+ yum:
+ name: httpd
+ state: latest
+ update_only: yes
+ register: yum_result
+
+- name: verify httpd not installed
+ assert:
+ that:
+ - "not yum_result is changed"
+ - "'Packages providing httpd not installed due to update_only specified' in yum_result.results"
+
+- name: try to install uncompatible arch rpm on non-ppc64le, should fail
+ yum:
+ name: https://ci-files.testing.ansible.com/test/integration/targets/yum/banner-1.3.4-3.el7.ppc64le.rpm
+ state: present
+ register: yum_result
+ ignore_errors: True
+ when:
+ - ansible_architecture not in ['ppc64le']
+
+- name: verify that yum failed on non-ppc64le
+ assert:
+ that:
+ - "not yum_result is changed"
+ - "yum_result is failed"
+ when:
+ - ansible_architecture not in ['ppc64le']
+
+- name: try to install uncompatible arch rpm on ppc64le, should fail
+ yum:
+ name: https://ci-files.testing.ansible.com/test/integration/targets/yum/tinyproxy-1.10.0-3.el7.x86_64.rpm
+ state: present
+ register: yum_result
+ ignore_errors: True
+ when:
+ - ansible_architecture in ['ppc64le']
+
+- name: verify that yum failed on ppc64le
+ assert:
+ that:
+ - "not yum_result is changed"
+ - "yum_result is failed"
+ when:
+ - ansible_architecture in ['ppc64le']
+
+# setup for testing installing an RPM from url
+
+- set_fact:
+ pkg_name: noarchfake
+ pkg_path: '{{ repodir }}/noarchfake-1.0-1.noarch.rpm'
+
+- name: cleanup
+ yum:
+ name: "{{ pkg_name }}"
+ state: absent
+
+# setup end
+
+- name: install a local noarch rpm from file
+ yum:
+ name: "{{ pkg_path }}"
+ state: present
+ disable_gpg_check: true
+ register: yum_result
+
+- name: verify installation
+ assert:
+ that:
+ - "yum_result is success"
+ - "yum_result is changed"
+ - "yum_result is not failed"
+
+- name: verify yum module outputs
+ assert:
+ that:
+ - "'changed' in yum_result"
+ - "'msg' in yum_result"
+ - "'results' in yum_result"
+
+- name: install the downloaded rpm again
+ yum:
+ name: "{{ pkg_path }}"
+ state: present
+ register: yum_result
+
+- name: verify installation
+ assert:
+ that:
+ - "yum_result is success"
+ - "not yum_result is changed"
+ - "yum_result is not failed"
+
+- name: verify yum module outputs
+ assert:
+ that:
+ - "'changed' in yum_result"
+ - "'msg' in yum_result"
+ - "'results' in yum_result"
+
+- name: clean up
+ yum:
+ name: "{{ pkg_name }}"
+ state: absent
+
+- name: install from url
+ yum:
+ name: "file://{{ pkg_path }}"
+ state: present
+ disable_gpg_check: true
+ register: yum_result
+
+- name: verify installation
+ assert:
+ that:
+ - "yum_result is success"
+ - "yum_result is changed"
+ - "yum_result is not failed"
+
+- name: verify yum module outputs
+ assert:
+ that:
+ - "'changed' in yum_result"
+ - "'msg' in yum_result"
+ - "'results' in yum_result"
+
+- name: Create a temp RPM file which does not contain nevra information
+ file:
+ name: "/tmp/non_existent_pkg.rpm"
+ state: touch
+
+- name: Try installing RPM file which does not contain nevra information
+ yum:
+ name: "/tmp/non_existent_pkg.rpm"
+ state: present
+ register: no_nevra_info_result
+ ignore_errors: yes
+
+- name: Verify RPM failed to install
+ assert:
+ that:
+ - "'changed' in no_nevra_info_result"
+ - "'msg' in no_nevra_info_result"
+
+- name: Delete a temp RPM file
+ file:
+ name: "/tmp/non_existent_pkg.rpm"
+ state: absent
+
+- name: get yum version
+ yum:
+ list: yum
+ register: yum_version
+
+- name: set yum_version of installed version
+ set_fact:
+ yum_version: "{%- if item.yumstate == 'installed' -%}{{ item.version }}{%- else -%}{{ yum_version }}{%- endif -%}"
+ with_items: "{{ yum_version.results }}"
+
+- name: Ensure double uninstall of wildcard globs works
+ block:
+ - name: "Install lohit-*-fonts"
+ yum:
+ name: "lohit-*-fonts"
+ state: present
+
+ - name: "Remove lohit-*-fonts (1st time)"
+ yum:
+ name: "lohit-*-fonts"
+ state: absent
+ register: remove_lohit_fonts_1
+
+ - name: "Verify lohit-*-fonts (1st time)"
+ assert:
+ that:
+ - "remove_lohit_fonts_1 is changed"
+ - "'msg' in remove_lohit_fonts_1"
+ - "'results' in remove_lohit_fonts_1"
+
+ - name: "Remove lohit-*-fonts (2nd time)"
+ yum:
+ name: "lohit-*-fonts"
+ state: absent
+ register: remove_lohit_fonts_2
+
+ - name: "Verify lohit-*-fonts (2nd time)"
+ assert:
+ that:
+ - "remove_lohit_fonts_2 is not changed"
+ - "'msg' in remove_lohit_fonts_2"
+ - "'results' in remove_lohit_fonts_2"
+ - "'lohit-*-fonts is not installed' in remove_lohit_fonts_2['results']"
+
+- block:
+ - name: uninstall {{ package2 }}
+ yum: name={{ package2 }} state=removed
+
+ - name: check {{ package2 }} with rpm
+ shell: rpm -q {{ package2 }}
+ ignore_errors: True
+ register: rpm_package2_result
+
+ - name: verify {{ package2 }} is uninstalled
+ assert:
+ that:
+ - "rpm_package2_result is failed"
+
+ - name: exclude {{ package2 }} (yum backend)
+ lineinfile:
+ dest: /etc/yum.conf
+ regexp: (^exclude=)(.)*
+ line: "exclude={{ package2 }}*"
+ state: present
+ when: ansible_pkg_mgr == 'yum'
+
+ - name: exclude {{ package2 }} (dnf backend)
+ lineinfile:
+ dest: /etc/dnf/dnf.conf
+ regexp: (^excludepkgs=)(.)*
+ line: "excludepkgs={{ package2 }}*"
+ state: present
+ when: ansible_pkg_mgr == 'dnf'
+
+ # begin test case where disable_excludes is supported
+ - name: Try install {{ package2 }} without disable_excludes
+ yum: name={{ package2 }} state=latest
+ register: yum_package2_result
+ ignore_errors: True
+
+ - name: verify {{ package2 }} did not install because it is in exclude list
+ assert:
+ that:
+ - "yum_package2_result is failed"
+
+ - name: install {{ package2 }} with disable_excludes
+ yum: name={{ package2 }} state=latest disable_excludes=all
+ register: yum_package2_result_using_excludes
+
+ - name: verify {{ package2 }} did install using disable_excludes=all
+ assert:
+ that:
+ - "yum_package2_result_using_excludes is success"
+ - "yum_package2_result_using_excludes is changed"
+ - "yum_package2_result_using_excludes is not failed"
+
+ - name: remove exclude {{ package2 }} (cleanup yum.conf)
+ lineinfile:
+ dest: /etc/yum.conf
+ regexp: (^exclude={{ package2 }}*)
+ line: "exclude="
+ state: present
+ when: ansible_pkg_mgr == 'yum'
+
+ - name: remove exclude {{ package2 }} (cleanup dnf.conf)
+ lineinfile:
+ dest: /etc/dnf/dnf.conf
+ regexp: (^excludepkgs={{ package2 }}*)
+ line: "excludepkgs="
+ state: present
+ when: ansible_pkg_mgr == 'dnf'
+
+ # Fedora < 26 has a bug in dnf where package excludes in dnf.conf aren't
+ # actually honored and those releases are EOL'd so we have no expectation they
+ # will ever be fixed
+ when: not ((ansible_distribution == "Fedora") and (ansible_distribution_major_version|int < 26))
+
+- name: Check that packages with Provides are handled correctly in state=absent
+ block:
+ - name: Install test packages
+ yum:
+ name:
+ - https://ci-files.testing.ansible.com/test/integration/targets/yum/test-package-that-provides-toaster-1.3.3.7-1.el7.noarch.rpm
+ - https://ci-files.testing.ansible.com/test/integration/targets/yum/toaster-1.2.3.4-1.el7.noarch.rpm
+ disable_gpg_check: true
+ register: install
+
+ - name: Remove toaster
+ yum:
+ name: toaster
+ state: absent
+ register: remove
+
+ - name: rpm -qa
+ command: rpm -qa
+ register: rpmqa
+
+ - assert:
+ that:
+ - install is successful
+ - install is changed
+ - remove is successful
+ - remove is changed
+ - "'toaster-1.2.3.4' not in rpmqa.stdout"
+ - "'test-package-that-provides-toaster' in rpmqa.stdout"
+ always:
+ - name: Remove test packages
+ yum:
+ name:
+ - test-package-that-provides-toaster
+ - toaster
+ state: absent
+
+- yum:
+ list: "{{ package1 }}"
+ register: list_out
+
+- name: check that both yum and dnf return envra
+ assert:
+ that:
+ - '"envra" in list_out["results"][0]'
+
+- name: check that dnf returns nevra for backwards compat
+ assert:
+ that:
+ - '"nevra" in list_out["results"][0]'
+ when: ansible_pkg_mgr == 'dnf'
diff --git a/test/integration/targets/yum/tasks/yum_group_remove.yml b/test/integration/targets/yum/tasks/yum_group_remove.yml
new file mode 100644
index 0000000..22c6dcb
--- /dev/null
+++ b/test/integration/targets/yum/tasks/yum_group_remove.yml
@@ -0,0 +1,152 @@
+- name: install a group to test and yum-utils
+ yum:
+ name: "{{ pkgs }}"
+ state: present
+ vars:
+ pkgs:
+ - "@Custom Group"
+ - yum-utils
+ when: ansible_pkg_mgr == "yum"
+
+- name: install a group to test and dnf-utils
+ yum:
+ name: "{{ pkgs }}"
+ state: present
+ vars:
+ pkgs:
+ - "@Custom Group"
+ - dnf-utils
+ when: ansible_pkg_mgr == "dnf"
+
+- name: check mode remove the group
+ yum:
+ name: "@Custom Group"
+ state: absent
+ check_mode: yes
+ register: yum_result
+
+- name: verify changed
+ assert:
+ that:
+ - "yum_result.changed"
+
+- name: verify yum module outputs
+ assert:
+ that:
+ - "'changed' in yum_result"
+ - "'results' in yum_result"
+
+- name: remove the group
+ yum:
+ name: "@Custom Group"
+ state: absent
+ register: yum_result
+
+- name: verify changed
+ assert:
+ that:
+ - "yum_result.rc == 0"
+ - "yum_result.changed"
+
+- name: verify yum module outputs
+ assert:
+ that:
+ - "'changed' in yum_result"
+ - "'msg' in yum_result"
+ - "'results' in yum_result"
+
+- name: remove the group again
+ yum:
+ name: "@Custom Group"
+ state: absent
+ register: yum_result
+
+- name: verify changed
+ assert:
+ that:
+ - "not yum_result.changed"
+
+- name: verify yum module outputs
+ assert:
+ that:
+ - "'changed' in yum_result"
+ - "'msg' in yum_result"
+ - "'results' in yum_result"
+
+- name: check mode remove the group again
+ yum:
+ name: "@Custom Group"
+ state: absent
+ check_mode: yes
+ register: yum_result
+
+- name: verify changed
+ assert:
+ that:
+ - "not yum_result.changed"
+
+- name: verify yum module outputs
+ assert:
+ that:
+ - "'changed' in yum_result"
+ - "'results' in yum_result"
+
+- name: install a group and a package to test
+ yum:
+ name: "@Custom Group,sos"
+ state: present
+ register: yum_output
+
+- name: check mode remove the group along with the package
+ yum:
+ name: "@Custom Group,sos"
+ state: absent
+ register: yum_result
+ check_mode: yes
+
+- name: verify changed
+ assert:
+ that:
+ - "yum_result.changed"
+
+- name: verify yum module outputs
+ assert:
+ that:
+ - "'changed' in yum_result"
+ - "'results' in yum_result"
+
+- name: remove the group along with the package
+ yum:
+ name: "@Custom Group,sos"
+ state: absent
+ register: yum_result
+
+- name: verify changed
+ assert:
+ that:
+ - "yum_result.changed"
+
+- name: verify yum module outputs
+ assert:
+ that:
+ - "'changed' in yum_result"
+ - "'msg' in yum_result"
+ - "'results' in yum_result"
+
+- name: check mode remove the group along with the package
+ yum:
+ name: "@Custom Group,sos"
+ state: absent
+ register: yum_result
+ check_mode: yes
+
+- name: verify not changed
+ assert:
+ that:
+ - "not yum_result.changed"
+
+- name: verify yum module outputs
+ assert:
+ that:
+ - "'changed' in yum_result"
+ - "'results' in yum_result"
diff --git a/test/integration/targets/yum/tasks/yuminstallroot.yml b/test/integration/targets/yum/tasks/yuminstallroot.yml
new file mode 100644
index 0000000..028e805
--- /dev/null
+++ b/test/integration/targets/yum/tasks/yuminstallroot.yml
@@ -0,0 +1,132 @@
+# make a installroot
+- name: Create installroot
+ command: mktemp -d "{{ remote_tmp_dir }}/ansible.test.XXXXXX"
+ register: yumroot
+
+#- name: Populate directory
+# file:
+# path: "/{{ yumroot.stdout }}/etc/"
+# state: directory
+# mode: 0755
+#
+#- name: Populate directory2
+# copy:
+# content: "[main]\ndistropkgver={{ ansible_distribution_version }}\n"
+# dest: "/{{ yumroot.stdout }}/etc/yum.conf"
+
+- name: Make a necessary directory
+ file:
+ path: "{{ yumroot.stdout }}/etc/yum/vars/"
+ state: directory
+ mode: 0755
+
+- name: get yum releasever
+ command: "{{ ansible_python_interpreter }} -c 'import yum; yb = yum.YumBase(); print(yb.conf.yumvar[\"releasever\"])'"
+ register: releasever
+ ignore_errors: yes
+
+- name: Populate directory
+ copy:
+ content: "{{ releasever.stdout_lines[-1] }}\n"
+ dest: "/{{ yumroot.stdout }}/etc/yum/vars/releasever"
+ when: releasever is successful
+
+# This will drag in > 200 MB.
+- name: attempt installroot
+ yum: name=zlib installroot="{{ yumroot.stdout }}/" disable_gpg_check=yes
+ register: yum_result
+
+- name: check sos with rpm in installroot
+ shell: rpm -q zlib --root="{{ yumroot.stdout }}/"
+ failed_when: False
+ register: rpm_result
+
+- name: verify installation of sos
+ assert:
+ that:
+ - "yum_result.rc == 0"
+ - "yum_result.changed"
+ - "rpm_result.rc == 0"
+
+- name: verify yum module outputs
+ assert:
+ that:
+ - "'changed' in yum_result"
+ - "'msg' in yum_result"
+ - "'rc' in yum_result"
+ - "'results' in yum_result"
+
+- name: cleanup installroot
+ file:
+ path: "{{ yumroot.stdout }}/"
+ state: absent
+
+# Test for releasever working correctly
+#
+# Bugfix: https://github.com/ansible/ansible/issues/67050
+#
+# This test case is based on a reproducer originally reported on Reddit:
+# https://www.reddit.com/r/ansible/comments/g2ps32/ansible_yum_module_throws_up_an_error_when/
+#
+# NOTE: For the Ansible upstream CI we can only run this for RHEL7 because the
+# containerized runtimes in shippable don't allow the nested mounting of
+# buildah container volumes.
+- name: perform yuminstallroot in a buildah mount with releasever
+ when:
+ - ansible_facts["distribution_major_version"] == "7"
+ - ansible_facts["distribution"] == "RedHat"
+ block:
+ - name: install required packages for buildah test
+ yum:
+ state: present
+ name:
+ - buildah
+ - name: create buildah container from scratch
+ command: "buildah --name yum_installroot_releasever_test from scratch"
+ - name: mount the buildah container
+ command: "buildah mount yum_installroot_releasever_test"
+ register: buildah_mount
+ - name: figure out yum value of $releasever
+ shell: python -c 'import yum; yb = yum.YumBase(); print(yb.conf.yumvar["releasever"])' | tail -1
+ register: buildah_host_releasever
+ - name: test yum install of python using releasever
+ yum:
+ name: 'python'
+ state: present
+ installroot: "{{ buildah_mount.stdout }}"
+ releasever: "{{ buildah_host_releasever.stdout }}"
+ register: yum_result
+ - name: verify installation of python
+ assert:
+ that:
+ - "yum_result.rc == 0"
+ - "yum_result.changed"
+ - "rpm_result.rc == 0"
+ - name: remove python before another test
+ yum:
+ name: 'python'
+ state: absent
+ installroot: "{{ buildah_mount.stdout }}"
+ releasever: "{{ buildah_host_releasever.stdout }}"
+ - name: test yum install of python using releasever with latest
+ yum:
+ name: 'python'
+ state: latest
+ installroot: "{{ buildah_mount.stdout }}"
+ releasever: "{{ buildah_host_releasever.stdout }}"
+ register: yum_result
+ - name: verify installation of python
+ assert:
+ that:
+ - "yum_result.rc == 0"
+ - "yum_result.changed"
+ - "rpm_result.rc == 0"
+ always:
+ - name: remove buildah container
+ command: "buildah rm yum_installroot_releasever_test"
+ ignore_errors: yes
+ - name: remove buildah from CI system
+ yum:
+ state: absent
+ name:
+ - buildah
diff --git a/test/integration/targets/yum/vars/main.yml b/test/integration/targets/yum/vars/main.yml
new file mode 100644
index 0000000..a2a073f
--- /dev/null
+++ b/test/integration/targets/yum/vars/main.yml
@@ -0,0 +1 @@
+multiarch_repo_baseurl: https://ci-files.testing.ansible.com/test/integration/targets/yum/multiarch-test-repo/RPMS/
diff --git a/test/integration/targets/yum_repository/aliases b/test/integration/targets/yum_repository/aliases
new file mode 100644
index 0000000..6eae8bd
--- /dev/null
+++ b/test/integration/targets/yum_repository/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group1
+destructive
diff --git a/test/integration/targets/yum_repository/defaults/main.yml b/test/integration/targets/yum_repository/defaults/main.yml
new file mode 100644
index 0000000..4c1fbc6
--- /dev/null
+++ b/test/integration/targets/yum_repository/defaults/main.yml
@@ -0,0 +1,5 @@
+yum_repository_test_package: dinginessentail
+yum_repository_test_repo:
+ name: fakerepo
+ description: Fake Repo
+ baseurl: "file://{{ repodir }}"
diff --git a/test/integration/targets/yum_repository/handlers/main.yml b/test/integration/targets/yum_repository/handlers/main.yml
new file mode 100644
index 0000000..f96c239
--- /dev/null
+++ b/test/integration/targets/yum_repository/handlers/main.yml
@@ -0,0 +1,4 @@
+- name: remove listtest repo
+ yum_repository:
+ name: listtest
+ state: absent
diff --git a/test/integration/targets/yum_repository/meta/main.yml b/test/integration/targets/yum_repository/meta/main.yml
new file mode 100644
index 0000000..56539a4
--- /dev/null
+++ b/test/integration/targets/yum_repository/meta/main.yml
@@ -0,0 +1,4 @@
+dependencies:
+ - role: setup_rpm_repo
+ vars:
+ install_repos: no
diff --git a/test/integration/targets/yum_repository/tasks/main.yml b/test/integration/targets/yum_repository/tasks/main.yml
new file mode 100644
index 0000000..7813af0
--- /dev/null
+++ b/test/integration/targets/yum_repository/tasks/main.yml
@@ -0,0 +1,196 @@
+- name: Run tests
+ when: ansible_facts.distribution in ['CentOS', 'Fedora']
+ block:
+ - name: ensure {{ yum_repository_test_package }} is uninstalled to begin with
+ action: "{{ ansible_facts.pkg_mgr }}"
+ args:
+ name: "{{ yum_repository_test_package }}"
+ state: absent
+
+ - name: disable {{ yum_repository_test_repo.name }}
+ yum_repository:
+ name: "{{ yum_repository_test_repo.name }}"
+ state: absent
+
+ - name: disable {{ yum_repository_test_repo.name }} (Idempotant)
+ yum_repository:
+ name: "{{ yum_repository_test_repo.name }}"
+ state: absent
+ register: test_repo_remove
+
+ - name: check return values
+ assert:
+ that:
+ - "test_repo_remove.repo == yum_repository_test_repo.name"
+ - "test_repo_remove.state == 'absent'"
+
+ - name: check Idempotant
+ assert:
+ that: not test_repo_remove.changed
+
+ - name: install {{ yum_repository_test_package }}, which should fail
+ action: "{{ ansible_facts.pkg_mgr }}"
+ args:
+ name: "{{ yum_repository_test_package }}"
+ state: present
+ ignore_errors: yes
+ register: test_package_result
+
+ - name: check that install failed
+ assert:
+ that:
+ - test_package_result.failed
+ - test_package_result.msg in expected_messages
+ vars:
+ expected_messages:
+ - No package matching '{{ yum_repository_test_package }}' found available, installed or updated
+ - Failed to install some of the specified packages
+
+ - name: re-add {{ yum_repository_test_repo.name }}
+ yum_repository:
+ name: "{{ yum_repository_test_repo.name }}"
+ description: "{{ yum_repository_test_repo.description }}"
+ baseurl: "{{ yum_repository_test_repo.baseurl }}"
+ gpgcheck: no
+ state: present
+ register: test_repo_add
+
+ - name: check return values
+ assert:
+ that:
+ - test_repo_add.repo == yum_repository_test_repo.name
+ - test_repo_add.state == 'present'
+
+ - name: get repolist
+ shell: yum repolist
+ register: repolist
+ until: repolist.rc == 0
+ retries: 5
+
+ - name: ensure {{ yum_repository_test_repo.name }} was added
+ assert:
+ that:
+ - yum_repository_test_repo.name in repolist.stdout
+ - test_repo_add.changed
+
+ - name: install {{ yum_repository_test_package }}
+ action: "{{ ansible_facts.pkg_mgr }}"
+ args:
+ name: "{{ yum_repository_test_package }}"
+ state: present
+ register: test_package_result
+
+ - name: check that {{ yum_repository_test_package }} was successfully installed
+ assert:
+ that:
+ - test_package_result.changed
+
+ - name: remove {{ yum_repository_test_package }}
+ action: "{{ ansible_facts.pkg_mgr }}"
+ args:
+ name: "{{ yum_repository_test_package }}"
+ state: absent
+
+ - name: change configuration of {{ yum_repository_test_repo.name }} repo
+ yum_repository:
+ name: "{{ yum_repository_test_repo.name }}"
+ baseurl: "{{ yum_repository_test_repo.baseurl }}"
+ description: New description
+ async: no
+ enablegroups: no
+ file: "{{ yum_repository_test_repo.name ~ 2 }}"
+ ip_resolve: 4
+ keepalive: no
+ module_hotfixes: no
+ register: test_repo_add1
+
+ - name: Get repo file contents
+ slurp:
+ path: "{{ '/etc/yum.repos.d/' ~ yum_repository_test_repo.name ~ '2.repo' }}"
+ register: slurp
+
+ - name: check that options are correctly getting written to the repo file
+ assert:
+ that:
+ - "'async = 0' in repo_file_contents"
+ - "'name = New description' in repo_file_contents"
+ - "'enablegroups = 0' in repo_file_contents"
+ - "'ip_resolve = 4' in repo_file_contents"
+ - "'keepalive = 0' in repo_file_contents"
+ - "'module_hotfixes = 0' in repo_file_contents"
+ vars:
+ repo_file_contents: "{{ slurp.content | b64decode }}"
+
+ - name: check new config doesn't change (Idempotant)
+ yum_repository:
+ name: "{{ yum_repository_test_repo.name }}"
+ baseurl: "{{ yum_repository_test_repo.baseurl }}"
+ description: New description
+ async: no
+ enablegroups: no
+ file: "{{ yum_repository_test_repo.name ~ 2 }}"
+ ip_resolve: 4
+ keepalive: no
+ module_hotfixes: no
+ register: test_repo_add2
+
+ - name: check Idempotant
+ assert:
+ that:
+ - test_repo_add1 is changed
+ - test_repo_add2 is not changed
+
+ - name: re-enable the {{ yum_repository_test_repo.name }} repo
+ yum_repository:
+ name: "{{ yum_repository_test_repo.name }}"
+ description: "{{ yum_repository_test_repo.description }}"
+ baseurl: "{{ yum_repository_test_repo.baseurl }}"
+ state: present
+
+ - name: re-enable the {{ yum_repository_test_repo.name }} repo (Idempotant)
+ yum_repository:
+ name: "{{ yum_repository_test_repo.name }}"
+ description: "{{ yum_repository_test_repo.description }}"
+ baseurl: "{{ yum_repository_test_repo.baseurl }}"
+ state: present
+ register: test_repo_add
+
+ - name: check Idempotant
+ assert:
+ that: test_repo_add is not changed
+
+ - name: Test list options
+ yum_repository:
+ name: listtest
+ description: Testing list feature
+ baseurl:
+ - "{{ yum_repository_test_repo.baseurl }}"
+ - "{{ yum_repository_test_repo.baseurl ~ 'another_baseurl' }}"
+ gpgkey:
+ - gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-{{ ansible_facts.distribution_major_version }}
+ - gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG2-KEY-EPEL-{{ ansible_facts.distribution_major_version }}
+ exclude:
+ - aaa
+ - bbb
+ includepkgs:
+ - ccc
+ - ddd
+ notify: remove listtest repo
+
+ - name: Get repo file
+ slurp:
+ path: /etc/yum.repos.d/listtest.repo
+ register: slurp
+
+ - name: Assert that lists were properly inserted
+ assert:
+ that:
+ - yum_repository_test_repo.baseurl in repofile
+ - another_baseurl in repofile
+ - "'RPM-GPG-KEY-EPEL' in repofile"
+ - "'RPM-GPG2-KEY-EPEL' in repofile"
+ - "'aaa bbb' in repofile"
+ - "'ccc ddd' in repofile"
+ vars:
+ repofile: "{{ slurp.content | b64decode }}"
+ another_baseurl: "{{ yum_repository_test_repo.baseurl ~ 'another_baseurl' }}"